• Home
  • About
    • Rhy7hm photo

      Rhy7hm

      天天被计算机教做人

    • Learn More
    • Email
  • Posts
    • All Posts
    • All Tags
  • Projects

【CBC字节翻转】bugku-web-login4-WP

23 Apr 2018

Reading time ~4 minutes

三文鱼都能看懂的CBC字节翻转:D


0x01 代码审计之前

这是在bugku里web分类中的最后一道题。 打开就看到一个连接和一个hint,hint说是CBC字节翻转攻击。

我们先用SourceLeakHacker扫一下,SourceLeakHacker是一款敏感目录扫描工具,敏感目录扫描是web ~~狗~~ 手的常用姿势之一。

看到那个绿色的200表示能够成功访问,这就是我们扫出来的敏感路径

我们这里要用到的是.index.php.swp这个文件

我们不用很麻烦就能通过搜索引擎知道这个文件是非正常关闭vi编辑器时生成的。

我们访问这个url,就能成功下载这个文件。 这个文件是需要恢复的。恢复方法如下: 将文件拖入虚拟机,在文件所在的目录(下图的第一条指令)使用

vi -r index.php.swp

这条命令 然后会出来这个样子的

那它说Press ENTER or type command to continue就按个enter呗

然后就出来我们需要的代码了 这时Esc进入命令模式其实不按也行,因为本来就在命令模式,这也是个人习惯吧

敲 :w index.php

把还原出来的php文件另存为 index.php 然后它会说index.php成功保存了

再敲 :q!

退出编辑器

把生成的文件拖回Windows

然后就可以开始看代码了


0x02 PHP代码审计进行时

拔出来的代码如下

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Login Form</title>
<link href="static/css/style.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="static/js/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
	$(".username").focus(function() {
		$(".user-icon").css("left","-48px");
	});
	$(".username").blur(function() {
		$(".user-icon").css("left","0px");
	});

	$(".password").focus(function() {
		$(".pass-icon").css("left","-48px");
	});
	$(".password").blur(function() {
		$(".pass-icon").css("left","0px");
	});
});
</script>
</head>

<?php
define("SECRET_KEY", file_get_contents('/root/key'));

define("METHOD", "aes-128-cbc");
session_start();

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);
	#info序列化后得到明文
    $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));
	#45行出来的密文再加一层base64加密
}

function check_login(){
    if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
        $cipher = base64_decode($_COOKIE['cipher']);
        $iv = base64_decode($_COOKIE["iv"]);
		#base64解密
        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'];
        }else{
            die("ERROR!");
        }
    }
}

