三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

CTF Web实战:联合查询注入与MD5认证绕过深度解析

CTF Web实战:联合查询注入与MD5认证绕过深度解析

1. 项目概述:一次典型的CTF Web题通关实录

最近在复盘一些经典的CTF Web题目,BUUCTF平台上的[GXYCTF2019]BabySQli 1这道题给我留下了挺深的印象。它不像那些单纯堆砌过滤规则的“炫技”题,而是把几个非常基础但关键的知识点——Base编码、MD5认证和联合查询注入——巧妙地串联在了一起,形成了一个逻辑闭环。整个过程就像在解一个设计精巧的谜题,每一步的发现都为下一步铺平道路,非常适合用来理解SQL注入攻击中“信息获取”与“逻辑绕过”的核心思想。这道题考察的不是多么偏门的技巧,而是对基础知识的扎实掌握和灵活运用能力。无论你是刚接触CTF的新手,想通过一道题串联起多个知识点,还是有一定经验的选手,想温故知新、梳理思路,这道题都是一个绝佳的练手对象。接下来,我就带你完整地走一遍我的解题思路和实操过程,我会把每个环节的“为什么”讲清楚,并分享一些在实战中容易踩坑的细节。

2. 环境初探与信息收集

2.1 题目界面与功能分析

拿到题目链接,第一件事永远是打开看看它长什么样。这是一个典型的登录界面,只有一个用户名(username)和一个密码(password)的输入框,外加一个提交按钮。功能极其简单:输入凭证,验证,返回结果。这种极简的界面往往意味着后端逻辑是考察的重点。

我首先尝试了几个常见的测试用例:

  1. 基础注入探测:在用户名框输入admin'1' or '1'='1,密码随意。发现页面返回了统一的错误信息:“wrong user!”。这个反馈很关键,它直接告诉我们,程序会先判断用户名是否存在。
  2. 密码错误测试:输入一个合理的用户名(比如猜测的admin)和一个错误密码,返回信息变成了:“wrong pass!”。这说明在用户名验证通过后,会进行密码校验。
  3. 错误信息差异wrong user!wrong pass!的差异是重要的突破口。在SQL注入中,这种差异化的回显可以被利用来进行基于布尔状态(True/False)的推断,也就是我们常说的布尔盲注。但本题是否必须用盲注呢?先别急。

注意:在实际CTF或渗透测试中,这种有差异的回显信息非常宝贵。wrong user!通常对应SQL查询结果为空(即WHERE条件不满足),而wrong pass!则意味着查询到了用户记录,但密码比对失败。这几乎明示了后端查询的结构。

2.2 关键线索:神秘的“Search”与编码发现

在页面源代码(右键查看页面源代码)里,我发现了本题的第一个“非预期”提示。注释里赫然写着一行:<!--MMZFM422K5HDASKDN5TVU3SKOZRFGQRRMMZFM6KJJBSG6WSYJJWESSCWPJNFQSTVLFLTC3CJIQYGOSTZKJ2VSVZRNRFHOPJ5-->这串字符看起来像是Base32或Base64编码。我的第一反应是尝试Base64解码,但直接解出来是乱码。考虑到CTF中常见的套路,我尝试了Base32解码。使用CyberChef在线工具或者本地Python的base64.b32decode()函数,轻松解码得到:c2VsZWN0ICogZnJvbSB1c2VyIHdoZXJlIHVzZXJuYW1lID0gJyRuYW1lJw==这串结果尾部有等号,明显是Base64编码。于是进行第二次解码,最终得到明文字符串:select * from user where username = '$name'

这里有两个非常重要的信息:

  1. 后端SQL语句:我们拿到了查询语句的模板。它是对user表进行查询,where条件只有用户名。这验证了我们之前的猜测。
  2. 注入点位置:注入点显然在$name这个变量上,也就是我们提交的username参数。

实操心得:CTF中藏在HTML注释、JS文件、HTTP响应头里的信息,往往是解题的钥匙。养成随手查看源代码、抓包分析所有响应的习惯至关重要。对于编码字符串,如果Base64解码失败,可以依次尝试Base32、Base16(Hex)、Base58、Base91等,或者观察字符集(Base32通常只有A-Z和2-7)来快速判断。

3. 核心漏洞原理与利用链拆解

3.1 从查询语句到联合注入的必然性

