从CTF实战解析SQL注入:过滤绕过与堆叠注入攻防
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、#、--、/*等常见注入符号和关键字都被后端WAFWeb应用防火墙或简单字符串替换函数拦截了。这看起来像是设置了一道“铜墙铁壁”。这种过滤通常是通过str_replace()、preg_replace()等函数将黑名单中的字符替换为空字符串或直接阻断请求。注意这种基于黑名单的过滤方式存在固有缺陷。攻击者可以利用双写绕过、大小写绕过、编码绕过或利用未被过滤的语法来突破。例如如果过滤程序只是简单地将union替换为空那么输入uniunionon在过滤掉中间的union后剩下的字符恰好能拼接成新的union。3. 核心注入技术堆叠注入原理与利用当union select这条“康庄大道”被阻断后我们就需要寻找“旁门左道”。这道题引导我们走向的就是堆叠注入Stacked Queries。3.1 什么是堆叠注入堆叠注入顾名思义就是一次性执行多条SQL语句。在MySQL中语句之间用分号;分隔。例如SELECT * FROM users WHERE id1; 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;列出指定表的所有列。因此我们可以构造这样的注入payload1; show databases;。如果注入成功页面可能会返回数据库列表其中通常包含information_schema、mysql、performance_schema以及题目自定义的数据库如geek、ctf等。4. 步步为营完整利用链实战拆解知道了原理我们来还原完整的攻击链条。假设我们面对的URL是http://target.com/index.php?id1注入点在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的payload1; handler flag open; handler flag read first; handler flag close;让我们拆解这个payload1;原查询用于通过前端校验可能返回一个正常结果。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 11; 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等二次攻击。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. 常见问题与排查技巧实录在实际解题或测试过程中你可能会遇到以下问题问题1Payload提交后页面空白或报错“Query failed”。排查思路检查分号;后是否有空格在某些解析环境下1;show和1; show可能不同后者更通用。表名或列名是否使用了MySQL保留字尝试用反引号包裹如 flag。handler命令的语法是否正确顺序必须是OPEN-READ-CLOSE。数据库连接是否支持堆叠注入虽然题目设计支持但某些配置或中间件可能已禁用。可以尝试1; select 1;#如果select和#没被过滤来二次确认。问题2看到了数据库名和表名但用handler读不出数据。排查思路确认表名和列名完全正确。大小写是否敏感仔细查看show columns的输出。数据是否不在第一行尝试read next多执行几次。回显位置可能不在主页面。查看网页源代码CtrlUflag可能藏在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等“非主流”命令的利用能极大丰富你的渗透测试工具箱。对于建设者则要牢记“参数化查询”这一铁律并构建纵深防御体系。