php-hash-bypass

php弱比较:

1
2
3
4
5
6
<?php
error_reporting(0);
$a = $_POST['a'];
$b = $_POST['b'];
if(md5($a) == md5($b) && $a != $b)
echo 'success';

使用md5值为0e开头的字符串即可绕过。(php会把0e开头的字符串当作科学计数法表示的数。比如:1.1e22 代表数 1.1 乘 10^22,0exxxxx 代表的是 0 乘 10^xxxxx,也就是 0),这种字符串有:

1
2
3
4
5
6
7
8
9
10
# php中MD5弱比较,常见的md5开头为0e的字符串

# QNKCDZO
# 240610708
# s878926199a
# s155964671a
# s214587387a
# 0e215962017

# 比如:POST a=QNKCDZO&b=240610708

如果若比较中多次进行md5或者和其他算法(如sha1)嵌套,那么需要python来爆破。比如下面的题目:

1
2
3
4
5
6
<?php
error_reporting(0);
$a = $_POST['a'];
$b = $_POST['b'];
if(md5(sha1($a)) == sha1(sha1(sha1($b))) && $a != $b)
echo 'success';

使用下面的python脚本来爆破:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env python3

import hashlib
from string import *
from itertools import permutations

"""
php hash brute force
"""

def get_hash(content: str, algorithm: list) -> str:
for algo in algorithm:
if algo not in hashlib.algorithms_available:
raise ValueError(f'{algo} is not available')

result = hashlib.new(algorithm[0], content.encode('utf-8')).hexdigest()
for algo in algorithm[1:]:
result = hashlib.new(algo, result.encode('utf-8')).hexdigest()
return result

# 默认情况下 爆破1次md5,目标长度是4,默认字符集是数字字母
def brute(algorithm=['md5'], charset=digits+ascii_letters, length=4):
for candidate in permutations(charset, length):
candidate_str = ''.join(candidate)
hash = get_hash(candidate_str, algorithm)
if hash.startswith('0e'):
return candidate_str, hash
raise Exception('Not Found')

if __name__ == '__main__':
# 对应php代码:md5(sha1($a))
print(brute(algorithm=['sha1', 'md5']))

# 对应php代码:sha1(sha1(sha1($b)))
print(brute(algorithm=['sha1']*3,))

php强比较

1
2
3
4
5
6
<?php
error_reporting(0);
$a = $_POST['a'];
$b = $_POST['b'];
if(md5($a) === md5($b) && $a !== $b)
echo 'success';

使用数组即可绕过。因为php中的数组经过md5处理,得到的是null。null === null。

1
# POST a[]=1&b[]=2

字符串强比较

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
error_reporting(0);
$a = $_POST['a'];
$b = $_POST['b'];
if(md5($a) === md5($b) && (string)$a !== (string)$b)
echo 'success';

$arr1 = array(1,2,3);
$arr2 = array();
echo (string)$arr1, "\n", (string)$arr2, "\n";
// Array
// Array
// 无论数组中是什么内容,都会被转换为Array,所以这个不能用数组绕过

此时数组不能绕过了,可以使用碰撞:[fastcoll](GitHub - brimstone/fastcoll)。

fastcoll

这个工具的作用是生成两个内容不同但md5相同的文件。

1
2
3
4
5
6
7
8
9
10
11
# 编译fastcoll
wget https://github.com/brimstone/fastcoll/archive/refs/heads/master.zip
unzip master.zip
cd fastcoll-master/
sudo apt install libboost1.74-all-dev
g++ *.cpp -std=c++11 -lboost_program_options -lboost_filesystem -o fastcoll-static -static
g++ *.cpp -std=c++11 -lboost_program_options -lboost_filesystem -o fastcoll

# 使用fastcoll生成两个md5相同内容不同的文件
./fastcoll -o a1.bin a2.bin
cp *.bin ..

工具的使用:

1
fastcoll -o a1.bin a2.bin

将碰撞出来的文件使用yakit读取,并且url编码:

1
apppple={{url({{file(C:\Users\zhao\Desktop\a1.bin)}})}}&banananana={{url({{file(C:\Users\zhao\Desktop\a2.bin)}})}}

为什么要用Yakit? 因为php和python的urlencode的结果发送出去都不行,离谱。

1
2
3
4
5
6
7
8
9
# 自己实现的url全编码,但是发出去也不对
def file2urlencode(file_path):
with open(file_path, "rb") as file:
binary_content = file.read()
url_encoded_content=''.join(["%"+hex(c)[2:].zfill(2) for c in binary_content])
return url_encoded_content

print(file2urlencode('./a1.bin'))
print(file2urlencode('./a2.bin'))

下面的php也是不行的:

1
2
3
<?php
echo urlencode(file_get_contents('a1.bin')), "\n";
echo urlencode(file_get_contents('a2.bin')), "\n";

