前言:

在CTF中可以说是经常碰到md5加密了,一般都是进行强比较抑或是弱比较,考法非常多,但是万变不离其中。只要我们掌握了原理,一切问题便迎刃而解了。

文章首发于 我的博客 ,格式可能比较清晰,有兴趣了解CTF中MD5碰撞的伙伴可以移步查看

简单了解MD5:

  • md5是一种加密算法,并且不能防止碰撞破解。
  • md5加密是不可逆的,这就意味着有两串不同的字符串加密出来的内容却是相同的
  • 加密过程简单,碰撞还原字符难

PHP的弱比较:

先提两个例子:

var_dump(“123a”==123)

var_dump(“123a”==”123”)

在没有认真总结前,完全不知道弱比较还要区分与字符串类型比较还是与int类型比较

上述实例的结果是:

True

False

字符串与int类型比较:

PHP规定当进行字符串与数字的弱比较时,会进行如下步骤:

先看字符串开头是否为数字,如果为数字,则截止到连续数字的最后一个数字,即”123abc456″=>123

如果开头不为数字,则判断为false,即0。因此

(“aaa123″==0) =>true

(“123a”==123) =>true

思维导图:

字符串与字符串比较:

正如上面所言:

var_dump(“123a”==”123”); //False

因为这个是字符串之间进行比较,想要绕过这个弱比较只能用0e的方式。

在PHP中”0e”判断为科学计数法,0e123就是0的10123次方

不难推出:0e123456789==0e1 // 因为0的任意次方都为0

不过有一个注意点:

“0e123456″==”0e345” //True

“0e12adfc”==”0e345” //False

在0e后面不能含有字母!!!

在0e后面不能含有字母!!!

在0e后面不能含有字母!!!

否则判断为False

#实例

<?php

if(“0e23253″==”0e2345”)

{

echo ‘yes’;

}

?>

输出:

yes

CTF的MD5弱比较

在CTF中,会遇到如下的MD5弱比较题目

1.md5($a)==md5($b) & $a != $b

<?php
include "flag.php";
$md5_1=$_GET['md5_1'];
$md5_2=$_GET['md5_2'];
if(md5($md5_1)==md5($md5_2) & $md5_1 != $md5_2)
{
    echo $flag;
}
else
{
    echo "try harder";
}
?>

这个时候就要利用0e的形式来解题,找到两个不同字符,md5加密后却都是0e324234的形式。如何寻找这样的字符串?

1.脚本寻找

# -*- coding: utf-8 -*-
import multiprocessing
import hashlib
import random
import string
import sys

CHARS = string.ascii_letters + string.digits


def cmp_md5(substr, stop_event, str_len, start=0, size=20):
    global CHARS
    while not stop_event.is_set():
        rnds = ''.join(random.choice(CHARS) for _ in range(size))
        md5 = hashlib.md5(rnds)
        value = md5.hexdigest()
        if value[start: start + str_len] == substr:
            # print rnds
            # stop_event.set()

            # 碰撞双md5
            md5 = hashlib.md5(value)
            if md5.hexdigest()[start: start + str_len] == substr:
                print rnds + "=>" + value + "=>" + md5.hexdigest() + "\n"
                stop_event.set()



if __name__ == '__main__':
    substr = sys.argv[1].strip()
    start_pos = int(sys.argv[2]) if len(sys.argv) > 1 else 0
    str_len = len(substr)
    cpus = multiprocessing.cpu_count()
    stop_event = multiprocessing.Event()
    processes = [multiprocessing.Process(target=cmp_md5, args=(substr,
                                                               stop_event, str_len, start_pos))
                 for i in range(cpus)]
    for p in processes:
        p.start()
    for p in processes:
        p.join()

用法:

输入命令

python md5.py “0e” 0

“0e” =>要跑的字符

0 =>要跑的字符的起始位置

脚本寻找要浪费大概十分钟左右的时间才能找出一个,可以用网上现成的,如果题目要求比较特殊的话,再利用自己的脚本跑

2.百度

MMHUWUV 0e701732711630150438129209816536

MAUXXQC 0e478478466848439040434801845361

IHKFRNS 0e256160682445802696926137988570

GZECLQZ 0e537612333747236407713628225676

GGHMVOE 0e362766013028313274586933780773

GEGHBXL 0e248776895502908863709684713578

EEIZDOI 0e782601363539291779881938479162

DYAXWCA 0e424759758842488633464374063001

这样子就出flag了

2.$a==md5($a)

这一类题型要求满足$a是0e开头,且加密后也是0e开头

在网上收集了这些结果:

0e215962017 0e291242476940776845150308577824

0e1284838308 0e708279691820928818722257405159

0e1137126905 0e291659922323405260514745084877

0e807097110 0e318093639164485566453180786895

0e730083352 0e870635875304277170259950255928

弱比较主要就是以上两种类型。

CTF的MD5强比较

