从一道ctf题学习cbc-padding-oracle和cbc翻转

之前一直不怎么清楚cbc加密的padding-oracle和cbc翻转攻击方法,这次NPUCTF比赛遇到了,就记录一下吧

0x01 CBC介绍

CBC是分组加密的一种模式,常见的分组加密算法有DES、AES等,分组加密首先将明文分成若干固定长度的分组(分组长度视加密算法而定,DES分组长度为八字节、AES分组长度为16字节),然后分别对每组的明文进行加密。

下面引用维基百科对CBC的介绍:

在CBC模式中,每个明文块先与前一个密文块进行异或后,再进行加密。在这种方法中,每个密文块都依赖于它前面的所有明文块。同时,为了保证每条消息的唯一性,在第一个块中需要使用初始化向量。

话说得越多也不如一个图清晰。

Plaintext:明文

IV:初始化向量

Key:密钥

Ciphertext:密文

加密图:

Cbc encryption.png

解密图:

Cbc decryption.png
  • padding-oracle

利用场景:密文可控,服务器对解密失败与否有不同的回显

padding-oracle攻击原理是分组密码加密时在对明文进行分组时,采用PKCS#5填充标准,在最后一个分组填充满剩余字节个数对应的字符,如下图所示:

图片.png

如果服务器利用这种加密模式,我们的密码经过CBC加密传输到服务器,服务端在解密密文时,第一步是判断密文最后的一个分组是否正确,是否是采用如上图所示的填充规则,如果填充错误,则直接返回错误;如果正确的话,再将解密后的明文与服务器存储的明文比对,进行下一步的认证。一般会产生三种结果。

  1. 解密成功并且得到的明文正确,认证成功返回200
  2. 可以解密(padding填充正确)但是解密获得的明文不正确,返回200,但认证失败
  3. 不可以解密(padding填充失败),返会错误500

因为第一、二种和第三种返回的结果不同,所以可以利用这个不同来进行另一种“盲注”,来找到中间值,即下图种红框中的值。

image-20200422123432889

Padding oracle attack详细解析

如上图所示,明文填充了四位时,如果最后一组密文解密后的结果(Intermediary Value也就是中间值)与前一组密文(Initialization Vector也就是IV值)异或得到的最后四位是0×04,那么服务器就会返回可以正常解密。

回忆一下前面我们说过的CBC模式的解密过程,第n组密文解密后的中间值与前一组的密文异或便可得到明文,现在我们不知道解密的密钥key,但我们知道所有的密文,因此只要我们能够得到中间值便可以得到正确的密文(进行一次异或运算便可),而中间值是由服务器解密得到的,因此我们虽然不知道怎么解密但我们可以利用服务器帮我们解密,我们所要做的是能确定我们得到的中间值是正确的,这也是padding oracle attack的核心——找出正确的中间值。

下面是攻击步骤

(1)假设我们捕获到了传输的密文并且我们知道是CBC模式采用的什么加密算法,我们把密文按照加密算法的要求分好组,然后对倒数第二组密文进行构造;

(2)先假定明文只填充了一字节,对倒数第二组密文的最后一字节从0×00到0xff逐个赋值并逐个向服务器提交,直到服务返回值表示构造后的密文可以正常解密,这意味着构造后的密文作为中间值(图中黄色的那一行)解密最后一组明文,明文的最后一位是0×01(如图所示),也就是说构造的倒数第二组密文的最后一字节与最后一组密文对应中间值(绿色的那一行)的最后一位相异或的结果是0×01;

Padding oracle attack详细解析

(3)利用异或运算的性质,我们把我们构造的密文的最后一字节与0×01异或便可得到最后一位密文解密后的中间值是什么,这里我们设为M1,这一过程其实就是对应下图CBC解密过程中红圈圈出来的地方,1就是我们想要得到的中间值,2就是我们构造的密文也就是最后一组密文的IV值,我们已经知道了plaintext的最后一字节是0×01,从图中可以看到它是由我们构造的IV值与中间值的最后一字节异或得到的;

Padding oracle attack详细解析

(4)再假定明文填充了两字节也就是明文最后两字节是0×02,接着构造倒数第二组密文,我们把M1与0×02异或可以得到填充两字节时密文的最后一位应该是什么,这时候我们只需要对倒数第二位进行不断地赋值尝试(也是从0×00到0xff),当服务器返回值表示可以正常解密时,我们把此时的倒数第二位密文的取值与0×02异或便可得到最后一组密文倒数第二字节对应的中间值;

