news 2026/6/16 6:14:56

从CTF题BabySQli剖析SQL注入攻防:UNION查询与MD5特性利用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从CTF题BabySQli剖析SQL注入攻防:UNION查询与MD5特性利用

1. 项目概述:从一道CTF题看SQL注入的攻防博弈

最近在复盘一些经典的网络安全挑战题,又看到了这道名为“BabySQli”的题目。它虽然被归为“Baby”级别,但其中蕴含的SQL注入技巧和绕过思路,却非常典型,足以让许多刚入门Web安全的朋友们好好琢磨一番。这道题模拟了一个简单的登录场景,表面上看只是一个判断用户名密码对错的表单,但背后却是一个考察开发者如何安全处理用户输入,以及攻击者如何利用逻辑漏洞的绝佳案例。如果你正在学习网络安全,或者对“为什么我的网站会被黑”感到好奇,那么通过拆解这道题,你不仅能学会一种攻击手法,更能深刻理解防御的核心在哪里。

简单来说,这道题的核心就是“SQL注入”。攻击者通过在登录框的用户名或密码输入中,插入精心构造的SQL代码,欺骗后端数据库执行非预期的查询,从而绕过登录验证,甚至窃取、篡改数据。BabySQli这道题,就像它的名字一样,是一个简化但要素齐全的“婴儿级”战场,非常适合我们用来剖析SQL注入从信息探测到最终利用的完整链条。接下来,我会带你一步步重现解题过程,并重点讲解每一步背后的原理和思考,让你不仅知道“怎么做”,更明白“为什么这么做”。

2. 解题思路拆解:逆向工程与逻辑推理

面对一个黑盒的登录框,我们的第一步永远是信息收集和试探。题目给了一个登录界面,输入admin/admin返回“wrong pass!”,这首先告诉我们用户admin是存在的,只是密码不对。这是一个关键信息,它缩小了我们的攻击范围。

2.1 初探与源码分析

常规的渗透测试思维会立刻尝试一些常见的注入载荷。比如使用万能密码admin' or '1'='1'#,试图让SQL查询条件永真。但题目在这里设置了一个简单的防护,返回了“do not hack me!”,这提示我们可能存在简单的关键词过滤。此时,一个重要的习惯是查看网页源代码。在前端源码中,开发者有时会留下注释掉的信息、隐藏的输入框或者像本题这样的线索。

在本题的源码中,我们发现了一段被注释的Base32编码字符串。这是CTF比赛中常见的“隐写”或信息传递方式。我们将其进行Base32解码,得到另一段Base64编码的字符串,再次解码后,得到了核心的SQL查询语句:select * from user where username = '$name'。这个发现至关重要,它让我们从完全的盲注变成了“半知”状态。我们知道了查询的模板,知道了变量$name是直接拼接进去的,这证实了SQL注入漏洞的存在,并且指明了注入点就在username参数。

2.2 后台逻辑推理与联合查询构造

知道了SQL语句,我们还需要推断后台的完整验证逻辑。通常的登录验证流程是:用用户名查询数据库,如果查不到记录,返回“用户不存在”;如果查到了,再比对查询结果中的密码字段和用户提交的密码(通常经过哈希处理)。题目在返回“wrong pass!”时,已经暗示我们它执行了密码比对环节。

我们通过构造Payload来探测查询结果的结构。当我们提交name=admin时,返回“wrong pass!”,说明查询到了记录。当我们提交一个不存在的用户名,如name=test时,返回“wrong user!”。更有趣的测试是,我们提交name=1‘ union select 1,2,3#。如果页面返回“wrong pass!”,说明联合查询执行成功,并且后端程序拿到了我们“伪造”的查询结果(即1,2,3这一行)去进行密码比对。通过改变数字的位置,我们可以判断哪个字段被用作用户名、哪个字段被用作密码。

例如,Payload:name=1‘ union select 1,’admin‘,3#。如果此时返回“wrong pass!”,而name=1‘ union select ’admin‘,1,3#返回“wrong user!”,那么我们就可以确定,查询结果中的第二个字段被程序当作username进行比对。这就是通过布尔状态(对/错)进行字段推断的方法。

3. 核心攻击手法详解:利用UNION与MD5特性

在推断出字段顺序(假设为:字段1-ID,字段2-username,字段3-password)后,我们需要构造一个能够通过验证的记录。关键点在于密码比对。从源码注释或常见逻辑可知,密码通常以MD5哈希值的形式存储在数据库中。后台的验证逻辑很可能是这样的:

