0x00 前言

有时候日志审计会看到Padding Oracle Attack相关的事件,但个人对这块不熟就学习做下笔记吧,都是参考学习网上大佬们的文章。

0x01 基本概念

异或(XOR)

异或(XOR)是一个数学运算符。它应用于逻辑运算。异或的数学符号为“⊕”,计算机符号为“xor”。其运算法则为:

a⊕b = (¬a ∧ b) ∨ (a ∧¬b)

如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。

在计算机中,存储的数据是以二进制的格式存入的,把两段二进制数字进行异或运算的话,相同的得0,不同的得1。字符在计算机中有对应的ASCII码值,对字符进行异或运算就是将两串字符对应的ASCII码值进行异或。

异或运算具有可逆性,a xor b = c 等价于 b xor c = a 等价于 a xor c = b。

CBC模式

CBC(Cipher Block Chaining)即密码分组链接,是一种加密模式。在CBC模式中,每个明文块先与前一个密文块进行异或后,再进行加密。在这种方法中,每个密文块都依赖于它前面的所有明文块。同时,为了保证每条消息的唯一性,在第一个块中需要使用初始化向量。

加密流程

CBC模式加密流程如图:

  • Plaintext:明文数据;
  • Initialzation Vector(IV):初始向量;
  • Key:分组加密使用的密钥;
  • Ciphertext:密文数据;

