这篇文章给大家介绍如何通过欺骗性的React元素实现XSS,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。
相关角色介绍
HackerOne 是一个安全响应和漏洞赏金的平台,口号是「从头开始 (构建),并把安全作为头等大事」。创建 HackerOne 的团队包括了几名安全工程师和渗透测试人员,因此,能够在他们的网站上发现漏洞,我感到非常兴奋。
React 是一个流行的 JavaScript 库,用于构建用户界面。
我是 Trello(一个团队协作应用)的一名开发人员,并且负责运营 Trello 在 HackerOne 上的漏洞赏金项目。偶尔,我也会尝试在我使用的应用中发现安全问题。
背景
这个问题是由我以前针对 HackerOne 的另外一个 XSS 漏洞,一个暂定还未公开披露的报告以及一个CSP绕过漏洞而引起的。
发送非法的 JSON 数据
当我在寻找安全问题时,我花费了大量的时间尝试创建「意外」的情况。一个网站要求我填写名字,我给它发送一些 HTML。他们要求我填写 URL,我给他们一个 URL,其中包含了一堆的引号,换行符,空字符等等。你明白我的意思了吧。
浏览 HackerOne 网站时,我查看了一些发送的 AJAX 请求,注意到其中的一些请求将复杂的 JSON 对象作为请求体来发送,例如:
{ state: "open", substate: "triaged", report_ids: [ 49652, 46916 ], reply_action: "change-state", reference: "http://danlec.com" }
我尝试发送类似的请求,并将某些字段设置为我认为无效的类型。例如,在期望的参数类型是数字的地方,发送一个字符串类型的值 danlec。或者在通常应该发送字符串类型的地方,发送一个数组。例如:
{ state: { "foo": "danlec", "bar": 42 }, substate: 3.2, report_ids: [ "xyzzy", 46916 ], reply_action: [2, "change-state"], reference: { "a": 1, b: ["2"] } }
不出意外地,这些值几乎普遍被拒绝了,或者被转换为适当类型的值(例如,{ 'foo': 'bar' } 可能会转换为字符串类型'{ "foo" => "bar" }')。
然而,在有些地方,错误类型的值并没有直接被拒绝,而是从服务器返回。也就是说,似乎错误类型的值被存储了,并在后续的 API 调用的响应中被返回了。
我发现了两个例子,一个是名为 reference 的字段,被用来对报告进行分类。另一个是名为 data 的字段,该字段与触发器条件关联。
不幸的是,尽管这绝对是奇怪的,但实际上这些错误类型的值被安全地渲染到页面上了。我无法立即想到利用此行为的方法。我将其添加到有潜力进行深度挖掘的 bug 列表中。
几乎放弃
在接下来的一周中,我不断重试这个 bug。我尝试了几种不同的值,虽然我无法做任何的坏事,但我确实注意到有些值被奇怪地渲染了。
通常,像"foo"这样的字符串值将被渲染为类似于
<span>foo</span>
我注意到当该值改为一个数组时,比如 ["foo", "bar"],它会被渲染为
<span><span>foo</span><span>bar</span></span>
甚至更令人兴奋的是,当使用 { foo: "bar" } 之类的对象时,在渲染时,foo 被包含在了 span 元素的 react-id 属性中,例如
<span react-id="…1.2.3.foo.…">bar</span>
我肯定能对最终渲染出来的 DOM 元素产生一些影响,但是负责渲染的代码在内容净化(对非法字符进行转义)方面做得很好。我仍然无法利用它做任何不好的事情。
我已经准备要把这个 bug 提交了,并定义为「它是个奇怪的东西,很可能是一个 bug,但没有任何安全隐患」。但是我决定最后一次尝试,看看是否可以通过单步调试负责渲染元素的客户端代码来找到任何东西。
单步调试 minified JS
在错误类型的值被渲染的地方设置了断点之后,我开始逐步调试所有代码,来看看是否有什么有趣的事情可以为我所用。大多数字符都被压缩了(minified JS 文件中,变量名通常会被替换成随机单个字符),所以并不总是很清楚到底发生了什么,但是当我看到错误类型的值被传递给这个函数时:
l.isValidElement = function(e) { var t = !(!e || !e._isReactElement); return t }
我开始变得兴奋。(这是安全性研究中真正有趣的部分,你知道自己发现了一些不好的东西,现在你只需要弄清楚它到底能有多糟糕)
我已向自己证明了,我可以使 e 变成任何我想要输入的 JSON 值。所以,不需要任何技巧,便可以创建一个对象并且将其_isReactElement 的值设置为 true,这似乎可以告诉客户端我创建的这个对象是一个「有效的元素」。
一旦我添加_isReactElement,和其他几个键(_store,type,props),我创建的对象会使用一种完全不同的渲染方式,这种方式最终会检查 dangerouslySetInnerHTML 属性。现在有了一个看起来很有趣的属性可被操作。
显然,该属性名称正试图警告我,包含原始 HTML 是很危险的。但是,当然,我对于将 dangerouslySetInnerHTML 属性添加到伪造的 React 元素中没有任何的不安。一旦该属性设置好之后,render 方法会渲染出我传入的 HTML 片段,允许任意 HTML 注入,XSS 等。
漏洞利用已实现!
那么……这里实际上发生了什么?
当我使 XSS 正常工作后,就会回退一步来弄清楚实际发生了什么。HackerOne 使用 React,结果证明 React 的 createElement 方法有一个参数,期望接受的值是一个 React 节点,可以是字符串(对于简单的文本内容)或者是数组,或者是 React 元素。
还记得我是怎么注意到有时我的输入会被渲染成多个 span 吗?
我尝试从控制台运行一些 React 方法,结果很清楚实际发生了什么:
> React.renderToString(React.createElement("span", null, "abc")) "<span data-reactid=".7" data-react-checksum="-876606633">abc</span>" > React.renderToString(React.createElement("span", null, ["abc"])) "<span data-reactid=".8" data-react-checksum="-171174425"> <span data-reactid=".8.0">abc</span></span>" > React.renderToString(React.createElement("span", null, { foo: "bar" })) "<span data-reactid=".a" data-react-checksum="1979389930"> <span data-reactid=".a.$foo:0">bar</span></span>"
HackerOne 的客户端代码中,假定它传递的值始终是一个字符串,但是我能够让它传递我特意构造的 JSON 对象,并且通过设置正确的属性,使 React 认为它正在渲染一个元素。
dangerouslySetHTML 是一个特殊的非 DOM 属性,用于提供插入原始 HTML 到元素中的这种功能,这正是像我这样的 XSSer 所寻找的。
我使用的是完整的 JSON 对象,而不是 HackerOne 期望的字符串,类似于
{ _isReactElement: true, _store: {}, type: "body", props: { dangerouslySetInnerHTML: { __html: "<h2>Arbitrary HTML</h2> <script>alert('No CSP Support :(')</script> <a href='http://danlec.com'>link</a>" } } }
在控制台上渲染这个 JSON 对象:
> React.renderToString(React.createElement("span", null, { _isReactElement: true, …})) "<span data-reactid=".9" data-react-checksum="-1151650166"> <body data-reactid=".9.0"><h2>Arbitrary HTML</h2> <script>alert('No CSP Support :(')</script> <a href='http://danlec.com'>link</a></body></span>"
关于如何通过欺骗性的React元素实现XSS就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。
原创文章,作者:carmelaweatherly,如若转载,请注明出处:https://blog.ytso.com/tech/safety/222478.html