1. 靶场初探:为什么从SQL注入开始?
如果你刚接触网络安全,或者想从CTF、渗透测试的实战中找点感觉,那么SQL注入(SQL Injection)绝对是你绕不开的第一个“老朋友”。它不像缓冲区溢出那样需要深厚的底层知识,也不像逻辑漏洞那样考验天马行空的思维。SQL注入的原理直白、危害巨大,且至今仍是Web应用中最常见、最危险的漏洞之一。而sqli-labs这个开源靶场,就是无数安全从业者梦开始的地方。它由印度安全研究员Audi-1搭建,通过一系列精心设计的关卡(Lessons),由浅入深地带你领略SQL注入的“十八般武艺”。
今天要聊的Less-1,就是这个庞大迷宫的第一扇门。别看它编号靠前,就以为简单。恰恰相反,Less-1是基于错误的字符型注入的经典教学案例。它模拟了一个非常常见的场景:一个使用GET方法传递参数、并且后端代码对用户输入未做任何过滤的登录查询。通关它,你不仅能理解最基本的注入原理,更能掌握一套适用于后续绝大多数关卡的核心方法论——如何观察、如何测试、如何构造Payload、如何获取数据。很多人在后续关卡卡壳,问题往往就出在对第一关的基础理解不够扎实。所以,咱们不追求速度,而是扎扎实实地把每一步的原理和操作都掰开揉碎,让你真正“通关”而不仅仅是“通过”。
2. 环境准备与靶场搭建:你的专属“黑客实验室”
工欲善其事,必先利其器。在开始“黑”之前,我们得先有个安全的、合法的环境来练手。直接在网上找现成的在线靶场当然方便,但为了学习过程的完整性和可控性,我强烈建议你在本地搭建一套。这样,你不仅可以随意测试、查看源码,还能深入数据库,亲眼看到你的注入语句是如何被执行的。
2.1 本地环境搭建方案
对于新手,最省心的方案是使用集成环境包,比如XAMPP、PHPStudy或WAMP。它们把Apache(Web服务器)、MySQL(数据库)、PHP(后端语言)打包在一起,一键安装即可。
以PHPStudy为例:
- 下载安装:去官网下载对应你操作系统(Windows)的版本,安装过程基本就是一路“下一步”。
- 启动服务:安装完成后,打开PHPStudy,你会看到界面。点击“启动”按钮,它会同时启动Apache和MySQL服务。当Apache和MySQL旁边的图标都变成绿色,就表示服务运行正常了。
- 部署靶场:去GitHub上搜索“sqli-labs”,找到Audi-1的官方仓库,下载ZIP包并解压。将解压后的整个文件夹(通常叫
sqli-labs或sqli-labs-master)复制到PHPStudy的网站根目录下。这个根目录的路径通常是phpstudy_pro/WWW/(具体路径取决于你的安装位置)。 - 访问靶场:打开浏览器,在地址栏输入
http://localhost/sqli-labs/(如果你的文件夹名不同,请相应修改)。如果一切顺利,你会看到sqli-labs的首页,点击“Setup/reset Database for labs”链接来初始化数据库。页面提示“Congratulations”等字样,就表示数据库和表结构创建成功了。
注意:如果初始化失败,最常见的原因是MySQL的密码问题。PHPStudy的MySQL默认密码可能是
root或空。你需要打开sqli-labs文件夹下的sql-connections目录,编辑db-creds.inc文件,确保里面的数据库用户名和密码与你本地MySQL的配置一致。
2.2 核心工具:你的“瑞士军刀”
有了靶场,我们还需要几件趁手的工具。对于SQL注入,浏览器和一款专业的HTTP代理工具就足够了。
浏览器(推荐Chrome或Firefox):现代浏览器的开发者工具(F12打开)是神器。我们主要用它的“网络”(Network)标签页,可以查看每个请求的详细信息,包括URL、参数、响应头、响应体。在“控制台”(Console)标签页,我们还可以快速执行一些JavaScript代码来编码或解码数据,非常方便。
Burp Suite Community(必备):这是安全测试的行业标准工具,社区版对个人学习完全免费。它的核心功能是代理拦截和重放。简单说,它能让你看到浏览器发送给服务器的所有请求,并且允许你暂停请求、修改其中的任何部分(比如参数值),然后再发送出去。这对于我们手动构造和测试复杂的注入Payload至关重要。
- 安装与配置:从PortSwigger官网下载安装。启动后,你需要将浏览器的代理设置为Burp Suite监听的地址(默认是
127.0.0.1:8080)。Burp Suite有详细的入门教程,跟着做一遍就能掌握基础用法。 - 为什么不用HackBar之类的浏览器插件?插件固然方便,但功能有限,且不利于你理解HTTP请求的本质。Burp Suite能给你更全局的视角和更强大的操控能力,是走向专业的必经之路。
- 安装与配置:从PortSwigger官网下载安装。启动后,你需要将浏览器的代理设置为Burp Suite监听的地址(默认是
3. Less-1 通关全流程深度解析
好了,实验室和工具都齐了,让我们正式进入Less-1。访问http://localhost/sqli-labs/Less-1/,你会看到一个简单的页面,上面有一个输入框,提示你输入ID作为参数。
3.1 第一步:信息收集与漏洞探测
任何一次有效的攻击都始于充分的信息收集。我们的目标是搞清楚这个页面是如何工作的。
观察正常行为:在输入框里输入数字
1,然后提交。页面显示了一个用户的ID、用户名和密码。URL变成了http://localhost/sqli-labs/Less-1/?id=1。这告诉我们几个关键信息:- 参数:使用
GET方法,参数名是id。 - 功能:看起来是根据ID从数据库中查询用户信息。
- SQL语句猜测:后端很可能执行了一条类似
SELECT * FROM users WHERE id='$id'的查询。
- 参数:使用
触发错误,确认注入点与数据库类型:这是最关键的一步。我们故意输入一些“不正常”的数据,看服务器返回什么错误。
- 输入一个单引号
',然后提交。页面返回了类似下面的错误信息:You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''1'' LIMIT 0,1' at line 1 - 分析:错误信息明确指出了是“MySQL server”。这太好了!我们不仅确认了存在SQL注入漏洞(因为我们的输入改变了SQL语法结构),还知道了后端数据库是MySQL。错误信息中
near ''1''这部分,揭示了后端代码拼接SQL语句的方式:它很可能是SELECT ... WHERE id='$id' LIMIT 0,1。我们输入'后,语句变成了SELECT ... WHERE id=''' LIMIT 0,1,多出的单引号破坏了语法。
- 输入一个单引号
判断注入类型:我们尝试输入
1' and '1'='1和1' and '1'='2。- 输入
1' and '1'='1:页面正常返回了ID为1的用户信息。因为拼接后的SQL是... WHERE id='1' and '1'='1' LIMIT 0,1,这是一个永真条件,所以查询成功。 - 输入
1' and '1'='2:页面没有返回用户信息(可能是空白或报错)。因为拼接后的SQL是... WHERE id='1' and '1'='2' LIMIT 0,1,这是一个永假条件,查询不到结果。 - 结论:这是一个基于布尔(Boolean)的真假条件注入,并且是字符型,因为参数值被单引号包裹。
- 输入
3.2 第二步:利用联合查询(UNION)获取数据结构
知道了是字符型注入,并且有错误回显,我们可以使用最强大的工具之一:UNION SELECT。它的作用是将我们自定义的查询结果,拼接到原始查询的结果后面显示出来。但使用它有两个前提:
- 原始查询与我们注入的查询,列的数目必须相同。
- 列的数据类型需要兼容。
所以,我们的首要任务是判断原始查询返回了多少列。
使用
ORDER BY测列数:ORDER BY子句用于根据列索引排序。如果指定的列索引超过了实际列数,数据库就会报错。我们利用这一点来探测。- 输入
1' order by 1--+:页面正常。--+是MySQL中的单行注释符(--后面要跟一个空格,+在URL中常被编码为空格),它注释掉了原SQL语句中后面的单引号和LIMIT,确保语法正确。语句相当于SELECT ... WHERE id='1' order by 1-- ' LIMIT 0,1。 - 输入
1' order by 2--+:页面正常。 - 输入
1' order by 3--+:页面正常。 - 输入
1' order by 4--+:页面报错(语法错误或空白)。 - 结论:原始查询返回了3 列。因为
order by 3成功而order by 4失败。
- 输入
确定显示位:知道了有3列,我们需要找出哪几列的内容会显示在网页上。这样我们才能把想要的数据“投射”到这些位置上。
- 输入
1' union select 1,2,3--+ - 观察页面。你会发现,原本显示“用户名”和“密码”的地方,变成了数字
2和3。 - 结论:第2列和第3列是显示位。这意味着我们后续可以通过
union select,将数据库名、表名、字段名等信息放在2和3的位置上,它们就会在页面中显示出来。
- 输入
3.3 第三步:逐步获取数据库信息
现在,我们可以开始系统地“翻阅”数据库了。MySQL有一个名为information_schema的系统数据库,它就像数据库的“户口本”,存储了所有其他数据库、表、列的结构信息。这是我们注入攻击的“地图”。
获取当前数据库名:
- 输入
1' union select 1, database(), 3--+ database()是MySQL的函数,返回当前连接使用的数据库名称。执行后,你会在显示位2的位置看到数据库名,通常是security。
- 输入
获取数据库中的所有表名:
- 输入:
1' union select 1, group_concat(table_name), 3 from information_schema.tables where table_schema=database()--+ - 拆解:
information_schema.tables:系统表,存放所有表的信息。table_schema=database():条件限制,只取当前数据库(security)下的表。group_concat(table_name):将查询到的所有表名合并成一个字符串,用逗号分隔。因为union select通常只返回一行,用group_concat可以一次性看到所有结果。
- 执行后,你可能会看到类似
emails,referers,uagents,users这样的结果。显然,users表最有可能存放用户凭证。
- 输入:
获取目标表(users)的所有列名:
- 输入:
1' union select 1, group_concat(column_name), 3 from information_schema.columns where table_schema=database() and table_name='users'--+ - 拆解:
information_schema.columns:系统表,存放所有列的信息。table_name='users':条件限制,只取users表的列。
- 执行后,你会看到类似
id,username,password的列名。
- 输入:
3.4 第四步:最终目标——拖取数据
拿到了数据库名、表名、列名,最后一步就是提取数据了。
- 输入:
1' union select 1, group_concat(username), group_concat(password) from users--+ - 这个查询直接从
security.users表中,将所有用户名和密码分别合并后查询出来。执行后,你会在页面看到所有用户的用户名和密码,格式类似Dumb,Angelina,Dummy,...和Dumb,I-kill-you,p@ssword,...。
至此,Less-1的通关核心步骤就全部完成了。你已经成功地利用一个简单的ID参数,逐步获取了整个数据库的关键信息。
4. 核心原理与Payload构造心法
通关步骤看似线性,但背后每一步都蕴含着对SQL语法和Web交互的深刻理解。这里我分享一些教科书上不会细讲的“心法”。
4.1 为什么单引号能导致注入?
这是最本质的问题。假设后端PHP代码是这样的:
$id = $_GET['id']; $sql = "SELECT * FROM users WHERE id='$id' LIMIT 0,1"; $result = mysqli_query($conn, $sql);当你传入id=1时,SQL语句是SELECT * FROM users WHERE id='1' LIMIT 0,1,完全正确。 当你传入id=1'时,语句变成了SELECT * FROM users WHERE id='1'' LIMIT 0,1。数据库引擎解析到第二个单引号时,认为字符串'1'结束了,但后面又多了一个单引号,它不知道这个多出来的引号该和谁配对,于是抛出语法错误。你通过输入,改变了SQL语句的原始结构,这就是注入。
4.2 注释符(--+, #)的妙用与差异
在注入中,注释符用于“吃掉”原SQL语句中我们无法控制的后半部分,比如闭合的单引号、LIMIT子句等,确保我们注入的语句语法完整。
--(双减号加空格):是SQL标准注释符。在URL中,空格需要编码为+或%20,所以常写为--+。Burp Suite里直接写--(带空格)即可。#:是MySQL的注释符。在URL中,#被称为片段标识符,浏览器不会将其发送到服务器。所以,如果你想在URL中直接使用#,需要对其进行URL编码,写成%23。例如:1' union select 1,2,3%23。
实操心得:在浏览器地址栏手动测试时,用
--+最稳妥。在Burp Suite的Repeater模块里修改请求时,直接写--(注意有空格)或#都可以,因为Burp会帮你处理编码。务必注意这个差异,很多新手在这里卡住。
4.3UNION与ORDER BY的协同与取舍
ORDER BY测列数和UNION SELECT确定显示位,是两个相辅相成的步骤。
- 为什么先
ORDER BY?因为它更“温和”。ORDER BY 10如果报错,只是说明列数不够,不会暴露我们想联合查询的意图。如果一上来就用UNION SELECT 1,2,3,4,5...去盲猜,一旦列数不对就会报错,且错误信息可能不同,在某些配置下可能更显眼。 UNION查询的数据类型问题:我们用了select 1,2,3,这里的数字是整数。如果显示位原本是字符串类型(比如用户名),MySQL会进行隐式转换,通常能正常显示。但更严谨的做法是,用null代替数字,因为null可以匹配任何数据类型。例如union select null,null,null。
4.4group_concat()与数据截断
group_concat()函数非常实用,但它有一个默认的长度限制(group_concat_max_len变量,默认1024字节)。如果某个表的数据量非常大,group_concat()的结果可能会被截断,导致你无法看到完整数据。解决方案:
- 在注入语句中临时修改这个设置:
1' union select 1, (select group_concat(username) from users), 3--+可以改为先设置长度。但更通用的方法是分次查询。 - 使用
limit子句分页查询:1' union select 1, username, password from users limit 0,1--+一次只查一行,然后通过改变limit的参数(limit 1,1,limit 2,1...)来遍历所有数据。虽然麻烦,但绝对可靠。
5. 常见问题、调试技巧与防御思考
即使跟着步骤做,你也可能会遇到各种“坑”。这里记录一些我踩过的雷和解决方法。
5.1 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 页面空白,无任何输出 | 1. 数据库查询出错,但PHP错误显示被关闭。 2. union前后查询列数不一致。3. 注释符使用错误,导致语法错误。 | 1. 打开Burp Suite,查看原始HTTP响应,尾部可能有错误信息。 2. 用 order by重新确认列数。3. 检查注释符,在Burp中尝试 --、#、--+或%23。 |
输入'后不报错 | 1. 靶场环境配置了通用错误页面,屏蔽了详细错误。 2. 可能存在过滤,但被转义或处理了。 | 1. 尝试布尔盲注测试:1' and 1=1--+与1' and 1=2--+,看页面是否不同。2. 查看网页源代码(Ctrl+U),有时错误信息藏在HTML注释里。 |
union select后数字不显示 | 显示位判断错误。可能页面内容来自多行结果,默认只显示第一行。 | 让原始查询结果为空,使得union的结果成为第一行。将ID改为一个不存在的值,如-1' union select 1,2,3--+。 |
group_concat结果不完整 | 被group_concat_max_len限制截断。 | 使用limit分页查询,或注入语句中执行set group_concat_max_len=1000000;(需支持堆叠查询)。 |
使用%23(#) 仍然失败 | 可能服务器端或中间件对URL解码有特殊处理。 | 优先使用--+。在Burp Suite里直接修改请求体,使用#或--(空格)更直接。 |
5.2 高级调试技巧:让数据库“说话”
当注入不顺利时,你需要更深入地了解到底发生了什么。
- 利用错误信息提取数据:Less-1本身就是错误回显型。但有时错误信息会更直接。例如,你可以尝试
1' and extractvalue(1, concat(0x7e, (select database()), 0x7e))--+。extractvalue()是XML函数,如果第二个参数不是合法XPath格式,它会将查询结果连同错误一起爆出来。0x7e是波浪号~的十六进制,用于分隔。 - 时间盲注作为备用方案:如果页面无论输入什么,外观都完全一样(无错误、无内容差异),那就可能是盲注。你可以用
sleep()函数测试:1' and sleep(5)--+。如果页面响应延迟了大约5秒,说明注入存在且被执行了。Less-1不是盲注,但这是重要的思维拓展。 - 查看靶场源码:这是学习的最佳途径。找到
sqli-labs/Less-1目录下的index.php文件,打开它。你会看到类似下面的代码:
亲眼看到漏洞是如何产生的,理解会深刻十倍。你还可以修改源码,比如加上$id=$_GET['id']; $sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1"; $result=mysql_query($sql);mysql_error()函数打印错误,或者尝试添加一些简单的过滤,看看你的Payload如何被拦截,从而理解防御原理。
5.3 从攻击者视角看防御
通过这次通关,你应该深刻理解了一点:防御SQL注入,核心就是不要让用户输入的数据被当作SQL代码来执行。所有防御手段都围绕这个核心。
- 预处理语句(参数化查询):这是最有效、最根本的解决方案。它让SQL语句的“结构”和“数据”分离。数据库先编译带占位符的SQL模板(如
SELECT * FROM users WHERE id=?),然后再将用户输入的id值作为纯数据绑定上去。这样,即使用户输入1' OR '1'='1,它也会被整体当作一个字符串值去查询ID字段等于这个字符串的记录,而不会破坏SQL结构。PHP中使用PDO或MySQLi扩展即可实现。 - 输入验证与过滤:对输入进行严格的“白名单”验证。比如,如果ID应该是数字,就用
intval()或is_numeric()函数强制转换为整数。对于字符串,可以定义允许的字符集。但注意,过滤是辅助手段,容易被绕过,不能单独依赖。 - 最小权限原则:连接数据库的Web应用账号,不应该拥有
DROP,CREATE,FILE等高危权限。只赋予其完成业务所必需的SELECT,INSERT,UPDATE权限。这样即使被注入,破坏力也有限。 - 关闭错误回显:像Less-1这样详细的错误信息,是攻击者的指路明灯。在生产环境中,应关闭PHP的
display_errors设置,将错误记录到日志文件,而不是展示给用户。
通关Less-1,你收获的不仅仅是一个Payload。你建立了一套完整的SQL注入测试思维模型:探测 -> 确认 -> 查列 -> 定位 -> 取信息 -> 拿数据。这套模型在后续的字符型、数字型、搜索型、盲注等关卡中,万变不离其宗。真正的挑战在于,后续关卡会加入各种过滤、转义、编码,你需要像解谜一样,思考如何绕过这些限制。而这,正是sqli-labs这个靶场最迷人的地方。它告诉你漏洞的原理,更逼迫你思考攻击的演化。当你用自己的方法绕过层层防御,最终拿到数据时,那种成就感,就是安全研究的乐趣所在。