最近打了挺多ctf,碰到挺多關於md5的一些問題,或者一些變種的題目,雖然已經是爛大街的問題了,但是還是需要總結一下,方便下次比賽可以直接用腳本
CTF中的一些案例案例1——ciscn2020初賽——easytrick<?php
class trick{
public $trick1;
public $trick2;
public function __destruct(){
$this->trick1 = (string)$this->trick1;
if(strlen($this->trick1) > 5 || strlen($this->trick2) > 5){
die("你太長了");
}
if($this->trick1 !== $this->trick2 && md5($this->trick1) === md5($this->trick2) && $this->trick1 != $this->trick2){
echo file_get_contents("/flag");
}
}
}
highlight_file(__FILE__);
unserialize($_GET['trick']);
這裡我們拋開反序列化不談,這題本質上需要我們構造trick1=NAN,trick2=NAN即可繞過。原理也很簡單,NaN與所有值都不相等,包括它自己。當然也可以用INF繞過,原理類似,這裡不過多贅述
案例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();
這裡重點關注level 1,其他兩關都是老生常談的東西,無須過多贅述。這裡我已經寫好了腳本,下次遇到這種md4的題目直接爆破幹它
下面給出我已經爆破好的例子:
0e251288019
0e898201062
exp.php
<?php
$payload = "0123456789";
function calc_md4($s){
$s = "0e".$s;
$md4 = hash("md4", $s);
if (substr($md4, 0, 2) === "0e" && ctype_digit(substr($md4, 2))) {
echo $s.PHP_EOL;
}
}
function getstr($payload,$s,$slen){
if(strlen($s) == $slen){
calc_md4($s);
return $s;
}
for($i = 0;$i<strlen($payload);$i++){
$sl = $s.$payload[$i];
getstr($payload, $sl, $slen);
}
}
//字符串長度從3到30,肯定找得到
for($i = 3;$i<30;$i++){
getstr($payload,'',$i);
}
這裡我截取了一部分代碼,用作案例講解
$string_1 = $_GET['str1'];
$string_2 = $_GET['str2'];
//2nd
if(is_numeric($string_1)){
$md5_1 = md5($string_1);
$md5_2 = md5($string_2);
if($md5_1 != $md5_2){
$a = strtr($md5_1, 'cxhp', '0123');
$b = strtr($md5_2, 'cxhp', '0123');
if($a == $b){
echo '2nd ok'."<br>";
}
else{
die("can u give me the right str???");
}
}
else{
die("no!!!!!!!!");
}
}
這裡的關鍵就是,需要我們找兩個個是ce開頭的,然後ce後面是純數字的md5值
下面給出我已經爆破好的例子:
N9KU3
QlYRH
爆破的腳本如下
exp.py
import string
import hashlib
payload = string.ascii_letters+string.digits
def calc_md5(s):
md5 = hashlib.md5(s.encode("utf-8")).hexdigest()
if (md5[0:2] == "ce" and md5[2:].isdigit()) :
print(s)
def getstr(payload,s,slen):
if(len(s) == slen):
calc_md5(s)
return s
for i in payload:
sl = s+i
getstr(payload, sl, slen)
# 字符串長度從0到30,肯定找得到
for i in range(3,30):
getstr(payload,'',i)
<?php
if (isset($_GET['a']) && isset($_GET['b'])) {
$a = $_GET['a'];
$b = $_GET['b'];
if ($a != $b && md5($a) == md5(md5($b))) {
echo "flag{XXXXX}";
} else {
echo "wrong!";
}
} else {
echo 'wrong!';
}
?>
這裡其實我們只要找出md5(md5($b))是0e開頭的且0e後面是純數字的字符串即可
下面給出我已經爆破好的例子:
f2WfQ
iv2Cn
爆破腳本如下:
exp.py
import string
import hashlib
payload = string.ascii_letters + string.digits
def calc_md5(s):
md5 = hashlib.md5(s.encode("utf-8")).hexdigest()
md5_double = hashlib.md5(md5.encode("utf-8")).hexdigest()
if (md5_double[0:2] == "0e" and md5_double[2:].isdigit()):
print(s)
def getstr(payload, s, slen):
if (len(s) == slen):
calc_md5(s)
return s
for i in payload:
sl = s + i
getstr(payload, sl, slen)
# 字符串長度從0到30,肯定找得到
for i in range(3, 30):
getstr(payload, '', i)
<?php
$password = $_POST['password'];
$sql = "SELECT * FROM admin WHERE username = 'admin' and password = '".md5($password,true)."'";
$result = mysqli_query($link,$sql);
if(mysqli_num_rows($result)>0){
echo 'Success';
}else{
echo 'Failure';
}
?>
這裡md5輸出的是16 字符二進位格式,並不是我們平時看到的32字符十六進位數,所以我們只需要找md5加密後字符串中是否存在'or'字符串
下面給出我已經爆破好的例子:
ffifdyop
4SV7p
bJm4aG
bNas5p
ckHAEb
exp.php
<?php
$payload = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
function calc_md5_true($s)
{
$md5_true = md5($s,true);
if (strpos($md5_true,"'or'") !== false){
echo $s.PHP_EOL;
}
}
function getstr($payload, $s, $slen)
{
if (strlen($s) == $slen) {
calc_md5_true($s);
return $s;
}
for ($i = 0; $i < strlen($payload); $i++) {
$sl = $s . $payload[$i];
getstr($payload, $sl, $slen);
}
}
//字符串長度從3到30,肯定找得到
for ($i = 3; $i < 30; $i++) {
getstr($payload, '', $i);
}
這裡選取了部分代碼,用作演示
<?php
if((string)$_POST['param1']!==(string)$_POST['param2'] && md5($_POST['param1'])===md5($_POST['param2'])){
die("success!");
}
這裡因為對兩個參數都進行了強制類型轉換,所以一般的方法(用數組報錯繞過肯定是行不通的了),所以我們必須找到兩個文件,他們的內容不一樣,但是md5值相等
在網上找到兩張圖片,他們內容不一樣,但是md5值一樣
double_md5_1exp.py
# -*- coding: utf-8 -*-
import requests as req
url = 'http://localhost:82/WWW/test2.php'
with open('double_md5_1.jpg','rb') as f:
md51 = f.read()
with open('double_md5_2.jpg','rb') as f:
md52 = f.read()
data = {'param1':md51,'param2':md52}
r = req.post(url=url,data=data)
print(r.text)
下面給出我已經爆破好的例子:
exp.py
import string
import hashlib
payload = string.ascii_letters + string.digits
def calc_md5(s):
md5 = hashlib.md5(s.encode("utf-8")).hexdigest()
md5_double = hashlib.md5(md5.encode("utf-8")).hexdigest()
if (md5[0:2] == "0e" and md5[2:].isdigit() and md5_double[0:2] == "0e" and md5_double[2:].isdigit()):
print(s)
def getstr(payload, s, slen):
if (len(s) == slen):
calc_md5(s)
return s
for i in payload:
sl = s + i
getstr(payload, sl, slen)
# 字符串長度從0到30,肯定找得到
for i in range(3, 30):
getstr(payload, '', i)
在上個案例中,只是兩張圖片碰撞。這裡我們思考一個問題,如果是三張圖片呢,是否存在?答案是存在的
首先下載hashclash,如果是linux環境的話可以下載編譯好的二進位文件,如果是macos系統的話,就需要下載源碼,然後自行編譯,編譯成功如下:
mkdir ipc_workdir
cd ipc_workdir
echo -n "TEST" > prefix.txt
../scripts/poc_no.sh prefix.txt
稍等片刻後,就可以碰撞出來了
下載文件:
wget https://s3-eu-west-1.amazonaws.com/md5collisions/a.php
wget https://s3-eu-west-1.amazonaws.com/md5collisions/b.php
How I made two PHP files with the same MD5 hash
sha1碰撞下載文件
curl -sSO https://shattered.it/static/shattered-1.pdf
curl -sSO https://shattered.it/static/shattered-2.pdf
運用的原理如下:
MD5(file + col1_a + col2_a) === MD5(file + col1_a + col2_b) === MD5(file + col1_b + col2_a) === MD5(file + col1_b + col2_b)
這裡使用的工具還是hashclash
我們在在hashclash文件夾下創建工作目錄,然後執行下面的shell腳本
gen.sh
../bin/md5_fastcoll test.txt -o test_1.txt test_2.txt
../bin/md5_fastcoll test_1.txt -o test_1_1.txt test_1_2.txt
../bin/md5_fastcoll test_1_1.txt -o test_1_1_1.txt test_1_1_2.txt
./genfiles.py
genfiles.py
with open("test.txt","rb") as f:
base = f.read()
with open("test_1.txt","rb") as f:
file_1 = f.read()
coll_1 = file_1[len(base):]
with open("test_2.txt","rb") as f:
file_2 = f.read()
coll_2 = file_2[len(base):]
with open("test_1_1.txt","rb") as f:
file_1_1 = f.read()
coll_1_1 = file_1_1[len(file_1):]
with open("test_1_2.txt","rb") as f:
file_1_2 = f.read()
coll_1_2 = file_1_2[len(file_2):]
with open("test_1_1_1.txt","rb") as f:
file_1_1_1 = f.read()
coll_1_1_1 = file_1_1_1[len(file_1_1):]
with open("test_1_1_2.txt","rb") as f:
file_1_1_2 = f.read()
coll_1_1_2 = file_1_1_2[len(file_1_2):]
def w(fn, data):
f = open(fn, "wb")
f.write(data)
f.close()
w("test1.txt", base + coll_1 + coll_1_1 + coll_1_1_1)
w("test2.txt", base + coll_1 + coll_1_1 + coll_1_1_2)
w("test3.txt", base + coll_1 + coll_1_2 + coll_1_1_1)
w("test4.txt", base + coll_1 + coll_1_2 + coll_1_1_2)
w("test5.txt", base + coll_2 + coll_1_1 + coll_1_1_1)
w("test6.txt", base + coll_2 + coll_1_2 + coll_1_1_1)
這樣就可以生成6個文件內容不同,但是它們的md5都相同,事實上,你可以生成任意多個
總結其實很多題目都是類似的,需要我們自己動手寫腳本,本質上就是去找hash加密後的字符串前兩位是0e,後30位是純數字的,只要會寫腳本,就是萬變不離其宗
ReferenceMD5碰撞的一些例子
How to make two binaries with the same MD5 hash
How I made two PHP files with the same MD5 hash
Are there two known strings which have the same MD5 hash value?
SHA1 collision demo / example
ebctf 2013: MD5 COLLIDING
PHP中MD5碰撞Bypass