MD5长度扩展攻击

原理自行上网了解。
攻击需要已知明文的长度和该明文对应的MD5值。
攻击脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from struct import pack, unpack
from math import floor, sin

class MD5:
def __init__(self):
self.A, self.B, self.C, self.D = \
(0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476) # initial values
self.r: list[int] = \
[7, 12, 17, 22] * 4 + [5, 9, 14, 20] * 4 + \
[4, 11, 16, 23] * 4 + [6, 10, 15, 21] * 4 # shift values
self.k: list[int] = \
[floor(abs(sin(i + 1)) * pow(2, 32))
for i in range(64)] # constants
def _lrot(self, x: int, n: int) -> int:
return (x << n) | (x >> 32 - n)

def update(self, chunk: bytes) -> None:
w = list(unpack('<'+'I'*16, chunk))
a, b, c, d = self.A, self.B, self.C, self.D
for i in range(64):
if i < 16:
f = (b & c) | ((~b) & d)
flag = i
elif i < 32:
f = (b & d) | (c & (~d))
flag = (5 * i + 1) % 16
elif i < 48:
f = (b ^ c ^ d)
flag = (3 * i + 5) % 16
else:
f = c ^ (b | (~d))
flag = (7 * i) % 16
tmp = b+self._lrot((a+f+self.k[i] + w[flag]) & 0xffffffff, self.r[i])
a, b, c, d = d, tmp & 0xffffffff, b, c
self.A = (self.A + a) & 0xffffffff
self.B = (self.B + b) & 0xffffffff
self.C = (self.C + c) & 0xffffffff
self.D = (self.D + d) & 0xffffffff

def extend(self, msg: bytes) -> None:
assert len(msg) % 64 == 0
for i in range(0, len(msg), 64):
self.update(msg[i:i + 64])

def padding(self, msg: bytes) -> bytes:
length = pack('<Q', len(msg) * 8)
msg += b'\x80'
msg += b'\x00' * ((56 - len(msg)) % 64)
msg += length
return msg

def digest(self) -> bytes:
return pack('<IIII', self.A, self.B, self.C, self.D)
def attack(message_len: int, known_hash: str, append_str: bytes) -> tuple:
md5 = MD5()
previous_text = md5.padding(b"*" * message_len)
current_text = previous_text + append_str
md5.A, md5.B, md5.C, md5.D = unpack("<IIII", bytes.fromhex(known_hash))
md5.extend(md5.padding(current_text)[len(previous_text):])
return current_text[message_len:], md5.digest().hex()

if __name__ == '__main__':
message_len = int(input("[>] Input known text length: "))
known_hash = input("[>] Input known hash: ").strip()
append_text = input("[>] Input append text: ").strip().encode()
print("[*] Attacking...")
extend_str, final_hash = attack(message_len, known_hash, append_text)
from urllib.parse import quote
from base64 import b64encode
print("[+] Extend text:", extend_str)
print("[+] Extend text (URL encoded):", quote(extend_str))
print("[+] Extend text (Base64):", b64encode(extend_str).decode())
print("[+] Final hash:", final_hash)

例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
session_start();
highlight_file(__FILE__);

// 你以为这就结束了
if (!isset($_SESSION['random'])) {
// 这里的长度可以自己计算 为96
$_SESSION['random'] = bin2hex(random_bytes(16)) . bin2hex(random_bytes(16)) . bin2hex(random_bytes(16));
}

// 你想看到 random 的值吗?
// 你不是很懂 MD5 吗? 那我就告诉你他的 MD5 吧
$random = $_SESSION['random'];
echo md5($random);
echo '<br />';

$name = $_POST['name'] ?? 'user';

// check if name ends with 'admin'
if (substr($name, -5) !== 'admin') {
die('不是管理员也来凑热闹?');
}

$md5 = $_POST['md5'];
if (md5($random . $name) !== $md5) {
die('伪造? NO NO NO!');
}

// 认输了, 看样子你真的很懂 MD5
// 那 flag 就给你吧
echo "看样子你真的很懂 MD5";
echo file_get_contents('/flag');

payload:

1
2
3
4
5
6
7
8
9
10
11
12
POST / HTTP/1.1
Host: gz.imxbt.cn:20692
Upgrade-Insecure-Requests: 1
Cookie: PHPSESSID=8f6e3i7s91v0a2lah6g5gk7j16
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
DNT: 1
Content-Type: application/x-www-form-urlencoded

md5=bee0e0cbd622a1e162536bd3ad9ea067&name=%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%03%00%00%00%00%00%00admin

版权声明

本博客所有文章除特别声明外,均采用CC BY-NC-SA许可协议。转载请注明来源:学无止境-YS Zhao


php-hash-bypass
https://zhaoyinshan.github.io/2024/12/31/php-hash-bypass/
Author
Ys Zhao
Posted on
December 31, 2024
Licensed under