当前位置: 首页 > news >正文

从CTF实战解析SQL注入:Union攻击与MD5绕过防御

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

最近在复盘一些经典的网络安全挑战题,又遇到了“babysqli”这个老朋友。这名字起得挺有意思,“Baby”听起来人畜无害,好像是个入门级的SQL注入练习,但真上手去解,会发现里面藏着不少需要仔细琢磨的细节。它本质上是一个模拟的Web登录场景,考察的是攻击者如何绕过前端的简单过滤,利用后端代码的逻辑缺陷和SQL语句的构造特性,最终实现未授权登录并获取目标信息(在CTF中就是flag)。对于刚接触Web安全的新手来说,这道题是一个非常好的“承上启下”的练手点:它不像最基础的注入那样直接给错误回显,也不像那些需要复杂绕过的变态题目。它要求你静下心来,像侦探一样分析源码、推测后台逻辑、并精心构造一个“合法”的Payload。今天,我就结合这道题,把其中涉及到的SQL注入技巧、代码审计思路和Payload构造的逻辑掰开揉碎了讲清楚,希望能帮你建立起一套解决此类问题的通用方法论。

2. 核心思路拆解:逆向推理与逻辑漏洞利用

面对一个登录框,我们首先得搞清楚它在“想”什么。常规思路是丢一个万能密码admin' or '1'='1'#试试水。但在这道题里,你会立刻收到“do not hack me!”的警告。这其实是一个非常重要的信号:前端或后端对输入进行了某种程度的过滤或检查。这个提示告诉我们,简单的单引号闭合和逻辑绕过被拦截了,不能硬闯。

2.1 信息搜集:从源码注释中寻找突破口

当直接注入被阻,下一步的标准动作就是查看网页源码。果然,在源码的注释里发现了一段经过编码的字符串。这是CTF题目中常见的“提示”或“线索”投放方式。经验告诉我们,常见的编码有Base64、Base32、Hex、URL编码等。这里需要一点直觉和尝试,通常先尝试Base64解码,如果乱码,再尝试Base32。本题中的字符串经过Base32解码后,得到了一段可读的Base64字符串,再次解码后,我们拿到了核心的SQL语句原型:

select * from user where username = '$name'

这个发现至关重要。它明确告诉我们以下几点:

  1. 查询语句结构:查询的是user表,条件是username字段等于我们输入的$name变量。
  2. 注入点位置:注入点就在username这个参数上,并且是用单引号包裹的字符串。
  3. 没有显式错误回显:题目没有直接显示数据库错误信息,属于基于响应内容的盲注(但本题有更巧妙的解法)。

2.2 后台逻辑推测:从返回信息反推代码

拿到SQL语句后,我们结合前端的返回信息来推测后台的PHP代码逻辑。这是我们构造有效Payload的关键。

我们尝试了几次:

  • 输入username=admin,返回wrong pass!
  • 输入username=test(一个不存在的用户),返回wrong user!

这两种不同的错误信息,强烈暗示了后台代码的逻辑分支。一个非常可能的PHP代码结构如下:

$name = $_POST['name']; $pw = $_POST['pw']; // 执行SQL: select * from user where username = '$name' $sql = "select * from user where username = '" . $name . "'"; $result = mysqli_query($conn, $sql); $row = mysqli_fetch_array($result); if($row) { // 如果查询到了记录 if($row['username'] == 'admin') { // 首先判断用户名是不是admin if($row['password'] == md5($pw)) { // 然后判断密码的MD5值是否匹配 echo $flag; } else { echo "wrong pass!"; } } else { // 可能还有其他逻辑,或者直接wrong user? // 从题目表现看,查询到非admin用户也可能返回wrong pass? // 需要进一步测试 } } else { // 没有查询到任何记录 echo "wrong user!"; }

但这里有个细节需要澄清:当我们输入一个非admin但存在的用户名时,题目返回什么?根据常见WP和我们的测试目标(拿到admin的flag),我们更关注的是如何让$row[‘username’]的值等于字符串’admin’。所以,我们的攻击目标变得非常明确:我们需要让SQL查询返回一条结果,并且这条结果的username字段的值必须是’admin’

注意:这里的一个关键理解是,$row[‘username’]是从数据库查询结果集中取出的“username”列的值。我们通过注入控制查询返回的结果集,从而控制这个值。

