CTF中常见的Web密码学攻击方式

ctf中web题目一般会有一个或多个包含密码学的题目,这块一直是弱项,这几天恶补了一波,总结一下,方便之后复习、寻找思路。
写到最后发现有点多了,做个目录。。

ECB

参考: 分组密码模式: ECB模式(电子密码本模式)
加、解密过程如图:

可以看到明文分组和密文分组是一一对应的,且每一对分组都是单独加密的。那么攻击方式就很明显了,我们对某个密文分组进行修改,与之对应的明文在解密时就会被修改,由于每个分组是单独加密的,所以对其他的分组是没有影响的。
例题:
直接上代码吧:

<?php 
    function AES($data){     
        $privateKey = "12345678123456781234567812345678";     
        $encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $privateKey, $data, MCRYPT_MODE_ECB);     
        $encryptedData = (base64_encode($encrypted));     
        return $encryptedData; 
    } 

    function DE__AES($data){     
        $privateKey = "12345678123456781234567812345678";     
        $encryptedData = base64_decode($data);     
        $decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $privateKey, $encryptedData, MCRYPT_MODE_ECB);     
        $decrypted = rtrim($decrypted, "\0");     
        return $decrypted; 
    }

    if (@$_GET['a'] == 'reg'){     
        setcookie('uid', AES('9'));     
        setcookie('username', AES($_POST['username']));     
        header("Location: ecb.php");     
        exit(); 
    } 

    if (@!isset($_COOKIE['uid'])||@!isset($_COOKIE['username'])){     
        echo '<form method="post" action="ecb.php?a=reg"> Username:<br>'; 
        echo '<input type="text"  name="username"> <br>';
        echo 'Password:<br> <input type="text" name="password" > <br><br>'; 
        echo '<input type="submit" value="register">'; 
        echo '</form>'; 
    } else {     
        $uid = DE__AES($_COOKIE['uid']);     
        if ( $uid != '4'){         
            echo 'uid:' .$uid .'<br/>';         
            echo '' . DE__AES($_COOKIE['username']) .'<br/>';         
            echo 'You are not administrotor!!';     
        } else {           
            echo "Hi you are administrotor!!" .'<br/>';         
            echo 'Flag is 360 become better';     
        } 
    } 
?> 

简单分析一下,只要uid == 4,就可以得到flag。而在前面将uid设置为9并加密,而且将注册的username也进行了加密,两者都使用了ebc加密。username是可控的,可以通过注册一个特殊的用户名来获得uid的密文进行替换。
首先我们通过fuzz来获取分组的长度,代码如下:

import requests

url = 'http://10.10.10.1/test/ecb.php?a=reg'
for i in range(1, 31):
    cookies = requests.post(url, data={'username': 'a'*i, 'password':'1'}, allow_redirects=False).cookies
    username = base64.b64decode(urllib.unquote(cookies['username']))
    print len('a'*i), len(username)

通过查看username即密文的长度即可判断分组的长度

......
13 16
14 16
15 16
16 16
17 32
.......

可以看到分组的长度为16,根据分析写出利用代码:

import requests
import base64
import urllib

url = "http://10.10.10.1/test/ecb.php"

cookies = requests.post(url+'?a=reg', data={'username': 'aaaaaaaaaaaaaaaa4', 'password': '123456'}, allow_redirects=False).cookies
#分组长度为16,所以注册一个长度为17的用户名。
#得到32位的密文,前16位为aaaaaaaaaaaaaaaa的密文,后16位为4的密文
username = base64.b64decode(urllib.unquote(cookies['username']))
uid = urllib.quote_plus(base64.b64encode(username[16:]))

r = requests.get(url, cookies={'uid': uid, 'username': cookies['username']})
print r.text

CBC

参考:分组密码模式: CBC模式(密码分组链接模式)

cbc加密方式同样是先进行分组,区别在于将上一组的密文先与下一组的明文进行XOR,然后在进行加密,解密过程类似。
cbc字节翻转攻击(http://www.tuicool.com/articles/vEVFZz)在ctf中非常常见,一般有两种攻击方式:

  1. iv向量,影响第一个明文分组
  2. 第n个密文分组,影响第n+1个明文分组

其中第二个尤为常见,网上有很多题目,这里就不举例子了。放一个比较特殊的,上面两个攻击点都有利用,代码如下:

<!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>
</head>

<?php
define("SECRET_KEY", 'kH3LH3sk45HLsd3n');
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 SKCTF{CBC_wEB_cryptography_6646dfgdg6}</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>';
    }
}
?>
</html>

