目录
原型链污染是什么
第一章中说到,foo.__proto__指向的是Foo类的prototype。那么,如果我们修改了foo.__proto__中的值,是不是就可以修改Foo类呢?
做个简单的实验:
// foo是一个简单的JavaScript对象
let foo = {bar: 1}
// foo.bar 此时为1
console.log(foo.bar)
// 修改foo的原型(即Object)
foo.__proto__.bar = 2
// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)
// 此时再用Object创建一个空的zoo对象
let zoo = {}
// 查看zoo.bar
console.log(zoo.bar)
最后,虽然zoo是一个空对象{},但zoo.bar的结果居然是2:
原因也显而易见:因为前面我们修改了foo的原型foo.__proto__.bar = 2,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。
后来,我们又用Object类创建了一个zoo对象let zoo = {},zoo对象自然也有一个bar属性了。
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
原型污染是一个安全漏洞,非常特定于 JavaScript。它源于 JavaScript 继承模型,称为基于原型的继承。与 C++ 或 Java 不同,在 JavaScript 中,您不需要定义类来创建对象。您只需要使用大括号符号并定义属性,例如:
constobj={ prop1: 111, prop2: 222,}
该对象有两个属性:prop1和prop2。但这些并不是我们可以访问的唯一属性。例如调用obj.toString()将返回"[object Object]"。toString(连同其他一些默认成员)来自原型。JavaScript 中的每个对象都有一个原型(也可以是null)。如果我们不指定它,默认情况下对象的原型是Object.prototype.
在 DevTools 中,我们可以轻松检查以下属性的列表Object.prototype:
__proto__我们还可以通过检查其成员或调用来找出给定对象的原型是什么对象Object.getPrototypeOf:
__proto__同样,我们可以使用or设置对象的原型Object.setPrototypeOf:
简而言之,当我们尝试访问对象的属性时,JS 引擎首先检查对象本身是否包含该属性。如果是,则将其退回。否则,JS 会检查原型是否具有该属性。如果没有,JS 会检查原型的原型……以此类推,直到原型为null. 它被称为原型链。
JS 遍历原型链的事实有一个重要的影响:如果我们能以某种方式污染 Object.prototype(即用新属性对其进行扩展),那么所有 JS 对象都会具有这些属性。
考虑以下示例:
const user = { userid: 123 };if (user.admin) { console.log('You are an admin');}
乍一看,似乎不可能使 if 条件为真,因为userobject 没有名为 的属性admin。但是,如果我们污染了Object.prototype和定义名为 的属性admin,那么console.log将执行!
Object.prototype.admin = true;const user = { userid: 123 };if (user.admin) { console.log('You are an admin'); // this will execute}
这证明原型污染可能会对应用程序的安全性产生巨大影响,因为我们可以定义会改变其逻辑的属性。但是,只有少数已知的滥用该漏洞的案例
在进入本文的重点之前,需要再讨论一个话题:原型污染是如何发生的?
此漏洞的入口点通常是合并操作(即将一个对象的所有属性复制到另一个对象)。例如:
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
merge(obj1, obj2)
// returns { a: 1, b: 2, c: 3, d: 4}
有时操作会递归地工作,例如:
const obj1 = { a: { b: 1, c: 2, }};
const obj2 = { a: { d: 3 }};
recursiveMerge(obj1, obj2);
// returns { a: { b: 1, c: 2, d: 3 } }
递归合并的基本流程是:
- 遍历 obj2 的所有属性并检查它们是否存在于obj1.
- 如果存在属性,则对该属性执行合并操作。
- 如果属性不存在,则将其从 复制obj2到obj1。
在现实世界中,如果用户对要合并的对象有任何控制权,那么通常其中一个对象来自JSON.parse. AndJSON.parse有点特别,因为它被视为__proto__“普通”属性,即没有作为原型访问器的特殊含义。考虑以下示例:
在示例中,obj1使用 JS 的大括号符号obj2创建,而使用JSON.parse. 这两个对象都只定义了一个属性,称为__proto__. 但是,访问obj1.__proto__返回Object.prototype(__proto__返回原型的特殊属性也是如此),同时obj2.__proto__包含 JSON 中给出的值,即:123. 这证明了__proto__属性的处理方式与JSON.parse普通 JavaScript 不同。
所以现在想象一个recursiveMerge合并两个对象的函数:
- obj1={}
- obj2=JSON.parse('{"__proto__":{"x":1}}')
该功能或多或少类似于以下步骤:
- 遍历obj2. 唯一的属性是__proto__。
- 检查是否obj1.__proto__存在。确实如此。
- 遍历obj2.__proto__. 唯一的属性是x。
- 赋值:obj1.__proto__.x = obj2.__proto__.x。因为obj1.__proto__指向Object.prototype,则原型被污染。
在许多流行的 JS 库中都发现了这种类型的错误,包括lodash或jQuery。
例1
在www合适目录下创建js文件,然后再cmd运行node pp.js,如果没有相应模块则用npm install moudle即可
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 <b>flag{prototype_pollution_is_very_dangerous}</b>');
}
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!')
})
复现
取flag的条件是 传入的querytoken要和user数组本身的admintoken的MD5值相等,且二者都要存在。由代码可知,全文没有对user.admintokn 进行赋值,所以理论上这个值时不存在的,但是下面有一句赋值语句: matrix[client.row][client.col] = client.data;
可以进行matrix原型污染,最后使uesr拿到admintoken值
例2
'use strict';
const express = require('express');
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser');
const path = require('path');
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
function merge(a, b) {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
function clone(a) {
return merge({}, a);
}
// Constants
const PORT = 8080;
const HOST = '0.0.0.0';
const admin = {};
// App
const app = express();
app.use(bodyParser.json())
app.use(cookieParser());
app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {
var body = JSON.parse(JSON.stringify(req.body)); {"__proto__": {"admin":1}}
var copybody = clone(body)
if (copybody.name) {
res.cookie('name', copybody.name).json({
"done": "cookie set"
});
} else {
res.json({
"error": "cookie not set"
})
}
});
app.get('/getFlag', (req, res) => {
var аdmin = JSON.parse(JSON.stringify(req.cookies))
if (admin.аdmin == 1) {
res.send("hackim19{}");
} else {
res.send("You are not authorized");
}
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
复现
从源代码入手:
Merge()函数是以一种可能发生原型污染的方式编写的。这是问题分析的关键。
易受攻击的函数是在通过clone(body)访问/signup时被调用的,因此我们可以在注册时发送JSON有效负载,这样就可以添加admin属性并立即调用/getFlag来获取Flag。
如前所述,我们可以使用__proto__(points to constructor.prototype)来创建值为1的admin属性。
执行相同操作的最简单的payload:
{"__proto__": {"admin": 1}}