Nodejs命令执行 又是复现其他师傅博客上的题目的一天~
这题的整体结构还是比较简单的,但是做的过程还是踩了不少的坑。
看起来需要获得admin权限
抓个包看一下
coockie部分看起来有点像JWT,理由是由三部分加密组成,且由点进行连接。
到jwt.io进行解密
尝试将guest改成admin再传入,然而还是不行。
可以推测这里是使用了密钥进行加密,这里对jwt弱密钥进行爆破,编写如下脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import jwt jwt_str='' f=open ('keys.txt' )for i in f: try : jwt.decode(jwt_str, verify=True , key=i, algorithms='HS256' ) print ('the key is ' +i) break except (jwt.exceptions.ExpiredSignatureError,jwt.exceptions.InvalidAudienceError,jwt.exceptions.InvalidIssuedAtError,jwt.exceptions.InvalidIssuedAtError,jwt.exceptions.ImmatureSignatureError): print ('there are something wrong,but the key is' + i) break except jwt.exceptions.InvalidSignatureError: continue else : print ('found no key' )
重新进行编码成功进入/source
页面,里面是如下源码
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 67 const express = require ("express" );const jwt = require ("jsonwebtoken" );const app = express ();const bodyParser = require ("body-parser" );const path = require ("path" );const jwt_secret = "toor" ;const cookieParser = require ("cookie-parser" );const putil_merge = require ("putil-merge" ); app.use (cookieParser ()); app.use (bodyParser.urlencoded ({ extended : true })).use (bodyParser.json ());var Super = {};var safecode = function (code ) { let validInput = /global|mainModule|constructor|read|write|_load|exec|spawnSync|stdout|eval|stdout|Function|setInterval|setTimeout|var|\+|\*/gi ; return !validInput.test (code); }; app.all ("/code" , (req, res ) => { res.type ("html" ); if (req.method == "POST" && req.body ) { putil_merge ({}, req.body , { deep : true }); } res.send ("welcome to code" ); }); app.get ("/source" , (req, res ) => { res.type ("html" ); var auth = req.cookies .auth ; jwt.verify (auth, jwt_secret, function (err, decoded ) { if (decoded.user === "admin" ) { res.sendFile (path.join (__dirname + "/app.js" )); } else { res.end ("you are not admin" ); } }); }); app.all ("/root" , (req, res ) => { res.type ("html" ); code = req.body .code ; console .log (req.body .key ); if (!req.body .key || req.body .key === undefined || req.body .key === null ) { res.send ("please input key" ); } else { if (Super ["userid" ] === "Superadmin" + req.body .key ) { if (!safecode (code)) { res.send ("forbidden!" ); } else { res.send (eval (code)); } } else { res.send ("You are not the Super" ); } } }); app.get ("/" , (req, res ) => { res.type ("html" ); var token = jwt.sign ({ user : "guest" }, jwt_secret, { algorithm : "HS256" }); res.cookie ("auth " , token); res.end ("Only admin can get source in /source" ); }); app.listen (3000 , () => console .log ("Server started on port 3000" ));
在/root
路由下有一个eval函数可以执行命令,然而要进入eval函数的判断前提是Super["userid"] === "Superadmin"+req.body.key
和key
值不为空
在往上在code
路由下有一个putil_merge
函数进行merge操作,可以推断这里要使用原型链污染。
这里现在code
处污染userid
的值,然后再在root下传入key
和code
的值便可以进行命令执行了。
原先我不知道req.body.key
这个参数可以用POST或者json直接传参赋值,还在想要如何污染才能绕过判断,所以在这里卡了一会。。。
其实在程序最上面那一部分就说明了可以使用json或者POST传参
1 app.use (bodyParser.urlencoded ({ extended : true })).use (bodyParser.json ());
卡住我的第二个点是我不知道Super["userid"]
其实就相当于Super.userid
所以只要污染原型链上的userid
就够了,在头几行也定义了说Super
是一个空对象。这搞得我也想了一段时间要怎么去污染Super["userid"]
这个参数。。。
还有就是Super["userid"] === "Superadmin" + req.body.key
中的"Superadmin" + req.body.key
其实就是简单的字符串拼接,当时也不知道自己是脑袋抽了还是怎么了,想不过来这个要怎么处理。。。
该清楚上面那些原理后,接着可以构造发包了。
首先是code路由下的构造
这里需要注意的是需要在请求头中加入Content-type:application/json
,我一开始没有注意,把它加到Accept
头里去了,整了半天才发下加错地方了。。。。
接着是root路由下的构造
key
值传入zzz
与Superadmin
拼接通过判断,同时code
进行命令执行。
这里使用的是POST直接传参,所以要写成Content-type:application/x-www-form-urlencoded
在源码处我们有注意到code
其实是进行了正则匹配过滤,可以使用一些常见bypass进行绕过
1 2 3 4 5 6 16 进制编码unicode 编码 加号拼接 模板字符串concat 函数连接 base64编码
在这里我是用的是16进制编码。另外我发现只有进行同步进程创建才可以成功执行命令。
最后在记录一个大坑,今天这个洞其实是个CVE,影响版本从1.0.0 到 3.6.6。而我一开始安装的putil-merge是3.10.10的版本,已经修复了该反序列化漏洞,卡了我一下午。。。。后面才发现是因为版本原因,下次也要多注意一点了。。
参考文章 nodejs中代码执行绕过的一些技巧-安全客 - 安全资讯平台 (anquanke.com)
哈哈,骗你的!ヾ(゚∀゚ゞ)
https://www.jianshu.com/p/acbb936e87df