DGZ's Blog.

哈希扩展长度攻击及哈希碰撞

Word count: 3.6kReading time: 16 min
2020/04/28 Share

拖到第二天才弄懂(好的我知道我菜),虽然不常考这个玩意
有些人的博文起到了点误导作用emmm
主要参考:https://www.freebuf.com/articles/web/31756.html
https://blog.csdn.net/syh_486_007/article/details/51228628
https://err0rzz.github.io/2017/09/18/hash%E9%95%BF%E5%BA%A6%E6%89%A9%E5%B1%95%E6%94%BB%E5%87%BB/

哈希扩展长度攻击

装hashdump

使用以下命令在kali安装hashpump,但是make install似乎运行不了

1
2
3
4
5
git clone https://github.com/bwall/HashPump
apt-get install g++ libssl-dev
cd HashPump
make
make install

但似乎无伤大雅,随后pip install hashpumpy成功了,也能正常运行hashdump

介绍

哈希长度扩展攻击适用于加密情况为:hash(key, message),其中 hash 最常见的就是 md5、hash1,我们可以在不知道key的情况下推算出另外一个message的hash,前提是知道了一个hash(key, message)以及key的长度,因为我们要利用这个值进行扩展,用于构造拼接了新message的字符串的hash。

解决这个漏洞的办法是使用HMAC算法。该算法大概来说是这样 :MAC =hash(key + hash(key + message)),而不是简单的对密钥连接message之后的值进行哈希摘要。
具体HMAC的工作原理有些复杂,但你可以有个大概的了解。重点是,由于这种算法进行了双重摘要,密钥不再受本文中的长度扩展攻击影响。HMAC最先是在1996年被发表,之后几乎被添加到每一种编程语言的标准函数库中。

所以简单介绍一下原理

Message authentication codes(MACs)是用于验证信息真实性的。最简单的MAC算法是这样的:服务器把key和message连接到一起,然后用摘要算法如MD5或SHA1取摘要。例如,假设有一个网站,在用户下载文件之前需验证下载权限。这个网站会用如下的算法产生一个关于文件名的MAC:

1
2
3
def create_mac(key, fileName)
return Digest::SHA1.hexdigest(key + fileName)
End

最终产生的URL会是这样:
1
http://example.com/download?file=report.pdf&mac=563162c9c71a17367d44c165b84b85ab59d036f9

当用户发起请求要下载一个文件时,将会执行下面这个函数:
1
2
3
4
5
6
7
8
def verify_mac(key, fileName, userMac)
validMac = create_mac(key, filename)
if (validMac == userMac) do
initiateDownload()
else
displayError()
end
End

这样,只有当用户没有擅自更改文件名时服务器才会执行initiateDownload()开始下载。实际上,这种生成MAC的方式,给攻击者在文件名后添加自定义字串留下可乘之机。

我们可以构造出new_filename,若我们能够令validMac == userMac,也就是我们要知道对应的Mac,就可以下载到指定的文件

以md5算法为例:
我们要实现对于字符串 admin 的 md5 的值计算。首先我们要把 admin 转化为 16 进制。

补位

消息必须进行补位,即使得其长度在对 512 取模后的值为 448。也就是说,len(message) % 512 == 448。当消息长度不满 448 bit 时(注意是位,而不是字符串长度),消息长度达到 448 bit 即可。当然,如果消息长度已经达到 448 bit,也要进行补位。补位是必须的。
补位的方式的二进制表示是在消息的后面加上一个1,后面跟着若干个0,直到 len(message) % 512 == 448。在 16 进制下,我们需要在消息后补80,就是 2 进制的10000000。我们把消息admin进行补位到 448 bit,也就是 56 byte。

1
2
即:0x61646d696e800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
共10+102=112字节

补长度

补位过后,第 57 个字节储存的是补位之前的消息长度。admin是 5 个字母,也就是 5 个字节,40 bit。换算成 16 进制为 0x28。其后跟着 7 个字节的 0x00,把消息补满 64 字节。

1
即:0x61646d696e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002800000000000000