我们知道了查询语句是select * from user where username = '$name'。假设我们输入admin,那么查询就是select * from user where username = 'admin'

  • 如果admin用户存在,返回该用户的所有字段(包括username,password等)。
  • 如果不存在,返回空结果集。

前端逻辑据此判断,返回wrong user!或进入密码校验。

那么,如何绕过用户名的检查呢?一个直接的想法是:让这条SQL语句无论如何都返回一条有效的用户记录。这就是联合查询注入(Union Injection)大显身手的地方。我们可以构造输入,将原查询变成:select * from user where username = 'admin' union select 1, 'admin', 'hashed_password' -- '这样,即使where条件不成立(原表没有admin),union select也会强行“制造”出一条记录返回。关键在于,union前后查询的列数必须一致。我们不知道原表user有多少列。

3.2 确定查询列数

使用order byunion select来猜解列数。

  • 输入:username=admin' order by 1-- &password=1
  • 输入:username=admin' order by 2-- &password=1
  • 输入:username=admin' order by 3-- &password=1
  • 输入:username=admin' order by 4-- &password=1

order by 3时页面正常(可能返回wrong user!),而order by 4时页面可能报错或返回异常,说明原查询结果有3列

union select验证:username=admin' union select 1,2,3-- &password=1。如果页面返回wrong pass!,说明联合查询成功执行,并且我们“制造”的记录被后端当成了查询结果。页面可能会显示23的位置(如果存在数据回显的话),但本题没有直接回显,wrong pass!这个状态本身就是成功的信号。

3.3 理解MD5认证与密码绕过逻辑

为什么我们union select 1,2,3会导致wrong pass!?这引出了本题的第二个核心:密码校验机制

wrong pass!这个提示可以合理推断,后端逻辑大致如下:

