cbc字节翻转攻击

今天遇到一道题,感觉学习了一个新的姿势,所以记录一下过程。

右键查看源代码,发现泄漏提示,tip:index.txt

1

源代码贴一下:

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
77
78
79
80
<?php
include 'sqlwaf.php';
define("SECRET_KEY", "................");
define("METHOD", "aes-128-cbc");
session_start();

function get_random_iv(){
$iv='';
for($i=0;$i<16;$i++){
$iv.=chr(rand(1,255));
}
return $iv;
}
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));
}
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();
}
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!");
}
}
}

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();
}
}
?>
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>Paper login form</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div id="login">
<form action="" method="post">
<h1>Sign In</h1>
<input name='username' type="text" placeholder="Username">
<input name='password' type="password" placeholder="Password">
<button>Sign in</button>
</div>
</body>
</html>

首先这里登陆的时候已经禁止username为admin

2

但是我们可以通过反序列化$plain,重置$_SESSION['username']admin

3

前提知识

此攻击方法的精髓在于:通过损坏密文字节来改变明文字节。(注:借助CBC内部的模式)借由此可以绕过过滤器,或者改变用户权限提升至管理员,又或者改变应用程序预期明文以尽猥琐之事。

首先让我们看看CBC是如何工作的,(作者很懒所以)更多细节你可以看这里:wiki

在这里只是解释一下关于攻击必须要理解的部分。(即:一图胜千言)

加密过程

9

Plaintext:待加密的数据。

IV:用于随机化加密的比特块,保证即使对相同明文多次加密,也可以得到不同的密文。

Key:被一些如AES的对称加密算法使用。

Ciphertext:加密后的数据。

在这里重要的一点是,CBC工作于一个固定长度的比特组,将其称之为。在本文中,我们将使用包含16字节的块。

因为作者讨厌高数,所以作者造了一些自己的公式:

1
2
Ciphertext-0 = Encrypt(Plaintext XOR IV)—只用于第一个组块
Ciphertext-N= Encrypt(Plaintext XOR Ciphertext-N-1)—用于第二及剩下的组块

注意:正如你所见,前一块的密文用来产生后一块的密文

Decryption Process

10

1
2
Plaintext-0 = Decrypt(Ciphertext) XOR IV—只用于第一个组块
Plaintext-N= Decrypt(Ciphertext) XOR Ciphertext-N-1—用于第二及剩下的组块

注意:Ciphertext-N-1(密文-N-1)是用来产生下一块明文;这就是字节翻转攻击开始发挥作用的地方。如果我们改变Ciphertext-N-1(密文-N-1)的一个字节,然后与下一个解密后的组块异或,我们就可以得到一个不同的明文了!You got it?别担心,下面我们将看到一个详细的例子。与此同时,下面的这张图也可以很好地说明这种攻击:

enter image description here

举个例子

比方说,我们有这样的明文序列:

a:2:{s:4:"name";s:6:"sdsdsd";s:8:"greeting";s:20:"echo 'Hello sdsdsd!'";}

我们的目标是将“s:6”当中的数字6转换成数字“7”。我们需要做的第一件事就是把明文分成16个字节的块:

  • Block 1:a:2:{s:4:"name";
  • Block 2:s:6:"sdsdsd";s:8
  • Block 3::"greeting";s:20
  • Block 4::"echo 'Hello sd
  • Block 5:sdsd!'";}

因此,我们的目标字符位于块2,这意味着我们需要改变块1的密文来改变第二块的明文。

有一条经验法则是(注:结合上面的说明图可以得到),你在密文中改变的字节,会影响到在下一明文当中,具有相同偏移量的字节。

因此我们要改变在第一个密文块当中,偏移量是2的字节。正如你在下面的代码当中看到的,在第2行我们得到了整个数据的密文,然后在第3行中,我们改变块1中偏移量为2的字节,最后我们再调用解密函数。

  1. $v = "a:2:{s:4:"name";s:6:"sdsdsd";s:8:"greeting";s:20:"echo 'Hello sdsdsd!'";}";
  2. $enc = @encrypt($v);
  3. $enc[2] = chr(ord($enc[2]) ^ ord("6") ^ ord ("7"));
  4. $b = @decrypt($enc);

运行这段代码后,我们可以将数字6变为7:

11

但是我们在第3行中,是如何改变字节成为我们想要的值呢?

基于上述的解密过程,我们知道有,A = Decrypt(Ciphertext)B = Ciphertext-N-1异或后最终得到C = 6。等价于:

1
C = A XOR B

所以,我们唯一不知道的值就是A(注:对于B,C来说)(block cipher decryption);借由XOR,我们可以很轻易地得到A的值:

1
A = B XOR C

最后,A XOR B XOR C等于0。有了这个公式,我们可以在XOR运算的末尾处设置我们自己的值,就像这样:

A XOR B XOR C XOR "7"会在块2的明文当中,偏移量为2的字节处得到7。

下面是相关原理实现的PHP源代码:

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
#!php
define('MY_AES_KEY', "abcdef0123456789");
function aes($data, $encrypt) {
$aes = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
$iv = "1234567891234567";
mcrypt_generic_init($aes, MY_AES_KEY, $iv);
return $encrypt ? mcrypt_generic($aes,$data) : mdecrypt_generic($aes,$data);
}

define('MY_MAC_LEN', 40);

function encrypt($data) {
return aes($data, true);
}

function decrypt($data) {
$data = rtrim(aes($data, false), "\0");
return $data;
}
$v = "a:2:{s:4:\"name\";s:6:\"sdsdsd\";s:8:\"greeting\";s:20:\"echo 'Hello sdsdsd!'\";}";
echo "Plaintext before attack: $v\n";
$b = array();
$enc = array();
$enc = @encrypt($v);
$enc[2] = chr(ord($enc[2]) ^ ord("6") ^ ord ("7"));
$b = @decrypt($enc);
echo "Plaintext AFTER attack : $b\n";

收回来解题

简单针对login代码做个分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function login($info)
{
$iv=get_random_iv();
#随机生成一个初识向量。
$plain = serialize($info);
#序列化$info = array('username'=>$username,'password'=>$password);
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
#cbc模式下的aes加密明文
$_SESSION['username'] = $info['username'];
setcookie("iv", base64_encode($iv));
#初识向量经过base64加密。
setcookie("cipher", base64_encode($cipher));
#把密文再base64加密一次。
}

这里我们注意到最后一步, $_SESSION['username'] = $info['username'],而根据info的定义赋值可知,$info['username']是我们传进进去的账户名的值。而代码不让我们使用admin,所以此路不通。

然后继续看代码,找到了这串,也调用了flag()

1
2
3
4
5
6
else{
if(isset($_SESSION["username"])){
check_login();
show_homepage();
}
}

首先要不进入刚刚的那个if,就是不满足 set($_POST['username']) && isset($_POST['password']) 然后,要有$_SESSION[“username”]在。

看一下check_login()做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function check_login(){
if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
#存在cipher和iv的cookie值
$cipher = base64_decode($_COOKIE['cipher']);
#密文是base64解码后的cookie值cipher。
$iv = base64_decode($_COOKIE["iv"]);
if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
#如果能成功使用aes解密处铭文
$info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>")
#如果不能反序列化解密出的密文,则结束。
$_SESSION['username'] = $info['username'];
#如果可以反序列化,则将info中的username给session。
}else{
die("ERROR!");
}
}
}

