SQL注入实战:绕过黑名单过滤的堆叠注入与预处理语句利用
1. 从“[强网杯 2019]随便注”说起一次经典的SQL注入实战复盘几年前在网络安全竞赛圈子里“强网杯”是一个响当当的名字。它不仅是国内顶尖的CTF赛事更是一个巨大的“漏洞博物馆”和“攻防演武场”。其中2019年的一道名为“随便注”的题目以其看似简单实则内涵丰富的特性成为了无数安全爱好者入门Web安全、理解SQL注入的“必修课”。即使放到今天这道题所蕴含的绕过技巧、堆叠注入原理以及最终的解题思路依然具有极高的教学和实战价值。它绝不仅仅是一道“解出来就完事”的题目而是理解现代Web应用安全中当常规注入手段失效时攻击者如何“另辟蹊径”的绝佳案例。如果你正在学习网络安全或者对SQL注入的理解还停留在‘ or 11 --的阶段那么跟着我一起彻底拆解这道“随便注”你收获的将是一整套面对复杂过滤时的突破性思维。这道题的核心场景非常经典给你一个存在SQL注入漏洞的输入点但系统对常见的注入关键字如select,union,where等进行了严格的过滤。你的任务就是在这种“戴着镣铐跳舞”的限制下依然能够从数据库中提取出敏感信息即flag。这模拟了真实环境中开发者意识到SQL注入风险后试图通过黑名单过滤进行防护但防护措施并不完备的情况。接下来我将从环境搭建、逐步测试、原理深挖到最终利用完整还原这道题的解题过程并分享其中蕴含的、可以举一反三的安全经验。2. 靶场环境搭建与初步信息收集2.1 题目环境复现与核心接口分析要深入理解最好的方式就是亲手搭建。我们可以使用Docker快速构建一个与当年比赛环境近似的靶场。这里假设后端使用MySQL数据库有一个简单的查询页面。# Dockerfile 示例 (简化版) FROM php:7.4-apache RUN docker-php-ext-install mysqli docker-php-ext-enable mysqli COPY src/ /var/www/html/在src/index.php中模拟题目的核心逻辑?php session_start(); $db new mysqli(localhost, root, password, ctf); if ($db-connect_error) { die(Connection failed: . $db-connect_error); } $input $_GET[inject] ?? ; // 模拟黑名单过滤 $filter /select|union|where|and|or|sleep|benchmark|substr|mid|ascii|ord|if|case|regexp|like|rlike|xor|not|limit|procedure|rename|alter|drop|delete|update|insert|into|outfile|dumpfile|load_file|#|--|\/\\*.*\\*\/|\\/|\\*/i; if (preg_match($filter, $input)) { die(Hacker! Filtered.); } $sql SELECT * FROM 1919810931114514 WHERE id$input; $result $db-query($sql); if ($result $result-num_rows 0) { while($row $result-fetch_assoc()) { echo id: . $row[id]. - data: . $row[data]. br; } } else { echo 0 results or error.; } // 关键点这里使用了multi_query允许堆叠查询 // $db-multi_query($sql); // 实际题目可能用此方式 $db-close(); ?注意实际比赛环境可能更复杂但核心是两点1. 对select等关键词过滤2. 存在堆叠查询Stacked Queries能力。我们搭建的环境用于理解原理过滤规则可能不完全相同。访问页面你会看到一个简单的输入框。输入1‘数字1加一个单引号进行最基础的测试如果返回SQL语法错误就初步证实了注入点的存在。这是我们的第一步确认注入点与闭合方式。通过尝试1‘ --、1‘ #、1‘ or ‘1‘‘1等常见payload我们发现输入1‘报错而1‘ --返回正常说明注入点存在且为字符型单引号闭合。2.2 绕过过滤的初步尝试与错误信息利用当确认注入点后我们本能地会尝试union select来联合查询。但输入1‘ union select 1,2 --后页面直接返回“Hacker! Filtered.”证实了union和select都在黑名单中。常规的联合注入、布尔盲注、时间盲注依赖if,sleep,case等的路径基本被堵死。此时一个重要的习惯是仔细观察错误信息。在有些配置不当的环境中SQL错误会详细回显到页面上。我们可以尝试触发一个无法被黑名单过滤的语法错误例如输入1‘;分号。如果后端使用multi_query()允许执行多条SQL语句这个分号可能会被解析为语句结束符从而触发一个关于后续空语句的语法错误。错误信息有时会暴露出数据库类型MySQL、甚至部分查询结构这为我们后续的构造提供了线索。即使没有详细回显1‘;如果执行成功页面返回正常也强烈暗示了堆叠注入的可能性——我们可以执行多条SQL语句。3. 核心技术原理堆叠注入与预处理语句3.1 堆叠注入的本质与风险堆叠注入Stacked Queries Injection是SQL注入的一种高级形式。它的原理是利用Web应用与数据库交互时可能允许一次性提交多条SQL语句每条语句用分号;分隔。当攻击者能够控制输入并插入分号时他就可以在原有查询之后“堆叠”上自己精心构造的、全新的SQL命令。例如原本的查询是SELECT * FROM users WHERE id用户输入;如果用户输入是1‘; DROP TABLE users --最终的查询就会变成SELECT * FROM users WHERE id1; DROP TABLE users -- ;这相当于先后执行了两条语句先执行一个可能无结果的查询然后直接删除users表危害性极大。并非所有数据库接口都支持堆叠查询。在PHP中常用的mysqli_query()默认不支持但mysqli_multi_query()支持。Java的JDBC、Python的PyMySQL等驱动其默认配置也可能允许或不允许。这道题的环境之所以能被利用正是因为后端使用了支持堆叠查询的数据库操作方式。3.2 当SELECT被禁巧用预处理语句“曲线救国”黑名单过滤了select我们无法直接读取数据。但堆叠注入给了我们执行任意SQL的能力除了select我们还能执行show,set,prepare,execute,alter,rename等未被过滤的命令。解题的关键思路就变成了如何在不直接使用select关键字的情况下执行一个查询操作这里就用到了MySQL的预处理语句机制。预处理语句通常用于防御SQL注入它将SQL语句的结构与数据分离。但 ironically我们在这里用它来攻击。其核心步骤是PREPARE从一个字符串准备一个SQL语句模板并赋予其一个名称。EXECUTE执行这个准备好的语句。DEALLOCATE PREPARE释放预处理语句。例如正常的预处理用法是PREPARE stmt FROM SELECT * FROM users WHERE id?; SET id 1; EXECUTE stmt USING id;关键在于PREPARE语句的源是一个字符串。我们可以通过字符串拼接、十六进制编码等方式构造出包含select关键词的字符串然后PREPARE并EXECUTE它。因为黑名单过滤的是直接的SQL关键词而我们在字符串中传递的select只是一个普通的字符数据通常不会被过滤机制检测到。4. 解题实战步步为营获取Flag4.1 第一步侦察数据库结构既然可以堆叠查询我们首先需要摸清数据库里有什么。使用show命令是绝佳选择它通常不在黑名单内。查看所有数据库输入1‘; show databases; --返回结果中除了information_schema,mysql,test等系统库很可能会看到一个与题目相关的数据库名比如ctf或web。查看当前数据库的所有表假设数据库名为ctf输入1‘; use ctf; show tables; --或者直接1‘; show tables from ctf; --返回结果可能会显示两个表words和1919810931114514。这个数字串表名非常醒目很可能就是存放flag的表。查看表结构输入1‘; describe1919810931114514; --(注意数字表名需要用反引号包裹) 或者1‘; show columns from1919810931114514; --返回结果可能显示有两个字段flag(varchar) 和id(int)。同时查看words表结构可能发现它有id和data字段并且前端页面正常查询的就是这个表。至此我们弄清了局面数据存在于1919810931114514表的flag字段中但前端查询的代码固定从words表取数据。我们需要想办法让查询words表的语句实际去查询1919810931114514表。4.2 第二步两种经典的绕过方案方案一修改表结构与名称ALTER RENAME这是本题最巧妙的解法之一。思路是将存放flag的表1919810931114514改名为words或其它名字。将原来的words表改名为其它名字。修改新的words表即原flag表的结构使其列名与前端查询代码期望的列名id,data匹配或者通过修改查询逻辑来适配。具体Payload构造如下1‘; rename table words to temp; rename table 1919810931114514 to words; alter table words change flag data varchar(100); --执行这条语句后words表变成了原flag表。其flag列被改名为data。前端代码执行SELECT * FROM words WHERE id‘$input‘时实际上查询的就是我们重命名并修改后的表并且data列的内容即flag会被直接输出到页面。方案二利用预处理语句执行SELECT如果我们不想改动表结构可以采用更“文雅”的预处理方式。首先将我们想要执行的select语句进行十六进制编码避免在传输中被过滤。select flag from 1919810931114514的十六进制编码是0x73656c65637420666c61672066726f6d2031393139383130393331313134353134。构造堆叠查询Payload1‘; use ctf; SET sql 0x73656c65637420666c61672066726f6d2031393139383130393331313134353134; PREPARE stmt FROM sql; EXECUTE stmt; --这个Payload做了以下几件事切换到目标数据库。设置一个用户变量sql其值为我们编码后的select语句的十六进制形式。PREPARE从变量sql中准备一个语句命名为stmt。由于sql的内容是作为数据传递的绕过了关键词过滤。EXECUTE stmt执行这个准备好的语句从而查询出flag。4.3 第三步执行与获取结果将上述任一Payload输入到题目提交框中。如果使用方案一页面在提交后很可能只需要再输入一个简单的查询条件比如1‘ or 11 --flag就会作为data字段的内容显示出来。如果使用方案二执行后flag可能会直接回显在页面上或者需要结合UNION但UNION被过滤了所以此方案通常需要能直接输出结果或通过错误信息带出。在本题的经典环境中方案一更为直接可靠。5. 防御视角与实战经验总结5.1 从攻击到防御如何避免“随便注”这道题完美展示了黑名单过滤的苍白无力。作为开发者防御SQL注入必须遵循以下原则使用参数化查询预编译语句这是根治SQL注入的银弹。无论是PHP的PDO、Python的SQLAlchemy、Java的MyBatis还是各种ORM框架其核心都是将用户输入始终作为参数传递而非SQL语句的一部分。即使使用预处理语句也必须采用参数绑定的方式而不是像我们攻击时那样动态拼接SQL字符串。最小权限原则给Web应用使用的数据库账户分配最小的必要权限。禁止其执行DROP,ALTER,RENAME,PREPARE,EXECUTE等高风险命令。这样即使发生注入攻击者的破坏力也有限。禁用堆叠查询在大多数业务场景下Web应用不需要执行多条SQL语句的能力。应在数据库驱动层或框架层禁用此功能例如在PHP中使用mysqli_query而非mysqli_multi_query。输入验证与转义在参数化查询的基础上对输入进行严格的类型、格式、长度验证。对于确实需要拼接的场景使用数据库驱动提供的专用转义函数如mysqli_real_escape_string但切记这仅是辅助手段不能替代参数化查询。避免动态表名/列名如果业务必须使用动态表名或列名应使用白名单机制只允许预定义的、安全的标识符。5.2 给安全研究者的实操心得信息收集是灵魂在开始“注”之前一定要用尽一切方法收集信息。show,desc,select version,select database()甚至利用错误回显每一点信息都可能成为突破的关键。在这道题里知道表名和列名是成功的一半。思维不能僵化当select被禁不要立刻放弃。SQL语言很丰富show可以查信息prepare/execute可以变相执行查询handler语句MySQL也可以逐行读取表数据。多了解数据库的特性。善用编码与变形十六进制编码、CHAR()函数、Base64编码需数据库支持函数解码等都是绕过字符串过滤的利器。例如select可以用CHAR(115,101,108,101,99,116)表示。本地搭建测试环境像今天这样将遇到的经典漏洞在本地复现是提升技术最有效的方式。你可以随意修改过滤规则尝试不同的绕过方法加深理解。理解漏洞根源每解一道题都要问自己开发者哪里写错了是用了字符串拼接还是过滤逻辑有缺陷还是权限配置过大从防御角度思考能让你对漏洞的理解提升一个层次。这道“[强网杯 2019]随便注”之所以经典就是因为它逼迫你跳出union select的舒适区去理解更底层的数据库交互机制和SQL语言的灵活性。它告诉我们在安全的世界里没有绝对的黑名单只有未被发现的攻击路径。掌握这种在限制中寻找突破的思维才是安全工程师真正的价值所在。