0x01 前言
最近看到一篇原型鏈污染的文章,自己在這里總結一下
0x02 javascript 原型鏈
js在ECS6之前沒有類的概念,之前的類都是用funtion來聲明的。如下
可以看到b
在實例化為test對象
以后,就可以輸出test類中的屬性a
了。這是為什么呢?
原因在於js中的一個重要的概念:繼承。
而繼承的整個過程就稱為該類的原型鏈。
在javascript中,每個對象的都有一個指向他的原型(prototype)的內部鏈接,這個原型對象又有它自己的原型,直到null為止
function i(){ this.a = "test1"; this.b = "test2";}
可以看到其父類為object,且里面還有許多函數,這就解釋了為什么許多變量可以調用某些方法。
在javascript中一切皆對象,因為所有的變量,函數,數組,對象 都始於object的原型即object.prototype。同時,在js中只有類才有prototype屬性,而對象卻沒有,對象有的是__proto__
和類的prototype
對應。且二者是等價的
當我們創建一個類時
原型鏈為
b -> a.prototype -> object.prototype->null
創建一個數組時
原型鏈為
c -> array.prototype -> object.prototype->null
創建一個函數時
原型鏈為
d -> function.prototype -> object.prototype->null
創建一個日期
原型鏈為
f -> Data.prototype -> object.prototype->null
所以,測試之后會發現:javascript 一切皆對象,一切皆始於 object.prototype
原型鏈變量的搜索
下面先看一個例子:
我們實例要先於在i
中添加屬性,但是在j
中也有了c屬性。這是為什么呢
答:
當要使用或輸出一個變量時:首先會在本層中搜索相應的變量,如果不存在的話,就會向上搜索,即在自己的父類中搜索,當父類中也沒有時,就會向祖父類搜索,直到指向null,如果此時還沒有搜索到,就會返回 undefined
所以上面的過程就很好解釋了,原型鏈為
j -> i.prototype -> object.prototype -> null
所以對象j
調用c屬性
時,本層並沒有,所以向上搜索,在上一層找到了我們添加的test3
,所以可以輸出。
prototype 原型鏈污染
先看一個小例子:
mess.js
----
(function() { var secret = ["aaa","bbb"]; secret.forEach(); })();
attach.html
結果:
在mess.js中我們聲明了一個數組 secret
,然后該數組調用了屬於 Array.protottype
的foreach
方法,如下
但是,在調用js文件之前,js代碼中將Array.prototype.foreach
方法進行了重寫,而prototype鏈為secret -> Array.prototype ->object.prototype
,secret中無 foreach 方法,所以就會向上檢索,就找到了Array.prototype
而其foreach
方法已經被重寫過了,所以會執行輸出。
這就是原型鏈污染。很明顯,原型鏈污染就是:在我們想要利用的代碼之前的賦值語句如果可控的話,我們進行 ——__proto__ 賦值,之后就可以利用代碼了
如何應用?
在javascript中可以通過 test.a
or test['a']
對數組的元素進行訪問,如下:
同時對對象來說說也是一樣的
所以我們上述說的prototype也是一樣的
那就很明顯了,原型鏈污染一般會出現在對象、或數組的鍵名或屬性名
可控,而且是賦值語句的情況下。
下面我們先看一道題:hackit 2018
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
data
,row
,col
,都是我們post傳入的值,都是可控的。所以可以構造原型鏈污染,下面我們先本地測試一下。
下面我們給出payload和結果
注:要使用json傳值,不然會出現錯誤
下面再看另一道題:
'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}`);
先分析一下題目,獲取flag的條件是admin.аdmin == 1
而admin 本身是一個object,其admin 屬性本身並不存在,而且還有一個敏感函數 merg
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 }
merge 函數作用是進行對象的合並,其中涉及到了對象的賦值,且鍵值可控,這樣就可以觸發原形鏈污染了
下面我們本地測試一下
是undefined,為什么呢?下面我們看下
原來我們在創建字典的時候,__proto__
,不是作為一個鍵名,而是已經作為__proto__
給其父類進行賦值了,所以在test.__proto__
中才有admin屬性,但是我們是想讓__proto__
作為一個鍵值的.
那應該怎么辦呢?可以使用 JSON.parse
JSON.parse 會把一個json字符串 轉化為 javascript的object
這樣就不會在創建類的時候直接給父類賦值了
而題目中也出現了JSON.parse
var body = JSON.parse(JSON.stringify(req.body));
這樣我們就可以愉快地進行原型鏈污染了
payload:
轉自安全客