几个月之前就碰到了这类题,但是一直没搞懂,最近做ctf又碰到了,赶紧研究了一番,总算是搞懂了
CBC原理 首先还是要稍微了解一下CBC加密和解密的原理
如果用公式来表示的话,如下:(C表示密文,E表示进行加密,P表示明文,D表示进行解密,IV表示初始向量)
再来看看如何进行攻击:(下面图片摘自《图解密码技术》一书)
上面就是CBC原理,其实看上去一脸懵逼很正常,但是其实我们只需要关注CBC解密的过程,因为我们攻击的过程就是解密的过程,为什么呢,我们来看实例的代码分析就知道了
实例 实例来自bugkuctf web类最后一题login4
网址:http://118.89.219.210:49168/
扫描目录发现存在文件.index.php.swp,下载下来后用linux 的vim -r 命令恢复文件后可以查看到源代码
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 81 82 83 <?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); $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 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!"); } } } 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>'; } 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>'); }else{ $info = array('username'=>$username,'password'=>$password); login($info); show_homepage(); } }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>'; } } ?>
首先服务器接收我们POST的参数username和password,并对username进行检查,如果是admin,则退出程序,如果不是admin,则创建一个数组info,来存放我们输入的username和password,然后经过login函数,也就是cbc的加密,加密的过程是首先创建一个16位长度的随机字符串,然后与数组info序列化后的字符串plain进行CBC加密,也就是一系列的异或运算,具体的加密我们可以不用管,因为我们改变不了加密的过程,我们要操作的,是接下来的解密过程。再往下看程序,加密完以后,会将加密过程的初始化向量(也就是16位的随机字符串)iv和加密后的密文cipher经过base64加密后分别存放到cookie中,这样加密过程就算完成了。接下来,我们再次登录时,服务器执行check_login函数,将cookie中的iv和cipher字段值取出来进行base64解密后,进行cbc的解密,再将解密后字符串进行反序列化得到数组info,然后将info的username取出放入session的username字段,最后检查username如果是admin,则可以查看到flag,如果不是,则输出只有admin才可以查看flag,总的来说,这算是前后矛盾,一开始如果我们输入的username是admin,则提示admin查看不了flag,我们输入的如果不是admin,又提示admin才可以查看flag
所以,要拿到flag,我们的思路,就是一开始输入Admin,然后操作CBC解密的过程,让它最后解密出来的值变成admin,就可以拿到flag,为此,我们重点就是放在cbc解密的过程,来看一张比较清楚的思路图
分析解密的过程:
(1)首先以16位为长度对密文进行分组
(2)初始化向量iv与前16位的解密的密文分组1进行位异或运算得到明文分组1
(3)密文分组1与解密的密文分组2进行异或运算得到明文分组2
(4)密文分组2与解密的密文分组3进行异或运算得到明文分组3
(5)以此类推
那我们就按照解密的过程的顺序逐步演示攻击的过程
首先我们对明文进行分组,因为我们加密的过程也是位异或,所以最终得到的密文位数是等于明文位数的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php $info = array ('username' =>'Admin' ,'password' =>'admin' ); $plain = serialize($info); echo $plain.'<br>' ;for ($i = 0 ; $i < strlen($plain); $i++){ if ($plain[$i] == 'A' ){ $A_index = $i % 16 ; } if ($i % 16 == 0 ){ echo '<br>' .$plain[$i]; } else { echo $plain[$i]; } } echo '<br><br>' .'the index of A is: ' .$A_index;?>
这里我们定下一开始输入的username是Admin,password是admin,对info数组进行序列化处理然后分组
我们翻转的位置是A,我们要想办法将它翻转成a,从而将用户名变成admin,上面已经分析过了解密是位异或,A所处于的密文分组是分组2,它是与密文分组1进行位异或运算得到明文分组2的
所以我们要操纵的就是密文分组1的第9位,使它与A所处的密文分组2异或后得到a
但是我们还要考虑,我们改变了密文分组的第9位,只会影响下一组明文翻转的那一位,但却会影响本组明文的全部,因为我们翻转过的密文要先经过解密,然后才和IV变量进行异或,解密的时候是整串数据进行,所以整串明文受其影响。甚至可能会导致生成的明文部分乱码,至少绝对不再是原来的明文了。 所以,我们还需要操作iv,也就是初始化向量,使它与解密的密文分组1异或能得到原来的明文分组1,也就是a:2:{s:8:”userna
最后就需要考虑如何得到我们想要的字符,直接看公式吧
1 2 3 4 5 6 7 8 9 本组明文 = Decrypt(本组密文) ^ 上一组密文 A B C ========================================================= A = B ^ C A ^ A = 0; 0 ^ A = A C = A ^ A ^ C = B ^ C ^ A ^ C = A ^ B (即C = A ^ B ,即:上一组密文 = 本组明文 ^ Decrypt(本组密文) ) ascii('a') ^ C ^ A ^ B = ascii('a') ^ A ^ B ^ A ^ B = ascii('a') ^ 0 = ascii('a') (假设我们想要翻转成a,使用如上公式即可,即:想要的字符 = 上一组密文 ^ 本组明文 ^ Decrypt(本组密文) ^ 想要的字符 )
那么接下来,我们就可以开始编写脚本了,首先,我们先将用户名:Admin和密码:admin POST给服务器
然后获取cookie值的cipher字段,操作第一个密文分组的第九位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import requests,base64,urllib,reurl = "http://118.89.219.210:49168/" data = { 'username' :'Admin' , 'password' :'admin' } r = requests.post(url,data=data) cookies = requests.utils.dict_from_cookiejar(r.cookies) cipher = cookies['cipher' ] cipher = base64.b64decode(urllib.unquote(cipher)) index = 9 new_cipher = cipher[:index] + chr(ord(cipher[index])^ord('A' )^ord('a' )) + cipher[index+1 :] new_cipher = urllib.quote_plus(base64.b64encode(new_cipher)) cookies['cipher' ] = new_cipher r2 = requests.get(url,cookies=cookies) print r2.text
运行结果
可以看到修改cipher后,再次访问页面给出了提示解密后的明文的不能被序列化,就是因为我们操纵密文分组1,改变了全部的明文分组1,使之不能被序列化
我们将plain取出,解码
1 2 plain = base64.b64decode(re.findall("base64_decode\('(.*)'\)" ,r2.text)[0 ]) print plain
可以看到,我们已经成功的将A翻转成a,但是导致了本组明文乱码
所以接下来我们要操作的是iv的全部十六位,下面给出所有代码
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 import requests,base64,urllib,reurl = "http://118.89.219.210:49168/" data = { 'username' :'Admin' , 'password' :'admin' } r = requests.post(url,data=data) cookies = requests.utils.dict_from_cookiejar(r.cookies) cipher = cookies['cipher' ] cipher = base64.b64decode(urllib.unquote(cipher)) index = 9 new_cipher = cipher[:index] + chr(ord(cipher[index])^ord('A' )^ord('a' )) + cipher[index+1 :] new_cipher = urllib.quote_plus(base64.b64encode(new_cipher)) cookies['cipher' ] = new_cipher r2 = requests.get(url,cookies=cookies) plain = base64.b64decode(re.findall("base64_decode\('(.*)'\)" ,r2.text)[0 ]) iv = cookies['iv' ] iv = base64.b64decode(urllib.unquote(iv)) target = 'a:2:{s:8:"userna' new_iv = '' for i in range(16 ): new_iv = new_iv + chr(ord(target[i])^ord(plain[i])^ord(iv[i])) cookies['iv' ] = urllib.quote_plus(base64.b64encode(new_iv)) r3 = requests.get(url,cookies=cookies) print r3.text
运行结果
成功获得flag