1. 从一道经典CTF题看SQL注入的攻防博弈
最近在复盘一些经典的网络安全竞赛题目,发现“[极客大挑战 2019]easysql”这道题被反复提及,甚至衍生出“[suctf 2019]easysql”等多个变种。题目名字叫“easysql”,听起来简单,但里面蕴含的过滤绕过思路和堆叠注入技巧,对于想深入理解SQL注入防御与攻击的同学来说,绝对是一块绝佳的“磨刀石”。我花了些时间,把这道题的解题思路、背后的原理,以及从防御角度的思考,重新梳理了一遍。无论你是刚接触Web安全的新手,还是想巩固基础的老兵,相信这篇从实战出发的拆解都能给你带来一些启发。
这道题的核心场景是一个典型的登录/查询界面,你需要通过注入手段获取到隐藏的flag信息。它之所以经典,是因为它没有使用复杂的编码或冷门的数据库特性,而是聚焦于最基础的过滤绕过和**堆叠注入(Stacked Queries)**的利用。很多新手在掌握了union select之后,遇到过滤就束手无策,这道题正好补上了这一课。接下来,我会带你一步步拆解它,不仅告诉你“怎么做”,更重点分析“为什么能这么做”以及“如何防范”。
2. 靶场环境分析与初步侦察
2.1 界面功能与预期目标
通常,这类题目的前端是一个简单的输入框,可能伴有“登录”或“查询”按钮。我们的首要任务是理解应用程序在做什么。通过输入一些测试数据,比如一个单引号‘,观察返回的错误信息或页面行为变化,是判断是否存在SQL注入漏洞的第一步。
在“easysql”中,常见的初步测试会发现,输入1或admin等可能返回“登录成功”或一些查询结果,而输入1‘则可能导致页面报错或返回异常。这一步的目标不是直接拿到flag,而是确认注入点是否存在以及后端可能使用的数据库类型(如MySQL、SQL Server等)。从题目名称和常见出题环境推断,后端数据库极大概率是MySQL。
2.2 关键过滤机制探测
题目名为“easysql”,但往往暗藏玄机。经过初步测试,你会发现一些常见的注入关键词被过滤了。例如,尝试输入union select,页面可能返回空、报错或直接提示“非法输入”。我们需要系统地探测哪些字符或单词被禁止。
一个有效的方法是使用增量探测法:
- 测试单关键字:分别提交
union、select、from、where、or、and等。 - 测试符号:测试单引号
‘、双引号“、注释符--、#、/*等。 - 测试组合:测试
union select作为一个整体是否被过滤。
在我的测试中,发现union、select、from、where、or、and、#、--、/*等常见注入符号和关键字都被后端WAF(Web应用防火墙)或简单字符串替换函数拦截了。这看起来像是设置了一道“铜墙铁壁”。这种过滤通常是通过str_replace()、preg_replace()等函数,将黑名单中的字符替换为空字符串或直接阻断请求。
注意:这种基于黑名单的过滤方式存在固有缺陷。攻击者可以利用双写绕过、大小写绕过、编码绕过或利用未被过滤的语法来突破。例如,如果过滤程序只是简单地将
union替换为空,那么输入uniunionon,在过滤掉中间的union后,剩下的字符恰好能拼接成新的union。
3. 核心注入技术:堆叠注入原理与利用
当union select这条“康庄大道”被阻断后,我们就需要寻找“旁门左道”。这道题引导我们走向的就是堆叠注入(Stacked Queries)。
3.1 什么是堆叠注入?
堆叠注入,顾名思义,就是一次性执行多条SQL语句。在MySQL中,语句之间用分号;分隔。例如:
SELECT * FROM users WHERE id=1; DROP TABLE users;如果Web应用使用了支持多语句执行的数据库连接方法(如PHP的mysqli_multi_query()),那么上述输入就会先执行查询,再执行删表操作,这非常危险。
与union select注入相比,堆叠注入的优势在于:
- 更强大的操作能力:可以执行任何SQL语句,包括增删改查(DML)、数据定义(DDL)甚至控制命令。
- 可能绕过
union过滤:当union和select被过滤时,堆叠注入提供了一条替代路径。 - 灵活性更高:可以分步骤进行,例如先查库名,再查表名,最后查数据。
3.2 本题中的堆叠注入突破口
既然union、select等被过滤,我们如何构造堆叠注入的语句呢?关键在于找到一个未被过滤且能用于数据查询的替代命令。
在MySQL中,除了SELECT,还有SHOW和HANDLER等命令可以用于获取信息。经过测试,我们发现题目环境没有过滤show这个关键字。这就是我们的突破口。
利用SHOW命令进行信息收集:
SHOW DATABASES;:列出所有数据库。SHOW TABLES;:列出当前数据库中的所有表。SHOW COLUMNS FROM table_name;或DESC table_name;:列出指定表的所有列。
因此,我们可以构造这样的注入payload:1; show databases;。如果注入成功,页面可能会返回数据库列表,其中通常包含information_schema、mysql、performance_schema以及题目自定义的数据库(如geek、ctf等)。
4. 步步为营:完整利用链实战拆解
知道了原理,我们来还原完整的攻击链条。假设我们面对的URL是http://target.com/index.php?id=1,注入点在id参数。
4.1 第一步:确认堆叠注入可行性
提交payload:1; select 1;由于select被过滤,这个payload会失败。但我们换用show: 提交payload:1; show databases;
观察结果:如果页面正常返回,并且内容中出现了数据库列表(可能以数组、列表或纯文本形式展示),那么不仅证实了注入存在,还确认了堆叠注入可行,且show命令未被过滤。这是关键的第一步。
4.2 第二步:获取当前数据库与表信息
获取当前数据库名:在堆叠注入中,获取当前数据库名有时需要技巧。一种方法是利用
database()函数,但select被过滤。我们可以先通过show databases猜测,或者利用后续查表时的上下文。更直接的方法是,如果题目设计是单数据库,那么show tables列出的表就在当前库下。 提交payload:1; show tables;假设返回结果中包含flag、users、geek等表名。我们的目标很可能是flag表。获取目标表结构:知道了表名,我们需要知道列名。 提交payload:
1; show columns from flag;或者1; desc flag;返回结果会显示flag表有哪些列及其数据类型。假设我们看到一列名为flag,类型为varchar(100)。那么目标就是读取这一列的数据。
4.3 第三步:绕过过滤读取数据——核心技巧
最大的挑战来了:select被过滤,我们如何从flag表的flag列中读取数据?show命令无法直接查询特定数据。
这里就需要用到MySQL中一个不太常用但非常有用的命令:HANDLER。
HANDLER ... OPEN:打开一个表句柄。HANDLER ... READ FIRST/NEXT:读取一行数据。HANDLER ... CLOSE:关闭句柄。
构造读取flag的payload:
1; handler `flag` open; handler `flag` read first; handler `flag` close;让我们拆解这个payload:
1;:原查询,用于通过前端校验,可能返回一个正常结果。handler `flag` open;:打开名为flag的表。注意,表名如果是关键字或含有特殊字符,最好用反引号包裹。handler `flag` read first;:读取表的第一行数据。如果flag有多行,可以多次使用read next。handler `flag` close;:关闭句柄,释放资源。
提交这个payload后,页面很可能会在某个位置(可能是原查询结果下方,也可能是报错信息中)直接输出flag字段的内容。这是因为handler read操作的结果集会被直接返回。
实操心得:
HANDLER命令是绕过SELECT过滤的利器,但它有一些限制。它提供对表存储引擎接口的直接访问,比SELECT更快,但功能也相对原始。在一些严格过滤select甚至from的CTF题或真实场景中,这个方法往往能出奇制胜。记得在表名可能引起歧义时使用反引号。
4.4 第四步:Payload的变形与优化
实际测试中,可能需要根据回显位置调整payload。例如,如果页面只显示第一条查询的结果,我们可以尝试将原查询设置为永假,让页面只显示我们注入语句的结果。
- 原payload:
1; handler flag open; read first; close; - 优化payload:
-1‘ or 1=1; handler flag open; read first; close; --+(注意:这里假设单引号‘和注释符--未被过滤,仅作思路展示。本题中它们很可能被过滤,所以我们的核心是分号堆叠)。
如果handler也被过滤了(虽然本题没有),我们还有什么后招?理论上,还可以尝试:
- 使用
PREPARE语句(预编译语句):通过字符串拼接和EXECUTE来动态执行SQL,但构造起来更复杂,且可能依赖其他未被过滤的函数。 - 利用
LOAD_FILE()或INTO OUTFILE:如果知道绝对路径且有写权限,可以尝试读取服务器文件或将查询结果写入文件再访问,但这通常需要更高的权限和更宽松的配置。
对于本题,handler命令已经足够。
5. 防御视角:从攻击中学习如何加固
作为开发者或安全工程师,我们更应该从这道题中学到如何避免自己的系统出现类似问题。攻击手段是标,防御体系才是本。
5.1 黑名单过滤为何总是失效?
本题模拟的正是基于黑名单的过滤机制。它存在几个致命弱点:
- 覆盖不全:无法穷尽所有危险的SQL关键字和变形(如
SELSELECTECT、UnIoN、/**/注释绕过)。 - 上下文无关:简单替换可能破坏合法数据(例如,用户昵称恰好包含“union”这个词)。
- 容易被绕过:如前所述,双写、大小写、等价替换(如
||代替or)、使用冷门命令(如handler)都能轻松突破。
结论:绝对不要依赖黑名单过滤作为主要的SQL注入防御手段。它至多只能作为一道辅助的、浅层的防线。
5.2 有效的防御方案推荐
使用参数化查询(预编译语句):这是防止SQL注入的黄金标准。无论是PHP的PDO、Python的
sqlite3或MySQLdb,还是Java的PreparedStatement,其原理都是将SQL语句的结构(模板)与用户输入的数据分离。数据库先编译语句结构,再将输入的数据当作纯参数处理,从根本上杜绝了数据被解释为代码的可能。// PHP PDO 示例(正确做法) $stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id"); $stmt->execute(['id' => $user_input]);使用安全的ORM框架:成熟的ORM(对象关系映射)框架,如Laravel的Eloquent、Django的ORM、SQLAlchemy等,在内部通常使用参数化查询,能极大降低手写SQL出错的风险。
实施最小权限原则:为数据库连接账户分配仅能满足应用需求的最小权限。例如,一个只用于查询的页面,其数据库连接账号不应该拥有
DROP、UPDATE、CREATE等权限。这样即使发生注入,危害也被限制在可控范围内。关闭多语句执行:在数据库连接配置中,禁用多语句查询功能。例如,在PHP的mysqli中,避免使用
mysqli_multi_query(),而使用mysqli_query()。在连接字符串中,可以设置参数禁止多语句(如MySQL的mysqli_real_connect()中的MYSQLI_OPT_MULTI_STATEMENTS选项)。严格的输入验证与输出编码:
- 输入验证:根据业务逻辑,对输入进行严格类型、格式、长度检查(例如,id必须是数字,就用
intval()转换)。 - 输出编码:将所有动态输出到HTML的内容进行适当的编码(如HTML实体编码),防止XSS等二次攻击。
- 输入验证:根据业务逻辑,对输入进行严格类型、格式、长度检查(例如,id必须是数字,就用
5.3 针对本题漏洞的修复代码示例
假设漏洞代码是经典的字符串拼接:
// 漏洞代码(错误示范) $id = $_GET['id']; $sql = "SELECT * FROM articles WHERE id = " . $id; $result = mysqli_query($conn, $sql);修复方案1:参数化查询
// 修复代码:使用mysqli预处理语句 $stmt = $conn->prepare("SELECT * FROM articles WHERE id = ?"); $stmt->bind_param("i", $_GET['id']); // “i”表示整数类型 $stmt->execute(); $result = $stmt->get_result();修复方案2:强制类型转换 + 禁用多语句(辅助)
// 如果因历史原因无法大改,至少做如下加固 $id = intval($_GET['id']); // 强制转为整数,非数字输入会变为0 $conn->set_option(MYSQLI_OPT_MULTI_STATEMENTS, false); // 禁用多语句执行 $sql = "SELECT * FROM articles WHERE id = " . $id; $result = $conn->query($sql); // 使用query而非multi_query即使这样,也远不如参数化查询安全,因为复杂字符串过滤仍可能出错。
6. 常见问题与排查技巧实录
在实际解题或测试过程中,你可能会遇到以下问题:
问题1:Payload提交后页面空白或报错“Query failed”。
- 排查思路:
- 检查分号
;后是否有空格?在某些解析环境下,1;show和1; show可能不同,后者更通用。 - 表名或列名是否使用了MySQL保留字?尝试用反引号包裹,如 `flag`。
handler命令的语法是否正确?顺序必须是OPEN->READ->CLOSE。- 数据库连接是否支持堆叠注入?虽然题目设计支持,但某些配置或中间件可能已禁用。可以尝试
1; select 1;#(如果select和#没被过滤)来二次确认。
- 检查分号
问题2:看到了数据库名和表名,但用handler读不出数据。
- 排查思路:
- 确认表名和列名完全正确。大小写是否敏感?仔细查看
show columns的输出。 - 数据是否不在第一行?尝试
read next多执行几次。 - 回显位置可能不在主页面。查看网页源代码(Ctrl+U),flag可能藏在HTML注释、响应头(Header)或某个JSON字段里。
- 尝试使用
handler ... read first limit 1;的变体,但注意handler语法本身不支持limit,所以可能需要循环read next。
- 确认表名和列名完全正确。大小写是否敏感?仔细查看
问题3:题目变种,过滤了show和handler。
- 排查思路:这加大了难度,但思路需拓宽。
- 时间盲注:如果页面有布尔或时间回显差异,可以尝试用
sleep()函数进行时间盲注,虽然select被过滤,但benchmark()或复杂的数学运算可能未被过滤,可用于制造时间延迟。 - 错误注入:尝试触发数据库报错,让错误信息中包含数据。例如,利用
exp()、updatexml()、extractvalue()等函数,但它们的参数中往往也需要select。 - 二次注入或间接攻击:寻找其他可能存在注入且过滤较弱的参数,或者利用已有信息(如已知的数据库名、表名)结合文件操作(如果权限极高)等。
- 重新审视过滤规则:是否真的过滤了所有字母组合?是否存在正则表达式缺陷?例如,过滤
union和select,但没过滤UNION和SELECT(大小写绕过),或者过滤了union select这个整体,但分开写union all select却能通过。
- 时间盲注:如果页面有布尔或时间回显差异,可以尝试用
问题4:在真实环境中测试堆叠注入不成功。
- 重要提示:在未经授权的真实网站进行任何渗透测试都是非法且不道德的。本文所有技术仅用于CTF竞赛、授权测试或自身系统加固学习。
- 技术原因:绝大多数成熟的Web框架和安全的编码实践都已禁用多语句查询。
mysqli_query()默认不支持多语句,PDO默认也可以通过设置PDO::MYSQL_ATTR_MULTI_STATEMENTS为false来禁用。因此,堆叠注入在真实Web应用中成功利用的条件比较苛刻,多见于老旧系统或开发者安全意识薄弱的自研代码中。
这道“[极客大挑战 2019]easysql”就像一把钥匙,为我们打开了SQL注入中过滤绕过和堆叠注入这两扇门。它告诉我们,安全防御不能停留在简单的字符串匹配层面,攻击者的思维总是活跃的。对于学习者,掌握handler、prepare等“非主流”命令的利用,能极大丰富你的渗透测试工具箱。对于建设者,则要牢记“参数化查询”这一铁律,并构建纵深防御体系。