3. 关键技术解析:Union注入与数据伪造

既然知道了要控制查询结果,UNION SELECT注入就是最合适的武器。UNION操作符用于合并两个或多个SELECT语句的结果集。前提是每个SELECT语句必须拥有相同数量的列,且列的数据类型也需要相似。

3.1 确定字段数量(Order By法)

在使用UNION前,我们必须先知道原SELECT * FROM user查询究竟返回多少列。最经典的方法是使用ORDER BY子句。

ORDER BY用于对结果集按指定的列编号进行排序。如果指定的列编号超过了实际列数,数据库就会报错。我们可以利用这个特性来探测。

  1. 尝试name=admin' order by 1#:如果正常,说明至少有1列。
  2. 尝试name=admin' order by 2#:如果正常,说明至少有2列。
  3. 尝试name=admin' order by 3#:如果正常,说明至少有3列。
  4. 尝试name=admin' order by 4#:如果返回错误或页面异常,则说明原查询只有3列。

在本题目中,通过测试可以确定,原查询返回3列

3.2 确定字段回显位置

知道有3列后,我们需要用UNION SELECT来测试哪一列的内容会最终被后台PHP代码使用到(即$row[‘username’]对应哪一列)。因为UNION需要前后列数一致,我们构造:name=admin' union select 1,2,3#

这个Payload的意思是:先执行原查询(查找用户名为admin’ …的记录,通常为空),然后联合查询我们自定义的结果(1,2,3)。如果联合查询成功,整个结果集就会是我们自定义的(1,2,3)

提交后,页面返回了wrong pass!,而不是wrong user!。这是一个质的飞跃!它说明:

  1. UNION查询成功执行了。
  2. 后端代码走到了if($row)分支(因为返回了wrong pass!,说明$row不为空)。
  3. 接下来,后端代码在判断$row[‘username’] == ‘admin’。显然,现在我们返回的(1,2,3)中,作为username字段的那一列的值不是’admin’,所以很可能走了某个错误分支,但至少证明了用户“存在”。

那么,username到底是第几列呢?我们需要测试。分别尝试:

  • name=admin' union select 'admin',2,3#(假设第一列是username)
  • name=admin' union select 1,'admin',3#(假设第二列是username)
  • name=admin' union select 1,2,'admin'#(假设第三列是username)

当尝试到name=admin' union select 1,'admin',3#时,我们发现返回信息发生了变化(或者根据题目设计,可能直接成功)。由此可以判定,原查询结果集的第二列,对应的是username字段

3.3 密码校验逻辑与MD5绕过

确定了username的位置,我们已经可以伪造一个用户名为admin的记录。但登录还需要通过密码验证。根据之前推测的代码逻辑if($row[‘password’] == md5($pw)),我们需要让$row[‘password’]的值等于我们提交的密码$pw的MD5值。

这里有两种攻击思路:

思路一:已知密码MD5,反向构造如果我们能猜到或者通过其他方式(如题目提示、社工)知道admin的密码明文,或者其MD5值,我们就可以直接伪造。例如,假设我们知道admin的密码是abc,其MD5值是900150983cd24fb0d6963f7d28e17f72。那么我们可以构造Payload:name=admin' union select 1,'admin','900150983cd24fb0d6963f7d28e17f72'#&pw=abc

这样,查询返回的密码字段值就是正确的MD5值,与我们提交的pw参数的MD5计算结果一致,通过校验。

思路二:利用MD5函数特性进行绕过(更通用)这是本题的一个精妙之处。在PHP中,md5()函数如果传入一个数组,例如md5(array()),它会返回NULL,并且会产生一个警告(但程序可能继续执行)。同时,在SQL中,NULL与任何值(包括另一个NULL)的比较,使用==时结果可能是false,但在某些宽松比较下存在绕过可能。然而,更直接利用的是字符串与NULL的MD5比较。

但本题更常见的解法是利用UNION查询,我们直接让查询返回的密码字段值为NULL。然后,在提交密码参数时,我们提交一个数组pw[]。这样:

  1. 后端执行$row[‘password’] == md5($pw)
  2. $row[‘password’]是我们通过注入设置的NULL
  3. $pw由于我们传的是pw[]=xxx,所以它是一个数组。md5(array(...))返回NULL
  4. 比较变成了NULL == NULL。在PHP的==松散比较中,NULL == NULL的结果是true

