想练SQL注入又不想碰真实站点,pikachu靶场是我用过最顺手的练习环境之一。它把SQL注入按数字型、字符型、搜索型、POST型、报错、盲注等细分成独立关卡,你只要把它跑起来,就能在一个完全可控的本地环境里把各种payload从头到尾试个遍。这篇就围绕pikachu的SQL注入关卡,把环境搭建、注入类型识别、完整测试链路以及我踩过的那些坑一次说清。
1. 靶场初识与部署:先让皮卡丘亮起来
1.1 pikachu是什么,为什么拿它练SQL注入
pikachu是一个开源的Web漏洞演练靶场,开发者用皮卡丘这个形象作为项目标识,寓意是“像电老鼠一样,能准确电到漏洞点”。和很多靶场只给你一个黑盒目标不同,pikachu的每一个练习关卡旁边都附带了源代码片段,你可以一边试探漏洞,一边对照代码确认自己的判断,对学习阶段的人来说这个设计非常友好。
SQL注入是pikachu重点覆盖的漏洞类型之一。它的关卡按注入出现的位置和利用方式拆得很细:有数字型注入、字符型注入、搜索型注入、POST型注入,后面还有报错注入、布尔盲注、时间盲注和堆叠注入。这种粒度在同类靶场中不多见。很多靶场只提供一个统一的登录框让你去测,测完也不知道自己到底在用哪种注入方式,而pikachu把每一个“变种”单独拎出来,逼着你先搞清楚闭合方式,再思考怎么构造payload,练下来对注入的理解会扎实得多。
它适合的人群也很广:
- 刚学Web安全、听了一堆概念但没动手试过的新手,可以用它建立手感;
- 有一定基础、想系统梳理注入类型的人,它的分类本身就是一份很好的提纲;
- 开发人员想验证“预编译为什么能防注入”“过滤函数为什么会被绕过”,也能从关卡源码里直观看到问题。
1.2 本地环境部署全流程
部署pikachu不复杂,它本身是PHP+MySQL的应用,常见做法是放到本地PHP集成环境里跑。整个流程大概四步:
- 部署PHP运行环境,我用的是一款常见的PHP集成环境,启动Apache和MySQL服务即可;
- 把pikachu源码解压到Web根目录,例如htdocs/pikachu;
- 修改数据库配置文件。pikachu的配置在inc/config.inc.php里,默认是root账号、空密码,如果你本地MySQL设置了密码,这里必须同步修改;
- 浏览器访问http://localhost/pikachu/,页面会提示初始化数据库,点一下初始化按钮,会自动建库建表,初始化完成后首页就会出现皮卡丘的形象。
这里有一个很容易踩的坑:初始化脚本只需要执行一次,如果你因为好奇反复点击初始化,第二次会看到“数据表已存在”之类的报错,虽然不影响主体使用,但会让页面状态变得混乱。另外,如果你用的PHP版本偏高,老代码里的部分函数可能会产生兼容性警告,这不影响练习,但建议把PHP的报错级别调低,避免警告信息刷屏影响观察注入结果。
部署好之后,建议先打开一个注入关卡,确认页面能正常查询数据,再做后续测试。很多新手一上来就急着打payload,结果环境根本没跑通,最后误判成“靶场有问题”,实际上只是数据库配置文件没改。
2. 注入攻击面拆解:皮卡丘的关卡设计逻辑
2.1 注入成因与代码层的两条线
SQL注入的成因用一句话说就是:开发者把用户输入直接拼接进了SQL语句,导致输入里的“内容”变成了“SQL语法”。pikachu的每个SQL注入关卡都是刻意写出来的漏洞代码,作为练习者,你得先从代码里去识别它是怎么拼的。
举个例子,数字型注入的代码通常长这样:
select id,name from users where id = $id这里$id是用户传进来的参数,没有加引号,没有过滤,直接拼在等号后面。而字符型注入的代码则是:
select id,name from users where name = '$name'参数被一对单引号包起来了。这两种写法对应的注入手法完全不同,前者只需要修改数字本身就能改变SQL语义,后者必须先“闭合引号”,再用注释符把后面的单引号“吃掉”。
我经常用一个生活化的类比来解释闭合:你把一张写好的请假条塞给门卫,门卫把你的内容直接念给校长听,那你就可以在请假条里偷偷加一句“再批一天年假”。字符型注入里,那个单引号就相当于请假条的引号边界,你的payload要在SQL语句里自己制造一个“新句子”,同时还要让原来的句子闭嘴。
2.2 数字型与字符型:闭合方式是分水岭
很多初学者把注入payload背得滚瓜烂熟,但换个场景就完全不会用,核心原因就是没搞懂“闭合”。数字型注入和字符型注入的分水岭,就在这一个引号上。
数字型注入的测试思路最快,因为你直接在数字后面拼逻辑表达式就行:
?id=1 and 1=1 // 页面正常 ?id=1 and 1=2 // 页面无结果或异常字符型注入就没这么直接。你试?name=admin and 1=1是没用的,因为SQL解析器把整段都当成一个字符串,你要先让admin后面的引号闭合掉:
?name=admin' and '1'='1这条payload拼到SQL里后就变成:
select ... from users where name = 'admin' and '1'='1'admin'先闭合了原SQL里name='admin的引号,然后and '1'='1'构造了一个恒真的表达式,最后原SQL末尾自带的那个单引号,被'1'这一个字符里的后引号顺带对应上了,整个语句语法依然成立。
用pikachu的字符型关卡测试时,我建议顺序这样做:先在输入框随便输一个正常名字,观察正常页面,再输入admin'看是否报错。报错不一定意味着注入,但报错的信息会告诉你SQL语句的拼接结构,很多情况下错误信息里会直接回显部分SQL语句,省去很多猜测。
2.3 搜索型与POST型:容易被忽略的入口
搜索框是开发中最常见的功能之一,也是最容易被写出注入点的地方。pikachu的搜索型注入关卡模拟的就是一个后台用户搜索功能,代码大概是:
select ... from member where username like '%$keyword%'这里的闭合就更麻烦一点,因为除了单引号,还有两个百分号。你输入的关键字被前后各一个%包裹,所以构造payload时不能只关单个引号,还得考虑百分号的位置。
基本的试探方法是输入一个单引号',页面很可能报错,因为SQL变成了:
select ... where username like '%'%'语法错乱,数据库直接抛异常。真正想执行注入逻辑,可以构造:
keyword=x%' or 1=1 -- '拼进SQL后变成:
select ... where username like 'x%' or 1=1 -- '%'这里x%'先闭合了搜索词,or 1=1让查询返回所有成员,--注释掉了最后的收尾符号。
POST型注入的思路与字符型类似,差别只在传参位置。pikachu的POST注入关卡是一个登录表单,用户名和密码通过POST方式提交。上手最快的办法是直接开代理抓包,用Burp Suite这类工具在请求体里修改参数,把用户名位置改成payload。你也可以不用工具,直接在浏览器的表单输入框里粘贴payload,效果是一样的,只是没法精细调整个别字符。
2.4 报错注入、盲注、堆叠注入:进阶三连
如果页面会回显SQL错误信息,报错注入就可以用。原理是让数据库执行一个会报错的表达式,并且把你要查询的数据嵌入到错误信息中。pikachu的报错注入关卡,核心就是利用updatexml这类函数,让它处理一个格式错误的XPath,同时在参数里拼上concat()结果:
1' and updatexml(1,concat(0x7e,(select database()),0x7e),1) -- -数据库会把concat()生成的字符串当作错误信息回显,你就能在页面里看到数据库名。使用0x7e(波浪号)包围数据是因为某些数据库对错误信息长度有截断,加个特殊符号方便你快速定位数据位置。
如果页面既没有报错回显,也没有任何查询结果展示,那就只能上盲注。盲注有两种常见打法:布尔盲注和时间盲注。两者的区别在于“观察对象不同”,布尔盲注观察页面真假差异,时间盲注观察响应时间差异。pikachu把这两个场景拆成了独立关卡,便于逐个练习。
堆叠注入是pikachu里一个比较进阶的关卡。它利用的是某些数据库驱动支持一次执行多条语句的特性,在原有SQL语句后面用分号隔开,再拼接一条完全独立的SQL。比如:
1'; insert into users(username,password) values('test','test') -- -堆叠注入的破坏力远大于普通注入,因为它可以执行任意SQL语句,不只是查询。但它的成功前提很多,不是每个注入点都能用,后面在实操里我会细说。
3. 实操透析:六个注入点的完整测试记录
3.1 字符型GET注入:从试探到拖库
pikachu的字符型GET注入关卡,界面是一个输入框,填入用户ID对应的名字会返回查询结果。我当时是以“正常输入→错误试探→字段探测→数据提取”四步走下来的。
正常输入1,页面返回一个用户名。接着输入1',页面报错,说明参数被引号包裹,SQL结构已经破坏。再输入1' and '1'='1,页面正常返回,输入1' and '1'='2,页面无结果,这就确认了是字符型注入。
确认注入点后,下一步用order by探测字段数量。逐步加大数字,直到页面报错,比如:
1' order by 3 -- -返回正常,说明查询结果至少有3列。改成4以后报错,字段数就是3。接下来用union select找显示位:
1' union select 1,2,3 -- -页面显示出了2和3两个数字的位置,说明这两个字段位在页面上有回显。于是把3号位替换成database(),得到当前数据库名,再替换成version()就能拿到数据库版本。
最后一步就是查表。先用information_schema查表名,查到目标表后用group_concat(column_name)查字段名,最后把用户名和密码一次性拖出来。整条链路跑下来,你会对“union查询为什么需要相同列数”“注释符为什么必须加空格或特殊符号”这些细节有直观的体会。
有一个容易忽略的点:注释符的写法。MySQL里--后面必须跟一个空格,或者写成#。pikachu的字符型关卡我用-- -会稳定生效,但用--不加空格时,SQL语句会把自己整个后面都当成注释,导致payload解析异常。这个细节在不同数据库和不同环境中差异很大,建议都试一下。
3.2 数字型注入的快速判断
pikachu的数字型注入关卡和字符型GET注入在界面上很接近,但当你在数字后面拼and 1=1的时候,页面反应会立刻给出答案。
我当时的判断顺序是:
?id=1 and 1=1 // 正常返回 ?id=1 and 1=2 // 无结果两条请求差异明显,说明注入点对“数字与逻辑表达式”的拼接是直通的,没有引号干扰。为了确认,我又试了?id=1',结果页面还是正常显示,说明单引号被当成普通字符处理了,这就进一步验证了数字型注入。
数字型的union select测试和字符型略有不同,不需要闭合和注释,直接:
?id=1 union select 1,2,3就能看到回显位置。字符型和数字型在实际信息提取阶段的payload差异不大,真正的分水岭只在闭合前期的判断。所以我一再强调,先花时间搞清楚闭合方式,比急着背payload高效得多。
3.3 POST型注入:用改包工具还是直接填表
POST型注入关卡是一个登录表单,界面看起来就是一个普通的用户名密码输入页。第一次测试时,我直接在用户名框输入了admin' and '1'='1,密码随便填,结果页面报错,说明用户名位置存在注入。
为了把payload调得更精细,我开了抓包工具,拦截POST请求后,在请求体里修改username参数。这样做的优势是你可以随时调整payload的每一个字符,而不必受浏览器表单的限制。比如测试字符型闭合时,我先把用户名改成:
username=admin' order by 3 -- -&password=123然后逐步调整order by的数值,观察页面返回状态。在order by 4的时候页面报错,确认了字段数为3,之后再走union select提取数据。
如果不用抓包工具,直接填表也能测,但有一个问题:部分浏览器或前端脚本会对输入框做编码处理,导致引号、井号等特殊字符被转义。pikachu的前端没有做这种过滤,所以两种方式都行,但为了培养真实渗透场景下的操作习惯,我还是建议动手配置一下抓包工具,以后在复杂场景里好处很明显。
3.4 搜索型注入:百分号也要参与闭合
搜索型注入关卡从界面上看就是一个普通的关键字搜索框,输入任何字符串都会在页面下方返回匹配结果。我先输入了一个x,看到正常返回。然后输入单引号',页面报错,代码层的真相是搜索条件里的%和'一起把SQL搞乱了。
接下来构造payload,我用的是:
keyword=x%' and 1=1 -- '页面正常返回搜索结果,说明这个闭合是成功的。为了进一步确认可以查询所有数据,我改成:
keyword=x%' or 1=1 -- '页面把所有成员都返回了,说明注入点确实可控。后续的字段探测和数据提取,跟字符型GET注入的流程完全一样,只是payload前缀从1'变成了x%'。这个“%”很容易被忽略,但它恰恰是搜索型注入里最关键的闭合参与者。
3.5 报错注入与时间盲注:无回显场景的两把刀
在pikachu里,报错注入关卡给了我一个很好的“错误信息即回显”演示。当我输入:
1' and updatexml(1,concat(0x7e,(select database()),0x7e),1) -- -页面直接弹出错误信息,里面包含了当前数据库名。这里的关键认知是:报错注入不是“出错了就完事”,而是刻意制造一个“能被数据库识别并回显的报错”,让错误信息变成传输数据的通道。
如果说报错注入是“利用报错内容”,那盲注就是“利用查询结果的真假”。布尔盲注关卡页面会根据SQL条件的真假显示不同内容,我用:
1' and ascii(substr(database(),1,1))>100 -- -如果页面返回正常,则说明数据库名字符串的第一个字符ASCII码大于100,再逐步缩小范围,就能把数据库名一个字符一个字符猜出来。手工猜解效率很低,我实际测试时把判断条件稍微写复杂了点,然后配合二分法逻辑,几十次请求就能确定一个字符。
时间盲注关卡的原理更简单粗暴。页面不管条件真假都显示同样的内容,唯一能观察到的就是时间差。我的测试payload是:
1' and if(ascii(substr(database(),1,1))>100,sleep(2),0) -- -如果页面响应时间明显延长,说明条件为真,否则为假。这个手测起来非常累,一次只能验证一个字符的一个范围,等两秒、再等两秒。我后来写了一个简单的脚本,把请求循环起来,逐个字符判断。脚本本身不复杂,核心就是把HTTP请求包在循环里,根据响应时间判断真假。
需要提醒的是,pikachu的时间盲注关卡用的是sleep函数,实测下来如果遇到网络波动,误差会比较大,建议在本地环境测,网络延迟基本可忽略,判断结果才准确。
4. 排查实录与防御对照
4.1 环境与注入测试中的高频问题
我在部署和测试pikachu的过程中积累了一些高频问题的排查经验,整理成表格供你直接对照:
| 症状 | 可能原因 | 处理方法 |
|---|---|---|
| 首页能打开但初始化失败 | 数据库配置错误,账号密码不匹配 | 修改inc/config.inc.php里的数据库信息,重启服务 |
| 初始化时提示数据表已存在 | 已经初始化过一次,重复执行 | 清空数据库相关表再重新初始化,或直接跳过 |
| PHP页面直接显示警告或报错 | PHP版本与老代码不兼容 | 调整PHP版本,或在配置里关闭错误显示 |
| 注入payload后页面无任何变化 | 闭合方式判断错误,参数类型搞混 | 先判断数字型还是字符型,再决定payload前缀 |
--注释无效 | 注释符后缺少空格 | 改用-- -或# |
| 页面中文乱码 | 数据库连接字符集设置不对 | 检查数据库配置里的编码设置,统一为utf8 |
测试注入时的常见失误我也列几项:只测GET参数忽略了POST参数、在search框里沿用字符型payload而忘了处理百分号、没有区分注释符的标准写法导致payload被整体注释、以及在真实站点上直接跑注入测试——这是绝对的红线,务必在授权和本地靶场环境内练习。
4.2 注入点类型判断速查表
我做了一个判断速查表,方便你在pikachu各关卡间切换时能快速定位该用什么思路:
| 输入测试 | 观察结果 | 注入类型推断 | 下一步操作 |
|---|---|---|---|
数字参数后加and 1=1 | 页面正常 | 可能是数字型注入 | 改成and 1=2验证 |
| 数字参数后加单引号 | 页面报错 | 可能字符型注入 | 尝试' and '1'='1闭合 |
| 搜索框输入单引号 | 页面报错或返回异常 | 搜索型注入可能 | 用x%' or 1=1 -- '测试 |
用户名输入admin' or '1'='1 | 页面返回异常结果 | POST型字符注入 | 走union select流程 |
输入updatexml载荷后页面回显错误详情 | 报错注入成立 | 直接读取数据 | 在concat()里换查询目标 |
| 页面真假无变化,但多条件判断响应时间有差异 | 时间盲注 | 用sleep验证 | 脚本化逐字符提取 |
这张表的核心逻辑是:先通过小成本试探确认注入类型,再决定用哪种提取数据的方式。pikachu的关卡基本覆盖了这些组合,你可以把每个测试都自己跑一遍,形成肌肉记忆。
4.3 测试边界与安全底线
用pikachu练手,最大的价值是让你在“不出事”的前提下看清SQL注入的成因与利用方式。我在测试每个关卡时,都刻意做了一件事:每次发请求之前,先问自己“这条payload拼进SQL之后长什么样”。看不懂就不发,想清楚了再动手。
这种习惯在日后做开发或做安全测试时非常重要。真有攻击者盯着某个系统打时,他不会像你练习那么温柔,会直接上自动化工具、拖数据、清日志。所以每次练习都不只是学技术,也是在建立对攻击行为的认知,知道坏人是怎么做事的,才能知道代码哪里容易出事。
对自己有更高要求的,建议把pikachu的源码打开,重点看每个关卡后台文件的SQL拼接方式。你会发现,绝大多数注入点都长得很像,核心就是“直接把输入塞进SQL字符串”。理解了这一点,再看市面上各种过滤函数、安全方案为什么会被绕过,就会有更清晰的判断。
4.4 从攻方视角看防御
练完pikachu的SQL注入关卡后,我最有感触的是防御其实没那么玄乎。最有效的方式就是预编译,把SQL语句的结构和参数分开,让数据库引擎先解析结构,再绑定参数。无论你传什么字符串进去,都只会被当成数据,不会被解析成指令。
实践里值得做好的还有几件事:
- 严格限制数据库账号权限,网站应用只用最小权限账号,即使注入成功也拿不到全部库表;
- 关闭生产环境的详细报错输出,避免把SQL语句、数据结构泄露给攻击者;
- 对整数、枚举等类型参数做白名单校验,尽量不用字符串拼接;
- 在代码层维护统一的数据库查询封装,避免到处手写SQL。
这些防御手段单拎出来都不复杂,难的是在真实项目里坚持做到。pikachu的源码提供了一个绝佳的“反面教材”,你可以对照它的写法,反过来思考自己代码应该怎么写才安全。
我在实际使用pikachu的过程中,最大的体会是:这个靶场真正让人成长的地方不是“打进去”的那一瞬间,而是“反复琢磨为什么这样写payload”的过程。同一个注入点,第一次打通可能只需要几分钟,但如果你愿意停下来,分析代码、转换闭合方式、手写一遍脚本,收获会成倍增长。最后再分享一个小技巧:pikachu每个关卡旁边都有源码提示,建议你先猜再看不看答案,实在卡住了再点开对照,这样训练出来的判断力比看十篇教程都管用。多在本机靶场里折腾,把每一个参数当成“可疑的”,把每一条报错信息当成“有价值的”,你的SQL注入思路会很快清晰起来。