Iuhrey

一个常年被吊打的Web手 一个唱歌不好指弹垃圾的吉他手

CBC字节反转攻击

前言

最近在比赛中遇到了一道挺有意思的题目,密码学和web的结合让我学到了很多不错的知识,之前也有类似的题目,例如上次的Hash长度拓展攻击,这一次的是CBC字节反转攻击,和它有着异曲同工之妙。

CBC加解密原理


这是CBC模式加密的流程图,这种加密模式和Hash加密是相似的,都是前一部分加密的字符用于后一部分加密,具体流程是初始生成一个IV值,然后把字符按照16字节为一个单位进行分组(不足时用特殊字符填充)和IV值进行异或处理,得到的字符串使用密钥进行加密,然后把加密后的字符作为下一步的IV值,重复操作直到所有字符串加密完成。

解密的过程就不多说了。

异或

异或操作(Xor)是对二进制数据进行的运算操作,是一个数学运算符。它应用于逻辑运算。异或的数学符号为“⊕”,计算机符号为“xor”。其运算法则为:
a⊕b = (¬a ∧ b) ∨ (a ∧¬b)
如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。例如A=10010100,B=00100111。那么A Xor B =01001100。如果C = A Xor B ,那么A Xor B Xor C =0并且其中两个异或,都能得到第三者。

如何利用


这是大手子原理利用的图,这里可以注意到前一块Ciphertext用来产生下一块明文,如果我们改变前一块Ciphertext中的一个字节,然后和下一块解密后的密文xor,就可以得到一个不同的明文,而这个明文是我们可以控制的。利用这一点,我们就欺骗服务端或者绕过过滤器。基本的理论都讲完了,那么从题目中来理解如何利用的。
这是ISCC上的一道题目,打开题目地址,有点类似注入的题目,但是f12过后发现题目给出了源码。打开index.txt发现源码。
我们来理一理这些代码是如何执行的,首先是登入的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 if (isset($_POST['username'])&&isset($_POST['password'])) {
$username=waf((string)$_POST['username']);
$password=waf((string)$_POST['password']);
if($username === 'admin'){
exit('<p>You are not real admin!</p>');
}else{
$info = array('username'=>$username,'password'=>$password);
login($info);
show_homepage();
}
}
else{
if(isset($_SESSION["username"])){
check_login();
show_homepage();
}
}
?>

如果我们输入了username和password那么页面会先把username转化为字符串形式,这里是为了防止我们通过数组形式来绕过下面的username=admin,再waf过滤,这里我们的sql注入是不太可行的。如果我们输入的username为admin那么页面回显错误信息然后直接退出,不为admin那么接着调用了login()以及show_homepage()函数,如果我们通过其他方式给username赋值了,那么跳到check_login()和show_homepage()两个函数上。那我们接着来看这些函数。
login()

1
2
3
4
5
6
7
8
function login($info){
$iv=get_random_iv();
$plain = serialize($info);
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
$_SESSION['username'] = $info['username'];
setcookie("iv", base64_encode($iv));
setcookie("cipher", base64_encode($cipher));
}

show_homepage()

1
2
3
4
5
6
7
8
9
10
11
function show_homepage(){
if ($_SESSION["username"]==='admin'){
echo '<p>Hello admin</p>';
echo '<p>Flag is *************</p>';
}else{
echo '<p>hello '.$_SESSION['username'].'</p>';
echo '<p>Only admin can see flag</p>';
}
echo '<p><a href="loginout.php">Log out</a></p>';
die();
}

整体来说就是,先把我们输入进去的username和password序列化,然后把username的值赋给$_SESSION[“username”],如果为admin,那么输出flag,如果不为admin,则提示只有admin才能看flag。
如果按照上述常规的操作进行登入,那肯定是不行的,这里有一个矛盾,如果我们传入username为admin那么会提示我们You are not real admin!,但是我们不传入admin,那么就会提示我们只用admin才能看flag。所以我们需要考虑如何来绕过。所以接着看另一条路的函数
check_login

1
2
3
4
5
6
7
8
9
10
11
12
function check_login(){
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>");
$_SESSION['username'] = $info['username'];
}else{
die("ERROR!");
}
}
}