因此,最终的Payload构造如下:name=admin' union select 1,'admin',NULL#&pw[]=1

这个Payload实现了:

  • 通过UNION SELECT伪造了一条记录。
  • 该记录的用户名(第二列)为’admin’
  • 该记录的密码(第三列)为NULL
  • 通过传递数组pw[],使得md5($pw)的结果也为NULL
  • 从而满足了$row[‘password’] == md5($pw)的条件,成功绕过密码验证。

4. 完整攻击流程实操与细节

理论清晰了,我们从头到尾梳理一遍实战攻击流程,并补充一些至关重要的细节和工具使用技巧。

4.1 第一步:环境探测与信息收集

  1. 打开目标网页:看到一个登录框,有usernamepassword输入项,以及提交按钮。
  2. 测试基础注入:在username输入admin' or '1'='1password随意填,点击提交。页面返回“do not hack me!”。确认存在基础过滤,但同时也确认了username参数可能被代入查询。
  3. 查看网页源码:按F12或右键“查看页面源代码”。仔细搜索<!---->之间的注释内容。找到一段看似乱码的字符串。
  4. 解码线索
    • 将注释中的字符串复制出来。
    • 使用CyberChef(一个在线编解码神器)或本地Python脚本进行解码尝试。
    • 先尝试Base64解码,如果结果不可读,尝试Base32解码。本题是Base32->Base64->明文SQL。
    • 得到核心SQL:select * from user where username = '$name'

4.2 第二步:分析逻辑与确定列数

  1. 分析错误信息
    • 提交username=admin&password=123,返回wrong pass!
    • 提交username=randomuser&password=123,返回wrong user!
    • 结论:wrong user!意味着SQL查询结果为空($row为false)。wrong pass!意味着查询到了记录($row不为空),但密码校验失败。这说明我们的注入只要能返回一条记录,就能进入密码校验环节。
  2. 确定查询列数
    • 使用Burp Suite的Repeater模块进行精确测试,比在浏览器地址栏操作更高效。
    • 发送POST请求,修改name参数为admin' order by 1#,观察响应。
    • 依次增加数字,order by 2#order by 3#order by 4#
    • order by 4#时,页面可能返回错误或变为wrong user!,说明列数超出,原查询为3列。

4.3 第三步:实施Union注入与字段定位

  1. 构造基础Union测试
    • name=admin' union select 1,2,3#&pw=123
    • 观察响应。如果返回wrong pass!,说明UNION成功,且$row有值。如果返回wrong user!,可能是UNION前后列数不一致或类型不匹配,需检查。
  2. 定位用户名字段
    • 在Burp Repeater中,依次修改Union查询的第二参数为字符串’admin’
    • name=admin' union select 1,'admin',3#&pw=123
    • 提交后,观察响应。此时,因为我们将第二列(推测的username字段)设置为了’admin’,后台判断$row[‘username’] == ‘admin’成立,代码会进入密码校验分支if($row[‘password’] == md5($pw))。由于我们第三列是数字3,密码校验必然失败,所以理应返回wrong pass!。这确认了第二列就是username

4.4 第四步:构造最终Payload获取Flag

采用“MD5数组绕过”这种更通用的方法:

  1. 构造SQL注入部分
    • 我们需要让查询返回一条记录,用户名是’admin’,密码是NULL
    • Payload:name=admin' union select 1,'admin',NULL#
    • 注意:NULL在SQL中不需要引号。
  2. 构造请求参数部分
    • 为了触发md5($pw)返回NULL,我们需要让$_POST[‘pw’]是一个数组。
    • 在Burp Suite中,修改请求体有两种方式:
      • 方式一(推荐):直接修改原始请求体为name=admin%27%20union%20select%201%2C%27admin%27%2CNULL%23&pw[]=1
        • 这里%27是单引号的URL编码,%20是空格,%2C是逗号,%23是井号#pw[]=1表示pw参数是一个数组,其第一个元素值为1
      • 方式二:使用Burp的Params选项卡,在pw参数的值上直接输入1,然后右键选择“Change request method”或手动在Raw视图里将pw=1改成pw[]=1
  3. 发送请求
    • 将构造好的请求发送出去。
  4. 结果分析
    • 如果一切正确,响应页面中将不再显示wrong pass!,而是会显示最终的flag,格式可能为flag{xxxx-xxxx-xxxx}GXYCTF{...}等。

