行业资讯
CTF Web安全入门:从MD5漏洞原理到实战绕过技巧详解
1. 项目概述从“EZMD5”看Web安全入门最近在带新人入门CTFCapture The Flag夺旗赛的Web安全方向发现很多朋友对基础的漏洞原理一知半解尤其是在面对看似简单的“签到题”时容易因为思路不清而卡壳。“青岑CTF”的“EZMD5”系列就是一个非常经典的入门级题目集合它没有复杂的绕过却精准地覆盖了MD5相关漏洞最核心、最常考的几种攻击姿势。这个系列之所以叫“EZ”简单是因为它剥离了现实渗透中复杂的混淆和防护直指漏洞原理本身是理解Web安全中“哈希函数攻击”的绝佳跳板。无论你是刚接触CTF的新手还是想巩固Web基础的安全爱好者通过彻底拆解这个系列你不仅能拿到Flag更能建立起一套遇到哈希比较、弱类型、SQL注入等经典漏洞时的条件反射和解题逻辑。接下来我就以从业者的视角带你一步步复盘“EZMD5”系列的解题思路与背后的安全原理。2. 核心漏洞原理与解题思路总览在深入每一道题之前我们必须先建立对MD5这个“主角”的完整认知。MD5是一种广泛使用的密码散列函数可以产生出一个128位16字节的散列值用于确保信息传输完整一致。在CTF和早期Web开发中它常被用于校验用户输入、进行密码哈希存储或作为简单的“签名”验证。然而其设计上的特性如碰撞、长度扩展攻击和开发人员的使用不当共同催生了一系列安全漏洞。“EZMD5”系列正是围绕这些“不当使用”展开的。整个系列的解题思路可以概括为以下几个核心方向它们也构成了Web安全中处理哈希类问题的通用方法论弱比较与类型混淆这是PHP语言特性与松散比较运算符或!结合产生的经典问题。当哈希值以“0e”开头且后面全为数字时在松散比较中会被当作科学计数法解析为数字0。利用这个特性我们可以寻找两个不同的字符串它们的MD5哈希值都是“0e”开头的数字串从而绕过的比较。强比较与哈希碰撞当代码使用严格比较时“0e”漏洞失效。此时需要寻找真正的MD5碰撞即两个不同的明文输入经过MD5计算后得到完全相同的哈希值。虽然理论上寻找任意碰撞极其困难但对于特定格式如序列化字符串或利用已知的碰撞对题目可能会降低难度。SQL注入与哈希绕过将用户输入的MD5哈希值直接拼接到SQL语句中是另一个常见漏洞。如果哈希值中的某些字符如单引号未被转义就可能闭合SQL语句实现注入。我们需要构造一个输入使其MD5哈希值本身包含注入载荷。数组绕过与NULL特性MD5函数和许多其他哈希函数在PHP中有一个重要特性如果传入的参数是一个数组函数会返回NULL并可能产生警告。利用这一点如果代码中使用了md5($a) md5($b)这样的比较且我们可以控制$a和$b为数组那么两边都会返回NULLNULL NULL在松散比较下为真从而绕过验证。理解这四点就掌握了破解“EZMD5”系列乃至类似题目的钥匙。下面我们进入具体的实战拆解环节。2.1 环境准备与工具选择工欲善其事必先利其器。在开始解题前一个高效的本地测试环境至关重要。我推荐使用Docker快速搭建一个包含PHP和MySQL的Web环境这能让你随意修改代码、调试而不用担心影响线上题目。对于“EZMD5”这类题目我们主要需要两类工具编程/脚本环境Python是首选因为它有强大的hashlib库用于计算MD5requests库用于发送HTTP请求写起EXP漏洞利用脚本来非常快捷。Web调试与抓包工具Burp Suite是绝对的核心。它的Repeater功能允许你手动修改和重放请求Intruder功能可以用于爆破Proxy能拦截所有流量是分析HTTP请求/响应的瑞士军刀。浏览器开发者工具F12则用于快速查看前端代码和网络请求。一个简单的本地测试PHP脚本可能长这样用于验证你的思路?php $a 240610708; $b QNKCDZO; var_dump(md5($a)); // string(32) 0e462097431906509019562988736854 var_dump(md5($b)); // string(32) 0e830400451993494058024219903391 var_dump(md5($a) md5($b)); // bool(true) var_dump(md5($a) md5($b)); // bool(false) ?注意在真实解题时题目源码不会直接给你。你需要通过前端代码分析、参数尝试、错误信息回显等方式反推后端可能的逻辑。这本身就是CTF Web题的重要能力。3. EZMD5 典型题目实战拆解我们假设“青岑CTF EZMD5”系列包含5道题目难度循序渐进。下面我将逐一进行详解并附上我实战中的思考过程和踩过的坑。3.1 题目一弱比较的经典“0e”绕过题目场景通常是一个简单的登录框或参数校验前端提示需要传递两个参数a和b后端代码逻辑大致如下if ($_GET[a] ! $_GET[b] md5($_GET[a]) md5($_GET[b])) { echo $flag; }解题思路条件分析需要a和b的值不相等但它们的MD5值在松散比较下相等。利用原理寻找MD5哈希值为“0e”开头的数字串的字符串。在PHP松散比较中0e123456 0e987654会被判定为True因为两者都被当作科学计数法0的N次方解析为数字0。寻找Payload网络上有很多已知的“0e”MD5碰撞对。最经典的一对是a240610708- md5:0e462097431906509019562988736854bQNKCDZO- md5:0e830400451993494058024219903391你可以写个Python脚本快速验证import hashlib print(hashlib.md5(240610708.encode()).hexdigest()) print(hashlib.md5(QNKCDZO.encode()).hexdigest())构造请求使用Burp Suite的Repeater发送一个GET请求例如GET /challenge1.php?a240610708bQNKCDZO HTTP/1.1获取Flag服务器返回响应其中包含Flag。实操心得这道题是送分题但关键在于建立“遇到比较MD5先想0e”的条件反射。有时候题目可能会要求POST传参或者参数名不是a和b仔细查看前端源码或进行参数爆破就能发现。3.2 题目二严格比较下的“真”碰撞题目场景代码升级了使用了严格比较if ($_GET[a] ! $_GET[b] md5($_GET[a]) md5($_GET[b])) { echo $flag; }解题思路条件分析要求类型和值都完全相同“0e”漏洞失效。需要找到两个不同的字符串它们的MD5哈希值完全一样。利用原理寻找MD5碰撞。MD5碰撞在2004年已被证明可行并且存在公开的碰撞实例。题目很可能期望你使用一个著名的碰撞对或者利用文件上传等场景上传两个MD5值相同的不同文件。已知碰撞对一个非常著名的碰撞对是字符串A:\x4d\xc9\x68\xff\x0e\xe3\x5c\x20\x95\x72\xd4\x77\x7b\x72\x15\x87\xd3\x6f\xa7\xb2\x1b\xdc\x56\xb7\x4a\x3d\xc0\x78\x3e\x7b\x95\x18\xaf\xbf\xa2\x00\xa8\x28\x4b\xf3\x6e\x8e\x4b\x55\xb3\x5f\x42\x75\x93\xd8\x49\x67\x6d\xa0\xd1\x55\x5d\x83\x60\xfb\x5f\x07\xfe\xa2字符串B:\x4d\xc9\x68\xff\x0e\xe3\x5c\x20\x95\x72\xd4\x77\x7b\x72\x15\x87\xd3\x6f\xa7\xb2\x1b\xdc\x56\xb7\x4a\x3d\xc0\x78\x3e\x7b\x95\x18\xaf\xbf\xa2\x02\xa8\x28\x4b\xf3\x6e\x8e\x4b\x55\xb3\x5f\x42\x75\x93\xd8\x49\x67\x6d\xa0\xd1\xd5\x5d\x83\x60\xfb\x5f\x07\xfe\xa2这两个二进制串的MD5都是008ee33a9d58b51cfeb425b0959121c9。在Web题目中我们需要对其进行URL编码后传递。构造请求由于这些是不可打印字符需要通过Burp Suite的Hex视图直接编辑原始十六进制值或者使用Python的requests库以字节流形式发送。import requests url http://target/challenge2.php a b\x4d\xc9\x68\xff\x0e\xe3\x5c\x20\x95\x72\xd4\x77\x7b\x72\x15\x87\xd3\x6f\xa7\xb2\x1b\xdc\x56\xb7\x4a\x3d\xc0\x78\x3e\x7b\x95\x18\xaf\xbf\xa2\x00\xa8\x28\x4b\xf3\x6e\x8e\x4b\x55\xb3\x5f\x42\x75\x93\xd8\x49\x67\x6d\xa0\xd1\x55\x5d\x83\x60\xfb\x5f\x07\xfe\xa2 b b\x4d\xc9\x68\xff\x0e\xe3\x5c\x20\x95\x72\xd4\x77\x7b\x72\x15\x87\xd3\x6f\xa7\xb2\x1b\xdc\x56\xb7\x4a\x3d\xc0\x78\x3e\x7b\x95\x18\xaf\xbf\xa2\x02\xa8\x28\x4b\xf3\x6e\x8e\x4b\x55\xb3\x5f\x42\x75\x93\xd8\x49\x67\x6d\xa0\xd1\xd5\x5d\x83\x60\xfb\x5f\x07\xfe\xa2 # 注意需要根据题目实际情况对参数进行编码有时是raw body有时是multipart data {a: a, b: b} r requests.post(url, datadata) print(r.text)获取Flag成功绕过返回Flag。踩坑记录这道题最大的坑在于编码。如果你直接把碰撞对的十六进制字符串当成普通字符串粘贴到Burp的Params里它会进行URL编码破坏原始字节结构导致MD5计算错误。必须使用Burp Suite的“Paste from file”功能加载原始二进制文件或者在Repeater的Hex标签页直接编辑原始请求体。这是区分新手和老手的关键操作。3.3 题目三MD5哈希注入漏洞题目场景题目模拟了一个查询场景将用户输入的MD5值直接用于数据库查询。$id $_POST[id]; $hash md5($id); $sql SELECT * FROM users WHERE id{$id} AND hash{$hash}; // 如果查询结果不为空则输出flag解题思路漏洞分析这里存在一个二次注入的变种。用户输入的id先被MD5哈希然后id和hash都被直接拼接到SQL语句中。目标是让最终的SQL语句语法正确且能查询出数据或利用布尔盲注等。构造Payload我们需要构造一个id使得其MD5值hash中包含SQL注入的闭合符号。例如我们想让最终的SQL变成SELECT * FROM users WHERE id1 AND hashxxxxxx or 11那么我们需要md5(id)的结果是xxxxxx or 11。这看起来不可能因为MD5输出是固定的32位十六进制字符串0-9, a-f不包含单引号。但是如果后端在接收hash参数时不是用md5($id)计算而是直接取$_POST[‘hash’]呢题目可能在这里设置了陷阱。我们需要仔细审题或者进行参数探测。另一种常见设定更常见的考法是代码逻辑是if ($_POST[‘hash’] md5($_POST[‘id’])) { // 执行SQL查询 }。那么注入点就在id参数里。我们需要构造一个id其本身是一个注入Payload同时保证md5(id)与提交的hash值相等。这通常需要结合“0e”绕过。例如提交id1 or 11和hash0e...但需要找到一个以1 or 11为明文MD5是0e开头的字符串这很难。实战技巧遇到这种题先进行常规的SQL注入测试在id参数后加单引号‘看是否有报错。如果有报错说明id参数可直接注入。如果没报错再尝试同时提交hash参数并观察hash参数是否被使用。有时题目可能只是简单地检查$_REQUEST[‘hash’] md5($_REQUEST[‘id’])那么我们可以通过数组绕过见下一题来使等式成立从而进入SQL查询环节再在id里进行注入。注意事项SQL注入与MD5结合的题目考察的是对代码逻辑的完整理解。不要一看到MD5就只想着碰撞和0e要清晰地画出数据流用户输入 - 变量赋值 - 哈希计算 - 字符串拼接 - 查询执行。每一步都可能存在可控点。3.4 题目四数组绕过与NULL的妙用题目场景代码可能如下if (md5($_GET[a]) md5($_GET[b])) { if ($_GET[a] ! $_GET[b]) { echo $flag; } }或者更直接的if (md5($_GET[a]) md5($_GET[b])) { echo $flag; }解题思路条件分析需要md5(a) md5(b)。如果a和b是字符串我们又回到了寻找“0e”或碰撞的老路。但题目没有限制参数类型。利用原理在PHP中md5()函数处理数组时会返回NULL并产生一个警告符号可以抑制此警告。同时sha1()等哈希函数也有此特性。因此如果a[]1b[]2那么md5($a)和md5($b)的结果都是NULL。在松散比较中NULL NULL为True。构造Payload非常简单使用GET请求传递数组参数即可。GET /challenge4.php?a[]1b[]2 HTTP/1.1或者GET /challenge4.php?a[]1b[]1 HTTP/1.1即使a和b数组的值相同它们也是两个不同的数组变量满足$_GET[‘a’] ! $_GET[‘b’]的条件如果题目有此要求。获取Flag服务器返回Flag。实操心得这是最快的一道题也是很多新手容易忽略的“捷径”。它的核心是理解PHP的语言特性。在审计真实代码或CTF做题时一旦看到md5($var1) md5($var2)这种比较而$var1和$var2又来自用户可控输入如$_GET,$_POST,$_COOKIE要立刻想到数组绕过和0e绕过这两种可能性并优先尝试数组绕过因为它更通用不依赖特定的哈希值。3.5 题目五综合挑战与“双料”绕过题目场景一道题融合了多个技巧。例如error_reporting(0); include flag.php; $a $_GET[a]; $b $_GET[b]; if ($a ! $b) { if (md5($a) md5($b)) { if (hash(sha256, $a) hash(sha256, $b)) { echo $flag; } else { echo Nice try, but you need to go further.; } } else { echo MD5 mismatch.; } } else { echo a and b should be different.; }解题思路条件分析需要满足三个条件a ! bmd5(a) md5(b)松散比较sha256(a) sha256(b)严格比较矛盾点前两个条件可以用“0e”对轻松满足。但第三个条件要求sha256严格相等这意味着a和b必须是完全相同的字符串这与第一个条件a ! b矛盾。破局关键再次利用数组绕过的特性。md5(array)返回NULLsha1(array)返回NULLhash(sha256, array)同样会返回NULL实际上会返回False并报错但配合或错误抑制在比较时可能产生意外效果。但更稳妥的思路是题目可能期望我们利用PHP的引用或特殊字符处理不这太复杂了。回顾之前的知识对于严格比较数组返回的不是一个可用于比较的有效字符串。hash(‘sha256’, $a)如果$a是数组会返回False。那么False False是成立的。构造Payload因此最终的Payload仍然是传递数组。GET /challenge5.php?a[]1b[]2 HTTP/1.1验证a[]1和b[]2是两个不同的数组满足$a ! $b。md5($a)和md5($b)都是NULLNULL NULL为True。hash(sha256, $a)和hash(sha256, $b)都是FalseFalse False为True。获取Flag三个条件同时满足输出Flag。踩坑记录这道题是典型的“思维定势”杀手。很多人看到sha256严格比较第一反应是寻找sha256碰撞那几乎是不可能的。解题的关键在于跳出“哈希函数一定接收字符串”的惯性思维回到PHP变量类型的本源。在CTF中当遇到看似无解的矛盾时一定要重新审视变量的类型和来源考虑非字符串输入数组、对象、NULL可能带来的奇效。4. 工具链深度使用与脚本编写手动构造请求适合学习原理但在实战或题目有变种时自动化脚本能极大提升效率。这里分享几个我常用的脚本片段和Burp Suite技巧。4.1 Python自动化利用脚本一个通用的用于探测和利用MD5相关漏洞的脚本框架import hashlib import requests import sys def find_0e_collision(): 查找0e开头的MD5字符串对本地预计算或使用已知对 known_pairs [ (240610708, QNKCDZO), (s878926199a, s155964671a), (s214587387a, s214587387a), ] # 可以在这里添加一个暴力查找小字典的循环用于应对未知的0e字符串 return known_pairs def send_payload(url, param_a, param_b, methodGET): 发送Payload并检查响应中是否包含flag特征 if method.upper() GET: params {a: param_a, b: param_b} resp requests.get(url, paramsparams) else: data {a: param_a, b: param_b} resp requests.post(url, datadata) # 根据题目Flag格式调整常见的有 flag{、FLAG{、青岑CTF{ 等 if flag{ in resp.text.lower() or 青岑 in resp.text: print(f[] Success! Response:\n{resp.text[:500]}) # 只打印前500字符 return True else: print(f[-] Attempt failed with a{param_a}, b{param_b}) return False def main(target_url): print(f[*] Testing target: {target_url}) # 1. 尝试数组绕过 (最高优先级最快) print([*] Trying array bypass...) if send_payload(target_url, 1[], 2[]): return # 2. 尝试经典0e绕过 print([*] Trying classic 0e bypass...) for a, b in find_0e_collision(): if send_payload(target_url, a, b): return # 3. 尝试POST请求如果GET不行 print([*] Switching to POST method...) if send_payload(target_url, 1[], 2[], POST): return for a, b in find_0e_collision(): if send_payload(target_url, a, b, POST): return print([!] All common bypass attempts failed.) print([!] Consider: 1. Parameter name different? 2. Need MD5 real collision? 3. Other tricks?) if __name__ __main__: if len(sys.argv) ! 2: print(fUsage: {sys.argv[0]} target_url) sys.exit(1) main(sys.argv[1])4.2 Burp Suite高效技巧Intruder爆破参数名如果题目隐藏了参数名可以使用Burp Intruder的“Sniper”模式加载一个常见的参数名字典如a, b, id, hash, key, value, param1, param2等对请求进行爆破观察响应长度或内容的变化。Repeater与Comparer结合在Repeater中测试不同Payload时可以将成功的响应和失败的响应分别发送到Comparer右键菜单通过对比差异快速定位到Flag出现的位置。Decoder模块处理编码当遇到需要传递二进制碰撞对或特殊编码时Burp Decoder非常有用。你可以将碰撞对的十六进制字符串粘贴进去先解码为Raw Bytes再编码为URL编码或Base64以适应题目要求。Match and Replace规则如果你需要反复测试但每次都要修改多个参数可以在Proxy - Options 里设置“Match and Replace”规则自动修改请求中的特定字符串节省时间。5. 漏洞原理延伸与防御思考通过“EZMD5”系列我们实际上是在学习如何攻击“错误使用密码学原语”的代码。作为开发者应该如何避免这些坑呢永远使用严格比较和!这是防御“0e”类弱类型比较攻击的最根本、最有效的方法。在PHP中凡是涉及用户输入的比较务必使用严格比较。避免将哈希值用于身份验证以外的目的MD5等哈希值不应该被直接拼接到SQL语句中。如果需要基于哈希值查询应该使用参数化查询Prepared Statements或至少进行正确的转义。理解函数输入类型像md5()、sha1()这类函数设计上是处理字符串的。如果传入数组会返回NULL。在代码中应该对用户输入进行类型检查确保传入的是字符串。可以使用is_string()函数进行判断。升级哈希算法MD5和SHA-1已经被证明是不安全的存在碰撞攻击。对于密码存储应该使用专门设计的、慢哈希函数如bcrypt、Argon2或PBKDF2并配合唯一的盐值Salt。对于数据完整性校验至少使用SHA-256或更强的算法。不要抑制错误在生产环境中避免使用错误抑制运算符。错误和警告是发现潜在问题如数组被传入md5()的重要信号。应该正确配置错误日志并处理可能的异常。一个安全的哈希比较示例应该是这样的// 安全的比较方式 function safe_compare($input, $known_hash) { // 1. 确保输入是字符串 if (!is_string($input)) { return false; } // 2. 使用hash_equals进行恒定时间比较防止时序攻击 $input_hash hash(sha256, $input); return hash_equals($known_hash, $input_hash); } // 使用参数化查询防止SQL注入 $stmt $pdo-prepare(SELECT * FROM users WHERE id ? AND hash ?); $stmt-execute([$id, $hash]);6. 实战中可能遇到的变种与应对“EZMD5”是入门但现实中的题目和代码会更复杂。这里列举几种变种及思路双重MD5或嵌套哈希如md5(md5($str))。破解思路不变依然寻找第一层或第二层MD5的“0e”碰撞或数组绕过。有时需要写脚本暴力查找。哈希值部分比较如substr(md5($a), 0, 10) substr(md5($b), 0, 10)。比较哈希值的前N位。这降低了碰撞难度可以尝试用脚本暴力破解或者依然尝试数组绕过substr(NULL, ...)会返回什么需要测试。结合其他验证比如先检查a和b是否都是数字再比较MD5。这时数组绕过可能失效因为is_numeric(array)为False需要寻找数字字符串的“0e”碰撞对例如0e215962017和0e291242476的MD5都是“0e”开头需要验证。JSON或序列化输入参数可能是一个JSON字符串如{a:value1, b:value2}后端会解析后再进行MD5比较。这时需要将整个Payload构造在JSON值中并注意转义。文件上传场景要求上传两个MD5值相同的不同文件。这就需要使用著名的MD5碰撞生成工具如fastcoll来生成两个内容不同但MD5相同的文件然后上传。应对变种的核心能力是代码审计能力和逻辑推理能力。你需要根据题目反馈源码、错误信息、回显差异来动态调整攻击链。我个人的习惯是拿到Web题先F12看前端源码和网络请求然后用Burp把所有能点的、能输入的地方都测试一遍观察响应在心里构建出后端代码的可能逻辑图然后再有针对性地设计Payload。最后再分享一个我自己的小技巧建立一个本地的“武器库”文件里面分类存放着各种Payload比如“0e字符串对”、“MD5碰撞对”、“特殊数组Payload”、“SQL注入测试语句”等。遇到题目时不是每次都从头开始想而是先尝试从武器库里匹配可能有效的Payload往往能快速打开突破口。CTF Web入门一半靠知识一半靠经验和效率。
郑州网站建设
网页设计
企业官网