这里我们是在没有传入username和password之后所调用的,这里我们需要通过cookies传入一个iv值和一个cipher值,然后cipher经过各种解密,反序列化后把username赋给$_SESSION[‘username’]然后进行身份的验证。
分析了源码发现两种登入方式独立开来并没有什么可以利用的东西,但是如果我们把两种方式结合起来,在他们中间那段可进行人为操作的过程中更改数据,那么是可以做到移花接木的。

具体操作

1 传参抓包
把username和password传参过去,注意username不能为admin。

2 更改数据
我们这个时候得到了密文和初始的iv值,那么可以通过cbc反转字节攻击来更改我们之前的数据。那么如何利用CBC反转字节呢?
我们已知了加密后的文本,以及明文,那我们可以来修改原来的字符串。
cipher

1
gEc6%2BRRLjY0Bf51gQLMCB1a%2F9wD2106%2BeTzmU%2Baum9Xfin%2BlY%2FF9FfcoPd7%2Buls1Q9sOQJ4zfbnen6c5cxFLcQ%3D%3D

进行处理后变为

1
�G:�K���`@�V����N�y<�S殛�ߊ�c�}�(=���[5C�@�3}�ޟ�9sKq

所对应的明文
为了便于理解,我们直接以16字节为一块分开。这是反序列化后的明文。
s:2:{s:8:”userna
me”;s:5:”xdmin”;
s:8:”password”;s
:3:”123”;}
接下来就是把对应的位置进行更改。
假设我们更改的明文为A
IV值对应的是B
密文为C
如果我们构造 C Xor A Xor “我们所需的字符”,那是不是就变成了A Xor B Xor A Xor “我们所需的字符”了?这不就等于B Xor “我们所需的字符”了吗?所以我们可以直接利用这个攻击来更改我们的字符。这里直接用大手子的脚本跑把吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#coding=utf-8
import base64
import requests
import urllib
iv_raw='bR1nkWOLhxJ4rKGJMBF36w%3D%3D' #这里填写第一次返回的iv值
cipher_raw='gEc6%2BRRLjY0Bf51gQLMCB1a%2F9wD2106%2BeTzmU%2Baum9Xfin%2BlY%2FF9FfcoPd7%2Buls1Q9sOQJ4zfbnen6c5cxFLcQ%3D%3D' #这里填写第一次返回的cipher值
print "[*]原始iv和cipher"
print "iv_raw: " + iv_raw
print "cipher_raw: " + cipher_raw
print "[*]对cipher解码,进行反转"
cipher = base64.b64decode(urllib.unquote(cipher_raw))
#a:2:{s:8:"username";s:5:"xdmin";s:8:"password";s:5:"12345"}
#s:2:{s:8:"userna
#me";s:5:"xdmin";
#s:8:"password";s
#:3:"12345";}
xor_cipher = cipher[0:9] + chr(ord(cipher[9]) ^ ord('x') ^ ord('a')) + cipher[10:] #请根据你的输入自行更改,原理看上面的介绍
xor_cipher=urllib.quote(base64.b64encode(xor_cipher))
print "反转后的cipher:" + xor_cipher

3 传新参
注意这个时候我们就不能把username和password传参过去了,把我们所得到的cipher值和初始的iv值给传过去。

4 修复所更改的数据块
这个时候发现我们传的cipher无法正常反序列化,这个时候我们就需要更改iv值来使得cipher能够正常反序列化,原理和上述一致,脚本如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
#coding=utf-8
import base64
import urllib
cipher = 'EK9/OgEKt1XNxlpRcDv8+21lIjtzOjU6IntkbWluIjtzOjg6InBhc3N3b3JkIjtzOjM6IjEyMyI7fQ=='#填写提交后所得的无法反序列化密文
iv = 'bR1nkWOLhxJ4rKGJMBF36w%3D%3D'#一开始提交的iv
#cipher = urllib.unquote(cipher)
cipher = base64.b64decode(cipher)
iv = base64.b64decode(urllib.unquote(iv))
newIv = ''
right = 'a:2:{s:8:"userna'#被损坏前正确的明文
for i in range(16):
newIv += chr(ord(right[i])^ord(cipher[i])^ord(iv[i])) #这一步相当于把原来iv中不匹配的部分修改过来
print urllib.quote(base64.b64encode(newIv))

5 再一次传参
把新得到的iv值传入,得到flag

本站总访问量