
1. 项目概述一次典型的CTF Web题通关实录最近在复盘一些经典的CTF Web题目BUUCTF平台上的[GXYCTF2019]BabySQli 1这道题给我留下了挺深的印象。它不像那些单纯堆砌过滤规则的“炫技”题而是把几个非常基础但关键的知识点——Base编码、MD5认证和联合查询注入——巧妙地串联在了一起形成了一个逻辑闭环。整个过程就像在解一个设计精巧的谜题每一步的发现都为下一步铺平道路非常适合用来理解SQL注入攻击中“信息获取”与“逻辑绕过”的核心思想。这道题考察的不是多么偏门的技巧而是对基础知识的扎实掌握和灵活运用能力。无论你是刚接触CTF的新手想通过一道题串联起多个知识点还是有一定经验的选手想温故知新、梳理思路这道题都是一个绝佳的练手对象。接下来我就带你完整地走一遍我的解题思路和实操过程我会把每个环节的“为什么”讲清楚并分享一些在实战中容易踩坑的细节。2. 环境初探与信息收集2.1 题目界面与功能分析拿到题目链接第一件事永远是打开看看它长什么样。这是一个典型的登录界面只有一个用户名username和一个密码password的输入框外加一个提交按钮。功能极其简单输入凭证验证返回结果。这种极简的界面往往意味着后端逻辑是考察的重点。我首先尝试了几个常见的测试用例基础注入探测在用户名框输入admin或1 or 11密码随意。发现页面返回了统一的错误信息“wrong user!”。这个反馈很关键它直接告诉我们程序会先判断用户名是否存在。密码错误测试输入一个合理的用户名比如猜测的admin和一个错误密码返回信息变成了“wrong pass!”。这说明在用户名验证通过后会进行密码校验。错误信息差异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这里有两个非常重要的信息后端SQL语句我们拿到了查询语句的模板。它是对user表进行查询where条件只有用户名。这验证了我们之前的猜测。注入点位置注入点显然在$name这个变量上也就是我们提交的username参数。实操心得CTF中藏在HTML注释、JS文件、HTTP响应头里的信息往往是解题的钥匙。养成随手查看源代码、抓包分析所有响应的习惯至关重要。对于编码字符串如果Base64解码失败可以依次尝试Base32、Base16Hex、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条件不成立原表没有adminunion select也会强行“制造”出一条记录返回。关键在于union前后查询的列数必须一致。我们不知道原表user有多少列。3.2 确定查询列数使用order by或union select来猜解列数。输入usernameadmin order by 1-- password1输入usernameadmin order by 2-- password1输入usernameadmin order by 3-- password1输入usernameadmin order by 4-- password1当order by 3时页面正常可能返回wrong user!而order by 4时页面可能报错或返回异常说明原查询结果有3列。用union select验证usernameadmin union select 1,2,3-- password1。如果页面返回wrong pass!说明联合查询成功执行并且我们“制造”的记录被后端当成了查询结果。页面可能会显示2或3的位置如果存在数据回显的话但本题没有直接回显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我们需要解决两个问题目标用户的密码哈希值是什么我们不知道。但我们可以自己设定。比如我们选择密码明文为123456其MD5哈希值是e10adc3949ba59abbe56e057f20f883e。如何让联合查询返回我们设定的哈希值我们需要知道密码字段在查询结果中的位置。通过之前的union select 1,2,3测试如果页面状态正常说明列数正确但具体哪一列对应username哪一列对应password需要猜测。在登录逻辑中通常查询结果的第一列是id第二列是username第三列是password。我们可以基于这个常见结构进行尝试。因此最终的Payload构造如下usernameadmin union select 1,admin,e10adc3949ba59abbe56e057f20f883e-- password123456逻辑解释union select 1,admin,e10adc3949ba59abbe56e057f20f883e伪造一条记录id1,usernameadmin,passworde10adc3949ba59abbe56e057f20f883e。--注释掉原SQL语句中剩下的单引号避免语法错误。当我们提交时username参数就是上面这一整段注入Payload。后端执行SQL因为where usernameadmin在真实表中可能找不到记录但union我们伪造的记录会返回。后端取到伪造记录的密码字段值e10adc3949ba59abbe56e057f20f883e。我们提交的password123456被后端进行MD5计算结果正好是e10adc3949ba59abbe56e057f20f883e。比对成功登录成功获取Flag。4. 完整实操过程与细节实现4.1 工具准备与测试环境我通常使用Burp Suite作为主要工具它的Repeater模块非常适合这种需要反复修改Payload、观察响应的场景。浏览器配合HackBar插件也能快速测试。配置代理确保浏览器流量经过Burp Suite。抓取登录请求在浏览器提交一次任意登录Burp Suite的Proxy模块会截获这个POST请求。发送至Repeater将抓到的请求右键发送到Repeater模块方便后续操作。原始请求大概长这样POST /challenge.php HTTP/1.1 Host: xxx.buuoj.cn ... Content-Type: application/x-www-form-urlencoded usernametestpasswordtest4.2 分步注入攻击流程在Burp Suite Repeater中我们逐步修改username参数进行测试。第一步验证注入点与错误回显usernameadminpassword1观察响应体确认是否包含wrong user!。如果存在说明单引号成功闭合了SQL语句注入点存在。第二步确定列数使用order by语句。usernameadmin order by 1-- password1 usernameadmin order by 2-- password1 usernameadmin order by 3-- password1 usernameadmin order by 4-- password1我发现order by 3时页面返回wrong user!状态正常而order by 4时页面返回了一个SQL语法错误或者空白页。这明确表明原查询有3列。第三步验证联合查询可行性usernameadmin union select 1,2,3-- password1提交后页面返回wrong pass!。这是一个极其积极的信号。它说明union select 1,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。构造Payloadusernameadmin union select 1,admin,4124bc0a9335c27f086f24ba207a4912-- passwordaa这里union select伪造了一条记录id1,usernameadmin,password4124bc0a9335c27f086f24ba207a4912。第五步发送请求获取Flag将上述构造好的请求在Burp Suite Repeater中发送。如果一切正确响应页面将不再显示wrong user!或wrong pass!而是会显示登录成功的消息其中包含本题的Flag。在我的实际测试中发送请求后页面返回了“恭喜你flag是flag{xxxx-xxxx-xxxx-xxxx}”。4.3 关键参数与编码处理细节URL编码在Burp Suite中直接输入、空格、--等字符工具通常会自动进行URL编码。空格会变成%20或单引号会变成%27--后面的空格有时很重要。最终在Repeater里看到的请求应该是usernameadmin%27%20union%20select%201%2C%27admin%27%2C%274124bc0a9335c27f086f24ba207a4912%27--%20passwordaa确保你的工具正确进行了编码否则可能导致语法错误。注释符MySQL中--是单行注释符但注意后面要跟一个空格即--。在URL编码里这个空格至关重要。有时也可以用#URL编码为%23作为注释符。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 11。特殊字符过滤题目是否过滤了union、select、空格可以尝试双写绕过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语句似乎对后续注入没有直接帮助它没有告诉我们列名也没有直接给出漏洞。 实际上它的作用是多方面的心理暗示与方向确认它直接证实了这是一道SQL注入题并且注入点在username参数节省了盲目测试的时间。揭示查询结构让我们100%确定查询语句是select * from user where username $name。这让我们可以精准地构思union注入的Payload结构而不需要去猜查询的是哪个表、哪个字段。降低难度如果没有这个提示解题者可能需要通过 and 11、 and 12进行布尔盲注来推断查询逻辑题目难度和耗时会增加。这个提示是出题人给的“捷径”确保考察重点落在MD5认证绕过这个核心逻辑上。5.3 从这道题延伸的防御思考作为开发者如何防御此类攻击预处理语句参数化查询这是根治SQL注入的终极手段。使用PDO或mysqli_prepare将用户输入始终作为参数传递而非SQL语句的一部分。最小化错误信息避免像wrong user!和wrong pass!这样详细的差异化回显。统一返回“用户名或密码错误”。密码哈希与加盐本题虽然用了MD5但现代应用应使用bcrypt、Argon2或PBKDF2等强哈希算法并务必为每个密码使用随机盐值。即使攻击者通过注入知道了哈希值也无法反向破解或预计算彩虹表。二次验证在关键操作前加入二次验证如验证码、Token增加自动化攻击的难度。Web应用防火墙WAF部署WAF可以拦截常见的注入攻击Payload。5.4 针对类似题目的通用解题框架遇到CTF登录框注入题可以遵循以下步骤信息收集查看源码、抓包看响应头、尝试常见用户名(admin/test/guest)。探测注入点与回显用、、\测试观察错误信息差异判断是字符型还是数字型是否有布尔状态回显。获取查询信息通过报错注入、布尔盲注或题目提示尝试获取查询语句片段、表名、列名信息。确定攻击方式有回显联合查询注入。有布尔状态回显布尔盲注。只有时间差异时间盲注。有报错信息报错注入。分析认证逻辑通过错误信息如wrong pass!判断是明文密码比对还是哈希比对。如果是哈希需要想办法获取或伪造哈希值。构造Payload根据攻击方式和认证逻辑精心构造绕过Payload。对于联合查询哈希认证核心就是union select伪造一条包含已知哈希的记录。自动化与优化如果步骤复杂如盲注可以编写Python脚本利用requests库进行自动化攻击。这道BabySQli就像是一个经典的数学公式推导每一步都建立在上一步的基础上逻辑严密。它没有复杂的过滤考验的就是你对SQL注入本质的理解和知识点串联的能力。下次再遇到登录框不妨先想想有没有提示错误信息有没有差别密码是怎么比的想清楚这几个问题解题的方向就有了。