freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

KeenLab Tech Talk(二)| 浅谈React框架的XSS及后利用
2021-11-16 18:15:56

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代码(结构有做简化)

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。
1637054388_619377b48e244e42a429e.png!small?1637054389450

滥用Ref

Ref是React提供的一种高级功能,允许开发者直接操作React组件渲染出来的DOM。React设计它的本意是实现动画、或是和某些基于DOM的第三方库配合使用(常见的如Prism等代码高亮库)、或是对 video 等媒体标签进行控制,但一个API被设计出来是很难不被滥用的。下图展示了其中的一种滥用。这种滥用的挖掘和利用和常规的挖掘DOM XSS相同。只要值可控(该项目中不可控)也可以造成XSS。
1637054449_619377f1218941c2f8cee.png!small?1637054449701

由于React有几个版本对Ref做了相当多的改动,因此在实际审计时看到的ref用法可能和图中的不同,对挖掘DOM  XSS无影响。

滥用dangerouslySetInnerHTML

某些时候,前端开发者需要直接往该标签内写入HTML。React希望开发者避免使用这种方式,特意给该API起了个又臭又长的名字,要求传入的对象长成{__html: 'HTML'}  的形式,还特意标注了个“dangerously”。虽然他们为了防止滥用做出了很多努力,但似乎没有起到什么成效,dangerouslySetInnerHtml的滥用仍然非常常见。如图,一看就是用户可控的XSS点。1637054489_61937819af7a87c009988.png!small?1637054490389直接全局搜索 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会通过某些方法来防止动态创建的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 、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__" 内,前端代码会读取这个标签内的内容作出处理。

1637054646_619378b6c8971ff3598b7.png!small?1637054648256

很多项目的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-considerationswindow.__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过滤。

1637054699_619378ebd74daf51751dc.png!small?1637054700735

原型链污染

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 ? (<h4style={titleStyles}className={classnames.notificationTitle}dangerouslySetInnerHTML={{__html: title}}/>) : (<h4 style={titleStyles} className={classnames.notificationTitle}>{title}</h4>))}

代码存在dangerouslySetInnerHTML,可以把这种危险参数作为原型链污染的目的地。按此处的逻辑,只需要Object.prototype.allowHTMLtrue,页面里就会直接把Object.prototype.title属性原样输出。而来自俄罗斯的More Smoked Leet Chicken战队给出了更精妙的解法。这个题目的UI框架 chakra-ui 给部分组件提供了一个特殊的属性as。该属性的效果大致如下:

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) || baseTagfor (let key in props) {if (shouldUseAs && key === 'as') continueif (finalShouldForwardProp(key)) {newProps[key] = props[key]}}newProps.className = classNamenewProps.ref = refconst 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元素内增加一个属性:1637054890_619379aaeb64966daa294.png!small?1637054891490在引入了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竞赛。

# 系统安全 # 漏洞分析 # 网络安全技术
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
  • 0 文章数
  • 0 关注者
文章目录