(5)后再构造出倒数第三倒数第四直到得到最后一组密文的中间值,把这个中间值与截获的密文的倒数第二位异或便可得到最后一组分组的明文;

(6)舍弃掉最后一组密文,只提交第一组到倒数第二组密文,通过构造倒数第三组密文得到倒数第二组密文的铭文,最后我们便可以得到全部的明文

  • CBC翻转

利用场景:输入密文可控,而且知道该密文对应的明文。

在贴一下cbc的解密图

image-20200422125141629

在上图解密过程中,会用红圈的字节与第二组密文解密后的中间值异或来得到第二组明文,所以我们就可以通过修改密文来达到准确修改明文的目的,若源明文为old_p,原密文为old_c,我们想将明文修改为tar_p,中间值为inter_v。

1
2
3
4
inter_v ^ old_c = old_p
将old_c => (old_c ^ tar_p ^ old_p)
inter_v ^ (old_c ^ tar_p ^ old_p) = old_p ^ tar_p ^old_p = tar_p
这样即完成了对明文的伪造。

0x02 NPUCTF web🐕

首先可以得到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php 
error_reporting(0);
include('config.php'); # $key,********$file1*********
define("METHOD", "aes-128-cbc"); //定义加密方式
define("SECRET_KEY", $key); //定义密钥
define("IV","6666666666666666"); //定义初始向量 16个6
define("BR",'<br>');
if(!isset($_GET['source']))header('location:./index.php?source=1');
/*
真不是注入
*/

#var_dump($GLOBALS); //听说你想看这个?
function aes_encrypt($iv,$data)
{
echo "--------encrypt---------".BR;
echo 'IV:'.$iv.BR;
return base64_encode(openssl_encrypt($data, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)).BR;
}
function aes_decrypt($iv,$data)
{
return openssl_decrypt(base64_decode($data),METHOD,SECRET_KEY,OPENSSL_RAW_DATA,$iv) or die('False'); #不返回密文,解密成功返回1,解密失败返回False
}
if($_GET['method']=='encrypt')
{
$iv = IV;
$data = $file1;
echo aes_encrypt($iv,$data);
} else if($_GET['method']=="decrypt")
{
$iv = @$_POST['iv'];
$data = @$_POST['data'];
echo aes_decrypt($iv,$data);
}
echo "我摊牌了,就是懒得写前端".BR;

if($_GET['source']==1)highlight_file(__FILE__);
?>

源码初始化了加密方式为aes-128-cbc、IV 、密钥,然后根据我们输入的method变量,若为encrypt会把$file1加密,若为decrypt会用我们输入的iv作为向量,加密输入的data值。首先可以获取到$file1的加密后的值,

image-20200422131503138

乱码因为源码的编码方式有点问题,这里就不改了。。。。

然后这里的解密功能

1
2
3
4
function aes_decrypt($iv,$data)
{
return openssl_decrypt(base64_decode($data),METHOD,SECRET_KEY,OPENSSL_RAW_DATA,$iv) or die('False'); #不返回密文,解密成功返回1,解密失败返回False
}

如果解密失败的话会返回False,那么就可以利用这点来进行padding oracle了,将那串base64编码解密后得到

image-20200422131849824

正好是十六个字节,可以知道它的明文就只有一个分组,然后就改了改网上的脚本来”盲注“中间值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import requests
import time
import base64
url = "http://ha1cyon-ctf.fun:30006/index.php?source=1&method=decrypt"
N = 16
proxy = {"http":"http://127.0.0.1:8080","https":"http://127.0.0.1:8080"}

def re(iv):
data={"iv":iv,"data":"ly7auKVQCZWum/W/4osuPA=="}
result=requests.post(url,data=data,proxies=proxy)
return result


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

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
c=chr(0)*(16-i)+chr(j)+padding
print c.encode('hex')
result=re(c)
print result.content
if "php" in result.content:
get=chr(j^i)+get
time.sleep(0.1)
break
return get
mid = padding_oracle(N)
print xor(mid,"6666666666666666")

之后可以得到$file1 为 FlagIsHere.php

然后访问FlagIsHere.php,可以得到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php 
#error_reporting(0);
include('config.php'); //**************$file2********last step!!
define("METHOD", "aes-128-cbc");
define("SECRET_KEY", "6666666");
session_start();

function get_iv(){ //生成随机初始向量IV
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;
}

$lalala = 'piapiapiapia';