$sql = "select * from user where username = '$name'"; $result = mysqli_query($conn, $sql); $row = mysqli_fetch_assoc($result); if (!$row) { echo "wrong user!"; } else { // 假设数据库存储的是密码的MD5哈希值 if (md5($_POST['password']) == $row['password']) { echo "Success! Flag is ..."; } else { echo "wrong pass!"; } }

也就是说,数据库里存的不是明文密码,而是密码的MD5哈希值。当我们union select 1,2,3时,后端从我们“制造”的记录中取出的密码字段(假设是第3列)的值是3。然后它对我们提交的密码进行MD5计算,试图和3比较。这显然不可能成功,所以返回wrong pass!

那么,攻击思路就清晰了:我们需要通过联合查询,伪造一条记录,其中密码字段的值,是我们已知明文的密码所对应的MD5哈希值。这样,当我们提交那个明文密码时,后端计算出的MD5值就会与我们伪造的哈希值匹配,从而通过验证。

3.4 构造终极Payload

我们需要解决两个问题:

  1. 目标用户的密码哈希值是什么?我们不知道。但我们可以自己设定。比如,我们选择密码明文为123456,其MD5哈希值是e10adc3949ba59abbe56e057f20f883e
  2. 如何让联合查询返回我们设定的哈希值?我们需要知道密码字段在查询结果中的位置。通过之前的union select 1,2,3测试,如果页面状态正常,说明列数正确,但具体哪一列对应username,哪一列对应password,需要猜测。在登录逻辑中,通常查询结果的第一列是id,第二列是username,第三列是password。我们可以基于这个常见结构进行尝试。

因此,最终的Payload构造如下:username=admin' union select 1,'admin','e10adc3949ba59abbe56e057f20f883e'-- &password=123456

逻辑解释

  • union select 1,'admin','e10adc3949ba59abbe56e057f20f883e':伪造一条记录,id=1,username='admin',password='e10adc3949ba59abbe56e057f20f883e'
  • --:注释掉原SQL语句中剩下的单引号,避免语法错误。
  • 当我们提交时,username参数就是上面这一整段注入Payload。
  • 后端执行SQL,因为where username='admin'在真实表中可能找不到记录,但union我们伪造的记录会返回。
  • 后端取到伪造记录的密码字段值:e10adc3949ba59abbe56e057f20f883e
  • 我们提交的password=123456,被后端进行MD5计算,结果正好是e10adc3949ba59abbe56e057f20f883e
  • 比对成功,登录成功,获取Flag。

4. 完整实操过程与细节实现

4.1 工具准备与测试环境

我通常使用Burp Suite作为主要工具,它的Repeater模块非常适合这种需要反复修改Payload、观察响应的场景。浏览器配合HackBar插件也能快速测试。

  1. 配置代理:确保浏览器流量经过Burp Suite。
  2. 抓取登录请求:在浏览器提交一次任意登录,Burp Suite的Proxy模块会截获这个POST请求。
  3. 发送至Repeater:将抓到的请求右键发送到Repeater模块,方便后续操作。

原始请求大概长这样:

POST /challenge.php HTTP/1.1 Host: xxx.buuoj.cn ... Content-Type: application/x-www-form-urlencoded username=test&password=test

4.2 分步注入攻击流程

在Burp Suite Repeater中,我们逐步修改username参数进行测试。

第一步:验证注入点与错误回显

username=admin'&password=1

观察响应体,确认是否包含wrong user!。如果存在,说明单引号成功闭合了SQL语句,注入点存在。

第二步:确定列数使用order by语句。

username=admin' order by 1-- &password=1 username=admin' order by 2-- &password=1 username=admin' order by 3-- &password=1 username=admin' order by 4-- &password=1

我发现order by 3时,页面返回wrong user!(状态正常),而order by 4时,页面返回了一个SQL语法错误(或者空白页)。这明确表明原查询有3列

第三步:验证联合查询可行性

username=admin' union select 1,2,3-- &password=1

提交后,页面返回wrong pass!。这是一个极其积极的信号。它说明:

  1. union select 1,2,3语法正确,列数匹配。
  2. 联合查询的结果被后端成功接收并处理。
  3. 后端走到了密码比对环节,因为密码不匹配(我们伪造的密码是3,而我们提交的密码1的MD5显然不是3),所以返回wrong pass!

如果返回wrong user!,则说明union查询可能因为数据类型等问题,整体结果集为空,需要调整select后的值,比如尝试'1','2','3'(用字符串代替数字)。

第四步:猜测列对应关系并构造最终Payload基于常见数据库设计,我假设3列分别是id,username,password。那么我需要让联合查询的第二列是一个有效的用户名(用于通过后续可能的会话赋值等,虽然本题可能不需要),第三列是我们可控的密码哈希值。

我选择密码明文为aa,因为它的MD5值比较简单易记:4124bc0a9335c27f086f24ba207a4912。你也可以用123456。 使用命令echo -n aa | md5sum或在线工具计算MD5。

构造Payload:

username=admin' union select 1,'admin','4124bc0a9335c27f086f24ba207a4912'-- &password=aa

这里,union select伪造了一条记录:id=1,username='admin',password='4124bc0a9335c27f086f24ba207a4912'

第五步:发送请求,获取Flag将上述构造好的请求在Burp Suite Repeater中发送。如果一切正确,响应页面将不再显示wrong user!wrong pass!,而是会显示登录成功的消息,其中包含本题的Flag。

在我的实际测试中,发送请求后,页面返回了“恭喜你,flag是:flag{xxxx-xxxx-xxxx-xxxx}”

4.3 关键参数与编码处理细节

  1. URL编码:在Burp Suite中直接输入'空格--等字符,工具通常会自动进行URL编码。空格会变成%20+单引号会变成%27--后面的空格有时很重要。最终在Repeater里看到的请求应该是:username=admin%27%20union%20select%201%2C%27admin%27%2C%274124bc0a9335c27f086f24ba207a4912%27--%20&password=aa确保你的工具正确进行了编码,否则可能导致语法错误。
  2. 注释符:MySQL中--是单行注释符,但注意后面要跟一个空格(即--)。在URL编码里,这个空格至关重要。有时也可以用#(URL编码为%23)作为注释符。
  3. MD5值格式:确保你填入的MD5哈希值是32位小写十六进制字符串,不要有多余的空格或换行。

5. 常见问题、排查技巧与深度思考

5.1 实战中可能遇到的问题与解决

问题1:无论怎么注入,都只返回wrong user!,没有wrong pass!

  • 排查:这说明你的注入Payload没有改变查询结果为空的事实。可能的原因:
    • 列数不对:重新用order by精确测试列数。有时数据类型不匹配会导致union失败,可以尝试union select null,null,null,因为null可以匹配任何类型。
    • 注释符未生效:检查--后面是否有空格(URL编码后是--%20)。或者尝试将Payload末尾的单引号闭合掉,例如:admin' union select 1,'admin','md5' where '1'='1
    • 特殊字符过滤:题目是否过滤了unionselect空格?可以尝试双写绕过ununionion selselectect,或用/**/代替空格。
  • 本题情况:BabySQli这道题没有过滤这些关键词,所以重点检查列数和注释符。

问题2:返回了wrong pass!,但换上正确的MD5哈希值后,依然wrong pass!

  • 排查
    • 列位置猜错:可能密码字段不在第3列。尝试交换union select后参数的位置。例如:union select 1, 'md5_hash', 'admin',同时交换用户名和密码的提交值(这需要你假设用户名和密码的列序互换)。
    • MD5比较方式:后端可能是===严格比较,而你的哈希值带有换行符?确保哈希值字符串纯净。或者后端存储的不是纯MD5,而是md5(md5($pass).$salt)等形式?但根据题目名称和难度,通常就是直接比较。
    • 密码字段类型:数据库密码字段可能是CHAR(32),直接存字符串哈希值。我们的Payload与之匹配。

问题3:页面返回SQL语法错误。

  • 排查:仔细检查Payload的语法。
    • 单引号闭合是否正确。
    • union前后查询的列数是否绝对相等。
    • --注释符是否正确使用,是否URL编码。
    • 在Burp Suite中,对比原始请求和你修改后的请求,看特殊字符是否被正确编码。

5.2 关于Base编码线索的再思考

很多同学会疑惑,为什么第一步解出的SQL语句似乎对后续注入没有直接帮助?它没有告诉我们列名,也没有直接给出漏洞。 实际上,它的作用是多方面的:

  1. 心理暗示与方向确认:它直接证实了这是一道SQL注入题,并且注入点在username参数,节省了盲目测试的时间。
  2. 揭示查询结构:让我们100%确定查询语句是select * from user where username = '$name'。这让我们可以精准地构思union注入的Payload结构,而不需要去猜查询的是哪个表、哪个字段。
  3. 降低难度:如果没有这个提示,解题者可能需要通过' and '1'='1' and '1'='2进行布尔盲注来推断查询逻辑,题目难度和耗时会增加。这个提示是出题人给的“捷径”,确保考察重点落在MD5认证绕过这个核心逻辑上。

5.3 从这道题延伸的防御思考

作为开发者,如何防御此类攻击?

  1. 预处理语句(参数化查询):这是根治SQL注入的终极手段。使用PDOmysqli_prepare,将用户输入始终作为参数传递,而非SQL语句的一部分。
  2. 最小化错误信息:避免像wrong user!wrong pass!这样详细的差异化回显。统一返回“用户名或密码错误”。
  3. 密码哈希与加盐:本题虽然用了MD5,但现代应用应使用bcryptArgon2PBKDF2等强哈希算法,并务必为每个密码使用随机盐值。即使攻击者通过注入知道了哈希值,也无法反向破解或预计算彩虹表。
  4. 二次验证:在关键操作前加入二次验证(如验证码、Token),增加自动化攻击的难度。
  5. Web应用防火墙(WAF):部署WAF可以拦截常见的注入攻击Payload。

5.4 针对类似题目的通用解题框架

遇到CTF登录框注入题,可以遵循以下步骤:

  1. 信息收集:查看源码、抓包看响应头、尝试常见用户名(admin/test/guest)。
  2. 探测注入点与回显:用'"\测试,观察错误信息差异,判断是字符型还是数字型,是否有布尔状态回显。
  3. 获取查询信息:通过报错注入、布尔盲注或题目提示,尝试获取查询语句片段、表名、列名信息。
  4. 确定攻击方式
    • 有回显:联合查询注入。
    • 有布尔状态回显:布尔盲注。
    • 只有时间差异:时间盲注。
    • 有报错信息:报错注入。
  5. 分析认证逻辑:通过错误信息(如wrong pass!)判断是明文密码比对还是哈希比对。如果是哈希,需要想办法获取或伪造哈希值。
  6. 构造Payload:根据攻击方式和认证逻辑,精心构造绕过Payload。对于联合查询+哈希认证,核心就是union select伪造一条包含已知哈希的记录。
  7. 自动化与优化:如果步骤复杂(如盲注),可以编写Python脚本利用requests库进行自动化攻击。

这道BabySQli就像是一个经典的数学公式推导,每一步都建立在上一步的基础上,逻辑严密。它没有复杂的过滤,考验的就是你对SQL注入本质的理解和知识点串联的能力。下次再遇到登录框,不妨先想想:有没有提示?错误信息有没有差别?密码是怎么比的?想清楚这几个问题,解题的方向就有了。

← 返回列表