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

語言: CN / TW / HK

主講人簡介

ashx:

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

前言

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

React應用長什麼樣?

可以使用 Create React App ( https://create-react-app.dev/ ) 來快速生成並執行一個React應用。

安裝了 React Developer Tools

( https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi ) 並訪問一個網站後,若瀏覽器 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程式碼(結構有做簡化)

const element = {  type: 'h1',  props: {    className: 'greeting',  
children: 'Hello, world!' }};
就像程式碼展示的那樣

沒有什麼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 ( https://zh-hans.reactjs.org/docs/refs-and-the-dom.html ) 是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。

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

我們看一下下列程式碼

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

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

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

這種將使用者輸入不限制地傳入屬性引數的做法顯然會導致XSS,一旦createElement的引數完全可控,實現完全使用者可控的動態元件建立,也可以直接導致XSS。

值得一提的是,react-dom會通過某些方法( https://github.com/facebook/react/blob/c88fb49d37fd01024e0a254a37b7810d107bdd1d/packages/react-dom/src/client/ReactDOMComponent.js#L395 )來防止動態建立的script標籤內的JS程式碼執行, 但是這個安全檢查繞過難度不高,可以直接用onerror等屬性替代。

特殊DOM標籤的特殊屬性

考慮以下程式碼:

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

當遇到某些特殊DOM標籤的特殊屬性可控時,可以直接造成XSS。 由於開發者們一般情況下不會把onError 等事件讓使用者可控,即使可控React也不接受字串為引數, (報錯:Uncaught Error: Expected onError   listener to be a function, instead got a value of string    type.)

所以能考慮的只有類似 frame   、 iframe  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

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

注意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。原因如下:

  1. 包括Babel和TypeScript的實現在內,按照標準,Object spread不會將原型鏈上的屬性複製到新物件上。

  2. React自身在遍歷Object的每個屬性的時候,會使用 hasOwnProperty 檢查其是否是原型鏈屬性。

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

const {id, username, password} = props

等同於

var _props = props,
id = _props.id,
username = _props.username,
password = _props.password;

以上程式碼顯然可以被原型鏈汙染攻擊。這種寫法在現代前端程式碼中極為常見,我們以0CTF 2021 Final的useCTF()題為例。

這一題的官方解答的攻擊目標是 reapop 這個庫。以下是相關程式碼:

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

程式碼存在 dangerouslySetInnerHTML ,可以把這種危險引數作為原型鏈汙染的目的地。按此處的邏輯,只需要 Object.prototype.allowHTML true ,頁面裡就會直接把 Object.prototype.title 屬性原樣輸出。

而來自俄羅斯的More Smoked Leet Chicken戰隊給出了更精妙的解法。這個題目的UI框架 chakra-ui 給部分元件提供了一個特殊的屬性 as https://chakra-ui.com/docs/layout/box#as-prop 。該屬性的效果大致如下:

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

在DOM內會輸出為:

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

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

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

這段程式碼有以下問題:

  • 沒有檢查 props.as 是否屬於props物件(對於UI框架一般也沒有檢查的必要)

  • finalTag = props.as ,即原型鏈汙染可控

  • 在複製 props newProps 時未檢查屬性是否屬於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實踐 ( https://hexo.imagemlt.xyz/post/javascript-codeql-learning/index.html )

騰訊安全科恩實驗室

關注我們,KeenLab Tech Talk系列將持續以影片/文字形式輸出技術乾貨,涵蓋: 漏洞分析、工具分享、技巧總結、演算法優化 、入門引導 、賽題講解 等安全領域主題。更有不定期 抽獎活動 等你參與!