大体流程就是注册一个用户,将数据序列化后加密,保存到cookie中。注册的用户不能是admin,但只有admin才能看到flag。iv和cipher都放在cookie中,就是可控的,我们可以注册一个1dmin用户,然后通过修改密文进行字节翻转将1翻转为a。但是这样做必然会将前一组数据破坏掉,导致解密出来的数据乱码。注意看check_login这个函数中的这行代码

$info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");

如果数据被破坏,unserialize是无法执行成功的,我们就拿不到flag。还记得cookie里的iv吗?它并不是没有用的,我们需要再对iv进行翻转来保证第一个明文分组数据的正确性。不知道说的清不清楚,上个脚本吧。

# -*- coding: utf-8 -*-
import requests
import base64
import urllib
import re

url = 'http://10.10.10.1/test/cbc/index.php'

def getCookies():
    global url
    data = {
        'username': '1dmin',
        'password': '12345',
    }
    cookies = requests.post(url, data=data).cookies
    return cookies

def exploit(cookies):
    global url

    cipher = base64.b64decode(urllib.unquote(cookies['cipher']))
    pos = 9 #偏移量为9
    cipher = cipher[0:pos] + chr(ord(cipher[pos]) ^ ord('1') ^ ord('a')) + cipher[pos+1:]
    cipher = urllib.quote_plus(base64.b64encode(cipher))
    cookies = {
        'PHPSESSID': cookies['PHPSESSID'],
        'iv'       : cookies['iv'],
        'cipher'   : cipher,
    }

    r = requests.get(url, cookies=cookies)
    #print r.text
    plain = base64.b64decode(re.findall(r'base64.decode\((.*?)\)', r.text)[0])
    iv = base64.b64decode(urllib.unquote(cookies['iv']))

    old = plain[0:16]
    new = 'a:2:{s:8:"userna'
    newiv = ''.join([chr(ord(iv[i]) ^ ord(old[i]) ^ ord(new[i])) for i in xrange(16)])  
    newiv = urllib.quote_plus(base64.b64encode(newiv))

    cookies = {
        'PHPSESSID': cookies['PHPSESSID'],
        'iv'       : newiv,
        'cipher'   : cipher,
    }

    r = requests.get(url, cookies=cookies)
    print (r.text)

if __name__ == '__main__':
    cookies = getCookies()
    exploit(cookies)

cbc就介绍这么多

padding oracle attack

padding oracle attack其实也是对cbc模式加密的一种攻击,具体分析下面这篇文章介绍的很详细,不再赘述。
我对Padding Oracle攻击的分析和思考(详细)
这里截取一些重要的部分(划个重点~)

攻击成立的两个重要条件

  1. 攻击者能够获得密文(Ciphertext),以及附带在密文前面的IV(初始化向量)
  2. 攻击者能够触发密文的解密过程,且能够知道密文的解密结果

padding只有可能在0x01~0x08之间

还有一些我们看着测试代码来分析,代码取自下面两位大佬的blog。
padding oracle攻击原理分析(附带rsa资料)
从DES加密到Padding Oracle Attack

<?php
    $string = 'bertram';
    $key = '123456789';
    $cipher = MCRYPT_DES;
    $modes = MCRYPT_MODE_CBC;

    if(isset($_GET['iv']) && isset($_GET['str'])){
        $iv = hex2bin($_GET['iv']);
        #echo $iv;
        $str = hex2bin($_GET['str']);
        $decode = mcrypt_decrypt($cipher, $key, $str, $modes, $iv);
        #echo bin2hex($iv)."br>";
        #echo bin2hex($encode)."br>";
        echo bin2hex($decode);
        $temp = bin2hex($decode);
    } else {
        $iv = hex2bin("08fe1649311fd298");
        $encode = mcrypt_encrypt($cipher, $key, $string, $modes, $iv);
        echo "the iv is <h2>".bin2hex($iv)."</h2><br>";
        echo "the cipher is <h2>".bin2hex($encode)."</h2><br>";
    }
?>

如果代码运行不了的话应该是没有安装或php没有开启Mcrypt扩展。
首先什么参数也不加,获取iv及密文,满足攻击所需的两个条件。

接着我们将密文取出,并将iv置为0000000000000000

下面就是不断调整iv直到算出正确的padding

这里个人认为还是要把这个图解释一下,图中的Intermediary Value是中间值,Initialization Vector才是初始化的iv。
以测试代码为例,padding 0x01的结果为

中间值 ^ 0x99 = 0x01
中间值 = 0x99 ^ 0x01 = 0x98
明文 = 中间值 ^ 原始iv = 0x98 ^ 0x98 = 0x00 

同理padding 0x02,需要再次调整iv(0x98 ^ 0x02 = 0x9a),这地方可能有点绕,靠个人理解吧。。。
利用脚本如下:

import requests

