CBC字节翻转攻击(CBC bitflipping attacks)

CBC模式加解密的特点为都是用前一块的密文来产生或解密后一块密文。如果我们改变前一块密文中的一个字节,那么和下一块解密后的密文xor,就会得到一个不同的明文;

Bugku-login4

点开链接,是个登录的页面;扫描目录发现有.index.php.swp,恢复得到源码

1.首先判断登录的用户名如果为admin则退出脚本:

1
2
3
4
5
6
if(isset($_POST['username']) && isset($_POST['password'])){
$username = (string)$_POST['username'];
$password = (string)$_POST['password'];
if($username === 'admin'){
exit('<p>admin are not allowed to login</p>');
}

如果用户名不为admin,则先后调用login()show_homepage()函数:

1
2
3
4
5
6
else{
$info = array('username'=>$username,'password'=>$password);
login($info);
show_homepage();
}
}

login()函数中,会将输入的用户名和密码数组进行序列化,并将序列化后的值通过AES-CBC模式进行加密;之后将$_SESSION['username']赋值为用户输入的用户名;最后将加密所用到的iv和加密得到的cipher通过Cookie返回到客户端:

1
2
3
4
5
6
7
8
9
10
11
define("SECRET_KEY", file_get_contents('/root/key'));
define("METHOD", "aes-128-cbc");

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));
}

加密所使用的iv为随机生成的16字节串:

1
2
3
4
5
6
7
function get_random_iv(){
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;
}

show_homepage()函数中,判断$_SESSION["username"]如果为admin,则输出flag,否则提示Only admin can see flag

1
2
3
4
5
6
7
8
9
10
function show_homepage(){
if ($_SESSION["username"]==='admin'){
echo '<p>Hello admin</p>';
echo '<p>Flag is $flag</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>';
}

2.如果没有POST用户名和密码,则检查是否存在$_SESSION["username"],若存在则调用check_login()show_homepage()函数,否则返回登录页面:

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
else{
if(isset($_SESSION["username"])){
check_login();
show_homepage();
}else{
echo '<body class="login-body">
<div id="wrapper">
<div class="user-icon"></div>
<div class="pass-icon"></div>
<form name="login-form" class="login-form" action="" method="post">
<div class="header">
<h1>Login Form</h1>
<span>Fill out the form below to login to my super awesome imaginary control panel.</span>
</div>
<div class="content">
<input name="username" type="text" class="input username" value="Username" onfocus="this.value=\'\'" />
<input name="password" type="password" class="input password" value="Password" onfocus="this.value=\'\'" />
</div>
<div class="footer">
<input type="submit" name="submit" value="Login" class="button" />
</div>
</form>
</div>
</body>';
}
}

check_login()函数接受用户传入的cipher和iv,分别将其base64解码后,将对cipher使用传入的iv进行解密,之后如果能成功反序列化解密后的值,那么将对$_SESSION['username']进行一次赋值:

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!");
}
}
}

登录所使用的用户名不能为admin,那么就不能将login()中的$_SESSION['username']赋值为admin,但是check_login()中也存在对$_SESSION['username']的一次赋值,这次赋值发生在AES_CBC解密后

那么我们着重看AES_CBC的加解密:

$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);

加密的时候,只有$plain是我们可控的,key是未知的,iv是随机生成的

解密的时候,cipher和iv是我们可控的,只要将cipher进行AES_CBC解密后,再反序列化得到username为admin即可实现登录

登录时构造:

username=1dmin
password=123

之后通过CBC字节翻转攻击,将username中1变成a即可


CBC模式解密时,假设密文分好组后第一个block的第一个字节为A,第二个block进行AES解密后的第一个字节为B,B解密得到的明文的第一个字节为C,那么在解密时,第一个字节进行运算时有:

A ^ B = C
A ^ B ^ C = 0

现在将A中的第一个字节替换为A ^ C,那么有:

C1 = A ^ C ^ B = 0

这样的话明文就变成了0,那么将A中的第一个字节替换为A ^ C ^ X,有:

C2 = A ^ C ^ X ^ B = 0 ^ X = X

这样即可得到我们想要的明文X,即实现了字节翻转攻击


登录信息序列化后的值:

a:2:{s:8:"username";s:5:"1dmin";s:8:"password";s:3:"123";}

将其分组:

block1: a:2:{s:8:"userna
block2: me";s:5:"1dmin";
block3: s:8:"password";s
block4: :3:"123";}

我们需要在解密时改变block2中偏移量为9的值1,由于加解密后一块密文时都是使用前一密文块的值进行加解密,因此需要改变block1中偏移量为9的值;

那么构造时,将block1中的第一个字节替换为A ^ C ^ X即可,即:

block1[9] ^ ord('1') ^ ord('a')

改变了block1,但解密block1得到的明文并不能改变,否则就无法进行反序列化了,因此需要改变初始iv的值。设block1原错误明文为error_plain,正确明文为right_plain,block1解密AES后密文为cipher:

error_plain = iv ^ cipher
cipher = error_plain ^ iv

因为:

right_plain = new_iv ^ cipher

那么:

new_iv = cipher ^ right_plain  = error_plain ^ iv ^ right_plain

使用1dmin登录后,得到随机生成的iv和加密得到的密文:

iv = '4%2FCGh9TAo5%2F1g5ynUM5HOw%3D%3D'
cipher = 'TUiTPRmhL43IHpQneHr5AdNXfyiibHREkWKUeMr9O6MoUow9OPdhBv0V%2F%2Fd8JcJpgzpw9QSPGN%2FAjIceUc02uA%3D%3D'

进行字节翻转后,改变cipher的值,发包,得到解AES后、反序列化失败的字符串:

4eP3BIrw1JBEk998MaNiyG1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjQ6ImFzZGYiO30=

在推出正确的iv,改变iv的值,发包得到flag.完整脚本:

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


def base_urldecode(s):
return base64.b64decode(urllib.parse.unquote(s))


def base_urlencode(s):
return urllib.parse.quote(base64.b64encode(s))


iv = '4%2FCGh9TAo5%2F1g5ynUM5HOw%3D%3D'
cipher = 'TUiTPRmhL43IHpQneHr5AdNXfyiibHREkWKUeMr9O6MoUow9OPdhBv0V%2F%2Fd8JcJpgzpw9QSPGN%2FAjIceUc02uA%3D%3D'

iv = base_urldecode(iv)
cipher = base_urldecode(cipher)

# 字节翻转
cipher = cipher[:9] + bytes(chr(cipher[9] ^ ord('1') ^ ord('a')), encoding='utf-8') + cipher[10:]
print(base_urlencode(cipher))

# 得到AES解密后的值:
AES_de = '4eP3BIrw1JBEk998MaNiyG1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjQ6ImFzZGYiO30='

error_plain = base64.b64decode(AES_de)
new_iv = bytearray(16)
right_plain = 'a:2:{s:8:"userna'
for i in range(16):
new_iv[i] = ord(right_plain[i]) ^ iv[i] ^ error_plain[i]

print(base_urlencode(new_iv))