加密步骤:

  1. 首先将明文分成长度相同(一般为8或16字节)的n组,其中最后一组位数不足的进行Padding操作(填充规则是PKCS #5或PKCS #7);
  2. 生成一个随机的初始向量(IV)和一个密钥(Key);
  3. 将IV与第一组明文进行XOR,将XOR后的结果使用Key进行加密得到第一组密文;
  4. 将第一组密文和第二组明文进行XOR,将XOR后的结果使用Key进行加密得到第二组密文;
  5. 依次类推,将第n-1组密文和第n组明文进行XOR,将XOR后的结果使用Key进行加密得到第n组密文;
  6. 将IV和得到的所有分组密文拼接到一起,得到最终的密文;

解密流程

CBC模式解密流程如图:

解密步骤:

  1. 首先从密文中提取出初始向量(IV),然后将密文分成n组(一般为8或16字节一组);
  2. 使用密钥(Key)对第一组密文进行解密,得到解密的中间值我们称为Intermediary Value;
  3. 使用IV与第一组Intermediary Value进行XOR,得到第一组明文;
  4. 使用Key对第n组密文进行解密,得到第n组Intermediary Value;
  5. 使用第n-1组密文与第n组Intermediary Value进行XOR,得到第n组明文;
  6. 将所有分组明文拼接到一起,得到最终的明文;

由此可得以下结论:

  • 对于第一组密文的解密:Plaintext[1] = Decrypt(Ciphertext[1]) XOR IV
  • 对于第n组密文的解密(n>1):Plaintext[n] = Decrypt(Ciphertext[n]) XOR Ciphertext[n-1]

PKCS #5/PKCS #7填充模式

The Public-Key Cryptography Standards (PKCS)是由美国RSA数据安全公司及其合作伙伴制定的一组公钥密码学标准,其中包括证书申请、证书更新、证书作废表发布、扩展证书内容以及数字签名、数字信封的格式等方面的一系列相关协议。

PKCS #5是8字节填充的,即填充一定数量的内容,使得成为8的整数倍,而填充的内容取决于需要填充的数目。例如,0x56在经过PKCS #5填充之后会成为0x56 0x07 0x07 0x07 0x07 0x07 0x07 0x07因为需要填充7字节,因此填充的内容就是7。当然特殊情况下,如果已经满足了8的整倍数,按照PKCS #5的规则,仍然需要在尾部填充8个字节,并且内容是0x08,目的是为了加解密时统一处理填充。

PKCS #7与PKCS #5的区别在于PKCS5只填充到8字节,而PKCS #7可以在1-255之间任意填充。

具体可参考:填充模式:PKCS#5/PKCS7

DES算法进行加密时的填充规则是PKCS #5、填充最多8位,而AES算法进行加密时的填充规则是PKCS #7、填充最多16位

举例看下PKCS #5的填充规则,其最多填充8位,填充字节的取值范围是0x01到0x08。以下图为例:

第一行最后还差5个字符,则在最后填充5个0x05,后面类比。需注意即便分组内容能正好平均分为n组,仍需要在最后一组后面填充一个八位分组

16字节的AES采用的是PKCS #7,填充的规则和PKCS #5是一样的,只是分组长度不一样,也就是说填充字节的取值范围是0x00到0x10。

Padding Oracle

Padding在这里的含义是“填充”,因为对于加密算法来说,它们是基于等长的“数据块”进行操作的(如对于RC2,DES或TripleDES算法来说这个长度是8字节,而对于Rijndael算法来说则是16、24或32字节)。但是,我们的输入数据长度是不规则的,因此必然需要进行“填充”才能形成完整的“块”。“填充”时比较常用的是PKCS #5规则,简单地说,便是根据最后一个数据块所缺少的长度来选择填充的内容。

在解密时,如果算法发现解密后得到的结果,它的填充方式不符合规则,那么表示输入数据有问题,对于解密的类库来说,往往便会抛出一个异常,提示Padding不正确。Oracle在这里便是“提示”的意思,和甲骨文公司没有任何关系。

简单地说,Padding Oracle就是提示输入数据的填充方式不符合规则的意思

0x02 CBC字节翻转攻击

基本原理

在前面CBC模式的解密流程中知道,Ciphertext[n-1](即第n-1组密文)用于与Decrypt(Ciphertext[n])即(使用Key对第n组密文解密后的结果)进行XOR生成下一组的明文Plaintext[n],而这个点正是CBC字节翻转攻击的漏洞点——如果攻击者修改了Ciphertext[n-1]的某个字节,然后提交到服务端与Decrypt(Ciphertext[n])进行XOR运算,此时就会得到一个不同的明文。因此,攻击者可以在不知道密钥的情况下,通过篡改初始向量或密文的方式来控制明文进而实现绕过某些服务端过滤机制等

原理如下图:

由前面知道,解密获取第n组密文为:

1
Plaintext[n] = Decrypt(Ciphertext[n]) XOR Ciphertext[n-1]

由XOR可逆性可得解密的中间值为明文和上一组密文的XOR结果,这里另起名字简称为IntermediaryValue:

1
IntermediaryValue[n] = Decrypt(Ciphertext[n]) = Plaintext[n] XOR Ciphertext[n-1]

攻击者想得到自己构造的恶意明文,是通过IntermediaryValue和自己构造的恶意上一组密文XOR实现的:

1
Evil_Plaintext[n] = IntermediaryValue[n] XOR Evil_Ciphertext[n-1]

由XOR可逆性可得:

1
Evil_Ciphertext[n-1] = IntermediaryValue[n] XOR Evil_Plaintext[n]

进而:

1
Evil_Ciphertext[n-1] = Plaintext[n] XOR Ciphertext[n-1] XOR Evil_Plaintext[n]

因此可得结论:在CBC字节翻转攻击中,攻击者传入的恶意IV或恶意上一组密文是根据原始明文、原始上一组密文、想得到的恶意明文三者进行异或运算得到的

Demo

参考网上改的Demo,已知原始明文Plaintext和原始初始向量IV,再结合想得到的恶意明文字符来通过构造恶意IV来实现攻击(这里只修改第一个分组,暂不考虑其他多个分组的情况):

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
import os
import codecs
from Crypto.Cipher import AES
from Crypto import Random


def padding(plaintext):
count = len(plaintext)
add = length - (count % length)
plaintext_padding = plaintext + (chr(add) * add)
return plaintext_padding


# 16字节密钥Key
SECRET_KEY = codecs.encode(os.urandom(8), 'hex_codec').upper()
# 16字节初始向量IV
IV = Random.new().read(16)
# 使用AES算法,该算法以16字节为处理单位
aes = AES.new(SECRET_KEY, AES.MODE_CBC, IV)
length = 16

# 原明文
plaintext = "milktea"
# 填充原明文
plaintext_padding = padding(plaintext)

# 使用AES加密填充后的原明文
cipher = aes.encrypt(plaintext_padding.encode())
print("[*]填充后的明文:", plaintext_padding)
print("[*]密钥Key:", SECRET_KEY)
print("[*]初始向量IV:", IV)
print("[*]填充后的明文序列:", list(plaintext_padding))
print("[*]密文:", aes.encrypt(plaintext_padding.encode()))

old_IVList = []
plaintextList = []
for i in range(0, len(codecs.encode(IV, 'hex_codec').decode()), 2):
old_IVList.append(int(codecs.encode(IV, 'hex_codec').decode()[i:i + 2], 16))

for i in list(plaintext_padding):
plaintextList.append(ord(i))

evil_IVList = old_IVList
# 修改制定字符
evil_IVList[4] = plaintextList[4] ^ old_IVList[4] ^ ord('7')
evil_IVList[2] = plaintextList[2] ^ old_IVList[2] ^ ord('1')
# 在末尾添加.com
evil_IVList[7] = plaintextList[7] ^ old_IVList[7] ^ ord('.')
evil_IVList[8] = plaintextList[8] ^ old_IVList[8] ^ ord('c')
evil_IVList[9] = plaintextList[9] ^ old_IVList[9] ^ ord('o')
evil_IVList[10] = plaintextList[10] ^ old_IVList[10] ^ ord('m')

evil_IV = ''
for i in evil_IVList:
# 为了保证构造出来的IV是16字节,1位的16进制补0变成2位
if len(hex(i)[2:]) == 1:
evil_IV += '0' + hex(i)[2:]
else:
evil_IV += hex(i)[2:]

# 恶意构造的16字节IV
evil_IV = codecs.decode(evil_IV, 'hex_codec')
print("[*]恶意构造出的IV:", evil_IV)

# 使用恶意IV进行AES解密来获取新的明文
aes = AES.new(SECRET_KEY, AES.MODE_CBC, evil_IV)
dsc = aes.decrypt(cipher).decode()
print("[*]利用恶意构造的IV解密得到的明文:", dsc)

# 假设服务端判断解密得到的明文满足某个条件就会进行其他恶意操作
if "mi1k7ea.com" in dsc:
print("[+]CBC Byte Flipping Attack!")

运行结果:

多个分组的场景可以参考这篇文章的代码示例:CBC字节翻转攻击测试

0x03 Padding Oracle Attack

本部分只写针对CBC模式的Padding Oracle Attack,针对ASP .NET的场景这里暂未研究。

前提条件

Padding Oracle Attack的场景需要如下条件:

  1. 攻击者知道服务端使用CBC模式的加密算法,且能获取到密文Ciphertext以及附带在密文前面的初始向量IV;
  2. 攻击者能向服务端提交数据触发解密操作,并能根据服务端返回的响应来判断是否能进行正常解密;

基本原理

Padding Oracle Attack是根据CBC字节翻转攻击、Padding规则以及服务端解密后返回的不同状态来穷举中间值进而获取明文的攻击,是针对CBC链接模式的攻击,而不是针对某个加密算法的攻击。

CBC字节翻转攻击和Padding Oracle在前面已经说过了,这里重点说下是如何根据服务端解密后返回的不同状态来穷举中间值进而获取明文的。

由Padding Oracle可知,如果输入的密文不合法(填充规则不对),类库则会抛出异常,这便是一种提示。通过CBC字节翻转攻击,攻击者可以不断地提供密文,让解密程序给出提示,不断修正,最终得到所需要的结果。关键就在于解密程序给出对于不同密文的解密结果有不同的提示。一般的,服务器对于接受的使用CBC模式加密敏感信息进行解密操作时,先是检测密文最后一组的填充值是否正确来确定能否正常解密(即检测是否符合PKCS #5/7),如果错误就直接返回错误,如果正确则进一步判断解密的内容是否正确,因此分如下三种情况:

  1. 密文不能正常解密;
  2. 密文可正常解密但解密结果不对;
  3. 密文可正常解密且解密结果正确;

这里第一种情况和第二三种的情况肯定是不一样的,很多服务器对于第一种情况都是返回500、对于第二三种情况则是返回200,这就是个有限的二元组,攻击者可以根据这个响应状态码来判断是否解密成功,给攻击者进行猜解攻击的可能。更细节点,就是让攻击者可以通过服务端解密后的响应状态来判断填充的字节是否正确来进行穷举攻击,攻击者只需要根据第一和第二种情况返回的不同状态就能实现穷举攻击,而无需服务端能解密得到正确的结果。

由前面CBC模式的解密流程知道,第n组密文解密后的中间值与前一组的密文XOR便可得到明文(Plaintext[n] = IntermediaryValue[n] XOR Ciphertext[n-1])。其中,密文解密部分是在服务器端进行的,我们无需考虑,因此关键在于得到正确的中间值。在Padding Oracle Attack中,攻击者可控的参数是IV和Ciphertext,通过对IV的穷举来请求服务器端对指定的Ciphertext进行解密,并对返回的结果进行判断,从而得到正确的中间值,进而通过XOR得到原始明文。

网上找的一个图如下,使用的是DES算法,明文填充了4位,如果最后一组密文解密后的结果(Intermediary Value)与前一组密文/初始向量(IV)异或得到的最后四位是0x04,那么服务器就会返回可以正常解密:

简单地说,就是利用PKCS规则来构造Padding、通过服务端解密后返回的不同状态来推出正确的中间值,再通过中间值与原密文XOR得到原明文,之后依次递归解出每一位的中间值和明文直至全部穷举成功。

攻击过程

攻击过程如下:

1、假设攻击者拥有密文且得知服务端是用CBC模式的DES算法进行加解密操作,然后把密文按照加密算法的要求分好组,再对倒数第二组密文进行构造;

2、先假设明文只填充了一字节,对倒数第二组密文的最后一字节从0x00到0xff逐个赋值后提交给服务器进行解密操作,直至服务器返回的响应状态码表示构造后的密文可以正常解密为止;

比如构造的倒数第二组密文(或IV)最后一字节为0x00时,在服务端解密后得到0x3D、这是无法正常解密的、会返回500响应状态码:

当构造的倒数第二组密文(或IV)最后一字节为0x66时,在服务端解密后得到0x01、这是正确的padding值,是能正常解密的,此时会返回200响应状态码:

3、利用XOR的可逆性,攻击者把前面构造的倒数第二组密文的最后一字节0x66和0x01进行XOR得到中间值Intermediary Value(后续简称M1)为0x67;

4、接着,假设明文填充了两字节,即明文最后两字节是0x02,再构造倒数第二组密文,把M1与0x02进行XOR得到填充两字节时密文的最后一位的值为0x65。此时,攻击者只需要对倒数第二位进行不断地赋值尝试(0x00-0xff),当服务器返回值表示可以正常解密时(如200响应状态码),就将此时的倒数第二位密文的取值与0x02进行XOR得到最后一组密文倒数第二字节对应的中间值即M2;

5、接着,再构造出倒数第三、四直至得到最后一组密文的中间值,把这个中间值与倒数第二组的原始密文进行XOR便可得到最后一组分组的明文;

6、舍弃掉最后一组密文,只提交第一组到倒数第二组密文,通过构造倒数第三组密文得到倒数第二组密文的明文,依次下去直至得到全部的明文;

工具

整个攻击过程相对来说是复杂了些,但是业界已经有工具来实现这个复杂过程的攻击利用:

https://github.com/AonCyberLabs/PadBuster

在Kali中也是有该命令的,需要安装:apt-get install padbuster

Demo

PentesterLab中有个Padding Oracle靶机环境,整个攻击利用过程是借助padbuster来实现,比较简单。

具体做法可参考:PentesterLab 的 Padding Oracle 漏洞靶机测试

0x04 参考

CBC Byte Flipping Attack—101 Approach

Padding oracle attack详细解析

CBC字节翻转攻击和Padding Oracle