随着 Node.js 的应用慢慢的变多,RESTful API 也好 RPC 也好,在应用广泛的同时,特别是 npm 仓库上存在大量质量参差不齐,年久失修的库,Node.js 的安全问题慢慢变得严峻起来,这里主要简单谈论一些 HTTP 相关的安全问题。
先说说几个所有语言都存在的几个问题
1. Directory Traversal,也就是任意目录遍历
这类问题主要存在于一些静态文件的 middleware 中,这里用 koa 写一个最简单的例子:
const fs = require('fs')
const path = require('path')
const static = dir => {
return async (ctx, next) => {
const filename = path.resolve(__dirname, path.join(dir, ctx.path))
ctx.assert(fs.existsSync(filename), 404)
ctx.body = fs.readFileSync(filename)
}
}
假设存在一个目录结构:
// .
// ├── index.js
// ├── package.json
// └── static
// └── 1.txt
// 使用中间件
app.use(static('./static'))
GET /1.txt
// 这里会返回 1.txt 的内容
GET /../index.js
// 这里会返回 index.js 的内容
案例:今年六月份 koajs 下的一个中间件 static-cache 就有这个问题,感兴趣可以去看看,koajs/static-cache #66
解决方法也很简单,path 模块提供了 normalize 方法, 对路径处理后判断是否在规定的目录下即可。
2. SQL Injection
这是个老问题了,还是让我们先举一个最简单的例子:
const mysql = require('mysql')
const sha256 = require('./util').sha256
const connection = mysql.createConnection({ /* */ })
const { username, password } = ctx.request.body
connection.query(`SELECT * FROM USERS WHERE username='${username}' and password='${sha256(password)}';`)
当此时 username = “admin’; #” 时,整个 sql 语句变成 SELECT * FROM USERS WHERE username=’admin’; # password=sha256hex; 也就实现了任意用户登录。
这里不再继续说 SQL Injection 了,有太多的 WAF,或者分析工具,比如 SQLChop。
平时开发的时候找个靠谱一点的 ORM 一般就没问题,但要注意的是就算用了 ORM 也有可能产生注入,做好安全升级即可。
案例:knex SQL Injection knex #737
再说说几个 Node.js 的几个问题
1. Uninitialized Memory Exposure
在较早一点的 node 版本中 (8.0 之前),当 Buffer 的构造函数传入数字时, 会得到与数字长度一致的一个 Buffer,并且这个 Buffer 是未清零的。8.0 之后的版本可以通过另一个函数 Buffer.allocUnsafe(size) 来获得未清空的内存。
new Buffer(4)
// <Buffer 1a 00 3c 22>
new Buffer(4)
// <Buffer 45 ed 10 9d>
在 HITCON 2016 上,就出过类似的题 (原来我也想出的,被 orange 大佬抢先了),贴出题目的源码:
"use strict";
var randomstring = require("randomstring");
var express = require("express");
var {VM} = require("vm2");
var fs = require("fs");
var app = express();
var flag = require("./config.js").flag;
app.get("/", function (req, res) {
res.header("Content-Type", "text/plain");
/* Orange is so kind so he put the flag here. But if you can guess correctly :P */
eval("var flag_" + randomstring.generate(64) + " = \"hitcon{" + flag + "}\";")
if (req.query.data && req.query.data.length <= 12) {
var vm = new VM({
timeout: 1000
});
console.log(req.query.data);
res.send("eval ->" + vm.run(req.query.data));
} else {
res.send(fs.readFileSync(__filename).toString());
}
});
app.listen(3000, function () {
console.log("listening on port 3000!");
});
这里能在 vm 环境中执行任意命令,req.query.data.length 的限制可以通过传入数组绕过,vm 模块会执行数组的最后一个元素。
由于 Buffer 可以得到未被清空的内存, 所以可以拿到不知道B变量名的 flag 变量,当然在老版本下也存在 12 个字符以下的 payload:Buffer(1e4)
这里比较有趣的一点是用到了 vm2 这个模块,至于这里为什么不用官方的 vm 模块,稍后会写到。
2. Arbitrary Code Execution & VM Escape
前一段时间被大肆报道的一个模块 node-serialize,看了一下内容,不是很清楚为什么这个模块被作为大新闻报道,还出现了各种进一步利用、完美利用等文章,不知道是不是模块名带有 node 的缘故, 不知情者把他当做成了官方库.
本质上这是一个序列化 <—> 反序列化的过程,因为反序列化的过程中采用了 new Function(), 导致了任意代码执行。不单单是 Node.js, 诸如 Java,PHP,Ruby 和 Python 也都曾出现过许多反序列化漏洞,许多 CTF 比赛上也经常出现 PHP 的反序列化题目。
在我搜寻 node-serialize 相关文章的时候,某篇文章下有人提到了不用 eval 或 Function 作为反序列化的工具,而采用官方提供的 vm 模块,举了另一个库 funcster 作为例子,这也是一个反序列化的库, 不过它把 vm 模块直接作为一个 sandbox 用来隔离代码环境,但这依旧不安全。
这里简单介绍一下 vm 模块,它用来创建一个干净的上下文环境,官方文档是这么写的:
JavaScript code can be compiled and run immediately or compiled, saved, and run later.
Note: The vm module is not a security mechanism. Do not use it to run untrusted code.
Node.js 文档也提到了不要把 vm 当做一个安全的沙箱,事实上也是如此。
举一个栗子:
const vm = require('vm')
vm.runInNewContext(`
console.log(require)
`, { console })
// undefined
看起来似乎隔离了代码环境,不过这样可以逃逸到外面的代码环境:
const vm = require('vm')
vm.runInNewContext(`
console.log(this.constructor.constructor('return require')())
`, { console })
// [Function: require]
让我们回到 funcster 这个模块,先看一下他的文档:
serialize(function() { return "Hello world!" });
// -> { __js_function: 'function() { return "Hello world!" }' }
把 __js_function 当做 function 反序列化的标志,此外在模块源码的 _generateModuleScript 函数:
_generateModuleScript: function(serializedFunctions) {
var body, entries, name;
entries = [];
for (name in serializedFunctions) {
body = serializedFunctions[name];
entries.push("" + (JSON.stringify(name)) + ": " + body);
}
entries = entries.join(',');
return "module.exports=(function(module,exports){return{" + entries + "};})();";
}
这里的 name 是 func_,那么结合它的拼接语句,可以得到一个任意执行代码的 payload:
const funcster = require('funcster')
const a = {
__js_function:
"1}}, (() => { this.constructor.constructor('return console')().log('pwn') })() , function(){return{"
}
funcster.deepDeserialize(a) // pwn
实现了任意代码执行。
如果实在想要一个安全的执行环境,可以看看修复了此问题的 vm2 模块,这个模块的潜在隐患还有很多,比如 SSR 就需要用到 vm。
写在最后
本文只是简单的对 Node.js 的安全方面做了一点介绍,表述不清楚或错误的地方请留言指正。希望能对想学习一点后端知识的前端有所帮助。