计算消息摘要

计算消息摘要必须用补位已经补长度完成之后的消息来进行运算,拿出 512 bit的消息(即64字节)。 计算消息摘要的时候,有一个初始的链变量(初始序列),用来参与第一轮的运算。MD5 的初始链变量为:

A=0x67452301
B=0xefcdab89
C=0x98badcfe
D=0x10325476

我们不需要关心计算细节,我们只需要知道经过一次消息摘要(hash)后,上面的链变量(初始序列)将会被新的值覆盖,用来加密下一分组,若没有下一分组,则初始序列就会作为hash值输出,这里是关键。
JrTB0s.png

以上是md5的一个分组构造的例子,下面以先知的文章做介绍

对文件下载的功能进行长度扩展攻击:
原文是如下:

1
2
xxxxxxxxxxxreport.pdf\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\xA8

A8的值来自(11+10)*8再转16进制,于对于其中的%A8的位置我觉得这里应该是有问题的,所以我改成如下的:
1
2
xxxxxxxxxxxreport.pdf\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\xA8\x00\x00\x00\x00\x00\x00\x00

以上完成了补位的阶段

放到url中后,需要编码,此处我们想要下载的文件是/../../../../../../../etc/passwd,也就是我们想获得/../../../../../../../etc/passwd的hash,令这一段字符串为extension

1
http://example.com/download?file=report.pdf%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%00%00%00%00%00%00%00%00%00%00%A8%00%00%00%00%00%00%00/../../../../../../../etc/passwd&mac=ee40aa8ec0cfafb7e2ec4de20943b673968857a5

在进行新的hash之前,我们要把链变量(初始序列)设置为原始message的hash,你可以将其想象为让SHA1函数从服务器上的函数运行结束的地方继续进行。

也就是攻击者最初的从系统获取的MAC=SHA1(key + message + padding)-> 作为初始序列加密下一分组-> 获得secret_SHA1(extension + padding) -> 输出

或者说,服务器要进行摘要运算的被攻击者篡改过的message如下:

secret + message + padding to the next block + extension + padding to the end of that block.
其中secret + message + padding to the next block是第一分组
extension + padding to the end of that block是第二分组

服务器算出的哈希值将是ee40aa8ec0cfafb7e2ec4de20943b673968857a5,正好与我们添加扩展字串并覆盖链初始值所计算出来的一样。这是因为攻击者的哈希计算过程,相当于从服务器计算过程的一半紧接着进行下去。

网站攻击方法:

继续之前的例子,假设当MAC验证失败时,这个存在漏洞的网站会返回一个错误信息(HTTP response code 或者response body中的错误消息之类)。当验证成功,但是文件不存在时,也会返回一个错误信息。如果这两个错误信息是不一样的,攻击者就可以计算不同的扩展值,每个对应着不同的密钥长度,然后分别发送给服务器。当服务器返回表明文件不存在的错误信息时,即说明存在长度扩展攻击,攻击者可以随意计算新的扩展值以下载服务器上未经许可的敏感文件。

题目 实验吧-让我进去

参考:https://blog.csdn.net/zz_Caleb/article/details/85082561
实验吧在维护中复现不了
bp抓包,观察发现cookie中有个source=0,在repeater中修改为source=1,然go一下,出来了一段源代码。
(神奇操作)

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
$flag = "XXXXXXXXXXXXXXXXXXXXXXX";
$secret = "XXXXXXXXXXXXXXX"; // This secret is 15 characters long for security!

$username = $_POST["username"];
$password = $_POST["password"];

if (!empty($_COOKIE["getmein"])) {
if (urldecode($username) === "admin" && urldecode($password) != "admin") {
if ($COOKIE["getmein"] === md5($secret . urldecode($username . $password))) {
echo "Congratulations! You are a registered user.\n";
die ("The flag is ". $flag);
}
else {
die ("Your cookies don't match up! STOP HACKING THIS SITE.");
}
}
else {
die ("You are not an admin! LEAVE.");
}
}