function show_homepage(){
    if ($_SESSION["username"]==='admin'){
		#这里可以get flag
		#可见我们的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>';
}

if(isset($_POST['username']) && isset($_POST['password'])){
    $username = (string)$_POST['username'];
    $password = (string)$_POST['password'];
    if($username === 'admin'){
		#如果username是admin的话,就会显示下面这句话然后退出
        exit('<p>admin are not allowed to login</p>');
    }else{
        $info = array('username'=>$username,'password'=>$password);
        login($info);
        show_homepage();
    }
}else{
    if(isset($_SESSION["username"])){
		#如果password为空
        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>';
    }
}
?>
</html>

我个人习惯是先看flag在哪,然后看怎么才能到达这个地方,

但是某不愿意透露姓名的web大佬是习惯从入口开始看的,和我刚好相反。

(以下纯属个人做法,参考一下就好,想要更厉害的代码审计方法可以去py一下其他web大佬)

Ctrl+F搜flag 看到在show_homepage()这个函数里,$_SESSION[“username”]===’admin’的话我们就能get flag了

那么哪里用到show_homepage()呢?有两个地方

第一个是 此时username和password都有post,而且username不能为admin,那么经过login函数之后,就会运行到show_homepage()了。

我们看看login干了啥

意思意思画个不规范的流程图

这里我们注意到最后一步, $_SESSION['username'] = $info['username']

而根据info的定义赋值可知,$info['username']是我们传进进去的账户名的值。

但是由第93-95行代码可知,如果我们穿进去的账户名值为username,就会在一开始直接返回admin are not allowed to login然后退出,所以此路不通。

幸好用到show_homepage()的还有另一个地方。 首先要不进入刚刚的那个if,就是不满足 set($_POST['username']) && isset($_POST['password']) 然后,要有$_SESSION[“username”]在

然后我们看看check_login()干了啥 其实就是相当于把login逆了过来而已。

在正常情况下,如果我们正正经经地post一个账户名和口令字,这个网页就会把你post的东西经过login那一系列的变化,用cookie保存下来,然后下次我们不需要再post东西了,凭借cookie,网站通过check_login得到我们post过得账户名和口令字,就能知道我们的身份。

这样经过check_login函数之后,$_SESSION['username']就还是我们最开始post的那个username。

但我们是正经人吗?我们是要搞事get flag的人,所以我们就要想如何把$_SESSION['username']变成admin,

即使我们最开始post的可能是admi2,

但是它经过了AES加密和解密阿,这就有得搞了阿。

这里就要用到hint里面提到的CBC字节翻转攻击

攻击的point就在check_login()的解密过程里。

PHP代码审计终于结束,可喜可贺可喜可贺

什么是加密模式就不多说了,下面直接

0x003 CBC加密模式介绍

CBC它,就是每个明文块先与前一个密文块进行异或后,再进行加密 详细一点表达加密过程,就是

当

P是明文,C是密文,下标表示第几组EK是加密,DK是解密,IV是初始向量,Z就是个用来辅助的

另

Z1 = P1 ⊕ IV

Z2 = P2 ⊕ C1

Z3 = P3 ⊕ C2

…

Zi = Pi ⊕ Ci-1

… 然后密文就是

C0 = IV

C1 = EK(Z1)

C2 = EK(Z2)

…

Ci = EK(Zi)

…

以上都是为了更好地理解

加密 ↓

Ci = EK(Pi⊕Ci-1)

所以

Pi ⊕ Ci-1 = DK(Ci)

Pi = DK(Ci) ⊕ Ci-1 <-解密

word里面辛辛苦苦做下标,改成.md一朝回到解放前

所以,我们可以通过修改Ci-1来达到修改Pi的目的

我们先来看一下CBC字节翻转攻击是怎么做的

这里直接引用这里 提到的一个例子

刚发现链接好像打不开了????


【复制粘贴开始】

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

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:

• [0] = s

• 1 = :

• 2 =6

因此我们要改变在第一个密文块当中,偏移量是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:

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

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

C = A XOR B

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

A = B XOR C

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

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

【复制粘贴结束】


然后回到我们这一题。

我们的明文序列是:

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

wait a minute ……这个明文序列怎么来的?

这是因为,我们知道:

$info = array('username'=>$username,'password'=>$password);

我们可以用

<?php
$info = array('username'=>'admi2','password'=>'Password');
echo serialize($info);
?>

开个phpstudy localhost访问一下就能得到上面那个明文序列

然后

我们也分个块

a:2:{s:8:"userna
me";s:5:"admi2";
s:8:"password";s
:8:"Password";}

我们要改的是admi2中的’2’,也就是第二列的第13个

插一段黑历史 请不要像我一样数的时候没数到python框住字符串的单引号以为第二行的”就是结束谢谢

get 不到?我当年是从上图第三行那里数出来’2’在倒数第二个也就是排15的……

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

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

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

也就是,如果crypto是我们那个明文序列经过aes加密后的密文

只要我们令

crypto[13] = crypto[13] ⊕ ‘2’ ⊕ ‘n’

即,把第一个分组的第13个字符(因为我们要改的是第二个分组中的第13个字符),变成

crypto[13] ⊕ ‘2’ ⊕ ‘n’

其实就是: 密文 ⊕ 我们要改掉的 ⊕ 我们要改成的

我们看看解密的时候会发生什么?

我们知道,解密是Pi = DK(Ci) ⊕ Ci-1

我们把crypto[13],也就是上式的Ci-1改了

当i= 2 时

Pi = DK(Ci) ⊕ Ci-1 = DK(Ci) ⊕ crypto[13] ⊕ ‘2’ ⊕ ‘n’

而我们又有

Pi = DK(Ci) ⊕ Ci-1

Pi ⊕ DK(Ci) ⊕ Ci-1 = 0

异或其实就是二进制里面的加法,而且相同的相加为零,这式子相当于两边同时加Pi

DK(Ci) ⊕ Ci-1 ⊕ Pi = 0

DK(Ci)[13] ⊕ Ci-1 [13]⊕ Pi [13]= 0

DK(Ci) ⊕ crypto[13] ⊕ ‘2’ = 0

DK(Ci) ⊕ crypto[13] ⊕ ‘2’ ⊕ ‘n’ = ‘n’

Pi = DK(Ci) ⊕ Ci-1 = DK(Ci) ⊕ crypto[13] ⊕ ‘2’ ⊕ ‘n’ = ‘n’

         ↑这里check_login在解密的时候就这么做,和DK(Ci)异或

                         ↑这是我们修改了的密文

↑这是check_login 解密之后产生的明文

综上,只要我们把第一组密文改一改,就能让它解密之后第二组的明文产生变化。

我们再回到这道题

Login函数中    
iv和序列化后的Info (usn,psd)    
↓ -> aes加密前的明文
aes-128-cbc    
↓ -> aes加密后的密文
base64    
↓    
赋值给cookie    
…    
check_login中    
从cookie里读入’cipher’和’iv’      
↓    
base64解码    
↓ -> aes解密前的密文
aes解密    
↓ -> aes解密后的明文
反序列化    
…    

我们的CBC字符翻转攻击就是在上表倒数第四列用的。

直接上脚本

# -*- coding:utf-8 -*-

import base64
from urllib import unquote
from urllib import quote_plus

bs = 'J67IvH4OY9t6QfcCP4Mp88bRfPKdsYszlev3LNbgPQ872VR633trbWfCqhcDYm6c3Eysp36W2SFkx4CPBX9nDQ%3D%3D'
#密文cipher,从Chrome的插件EditThisCookie可以很轻松get到
bs = unquote(bs)
#url解码
bs_de = base64.b64decode(bs)
#base64解码

ch = chr(ord(bs_de[13]) ^ ord('2') ^ ord('n'))
bs_de=bs_de[0:13]+ch+bs_de[14::]
#其实就是bs_de[13]=chr(ord(bs_de[13]) ^ ord('2') ^ ord('n'))
#因为python里面字符串不可变,即你直接bs_de = 'a'这样会报错

rs = base64.b64encode(bs_de)
#print rs
print quote_plus(rs)

我们在最开始的页面,第一行填admi2,第二行碰都不要碰(它会自动帮你填充Password,如果你点击了一下Password就gg为空了)

进去之后使用Chrome的插件EditThisCookie,把cipher的值赋给脚本中的bs 运行后,脚本返回修改了第一个密文分组的值的密文,当然它还经过了base64编码和urlencode 我们把这堆东西贴到cipher那,然后按 绿色这个勾保存,然后在回车栏按enter

注意:不要按刷新,因为刷新的话会gg重新提交表格,也就是之前post的username和password会重新提交,password不为空我们就又进去login而进不去check_login了,而我们直接回车栏enter是不会post东西的,但是cookie已经被我们改了。

然后它返回个错误,说反序列化失败 这是当然的,因为我们把第一组的密文改了,第一组解密之后又不是原来的那个字符串,当然反序列化失败。

我们把这串东西base64解密一下看看 我们看到,我们已经成功地把admi2变成admin了

所以下一步我们就要用相同的套路,来复原第一个密文分组,也就是初始向量。

还记得我之前提到的吗?

把上一分组对应的密文 变成 密文 ⊕ 我们要改掉的 ⊕ 我们要改成的

我还说过,密文是什么? C0 = IV C1 = EK(Z1) C2 = EK(Z2) 所以我们要第一个分组的明文,当然就要对C0 = IV下手了

原理和第一关一样,直接我们第二个脚本

# -*- coding:utf-8 -*-

import base64
from urllib import unquote
from urllib import quote_plus

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

iv = 'cFDYuzPtN1Q5ETd1E7s1Zw%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:]
#iv_de[i]=chr(ord(iv_de[i]) ^ ord(mingwen[i]) ^ ord(new[i]))

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

主要就是

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:]