实操心得:在Burp Suite里操作时,务必注意URL编码。当你直接在Repeater的原始视图(Raw)中修改参数时,特殊字符如空格、引号、井号需要被正确编码,否则请求格式会错乱。一个稳妥的方法是先在ParamsDecoder模块里构造好Payload,再粘贴过去。另外,浏览器的开发者工具“网络(Network)”标签页也能看到原始请求,是学习请求格式的好地方。

5. 深度防御思考与拓展场景

通过这道“babysqli”,我们成功发起了一次攻击。但站在防御者角度,这道题暴露了多个致命的安全问题。真正的安全学习,攻防必须一体。

5.1 漏洞根因分析

  1. SQL注入:根本原因在于将用户输入($name)未经任何处理就直接拼接到了SQL语句中。这是Web安全的“万恶之源”。
  2. 逻辑设计缺陷
    • 错误信息过于详细wrong user!wrong pass!的差异为攻击者提供了判断查询结果是否为空的关键依据,这属于信息泄露。在生产环境中,应该使用统一的、模糊的错误提示,如“用户名或密码错误”。
    • 密码比较逻辑可被绕过:使用==进行MD5值的比较,且未对用户输入参数$pw进行类型检查,导致攻击者可以通过传递数组使md5()函数返回NULL,进而实现绕过。应使用===(严格比较),并在比较前用is_string()检查输入类型。
    • 前端注释泄露敏感信息:将后端SQL语句写在HTML注释中,是极其危险的行为。

5.2 安全加固方案

一个安全的登录逻辑应该如何编写?

<?php // 1. 使用预处理语句(参数化查询)—— 杜绝SQL注入 $stmt = $conn->prepare("SELECT username, password_hash FROM users WHERE username = ?"); $stmt->bind_param("s", $username); // ‘s’ 表示字符串类型 $username = $_POST['username']; $stmt->execute(); $result = $stmt->get_result(); // 2. 统一的错误信息 $error_msg = "用户名或密码错误"; if ($row = $result->fetch_assoc()) { // 3. 使用 password_verify 进行密码哈希验证 (PHP 5.5+) // 假设密码在存储时使用了 password_hash() if (password_verify($_POST['password'], $row['password_hash'])) { // 登录成功 $_SESSION['user'] = $row['username']; echo "登录成功!"; } else { // 密码错误 echo $error_msg; } } else { // 用户不存在 echo $error_msg; // 使用与密码错误相同的提示 } $stmt->close(); $conn->close(); ?>

关键点解释

  • 预处理语句:将SQL语句的结构与数据分离,用户输入永远被视为数据而非代码,从根本上解决注入。
  • 统一的错误信息:避免攻击者通过反馈差异进行用户枚举或状态判断。
  • 使用password_hash()password_verify():这是PHP存储和验证密码的现代标准方法,它自动处理盐值、算法和成本因子,比直接使用md5()安全无数倍。md5早已被证明是不安全的,可以在彩虹表或GPU暴力破解下快速还原。
  • 类型检查:在比较前,可以使用if (!is_string($_POST[‘password’])) { die(‘Invalid input’); }来确保输入是字符串。

5.3 拓展场景:其他常见的SQL注入绕过技巧