这样经过check_login函数之后,$_SESSION['username']就还是我们最开始post的那个username。所以我们就要想如何把$_SESSION['username']变成admin,即使我们最开始post的可能是admi2,但是它经过了AES加密和解密阿,这就有得搞了阿。

开始搞事

通过题目先生成序列化的明文

1
2
3
4
<?php
$info = array('username'=>'admi2','password'=>'l1nk3r');
echo serialize($info)
?>

a:2:{s:8:"username";s:5:"admi2";s:8:"password";s:6:"l1nk3r";}

然后就开始分块

1
2
3
4
a:2:{s:8:"userna
me";s:5:"admi2";
s:8:"password";s
:8:"l1nk3r";}

按照CBC字节翻转攻击的套路,我们要修改的是上一个分组对应位置的密文

就是下面会放的脚本中的代码:

bs_de[13]=chr(ord(bs_de[13]) ^ ord('2') ^ ord('n'))

但有一点需要注意,这样改变后,密文的值发生了改变,将其进行解密后反序列化,是会失败的,从而会把无法反序列化的明文打印出来(因为我们对block 1进行了改变,虽然block 2变成了我们所希望的值,但block 1却变成了未知的量) 所以此时,需要改变iv的值来改变block 1的值 (注:这就是cbc的处理方式,iv的值改变block1的值,block1的值改变block2的值……) 故此,我们需要一个正确的Iv,使block 1依旧为a:2:{s:8:"userna

正式解题

首先页面通过username=admi2,password=l1nk3r登陆,然后通过Chrome的插件EditThisCookie,把cipher的值赋给以下脚本中的cipher

5

1
2
3
4
5
6
7
8
9
10
11
import base64
from urllib import unquote
from urllib import quote_plus

cipher = '9AOOYlmVvT4xmjViSqORmkZT%2Bxm2mJf0tsuHd%2BUcf1xrghHyXp8FRoQjkE%2FR4dJu8U8DJfcHcHEGJI2ntOYxHw%3D%3D'
cipher = unquote(cipher)
cipher_de = base64.b64decode(cipher)
ch = chr(ord(cipher_de[13]) ^ ord('2') ^ ord('n'))
cipher_de=cipher_de[0:13]+ch+cipher_de[14::]
rs = base64.b64encode(cipher_de)
print quote_plus(rs)

然后将运行结果替换cipher,并且点击绿的勾保存

4

然后在浏览器按住回车运行,这里前往不要刷新,因为刷新会重新提交表单,然后随机字符又变了。这是我的结果

6

然后base64解码下,其实已经替换成功了。

7

这里我们将第一组的密文改了,第一组解密之后又不是原来的那个字符串,当然反序列化失败。所以现在要改回来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- coding:utf-8 -*-

import base64
from urllib import unquote
from urllib import quote_plus

mingwen_de='x2FPwTAFf/svVLvvU25fDW1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjA6IiI7fQ=='
#base64_decode('这里面的') can't unserialize
mingwen = base64.b64decode(mingwen_de)
print mingwen

iv = '6S%2FcC7czBRvE0iSkTANYaQ%3D%3D'
#此时cookie里的iv
iv = unquote(iv)
iv_de = base64.b64decode(iv)
new = 'a:2:{s:8:"userna'
for i in range(16):
iv_de = iv_de[:i] + chr(ord(iv_de[i]) ^ ord(mingwen[i]) ^ ord(new[i])) + iv_de[i+1:]


print(base64.b64encode(iv_de))
#用这个结果把原来的iv换掉

8