KeenLab Tech Talk(二)| 淺談React框架的XSS及後利用

語言: CN / TW / HK

KeenLab Tech Talk系列

首期: 《虛擬化入門》

第三期: 《Android Auto 中一個普通的堆漏洞》

前言

隨著前端技術的高速發展,前後端分離的開發模式已經深入人心。由於後端不再直接輸出頁面,轉以API介面的方式提供服務,導致XSS在這類新專案中不再常見。不過安全永遠是一個動態的過程,新的開發技術會帶來新的攻擊面。現今前端最常用的主要是三個框架:React、Vuejs和Angular,本文將具體介紹基於React框架開發的前端應用的攻擊面。其他框架也可依此類推。

React應用長什麼樣?

可以使用 Create React App (來快速生成並執行一個React應用。安裝了 React Developer Tools 並訪問一個網站後,若瀏覽器DevTool會亮起“Component”等標籤,則代表這個網站使用了React框架開發。安全從業者往往會需要對一個網站的JavaScript程式碼進行審計。由於幾乎所有的React開發的專案都使用了JSX,因此React框架通常會配合Webpack(parcel / rollup)、babel(tsc)等前端編譯流程工具使用,如果沒有Sourcemap就很難從被編譯的原始碼還原出原始React程式碼。

什麼是JSX?

JSX是一種JavaScript的語法擴充套件,通常和React配合使用。Vuejs也支援JSX。JSX允許開發者直接在JavaScript內部編寫XML語法,不需要經過各種字串的中轉。由於沒有瀏覽器支援JSX,導致React應用的的開發環境通常需要一個編譯器負責將JSX編譯為瀏覽器可以識別的JS程式碼。編譯器(通常是babel)會將以下JSX程式碼:

const element = ( <h1 className="greeting"> Hello, world! </h1>);

編譯成以下能在瀏覽器中執行的JavaScript程式碼(結構有做簡化)

<code>const element = {  type: 'h1',  props: {    className: 'greeting',  </code><code>children: 'Hello, world!'  }};</code>

就像程式碼展示的那樣,沒有什麼DOM結構了,有的只有一個個Object。

我們注意這裡的 Hello, world! 對應的 children,如果我們可以控制這個屬性,是否可以進而導致XSS?答案是否定的。當 children 的型別是字串時,React將對DOM元素使用innerText來設定children,因此不會出現XSS;若可以將其控制為Object(這通常很難),但由於它的隱藏屬性 $$typeof 的值,它在高版本的React中的型別是 Symbol( Symbol 是ES6中引入的一種能表示一種唯一值的型別,Symbol('123') == Symbol('123') 的結果是 false ),我們也無法讓React渲染這個物件。所以對於一個基於React框架(Vue和Angular同樣)編寫的前端專案,即使沒有對XSS字串進行特殊過濾,一般也可以認為是安全的。但世界上總有喜歡劍走偏鋒的開發者,在某些React開發的專案內,仍然可以挖掘到不少XSS。

常規XSS

一些開發者未系統地學習React框架的思想,導致他們可能會使用各類DOM API來繞過React對DOM的管理。這些API包括 document.write 、document.appendChild  等。可以直接全文搜尋這些API。以下列舉的程式碼均來源於GitHub公開搜尋。如下圖,該專案儘管使用了React,但同時還在使用DOM API。此處 innerHTML 如果可控(該專案中不可控),就可以造成一個XSS。挖掘這種型別的漏洞等同於挖掘傳統的DOM XSS。

濫用Ref

Ref 是React提供的一種高階功能,允許開發者直接操作React元件渲染出來的DOM。React設計它的本意是實現動畫、或是和某些基於DOM的第三方庫配合使用(常見的如Prism等程式碼高亮庫)、或是對 video 等媒體標籤進行控制,但一個API被設計出來是很難不被濫用的。下圖展示了其中的一種濫用。這種濫用的挖掘和利用和常規的挖掘DOM XSS相同。只要值可控(該專案中不可控)也可以造成XSS。

由於React有幾個版本對Ref做了相當多的改動,因此在實際審計時看到的ref用法可能和圖中的不同,對挖掘DOM  XSS無影響。

濫用dangerouslySetInnerHTML

某些時候,前端開發者需要直接往該標籤內寫入HTML。React希望開發者避免使用這種方式,特意給該API起了個又臭又長的名字,要求傳入的物件長成{__html: 'HTML'}  的形式,還特意標註了個“dangerously”。雖然他們為了防止濫用做出了很多努力,但似乎沒有起到什麼成效,dangerouslySetInnerHtml的濫用仍然非常常見。如圖,一看就是使用者可控的XSS點。 直接全域性搜尋 dangerouslySetInnerHtml ,可以找到一個React專案的大多數XSS。

動態元件傳參/動態建立元件

我們看一下下列程式碼

<code>const a = JSON.parse(location.hash.substr(1)) </code><code>// hash = #{dangerouslySetInnerHTML: </code><code>{__html: '<script>alert(1)</script>'}}</code><code>return <div {...a} /></code>

它和以下的ES5程式碼功能基本等價。

<code>var a = JSON.parse(location.hash.substr(1))</code><code>var b = {}</code><code>for (var key in a) { </code><code>// 此處存在原型鏈汙染,僅為示例  </code><code>b[key] = a[key]}</code><code>return React.createElement({  "type": "div",  "props": b})</code>

這種將使用者輸入不限制地傳入屬性引數的做法顯然會導致XSS,一旦createElement的引數完全可控,實現完全使用者可控的動態元件建立,也可以直接導致XSS。值得一提的是,react-dom會通過 某些方法 來防止動態建立的script標籤內的JS程式碼執行,但是這個安全檢查繞過難度不高,可以直接用onerror等屬性替代。

特殊DOM標籤的特殊屬性

考慮以下程式碼:

<code>const id = location.hash.substr(1)const a = <a href={id} /></code><code>const b = <iframe src={id} /></code>

當遇到某些特殊DOM標籤的特殊屬性可控時,可以直接造成XSS。由於開發者們一般情況下不會把onError 等事件讓使用者可控,即使可控React也不接受字串為引數,(報錯:Uncaught Error: Expected onError  listener to be a function, instead got a value of string  type.)所以能考慮的只有類似 frame  、iframe 、a 、meta 、object  等較為特殊的標籤。`script`標籤的src和內容不可控無法造成XSS。

SSR時的可疑輸入

SSR是Server Side Render的縮寫,即伺服器端渲染。由於前端框架只工作在前端,導致百度等搜尋引擎無法對網站內容進行抓取,頁面首屏載入速度也同樣會有大幅度的降低。SSR技術可以解決這些問題。只要開發者編寫的JS程式碼對DOMAPI沒有依賴,這些程式碼就可以直接在Nodejs上執行,所以基於React的前端專案只需要將react-dom 置換為 react-dom/server 即可直接複用前端程式碼,在後端渲染頁面並直接輸出HTML。在這種開發模式下,前端與後端伺服器共用一套程式碼,在SSR的配合下DOMXSS可以轉化為儲存型、反射型等其他型別的XSS。在後端渲染完成之後,前端需要基於後端的渲染結果繼續執行,所以後端在輸出HTML程式碼的同時也要將當前狀態返回給前端。這會涉及到物件的序列化與反序列化,會出現意料之外的安全問題。如圖為Nextjs,現代最常見的SSR框架的實現。它會把所有的狀態寫入到scriptid="__NEXT_DATA__" 內,前端程式碼會讀取這個標籤內的內容作出處理。

很多專案的SSR可能是迭代產生的新需求,因為Nextjs需要對專案結構進行相當大的改動,所以它們SSR部分有可能是自行開發的。Redux(一個狀態容器,通常與React配合使用)的官方網站提供了一個SSR的例子:https://redux.js.org/usage/server-rendering

<code>function renderFullPage(html, preloadedState) {</code><code>return `</code><code><!doctype html></code><code><html></code><code><head></code><code><title>Redux Universal Example</title></code><code></head></code><code><body></code><code><div id="root">${html}</div></code><code><script></code><code>// WARNING: See the following for security issues around embedding JSON in HTML:</code><code>// https://redux.js.org/usage/server-rendering#security-considerations</code><code>window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(</code><code>/</g,</code><code>'\\u003c'</code><code>)}</code><code></script></code><code><script src="/static/bundle.js"></script></code><code></body></code><code></html></code><code>`</code><code>}</code>

注意preloadState這個變數,它的值可以包含各種UGC內容,也有很多開發者會將react-router(和React配合使用的一個前端路由)的狀態和Redux同步。如果當前頁面存在訊息、微博等互動性功能,preloadState很有可能部分可控。正是因為這種原因,上述Redux的官方網站給出的程式碼加上了一層XSS過濾。而如果開發者閱讀的文件不是Redux官方文件,而是一些更新較為遲緩的資料,則可能由於這些文件編寫者沒有安全意識而受到攻擊。如下圖的文件內的示例程式碼就缺少了XSS過濾。

原型鏈汙染

SSR

SSR時的前端和後端的程式碼絕大多數是共用的,因此可以通過審計前端程式碼的方式來對SSR服務進行攻擊。如果前端打包時存在Sourcemap洩漏,就可以更直觀地看到具體的依賴庫,之後直接搜CVE或者npm audit。此處的原型鏈汙染想要RCE難度較大,一般的SSR框架,除了express(Nodejs下的WebServer框架)以外,不怎麼依賴別的Nodejs平臺相關的庫和API。對於原型鏈汙染來說,部署最廣泛的可攻擊目標是模板引擎,但SSR一般不使用這些庫,導致攻擊面相對較小。

XSS

const a = <div {...props} />

考慮這種使用了ES6的Object spread語法來複制一個新物件的程式碼,原型鏈汙染在這種場合下無法觸發XSS。原因如下:

hasOwnProperty

但同樣是ES6語法,Destructuring就不一樣了,如下程式碼

const {id, username, password} = props

等同於

<code>var _props = props,</code><code>id = _props.id,</code><code>username = _props.username,</code><code>password = _props.password;</code>

以上程式碼顯然可以被原型鏈汙染攻擊。這種寫法在現代前端程式碼中極為常見,我們以0CTF 2021 Final的useCTF()題為例。這一題的官方解答的攻擊目標是 reapop 這個庫。以下是相關程式碼:

<code>const {id, title, message, dismissible, showDismissButton, buttons, allowHTML, image} = notification</code><code>// ...</code><code>return (</code><code><div></code><code><div style={metaStyles} className={classnames.notificationMeta}></code><code>{title &&</code><code>(allowHTML ? (</code><code><h4</code><code>style={titleStyles}</code><code>className={classnames.notificationTitle}</code><code>dangerouslySetInnerHTML={{__html: title}}</code><code>/></code><code>) : (</code><code><h4 style={titleStyles} className={classnames.notificationTitle}></code><code>{title}</code><code></h4></code><code>))}</code>

程式碼存在 dangerouslySetInnerHTML ,可以把這種危險引數作為原型鏈汙染的目的地。按此處的邏輯,只需要 Object.prototype.allowHTMLtrue ,頁面裡就會直接把 Object.prototype.title 屬性原樣輸出。而來自俄羅斯的More Smoked Leet Chicken戰隊給出了更精妙的解法。這個題目的UI框架 chakra-ui 給部分元件提供了一個特殊的屬性 as 。該屬性的效果大致如下:

<code>const a = <Box as="button" /></code><code>const b = <Box /></code><code>return <div>{a}{b}</div></code>

在DOM內會輸出為:

<code><div></code><code><button></button></code><code><div></div></code><code></div></code>

往下閱讀 as 的實現,程式碼在此處:https://github.com/emotion-js/emotion/blob/23f43ab9f24d44219b0b007a00f4ac681fe8712e/packages/styled/src/base.js#L134

<code>const Styled: PrivateStyledComponent<Props> = withEmotionCache((props, cache, ref) => {</code><code>const finalTag = (shouldUseAs && props.as) || baseTag</code><code>for (let key in props) {</code><code>if (shouldUseAs && key === 'as') continue</code><code>if (finalShouldForwardProp(key)) {</code><code>newProps[key] = props[key]</code><code>}</code><code>}</code><code>newProps.className = className</code><code>newProps.ref = ref</code><code>const ele = React.createElement(finalTag, newProps)</code>

這段程式碼有以下問題:

  • 沒有檢查 props.as 是否屬於props物件(對於UI框架一般也沒有檢查的必要)
  • finalTag = props.as ,即原型鏈汙染可控
  • 在複製 propsnewProps 時未檢查屬性是否屬於props物件

因此通過原型鏈汙染可以完整控制一個React元件。參考本文“動態建立元件”一節,可以很輕鬆地構造出

<iframe src="javascript:alert(1)">

並。as屬性在幾乎所有UI框架中都存在,這使得一個原型鏈汙染漏洞可以在幾乎所有UI框架中造成XSS。(這題無法直接給Object設定一個 dangerouslySetInnerHTML 屬性,這會使其他程式碼無法執行) as 屬性有點類似各種CMS的反序列化鏈,雖然不是漏洞,但這種feature會被原型鏈汙染漏洞濫用。

React Native?

因為React Native不使用瀏覽器渲染資料,所以不太可能出現XSS漏洞。挖掘RCE漏洞更好的方式是尋找 eval /  new Function  等動態程式碼執行相關程式碼,或是尋找能呼叫某些Java / ObjectiveC API的地方。

後利用竊取資料

XSS不只是彈窗,後續利用也值得關注。獲取使用者資料是XSS漏洞的一大危害,而在React框架中獲取資料需要一些技巧。

最輕鬆的獲取資料的方法是從DOM或者是localStorage等資料展示/持久化儲存的地方獲取資料,但有些資料(例如Token)一般不會被渲染在頁面內,需要從React內部獲取這些資料。

在從React內部獲取資料之前,可以考慮通過Hook相關API的方式來獲取資料。對 fetch  API和 XMLHttpRequest  API進行hook (https://github.com/wendux/Ajax-hook ),或者是對資料附近的JavaScript/BOM/DOMAPI進行Hook,都是比較好實現又不依賴於React的通用解決方案。

如果實在難以獲得資料,必須從React內部獲得,則需要對React的相關概念進行學習。一個React專案的資料一般會儲存在這些地方:Prop、State、Context,或是ReduxStore。從外部很難獲取到React內部的值。

可以從React與外部互動的介面入手。React會在渲染出的DOM元素內增加一個屬性: 在引入了Fiber的React(16.8+),會多出 __reactFiber$xxxx 屬性,該屬性對應的就是這個DOM在React內部對應的FiberNode,可以直接使用child屬性獲得子節點。節點層級可以從React Dev Tool內檢視。通過讀取每個FiberNode的 memoizedProps  和 memoizedState  ,即可直接獲取需要的Prop和State。在高版本使用React Hooks的專案中,FiberNode的 memorizedState 是一個連結串列,該連結串列內的節點次序可以參考該元件原始碼內 useState 的呼叫順序。舊版React,引入的屬性是 __reactInternalInstance  。State也是一個Object而非連結串列,可以方便地看到每個state的名字。

Context等屬性可以在該屬性內的 stateNode  屬性找到,對於Redux只需要找到需要的資料在哪個React節點內被呼叫,讀取其props/state也可以間接獲取內部資料。獲取這些資料最主要的麻煩是如何尋找到對應的ReactDOM節點,這需要配合Dev Tool和原始碼慢慢挖掘。

自查React專案的安全問題

  1. 排查所有用到了 dangerouslySetInnerHtml 的元件,並充分論證此處使用該API的必要性。儘量改寫為使用JSX的方式。
  2. 排查所有的 useRef、refs 等涉及到Ref API使用的元件,並儘量規避其的使用。
  3. 排查所有的DOM API呼叫(關鍵詞包括  appendChildinner/HTML 等),將程式碼儘量改寫為不依賴DOM的形式。
  4. 排查SSR的資料同步部分,對使用者輸入進行過濾。
  5. 使用 npm audit 排查是否有某些第三方依賴存在漏洞。
  6. 自查原型鏈汙染漏洞。

擴充套件閱讀

codeql挖掘React應用的XSS實踐

作者

ashx——騰訊安全科恩實驗室安全研究員、Katzebin戰隊副隊長、TCTF出題人之一;主要研究Web安全,在各類的Web應用中發掘不少高危漏洞;作為A*0*E與Katzebin成員參與多場CTF競賽。