#方便起见直接将iv和密文写死
#需要获得这两个值才能进行padding oracle attack攻击
iv     = '08fe1649311fd298'
cipher = '262b4647cc70d363'

url    = 'http://seaii-blog.com/test/padding.php'
tmp_iv = '0000000000000000'

tail = ''
result = ''

for i in xrange(1, 9):  #iv长度为16,8个分组
    for j in xrange(256): #0x00~0xff进行穷举
        #两位为一组,不满两位补0
        test = ('0' + str(hex(j)[2:])) if j < 16 else str(hex(j)[2:])   
        #构造测试iv
        test_iv = tmp_iv[i*2:] + test + tail
        #print test_iv

        r = requests.get(url, params={'iv': test_iv, 'str': cipher})
        tmp = r.text
        if tmp[(8-i)*2:(9-i)*2] == '0'+str(i):
            #计算中间值
            mid_val = eval('0x'+test) ^ eval('0x'+str(i))   
            #计算明文(中间值 ^ 原iv)
            result = chr(mid_val ^ eval('0x'+iv[(8-i)*2:(9-i)*2])) + result
            #修改iv进行下一轮攻击
            tail = hex(mid_val ^ eval('0x'+str(i+1)))[2:] + tail
            print test_iv
            break

print result

只测试了第一轮,运行结果如下:

如果想测试下一分组,根据cbc加密的原理,只要把iv换为第一分组的密文即可。

利用工具:

实战: Padding Oracle Attack 笔记

CFB

参考:分组密码模式: CFB模式(密文反馈模式)

cfb的题目比较少,在这里找到了一个Hitcon 2015的题目
https://nusgreyhats.org/write-ups/HITCONCTF-Quals-2015-Simple-(Crypto-100)/
原题是用ruby写的,这里转化为php。

<!DOCTYPE html>
<html>
<head>
    <title></title>
</head>
<?php
    //error_reporting(0);
    define("SECRET_KEY", 'kH3LH3sk45HLsd3n');
    define("METHOD", "aes-128-cfb");

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

    $username = @$_POST['user'];
    $password = @$_POST['pass'];

    if(isset($username) && isset($password)) {    
        $iv = get_random_iv();

        $info = array(
            'username' => $username,
            'password' => $password,
        );
        $info = json_encode($info);
        $cipher = openssl_encrypt($info, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);

        setcookie('iv', $iv);
        setcookie('auth', $cipher);
        header('Location: cfb.php');
    } else {
        if(isset($_COOKIE['auth']) && isset($_COOKIE['iv'])){
            $cipher = $_COOKIE['auth'];
            $iv = $_COOKIE['iv'];
            $info = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
            $info = (array)json_decode($info);

            if($info['admin'] === true){
                exit('flag{123456}');
            } else {
                echo 'Hello '.$info['username'];    
            }
        } else {
            echo '<body>
                  <form action="" method="post">
                      <input type="text" name="user"><br>
                      <input type="password" name="pass"><br>
                      <input type="submit" value="register">
                  </form>
                  </body>';
        }
    }
 ?>
 </html>

套路和上面的都差不多,注册一个用户,将usernaem和password json一下,然后加密存到cookie中,json_decode之后info['admin'] === true即可。但是正常情况下应该只有info['username']和info['password']。
利用脚本如下:

import requests
import base64
import urllib

url = 'http://10.10.10.1/test/cfb.php'
cookies = requests.post(url, data={'user': '1', 'pass': '123456'}, allow_redirects=False).cookies

old_iv = urllib.unquote(cookies['iv']).encode('hex')
cipher = urllib.unquote(cookies['auth']).encode('hex')

first_block = cipher[0:32] #取第一个密文分组
plain = '{"username":"1",' #与上面对应的明文分组
#注意看上面的加密过程,iv是先经过加密之后再与明文进行xor得到密文
#这个加密过程我们不知道,可以通过密文 ^ 明文得到加密后的iv
encrypted_iv = hex(int(plain.encode('hex'), 16) ^ int(first_block, 16))[2:-1]

#修改明文获取新的密文
new_plain = '{"admin": true }'
new_cipher = hex(int(encrypted_iv, 16) ^ int(new_plain.encode('hex'), 16))[2:-1]

new_cookies = {
    'auth': urllib.quote_plus(new_cipher.decode('hex')),
    'iv'  : cookies['iv'],
}
r = requests.get(url, cookies=new_cookies)
print r.text

具体分析代码中写的很详细了,但是这个题目没有体现cfb最典型的攻击——重放攻击,关于这种攻击的一个题目见:cfb重放攻击

hash长度扩展攻击

详细介绍见另一篇博文:hash长度扩展攻击研究

参考资料

标签: ctf, web, 密码学
评论列表
  1. 好文,拜读,以后常看学习。

添加新评论