setcookie("sample-hash", md5($secret . urldecode("admin" . "admin")), time() + (60 * 60 * 24 * 7));

if (empty($_COOKIE["source"])) {
setcookie("source", 0, time() + (60 * 60 * 24 * 7));
}
else {
if ($_COOKIE["source"] != 0) {
echo ""; // This source code is outputted here
}
}

意思就是username为admin,password不为admin,且$COOKIE["getmein"] === md5($secret . urldecode($username . $password))

我们看下面的setcookie是md5($secret . urldecode("admin" . "admin")),意思是我们可以获得的cookie是$secret连接上adminadmin的MD5加密
而题目要求我们$COOKIE["getmein"]$secret连接上admin再连接上另一个不为admin的字符串的MD5加密,才可以获得flag

接下来我认为wp有误,以后复现验证一下
(这篇wp看起来就在乱搞然后刚好搞对了?不不不没准打脸了)
原文:
JslmZT.png

input signature是没错的,就是那一串
input data是错的,应该是adminadmin
input key length应该是15,代表secret的长度
input data to add随意

因为hash是要在原hash的基础上继续的,原hash的值是md5($secret . urldecode("admin" . "admin"))
message是adminadmin,第一分组就是(xxxxxxxxxxxxxxx + adminadmin + padding to the next block)第二分组就是(extension(zzzz) + padding to the end of that block.)

所以password传参hashdump扩展攻击得到的url编码后的字符串,$COOKIE["getmein"]为生成的hash,这样理论上才对。

题目 javisoj-web350

未知secret的长度,就要爆破
参考: https://err0rzz.github.io/2017/09/18/hash%E9%95%BF%E5%BA%A6%E6%89%A9%E5%B1%95%E6%94%BB%E5%87%BB/
url:http://web.jarvisoj.com:32778/index.php
首先是源码泄露,扫到index.php~用vim命令恢复

1
vim -r index.php.swp

得到
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
<!DOCTYPE html>
<html>
<head>
<title>Web 350</title>
<style type="text/css">
body {
background:gray;
text-align:center;
}
</style>
</head>
<body>
<?php
$auth = false;
$role = "guest";
$salt =
if (isset($_COOKIE["role"])) {
$role = unserialize($_COOKIE["role"]);
$hsh = $_COOKIE["hsh"];
if ($role==="admin" && $hsh === md5($salt.strrev($_COOKIE["role"]))) {
$auth = true;
} else {
$auth = false;
}
} else {
$s = serialize($role);
setcookie('role',$s);
$hsh = md5($salt.strrev($s));
setcookie('hsh',$hsh);
}
if ($auth) {
echo "<h3>Welcome Admin. Your flag is
} else {
echo "<h3>Only Admin can see the flag!!</h3>";
}
?>

</body>
</html>

先抓个包看看
Js8hfe.png
role值有一串url编码,解码后s:5:"guest"; 还有hsh值,还有一串不知道啥UM-distinctid