也就是

iv_de[i]=chr(ord(iv_de[i]) ^ ord(mingwen[i]) ^ ord(new[i]))

i遍历第一个分组的所有字符

搞这么复杂的原因在cbc1.py的注释有提到,因为python不给我们直接改字符串

这其中,iv_de就是IV,mingwen就是当前它解密出来的明文,也就是我们要改的,new是正确的明文,就是我们想要改变的

这一点满足我之前说的

密文 = 密文 ⊕ 我们要改掉的 ⊕ 我们要改成的

这个脚本运行出来的东西就是我们改变后的IV的密文,再base64一下urlencode一下,然后把当前的IV换成这个,然后刷新或者地址栏回车都可以(因为上一次操作并没有携带post的内容),然后我们就能get flag了


最后再啰嗦一次,这道题怎么做呢?

打开bugku

然后SourceLeakHacker 把index.php.swp扫出来

然后vi -r index.php.swp 还原出php文件

然后代码审计

然后在登录页面的第一个框输入admi2,第二个框不要碰

然后我们打开EditThisCookie,把cipher的值赋给脚本cbc1.py中的bs,运行

把运行结果赋值给cookie中的cipher,地址栏按回车

把页面出现的base64_decode(‘这里面的’) can’t unserialize,单引号中的那个字符串,赋值给脚本cbc2.py中的mingwen_de

打开EditThisCookie,把iv的值赋给脚本cbc2.py中的iv

用运行脚本后得到的第二个字符串,替换掉cookie里的iv

刷新,get flag


最后,欢迎交流 :D



CBC 字节翻转攻击bugkuwebCryptoCTFWrite up Share Tweet +1