$sql = "select * from user where username = '$name'"; $result = mysqli_query($conn, $sql); $row = mysqli_fetch_assoc($result); if($row['password'] == md5($_POST['pw'])) { // 登录成功 }

也就是说,程序会将我们通过UNION查询“伪造”出来的password字段值,与用户提交的密码经过MD5计算后的值进行比对。要让这个等式成立,我们有两种经典的思路。

3.1 方法一:已知明文,匹配哈希

如果我们能猜到或者通过其他方式知道管理员密码的明文(在CTF中有时是弱口令),比如是abc,那么其MD5值是900150983cd24fb0d6963f7d28e17f72。我们可以直接伪造一条记录,其中密码字段就是这个哈希值。 构造Payload:name=1‘ union select 1,’admin‘,’900150983cd24fb0d6963f7d28e17f72‘#&pw=abc这个Payload的意思是:原查询查不到数据(因为1‘导致查询失败),然后通过UNION,我们拼接上自己构造的一行数据(1, ‘admin‘, ‘正确的MD5哈希值‘)。当程序执行时,$row得到的就是我们这行数据。此时,用户提交的密码pw=abc,经过md5(‘abc‘)计算后,正好等于我们伪造的密码字段值900150983cd24fb0d6963f7d28e17f72,于是验证通过。

3.2 方法二:利用MD5函数特性,构造NULL匹配

这是一种更巧妙、更通用的方法,它不依赖于知道任何明文密码。它利用了PHP中md5()函数的一个特性:当传入的参数是一个数组时,md5()函数会返回NULL,并可能产生一个警告(但程序可能配置为不显示警告)。

后台的比对逻辑是:$row[‘password‘] == md5($_POST[‘pw‘])。 如果我们能让$row[‘password‘]NULL,同时让md5($_POST[‘pw‘])的结果也为NULL,那么NULL == NULL在PHP的松散比较(==)下结果是true

如何实现?

  1. 让查询结果的密码字段为NULL:在UNION查询中,我们直接将密码字段设为NULL。 构造Payload:name=1‘ union select 1,’admin‘,NULL#
  2. 让提交的密码参数使md5()返回NULL:我们需要以数组的形式提交pw参数。在HTTP POST请求中,可以通过将参数名改为pw[]来传递一个数组。例如pw[]=123。 在PHP中,$_POST[‘pw‘]此时接收到的是一个数组array(0 => ‘123‘)md5(array(…))的返回值就是NULL

因此,完整的攻击链是:

  • 前端提交:name=1‘ union select 1,’admin‘,NULL#&pw[]=123
  • 后端执行SQL,得到一行数据:(1, ‘admin‘,NULL)
  • 后端执行比对:NULL == md5(array(‘123‘))=>NULL == NULL=>true
  • 登录验证通过。

注意:这种方法成功的关键在于后端使用了松散比较==,而不是严格比较===。在严格比较下,NULL === NULL为真,但md5(array())返回的NULL其类型和值虽然与查询得到的NULL相同,但整个表达式能否依然为真,取决于代码的具体写法。在实际渗透测试中,这是一种需要尝试的旁路方法。

4. 实战操作过程:从抓包到注入

理论清楚了,我们来看具体操作。这里以使用Burp Suite工具为例。

4.1 步骤一:拦截登录请求

  1. 在浏览器中打开题目登录页面。
  2. 打开Burp Suite,配置浏览器代理。
  3. 在登录框随意输入用户名密码(如test/test),点击登录。
  4. 此时Burp Suite会拦截到这个HTTP POST请求。

4.2 步骤二:修改请求参数进行注入

拦截到的请求大概长这样:

POST /login.php HTTP/1.1 Host: target.com Content-Type: application/x-www-form-urlencoded ... name=test&pw=test

我们将这个请求发送到Burp Suite的Repeater模块,方便反复测试。

测试用户是否存在: 将name参数改为admin,发送请求。回应体中出现“wrong pass!”,确认admin用户存在。

探测查询字段数及可用字段: 使用order by语句。修改name参数为admin‘ order by 1#,发送请求。正常返回“wrong pass!”。依次尝试order by 2#,order by 3#,order by 4#。当尝试到order by 4#时,可能会报错或返回内容不同,说明查询结果共有3个字段。

使用UNION SELECT确定字段回显位置: 修改name参数为:name=1‘ union select 1,2,3#发送请求。如果返回“wrong pass!”,说明UNION查询成功执行,并且程序使用了我们构造的数据。如果返回“wrong user!”,可能需要将1‘换成其他不存在的用户名,或者调整闭合方式(如将单引号闭合)。

确定用户名和密码字段位置: 假设union select 1,2,3#返回“wrong pass!”。我们分别测试:

  • name=1‘ union select 1,’admin‘,3#-> 如果返回“wrong pass!”,说明第二个字段被当作用户名比对。
  • name=1‘ union select 1,’admin‘,3#-> 如果返回“wrong user!”,则尝试name=1‘ union select ’admin‘,1,3#。 通过这种布尔判断,最终确定字段2是用户名,字段3是密码。

4.3 步骤三:实施最终攻击

采用上述的“NULL匹配”方法。 在Repeater中,修改请求体为:name=1‘ union select 1,’admin‘,NULL#&pw[]=注意,这里的pw参数名必须改为pw[],值可以为空或任意值,因为它是一个数组。发送请求。

4.4 步骤四:获取结果

如果一切顺利,响应包中就不会再出现“wrong pass!”或“wrong user!”,而是会显示登录成功的提示信息,在这道CTF题中,就是最终的flag

5. 防御策略与深度思考

通过这道BabySQli,我们演练了一次完整的基于UNION和逻辑绕过的SQL注入攻击。作为开发者,应该如何防御呢?这里提供几个层级的安全建议:

5.1 根本解决方案:使用参数化查询(预编译语句)

这是防御SQL注入最有效、最根本的方法。以PHP的PDO为例:

$stmt = $pdo->prepare("SELECT * FROM user WHERE username = :username"); $stmt->execute([‘:username‘ => $name]); $row = $stmt->fetch();

在这个例子中,SQL语句模板SELECT * FROM user WHERE username = :username被预先编译,用户输入的$name值在执行时作为纯数据传入,无法改变语句的结构。无论$name里面包含什么引号、SQL关键字,都只会被当作一个普通的字符串值来处理。

5.2 严格的输入验证与过滤

虽然不如参数化查询彻底,但在某些场景下可以作为补充。

  • 类型检查:如果字段是整数,使用intval()强制转换。
  • 白名单过滤:对于已知有限集合的输入(如状态、类型),只接受白名单内的值。
  • 转义函数:如果因历史原因必须使用字符串拼接,务必使用数据库特定的转义函数,如mysqli_real_escape_string()。但请注意,这不是绝对安全的,尤其是在宽字节等特殊字符集下可能存在绕过风险。

5.3 安全的密码比对逻辑

本题的漏洞也暴露了密码验证逻辑的问题。更安全的做法是:

  1. 使用PHP的password_hash()函数进行哈希(它自动处理盐值并使用强算法如bcrypt)。
  2. 使用password_verify()函数进行验证。
// 存储密码 $hashedPassword = password_hash($plainPassword, PASSWORD_DEFAULT); // 验证密码 if (password_verify($inputPassword, $row[‘password‘])) { // 登录成功 }

password_verify()函数能有效防止时序攻击,并且其内部逻辑决定了它不会出现md5(array())返回NULL这类奇怪的问题。

5.4 最小化错误信息与代码安全

  • 关闭错误回显:在生产环境中,避免将详细的数据库错误信息直接显示给用户。这能防止攻击者通过报错获取数据库结构信息(报错注入)。
  • 遵循最小权限原则:连接数据库的账号不应具有DROPDELETE等高危权限,仅赋予其应用所需的最小权限。
  • 定期安全审计与代码扫描:使用自动化工具和人工审查,定期查找代码中的安全隐患。

这道“BabySQli”就像一面镜子,照出了Web应用安全中一个经典且危险的漏洞。对于学习者而言,理解它的攻击原理是掌握防御技术的第一步。而对于开发者来说,时刻牢记“永远不要信任用户输入”,并采用参数化查询等现代、安全的编程实践,是构建稳固应用基石的唯一途径。安全不是一个功能,而是一种需要贯穿于设计、开发、测试全过程的思维方式。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/16 6:13:03

不存在GPT-5.5,但可构建GPT-5.5级AI系统

我需要指出一个关键事实:截至目前(2024年中),并不存在官方发布的“GPT-5.5”模型。OpenAI 官方从未发布、命名或确认过代号为“GPT-5.5”的模型。其公开演进路径为:GPT-2(2019)→ GPT-3&#xf…

作者头像 李华
网站建设 2026/6/16 6:06:52

STM32时钟门控机制解析:从RCC寄存器操作到低功耗设计实践

1. 项目概述:从一行代码看透STM32的时钟门控如果你刚开始接触STM32的固件库开发,看到RCC->APB2ENR | RCC_APB2Periph_GPIOA;这样的代码,心里可能会犯嘀咕:这行看起来有点“魔法”的语句到底在干什么?它为什么是操作…

作者头像 李华
网站建设 2026/6/16 6:05:50

Mythos Preview实战解析:AI原生攻防与可观测性驱动的安全新范式

1. 这不是一次普通模型发布:Mythos Preview 的真实分量与行业震感如果你过去三年一直在跟进大模型演进,大概率会记得2023年Claude 2发布时那种“稳扎稳打”的观感——推理更连贯、长上下文更可靠、安全护栏更细密,但没有让人拍案而起的“断层…

作者头像 李华
网站建设 2026/6/16 5:55:50

68个适合个人GPU部署的LLM:显存、带宽与引擎兼容性实战指南

1. 为什么“68个适合个人GPU部署的LLM”这个标题背后藏着一场静默革命?你有没有在深夜调试过PyTorch——明明nvidia-smi显示GPU在跑,torch.cuda.is_available()却返回False?有没有对着"No module named vllm"报错反复重装pip&#…

作者头像 李华
网站建设 2026/6/16 5:51:01

六顶点模型与高斯自由场的统计力学关联研究

1. 六顶点模型与高斯自由场的关联机制六顶点模型作为统计力学中研究二维冰型系统的经典格点模型,其高度函数的涨落行为与高斯自由场(Gaussian Free Field, GFF)存在深刻联系。当模型参数c∈[1,2]时,这种关联表现得尤为显著。1.1 模型基本设定与核心问题六…

作者头像 李华