看源码,我们最初的$role = "guest"然后if ($role==="admin" && $hsh === md5($salt.strrev($_COOKIE["role"]))) {过不了判断,所以$auth = false;
之后$hsh = md5($salt.strrev($s));
strrev()是将字符串翻转,所以hsh值就是$salt连接上guest的翻转序列化字符串,再进行md5加密得到的

而我们要知道admin的hsh,才可以进入获得flag
$_COOKIE["role"]相当于role的缓存值

我们的原始data初次进去加密时是翻转过的,我们之后的input data to add 也要翻转
用hashdump的关键词描述:

input signature是hsh,也就是3a4727d57463f122833d9e732f94e4e0
input data是;”tseug”:5:s
input key length是未知的key的长度,需要爆破
input data to add就是关键的;”nimda”:5:s 这里不可以随便填

导师提供的脚本要py3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests,urllib
from hashpumpy import hashpump
def attack():
url="http://web.jarvisoj.com:32778/"
hsh="3a4727d57463f122833d9e732f94e4e0"
guestS='s:5:"guest";'[::-1]
adminS='s:5:"admin";'[::-1]
i=0
html='Only'
newHsh=''
message=''
while ('Only' in html):
print("Tring Salt length:",i)
newHsh,message=hashpump(hsh,guestS,adminS,i)
message=message[::-1]
payload={"role":urllib.parse.quote(message),"hsh":newHsh}
html=requests.get(url,cookies=payload).text
i+=1
print("SaltLength:%d\nMessage:%s\nquotedMessage:%s\nHsh:%s\nHtml:\n%s\n"%(i,message,urllib.parse.quote(message),newHsh,html))

if __name__ == '__main__':
attack()

我跑成功的脚本都是py2环境运行
脚本一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import hashpumpy
import urllib
import requests
for i in range(1,30):
m=hashpumpy.hashpump('3a4727d57463f122833d9e732f94e4e0',';\"tseug\":5:s',';\"nimda\":5:s',i) #此处无需转义也可以
print i
url='http://web.jarvisoj.com:32778/index.php'
digest=m[0]
message=urllib.quote(urllib.unquote(m[1])[::-1])
cookie='role='+message+'; hsh='+digest
#print cookie
headers={
'cookie': cookie,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:55.0) Gecko/20100101 Firefox/55.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': ':zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3',
'Accept-Encoding': 'gzip, deflate'
}
print headers
re=requests.get(url=url,headers=headers)
print re.text
if "Welcome" in re.text:
print re;
break

脚本二,修改了导师的脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests,urllib
from hashpumpy import hashpump
def attack():
url="http://web.jarvisoj.com:32778/"
hsh="3a4727d57463f122833d9e732f94e4e0"
guestS='s:5:"guest";'[::-1]
adminS='s:5:"admin";'[::-1]
i=0
html='Only'
newHsh=''
message=''
while ('Only' in html):
print("Tring Salt length:",i)
newHsh,message=hashpump(hsh,guestS,adminS,i)
message=urllib.quote(urllib.unquote(message[::-1]))
payload={"role":message,"hsh":newHsh}
html=requests.get(url,cookies=payload).text
i+=1
print("SaltLength:%d\nMessage:%s\nquotedMessage:%s\nHsh:%s\nHtml:\n%s\n"%(i,message,urllib.quote(urllib.unquote(message)),newHsh,html))
if __name__ == '__main__':
attack()

html每轮测试中都会被新的html替换,最后出现的html含flag没有Only字符就不会继续循环

哈希碰撞

主要是fastcoll的利用吧,构造出两个文件的hash值相同,但是内容不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#xhash.py
class Unbuffered(object):
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
import sys
import hashlib
sys.stdout = Unbuffered(sys.stdout)

flag=open("/root/xhash/flag").read()

a=raw_input("filea:")
ma=a.decode("hex")

b=raw_input("fileb:")
mb=b.decode("hex")

if ma[0:3]=="xxx" and mb[0:3]=="xxx" and ma!=mb and hashlib.md5(ma).hexdigest()==hashlib.md5(ma).hexdigest():
print flag

要求我们上传两个字符串(文件),然后会把内容解码十六进制,若头三个字母都是xxx且两段字符不同且两段字符md5相同则输出flag
利用fastcoll,先打开一个pre.txt然后输入xxx,保存
再把这个文件拖到fastcoll_v1.0.0.5.exe 不用打开它
之后就会生成两个文件pre_msg1.txt和pre_msg2.txt
利用脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a=open("pre_msg1.txt","rb").read()
b=open("pre_msg2.txt","rb").read()

from zio import *
import base64
import time
import random
target=("106.14.204.93",5004)

io=zio(target)
io.read_until("filea:")
io.writeline(a.encode("hex"))
io.read_until("fileb:")
io.writeline(b.encode("hex"))
io.interact()

获得flag

CATALOG
  1. 1. 哈希扩展长度攻击
    1. 1.1. 装hashdump
    2. 1.2. 介绍
    3. 1.3. 题目 实验吧-让我进去
    4. 1.4. 题目 javisoj-web350
  2. 2. 哈希碰撞