if(!isset($_SESSION['Identity'])){
$_SESSION['iv'] = get_iv();

$_SESSION['Identity'] = base64_encode(openssl_encrypt($lalala, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $_SESSION['iv']));
}
echo base64_encode($_SESSION['iv'])."<br>";

if(isset($_POST['iv'])){
$tmp_id = openssl_decrypt(base64_decode($_SESSION['Identity']), METHOD, SECRET_KEY, OPENSSL_RAW_DATA, base64_decode($_POST['iv']));
echo $tmp_id."<br>";
if($tmp_id ==='weber')die($file2);
}

highlight_file(__FILE__);
?>

这里也是开始初始化了加密方式 aes-128-cbc、密钥,IV通过get_i()函数获得,默认会把'piapiapiapia'加密,并将密文和IV保存在SESSION中,会打印出IV值,然后可以通过我们输入的iv值作为IV对SESSION中保存的密文进行解密,若解密后为‘weber’,则会输出$file2,这里就可以通过cdc翻转来将明文改变成weber。

下面附上脚本

1
2
3
4
5
6
7
8
9
10
11
<?php
$enc = base64_decode("EjD+vzcXcP9n4iamEmdNMQ==");
$old = "piapiapiapia" . urldecode("%04%04%04%04");
$tar = "weber" . urldecode("%0b%0b%0b%0b%0b%0b%0b%0b%0b%0b%0b");
for ($i = 0; $i < 16; $i++) {
$enc[$i] = chr(ord($enc[$i]) ^ ord($old[$i]) ^ ord($tar[$i]));

}
echo base64_encode($enc);
?>
//FTz9qix9C50NmUTMHWhCPg==

然后将得到的IV传给服务器,服务器会回显

image-20200422135128872

然后访问/HelloWorld.7z可以得到一个java的class文件,放到jd-gui中可以得到

1
2
3
4
5
6
7
8
9
public class HelloWorld {
public static void main(String[] paramArrayOfString) {
System.out.println(");
byte[] arrayOfByte = {
102, 108, 97, 103, 123, 119, 101, 54, 95, 52,
111, 103, 95, 49, 115, 95, 101, 52, 115, 121,
103, 48, 105, 110, 103, 125 };
}
}

然后可以看出数组为flag的ascii码,然后解码就可以。

1
2
3
4
5
a = [102, 108, 97, 103, 123, 119, 101, 54, 95, 52, 111, 103, 95, 49, 115, 95, 101, 52, 115, 121, 103, 48, 105, 110, 103, 125 ]
flag=""
for i in a:
flag += chr(i)
print flag

这里我做题是遇到一个坑点,第一次我是利用下面的脚本生成的IV,提交时发现过不去验证,

1
2
3
4
5
6
7
8
9
10
11
<?php
$enc = base64_decode("EjD+vzcXcP9n4iamEmdNMQ==");
$old = "piapiapiapia"; //. urldecode("%04%04%04%04");
$tar = "weber" . urldecode("%00%00%00%00%00%00%00"); //%00%00%00%00");
for ($i = 0; $i < 12; $i++) {
$enc[$i] = chr(ord($enc[$i]) ^ ord($old[$i]) ^ ord($tar[$i]));

}
echo base64_encode($enc);
?>
FTz9qix2AJYGkk/HEmdNMQ==

在本地复现发现这样得到的字符串在浏览器显示确实为weber,但是用var_dump()输出一下可以看出

image-20200422135611513

这个字符串的长度为12,所以与weber强等于是通过不了的,所以要生成长度为5的weber字符串,所以正确的脚本应该是这样的。

1
2
3
4
5
6
7
8
9
10
<?php
$enc = base64_decode("EjD+vzcXcP9n4iamEmdNMQ==");
$old = "piapiapiapia" . urldecode("%04%04%04%04");
$tar = "weber" . urldecode("%0b%0b%0b%0b%0b%0b%0b%0b%0b%0b%0b");
for ($i = 0; $i < 16; $i++) {
$enc[$i] = chr(ord($enc[$i]) ^ ord($old[$i]) ^ ord($tar[$i]));

}
echo base64_encode($enc);
?>

参考链接

https://www.freebuf.com/articles/database/151167.html

https://www.smi1e.top/cbc%E5%AD%97%E8%8A%82%E7%BF%BB%E8%BD%AC%E6%94%BB%E5%87%BB%E5%92%8Cpadding-oracle/#padding_oracle