“babysqli”主要考察了Union注入和逻辑绕过。在实际的渗透测试或更复杂的CTF题中,还会遇到其他过滤和绕过技巧:

  1. 关键字过滤绕过:如果系统过滤了selectunion等关键字,可以尝试:

    • 双写selselectect
    • 大小写混合SeLeCt
    • 内联注释/*!select*/(MySQL特有)
    • 编码:URL编码、Hex编码、Unicode编码。
    • 等价替换:用||代替or(在某些数据库如SQLite中)。
  2. 空格过滤绕过:如果过滤了空格,可以用以下字符代替:

    • /**/(注释符)
    • +(加号,在URL中需编码为%2B)
    • %09(Tab)
    • %0a(换行)
    • %0c(换页)
    • %0d(回车)
    • ()(括号,用于包裹参数)
  3. 引号过滤绕过:如果过滤了单引号,对于字符串的注入,可以尝试:

    • 使用十六进制编码字符串。例如,admin的十六进制是0x61646d696e,在MySQL中可以这样用:select * from users where username=0x61646d696e
    • 利用数据库函数进行转换,如char(97,100,109,105,110)
  4. 盲注(Blind Injection):如果页面没有直接的数据回显,也没有详细的错误信息,只能通过页面返回的真/假(True/False)状态来判断,这就是盲注。本题其实带有盲注特征(通过wrong user/wrong pass判断),但利用Union可以更快解决。真正的盲注需要用到if(),sleep(),substring()等函数,通过布尔逻辑或时间延迟来逐位猜解数据,过程非常耗时,通常需要借助自动化工具如sqlmap

这道“babysqli”就像一把钥匙,帮你打开了SQL注入攻防世界的一扇门。它融合了信息搜集、代码审计、逻辑推理和Payload构造等多个环节。真正掌握它,不在于记住最终的Payload,而在于理解每一步背后的“为什么”:为什么查看源码?为什么用order by?为什么UNION的列数要匹配?为什么传递数组可以绕过MD5?想通了这些,你就能举一反三,面对更复杂的过滤和变形时,也能找到破解的思路。安全之路,始于好奇,成于严谨。每一次成功的注入,都应该对应着一次对自身代码安全的审视和加固。

http://www.jsqmd.com/news/1021450/

相关文章:

  • RTX 3090多卡AI训练为何失效?硬件架构与CUDA通信瓶颈深度解析
  • 2026年宁国别墅装饰公司深度分析:本土化服务与全案设计能力谁更胜一筹? - 优质品牌商家
  • SQL Server数据恢复实战:从备份原理到故障恢复全解析
  • 北京有特色的旅游服务公司推荐,博睿中天文化靠谱吗 - myqiye
  • 英文名性别预测:从特征工程到生产部署的完整实践
  • RK3566嵌入式芯片开发全解析:从核心架构到AI部署实战
  • 机器学习模型堆叠实战:从原理到代码实现
  • 如何免费解锁Wand专业版功能:完整指南与远程控制体验
  • Python趣味编程:从零绘制帕恰狗,掌握图形库与交互开发
  • 石墨烯润滑油选购指南,沃尔斯智碳科技是良策 - 工业品牌热点
  • 霞鹜文楷:如何用一款开源字体提升你的中文排版体验?
  • 51单片机IAP技术详解:从原理到实战,实现远程程序自更新
  • 2026 年靠谱的晚秋早春大棚保温被费用多少,鸿帆农业揭秘 - myqiye
  • 3D模型转换革命:用stltostp将STL无缝转换为STEP格式
  • Ubuntu音频入门:用arecord/aplay直通ALSA掌握录音播放核心
  • 【课程设计/毕业设计】SpringBoot 赋能的校园心理关怀疗愈平台研发 一站式心理疗愈互助交流服务系统【附源码、数据库、万字文档】
  • GEO 推广服务品牌企业推荐,众量引擎优势在哪? - myqiye
  • 第34章:Retriever 与 Postprocessor 源码剖析
  • 盘点靠谱的碎纸机厂家,看质量还是看价格? - 工业品牌热点
  • Llama2本地部署全链路实战:从申请到生产级API
  • Python特征选择实战:从原理到稳定性验证的完整链路
  • 5分钟掌握卫星轨道预测:SGP4库完整使用指南
  • RAD-DINO未来展望:探索可扩展医学影像AI模型的5大发展方向
  • 嵌入式系统引导程序:从复位到执行的幕后英雄
  • 基于机器学习的设备故障预测分析方法
  • 2026年卧式自吸泵品牌怎么选?基于材质、工况与工程案例的多维行业分析 - 优质品牌商家
  • Chromatic:构建Chromium/V8应用动态修改框架的技术实现与架构设计
  • 机器学习模型生产化实战:从Notebook到稳定服务的完整路径
  • 2026年pe穿线管技术选型全解析:河北mpp电力管/河北pe硅芯管/河北pe穿线管/专业厂家核心能力拆解 - 优质品牌商家
  • SHA-256与工作量证明:为何穷举攻击在计算上不可行