1.md5($a)===md5($b) & $a != $b

方法一:

数组绕过

<?php
include "flag.php";
$md5_1=$_GET['md5_1'];
$md5_2=$_GET['md5_2'];
if(md5($md5_1)===md5($md5_2) & $md5_1 != $md5_2)
{
    echo $flag;
}
else
{
    echo "try harder";
}
?>

md5_1[]=1&md5_2[]=2

因为PHP对无法md5加密的东西不加密,结果为NULL,虽然会报错,但是null=null,逻辑关系为True。所以可以输出flag

方法二:

两串不一样的字符,加密结果却相同:

$a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2

$b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2

这个我的脚本就跑不出来了,是网上收集的。

题目实战:

1.[BJDCTF2020]Easy MD5

<?php
error_reporting(0);
include "flag.php";

highlight_file(__FILE__);

if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2'])){
    echo $flag;
}

这题也是两个解法:

法一:

param1[]=1¶m2[]=2

法二:

param1=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2¶m2=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2

2.强网杯2020——Funhash

<?php
include 'conn.php';
highlight_file("index.php");
//level 1
if ($_GET["hash1"] != hash("md4", $_GET["hash1"]))
{
    die('level 1 failed');
}
//level 2
if($_GET['hash2'] === $_GET['hash3'] || md5($_GET['hash2']) !== md5($_GET['hash3']))
{
    die('level 2 failed');
}
//level 3
$query = "SELECT * FROM flag WHERE password = '" . md5($_GET["hash4"],true) . "'";
$result = $mysqli->query($query);
$row = $result->fetch_assoc(); 
var_dump($row);
$result->free();
$mysqli->close();
?>

levle 1

很明显,这种是要md4加密,并且是”0e”+”数字” 加密后还是”0e”+”数字”的形式,上脚本:

import multiprocessing
import hashlib
import random
import string
import sys

CHARS = string.digits


def cmp_md4(substr, stop_event, str_len, start=0, size=18):
    global CHARS
    while not stop_event.is_set():
        rnds = ''.join(random.choice(CHARS) for _ in range(size))
        rnds = "0e"+rnds
        md4 = hashlib.new('md4', rnds.encode("utf-8"))
        value = md4.hexdigest()
        if value[start: start + str_len] == substr:
            print(value)
            if value[2:].isdigit():
                print(rnds)
                stop_event.set()
                
if __name__ == '__main__':
    substr = sys.argv[1].strip()
    start_pos = int(sys.argv[2]) if len(sys.argv) > 1 else 0
    str_len = len(substr)
    cpus = multiprocessing.cpu_count()
    stop_event = multiprocessing.Event()
    processes = [multiprocessing.Process(target=cmp_md4, args=(substr,
                                                               stop_event, str_len, start_pos))
                 for i in range(cpus)]
    for p in processes:
        p.start()
    for p in processes:
        p.join()

测试一下,可行

level 2

法一:

hash2[]=1&hash3[]=2

法二:

hash2=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2&hash3=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2

level 3

结果:

只要构造 xx ‘or xxxx 的形式就可以了

具体可以看这篇文章

3.构造特定字符串

这个是我觉得以后为了避免大家都用现有的收集字符串解题而出的,比如:

<?php
include("flag.php");
$a = md5($_GET['a']);
if($a==123)
{
  echo $flag;
}
else
{
  echo "nonono";
}
?>

要构造一个开头是123且后面是字母的字符串:

脚本:

import multiprocessing
import hashlib
import random
import string
import sys

CHARS = string.ascii_letters + string.digits


def cmp_md5(substr, stop_event, str_len, start=0, size=20):
    global CHARS
    while not stop_event.is_set():
        rnds = ''.join(random.choice(CHARS) for _ in range(size))
        md5 = hashlib.md5(rnds)
        value = md5.hexdigest()
        if value[start: start + str_len] == substr:
            print rnds
            print value
            stop_event.set()
            
            
            if __name__ == '__main__':
                substr = sys.argv[1].strip()
                start_pos = int(sys.argv[2]) if len(sys.argv) > 1 else 0
                str_len = len(substr)
                cpus = multiprocessing.cpu_count()
                stop_event = multiprocessing.Event()
                processes = [multiprocessing.Process(target=cmp_md5, args=(substr,
                                                                           stop_event, str_len, start_pos))
                             for i in range(cpus)]
                for p in processes:
                    p.start()
                    for p in processes:
        p.join()

终端输入python .\MD5碰撞.py “123” 0

实践试一下:

注意点:跑出来的123后面第一个字符要是字母,如果不是多跑几次,概率还是挺大的。

尾言

还有关于双md5的题目等等,只要掌握了这些思想,看到题目就能想到解法了。这个脚本也是面向百度编程找到的,有一个脚本能跑是比较好的,可以应对各种新情况。使用python脚本是因为有多线程模式,速度更快。