1. 项目概述:一次经典的SQL注入实战复盘
最近在带新人做CTF(Capture The Flag)题目训练,翻到了这道来自“强网杯 2019”的经典题目——“随便注”。这名字起得挺有意思,听起来很随意,但实际考察的却是SQL注入中非常核心且基础的知识点。对于刚接触Web安全的新手来说,这道题是一个绝佳的“磨刀石”,它能帮你把SQL注入的整个攻击链条,从信息探测到最终利用,完整地走一遍。很多朋友可能觉得SQL注入是老生常谈,但真正能把每一步的原理、绕过技巧和利用手法都讲清楚、练扎实的并不多。今天,我就以这道题为例,带大家从头到尾拆解一遍,不仅告诉你“怎么做”,更重点剖析“为什么这么做”,以及在实际渗透测试中,你可能会遇到哪些变种和更复杂的防御手段。
这道题的环境是一个典型的、存在SQL注入漏洞的Web查询接口。我们的目标就是利用这个漏洞,获取数据库中的敏感信息(也就是所谓的“Flag”)。题目之所以经典,在于它没有设置过于花哨的WAF(Web应用防火墙)或过滤规则,而是聚焦于最基础的联合查询注入、堆叠注入以及MySQL数据库特性利用。通过它,我们可以清晰地理解如何手动构造Payload、如何判断注入点类型、如何一步步获取数据库结构,并最终读取数据。下面,我们就进入正题。
2. 初探靶场与注入点判断
2.1 目标界面与功能分析
通常,这类题目的前端是一个简单的输入框,比如一个搜索框或查询框,允许用户提交参数。我们假设目标URL是http://靶场地址/?inject=参数的形式。第一步永远是信息收集。我们打开页面,可能会看到一个提交查询的输入框。作为测试,我们先输入一个数字1,查看返回结果。页面返回了对应的数据记录,这很正常。
关键的第一步是探测这里是否存在SQL注入漏洞。最经典的方法是使用单引号‘来闭合SQL语句中的字符串。我们在输入框提交1‘。如果页面返回了与正常查询(比如1)不同的结果,比如报错信息、空白页或者查询逻辑异常,那么这里就可能存在注入点。在这道题中,提交1‘后,页面很可能会返回一个详细的SQL语法错误信息,例如: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’’ at line 1这个报错信息是黄金线索。它明确告诉我们两件事:第一,后端数据库是MySQL;第二,我们的输入被直接拼接到了SQL语句中,并且单引号破坏了语句结构,导致了语法错误。这初步确认了存在字符型注入漏洞。
2.2 确认注入类型与闭合方式
知道有注入点后,我们需要确定注入的类型和原SQL语句的闭合方式。常见的闭合方式有单引号‘、双引号“,有时还会包含括号()。从上面的报错信息near ‘‘1’’可以看出,我们输入的1‘在数据库中被处理成了‘1‘‘。这说明原语句大概是SELECT ... FROM ... WHERE id=‘用户输入‘这样的结构。我们输入1‘后,语句变成了SELECT ... FROM ... WHERE id=‘1‘‘,最后一个单引号成了多余的单引号,导致语法错误。
为了修复这个语法错误并让语句正确执行,同时又能插入我们自己的SQL代码,我们需要“注释掉”原语句后面的部分。在MySQL中,注释符有两种:#(URL中需编码为%23)和--(注意后面有个空格)。我们尝试构造Payload:1‘ --。这个Payload的意思是:1‘用于闭合前面的单引号,--(空格很重要)用于注释掉后面所有的原始SQL代码。提交这个Payload后,如果页面正常返回了id=1的数据,那么就证明我们成功控制了SQL语句,并且确认了闭合方式是单引号,注入类型为字符型注入。
注意:在浏览器URL中或表单提交时,空格可能会被处理。
--后面的空格有时会被忽略,为了确保万无一失,通常直接使用--+来代替。+在URL中表示空格。所以更常见的Payload是1‘ --+。
3. 信息获取与联合查询注入尝试
3.1 确定字段数量
在可以进行联合查询(UNION SELECT)的情况下,第一步是确定当前查询语句返回的字段列数。我们使用ORDER BY子句来探测。ORDER BY用于根据指定的列索引对结果进行排序。如果索引超过了实际列数,数据库就会报错。
我们依次提交Payload:
1‘ order by 1 --+(页面正常)1‘ order by 2 --+(页面正常)1‘ order by 3 --+(页面报错)
当order by 3报错时,说明当前查询结果只有2列。这是一个非常关键的结论,意味着我们后续使用UNION SELECT时,必须拼接两个字段。
3.2 尝试联合查询并遭遇过滤
知道了字段数,接下来自然是想通过联合查询来获取数据。我们构造Payload:-1‘ union select 1,2 --+。这里将id设为-1(一个不存在的值),是为了让原查询结果为空,从而确保页面显示的是我们union select出来的1和2。这两个数字是占位符,用于测试哪个字段的内容会回显在页面上。
然而,提交这个Payload后,页面返回了关键信息:return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);。这行代码是题目的核心过滤规则。它使用preg_match函数,以不区分大小写的方式(/i)检测我们的输入中是否包含以下关键词:select,update,delete,drop,insert,where以及点号.。
这意味着,我们想直接使用UNION SELECT来查询数据的常规路径被堵死了。select和union都被禁用了。同时,禁止了点号,也限制了我们使用database.table这样的形式进行跨库查询。这是一道典型的“代码层过滤”题目,需要我们思考绕过方法。
4. 堆叠注入与架构探秘
4.1 什么是堆叠注入?
当常规的联合查询被禁用后,我们就要考虑其他注入技术。这里题目留下了一个重要的“后门”:堆叠查询注入。堆叠注入是指通过分号;将多条SQL语句分隔开,并一次性提交执行。并非所有数据库或连接驱动都支持堆叠查询,但MySQL在特定配置下是支持的。幸运的是,这道题的环境支持堆叠注入。
我们可以构造Payload:1‘; show databases; --+。这个Payload的执行逻辑是:先执行原查询SELECT ... FROM ... WHERE id=‘1‘,然后分号开启一个新语句,执行SHOW DATABASES;,用于列出所有数据库名。提交后,页面除了显示id=1的查询结果,很可能还会将数据库列表也显示出来。通过这一步,我们验证了堆叠注入是可行的,并且看到了系统中有哪些数据库,比如information_schema,mysql,ctftraining, 以及一个可能存放flag的数据库。
4.2 深入数据库与表结构
既然show databases成功了,我们就可以利用MySQL的SHOW命令来绕过对SELECT关键词的过滤,一步步探查数据结构。
查看当前数据库:
1‘; show tables; --+这条命令会列出当前数据库中的所有表。假设我们看到的返回结果中有两个表:1919810931114514和words。这里注意,第一个表名是一串纯数字,这很不寻常,很可能就是存放flag的表。查看表结构:接下来需要知道每个表里有哪些列。由于
DESC 表名或SHOW COLUMNS FROM 表名内部可能也使用了SELECT(在某些版本或解释中),我们采用更直接的SHOW CREATE TABLE命令。这个命令会返回创建该表的完整SQL语句,里面就包含了列定义。- Payload for
words表:1‘; show create table words; --+ - Payload for 数字表:
1‘; show create table1919810931114514; --+(注意,纯数字的表名需要用反引号`包裹,否则会被解释为数字而语法错误)
- Payload for
假设返回信息显示:
words表结构:CREATE TABLE words (id int(10) NOT NULL, data varchar(20) NOT NULL)1919810931114514表结构:CREATE TABLE 1919810931114514 (flag varchar(100) NOT NULL)
至此,我们完全摸清了数据库结构:当前数据库下有两个表。words表有id和data两列,这很可能就是前端查询默认操作的表。而1919810931114514这个表只有一列flag,毫无疑问,目标flag就存放在这里。
5. 核心挑战:绕过SELECT过滤读取数据
现在我们面临核心难题:我们知道flag在1919810931114514表的flag列里,但过滤规则禁止使用SELECT来读取它。我们必须找到一种不使用SELECT关键词却能获取数据的方法。这里就需要一些MySQL的“骚操作”了。
5.1 思路一:使用HANDLER语句(本题可用方案)
MySQL提供了一个相对冷门但很有用的命令:HANDLER。它可以用于直接访问表的存储引擎接口,实现类似游标的方式逐行读取数据,而且语法中不包含SELECT。
使用HANDLER的基本步骤如下:
- 打开表:
HANDLER 表名 OPEN; - 读取第一行:
HANDLER 表名 READ FIRST; - 读取下一行:
HANDLER 表名 READ NEXT;(可重复执行以遍历数据) - 关闭句柄:
HANDLER 表名 CLOSE;
我们可以构造堆叠注入Payload来执行:1‘; HANDLER1919810931114514OPEN; HANDLER1919810931114514READ FIRST; --+
执行后,HANDLER ... READ FIRST语句会返回目标表的第一行数据,也就是我们梦寐以求的flag。页面会将其内容显示出来。这是解决此题最直接、最优雅的方法之一。
5.2 思路二:预编译语句配合字符串拼接(另一种强大绕过)
如果题目连HANDLER也过滤了(虽然本题没有),我们还可以祭出更高级的技巧:预编译语句。其核心思想是,将我们想要执行的SELECT语句拆分成字符串,利用PREPARE(准备)和EXECUTE(执行)来动态执行SQL,从而绕过对完整SELECT关键词的静态匹配。
整个流程如下:
- 设置变量:将查询语句的字符串形式存入一个变量。由于过滤了
select,我们可以用十六进制编码或concat函数来拼接。set @sql = concat('sel','ect flag from1919810931114514');这里用concat('sel','ect ...)巧妙地绕过了对完整单词select的匹配。 - 预编译语句:
prepare stmt from @sql;这条命令将变量@sql中的字符串准备成一个可执行的SQL语句,命名为stmt。 - 执行语句:
execute stmt;执行预编译好的语句,效果就等同于执行了SELECT flag FROM 1919810931114514。 - 释放资源:
deallocate prepare stmt;
我们可以将其组合成一个堆叠注入Payload:1‘; set @sql=concat('sel','ect flag from1919810931114514'); prepare stmt from @sql; execute stmt; --+
这个方法非常强大,因为它可以将被禁用的关键词拆解、编码或隐藏在变量中,是绕过WAF和简单正则过滤的利器。
5.3 思路三:修改表结构“偷梁换柱”(本题预期解?)
还有一种非常巧妙的思路,它不直接去读flag表,而是改变前端查询的逻辑。我们回顾一下:
- 前端查询大概率是
SELECT * FROM words WHERE id = ‘用户输入‘。 words表有id和data两列。1919810931114514表只有flag一列。
如果我们能把1919810931114514这个表的名字改成words,同时把它唯一的flag列改名为id或data,那么当前端程序执行它固有的SELECT * FROM words ...查询时,实际上查的就是我们改名后的、存放flag的表了!
具体操作步骤如下,通过堆叠注入执行:
重命名原words表:先把默认的
words表改个名,腾出位置。rename table words to any_other_name;重命名flag表为words:
rename table1919810931114514to words;修改新words表的结构:现在名为
words的表是原来的flag表,它只有一列flag。我们需要给它增加列,或者修改列名。但ALTER TABLE命令可能被过滤(虽然本题没提),且操作稍复杂。一个更简单的想法是,我们不需要修改它,只需要让前端查询能“碰到”flag就行。如果前端查询是SELECT id, data FROM words,那么因为新表没有这些列,会报错。但如果前端查询是SELECT * FROM words,且flag列的类型是字符串,那么查询1‘ or 1=1 --+可能会把所有的flag行都显示出来。 实际上,更精准的做法是修改列名。但MySQL中修改列名 (CHANGE COLUMN) 需要知道原列名和数据类型。我们已知原列名为flag。可以尝试:alter table words change flag id varchar(100);这会把flag列改名为id。但这里有个问题,原words表有两列,现在新words表只有一列(改名后的id),查询SELECT *可能还是会出错。更常见的预期解是,为flag表添加一个与words表结构一致的列。但考虑到步骤繁琐,且题目环境可能不支持多次执行,这种“偷梁换柱”的方法思路巧妙,但在实际操作中可能不如
HANDLER或预编译语句直接。不过,它体现了在SQL注入中,利用数据库本身的操作(DML和DDL)来改变应用程序上下文的高级思维。
6. 实操过程与最终Payload构造
综合以上分析,最稳妥、最快捷的解法是使用HANDLER语句。下面我们模拟完整的攻击链:
探测与确认:
- 访问目标,输入
1‘,看到MySQL报错,确认字符型注入。 - 输入
1‘ --+,页面正常,确认闭合方式与注释有效。
- 访问目标,输入
绕过联合查询:
- 输入
1‘ union select 1,2 --+,页面返回过滤规则,得知select等关键词被禁。
- 输入
利用堆叠注入探查:
- 输入
1‘; show databases; --+,查看所有数据库,确认当前库。 - 输入
1‘; show tables; --+,发现words和1919810931114514两个表。 - 输入
1‘; show create table `1919810931114514`; --+,确认该表存在flag列。
- 输入
最终读取数据:
- 构造最终Payload:
1‘; HANDLER `1919810931114514` OPEN; HANDLER `1919810931114514` READ FIRST; --+ - 提交Payload,页面成功显示
flag{th1s_1s_a_simp1e_f1ag}或类似内容。
- 构造最终Payload:
至此,Flag获取成功。整个过程中,我们绕过了对SELECT关键词的过滤,通过堆叠注入执行SHOW和HANDLER命令,完成了信息收集和数据读取。
7. 总结与实战经验延伸
这道“随便注”题目虽然基础,但涵盖的知识点非常全面:注入点判断、闭合方式确认、字段数探测、联合查询尝试、关键词过滤识别、堆叠注入利用、MySQL特有命令(SHOW,HANDLER)以及预编译语句的绕过思路。它完美地展示了当一条路(UNION SELECT)被堵死时,一名渗透测试人员应该如何灵活运用已有的知识库,寻找新的突破口。
在实际的渗透测试或CTF比赛中,以下几点经验尤为重要:
- 信息收集是基石:不要一上来就想着用自动化工具梭哈。手动测试,仔细阅读每一次的页面回显和报错信息。像本题中MySQL的详细报错,就是最宝贵的线索。
- 理解过滤逻辑:看到过滤规则(如
preg_match)不要慌,仔细分析它过滤了哪些关键词、字符。思考这些过滤是否可以绕过?是大小写绕过、双写绕过、编码绕过,还是像本题一样,寻找功能等效但关键词不同的替代命令(如用SHOW代替部分SELECT功能,用HANDLER代替SELECT读取数据)? - 善用数据库特性:不同的数据库(MySQL、PostgreSQL、SQL Server、Oracle)有大量特有的系统表、内置函数和命令。熟练掌握这些特性,往往能在关键时刻出奇制胜。比如MySQL的
information_schema库、SHOW命令、HANDLER命令、PREPARE/EXECUTE语句等。 - 堆叠注入的利用条件:堆叠注入并非万能,它取决于Web应用使用的数据库连接驱动(如PHP中的
mysqli_multi_query函数支持,而mysqli_query或PDO::query默认不支持)。在实战中,如果发现分号;后提交的语句被执行了,就要立刻想到堆叠注入的可能性。 - 工具与手工结合:Sqlmap等自动化工具很强大,但在面对复杂过滤或非常规注入点时,手工测试和构造Payload的能力不可或缺。这道题就是典型的手工优于自动化工具的场景,因为工具可能无法自动识别并利用
HANDLER这种非标准的注入方式。
最后,这道题也提醒我们开发人员,防御SQL注入绝不能仅仅依赖简单的关键词黑名单。参数化查询(预编译语句)才是从根本上解决注入问题的黄金标准。同时,要遵循最小权限原则,数据库用户不应拥有执行DROP,RENAME,HANDLER等高危命令的权限。安全是一个攻防对抗、不断演进的过程,只有深入理解攻击者的思路,才能更好地构筑我们的防线。