一、CBC 簡介
現代密碼體制
現代密碼中的加密體制一般分為對稱加密體制(Symmetric Key Encryption)和非對稱加密體制(Asymmetric Key Encryption)。對稱加密又分為分組加密和序列密碼。
分組密碼:也叫塊加密(block cyphers),一次加密明文中的一個塊。是將明文按一定的位長分組,明文組經過加密運算得到密文組,密文組經過解密運算(加密運算的逆運算),還原成明文組,有 ECB、CBC、CFB、OFB 四種工作模式。
序列密碼:也叫流加密(stream cyphers),一次加密明文中的一個位。是指利用少量的密鑰(制亂元素)通過某種復雜的運算(密碼算法)產生大量的偽隨機位流,用於對明文位流的加密。解密是指用同樣的密鑰和密碼算法及與加密相同的偽隨機位流,用以還原明文位流。
CBC 模式
CBC (Cipher Block Chaining, 密碼分組鏈接) 模式中每一個分組要先和前一個分組加密后的數據進行XOR異或操作,然后再進行加密。這樣每個密文塊依賴該塊之前的所有明文塊,為了保持每條消息都具有唯一性,第一個數據塊進行加密之前需要用初始化向量IV進行異或操作。CBC模式是一種最常用的加密模式,它主要缺點是加密是連續的,不能並行處理,並且與ECB一樣消息塊必須填充到塊大小的整倍數。
CBC 模式的優缺點
CBC算法優點:
串行運算、相同明文不同密文。
CBC算法缺點:
需要初始向量、加密是連續的,不能並行處理。
二、CBC 工作模式
Encryption
特殊名詞
Plaintext:明文,待加密的數據。
IV :初始向量,用於隨機化加密的比特塊,保證即使對相同明文多次加密,也可以得到不同的密文。
Key:對稱密鑰,由AES,Blowfish,DES,Triple DES等對稱加密算法使用。
Ciphertext:密文數據。
固定分組:CBC在一個固定長度的位組上工作,稱為塊。這里使用每個16字節的塊進行講解。
Encryption process
1、文字流程
Main:上一組密文塊用來產生下一組密文塊。
1、首先將明文分組(常見的以16字節為一組),位數不足的使用特殊字符填充。
2、生成一個隨機的初始化向量(IV)和一個密鑰。
3、將IV和第一組明文異或產生初步密文,再用密鑰對初步密文加密生成最終密文塊。
4、用3中產生的密文塊對第二組明文進行xor操作產生初步密文,再用密鑰對初步密文加密生成最終密文塊。
5、重復4,到最后一組明文。
6、將IV和加密后的每個密文塊拼接在一起,得到最終的密文。
從第一塊 Plaintext 開始,首先與一個初始向量iv異或(iv只在第一處起作用),然后把異或的結果經過key進行加密,得到第一塊的密文,並且把加密的結果與下一塊的明文進行異或,一直這樣進行下去。
2、公式描述:
Ciphertext-0 = Encrypt(Plaintext XOR IV)—只用於第一個組塊
Ciphertext-N = Encrypt(Plaintext XOR Ciphertext-(N-1))—用於第二及剩下的組塊 # N > 1
Decryption:
Decryption process
1、文字流程
Main:上一組密文塊影響下一組密文塊的還原。
1、從密文中提取出IV,然后將密文分組。
2、使用密鑰對第一組的密文解密,然后和IV進行xor得到明文。
3、使用密鑰對第二組密文解密,然后和2中的密文xor得到明文。
4、重復2-3,直到最后一組密文。
解密和加密的原理是一樣的,都是
2、公式描述:
Plaintext-0 = Decrypt(Ciphertext) XOR IV—只用於第一個組塊 Plaintext-N = Decrypt(Ciphertext) XOR Ciphertext-(N-1)—用於第二及剩下的組塊 # N > 1
三、CBC 攻擊原理
Attack 原理
1、在 CBC 解密的公式中可以注意到Ciphertext-(N-1)用來產生下一塊明文,這就是字節翻轉攻擊發揮作用的地方。如果我們改變Ciphertext-N-1中的一個字節,然后和下一塊解密后的密文xor,就可以得到一個不同的明文,而這個明文是我們可以控制的。
2、在1中的基礎上,通過破壞密文中的字節來改變明文中的字節,由此在破壞的密文中添加單引號等惡意字符來繞過過濾器,或通過將用戶ID更改為admin來提升權限,或者更改應用程序所需的明文造成其他后果。
Attack process
通過修改第一組的密文塊字節,來構造自己想要的第二組明文塊,當第一組密文塊字節發生改變時會影響第一組明文塊和第二組明文塊。
四、漏洞復現
漏洞源碼 (漏洞復現以 CTF 為例)
<?php include 'sqlwaf.php'; define("SECRET_KEY", "Dfa5cUiJb2Xquhgv"); define("METHOD", "aes-128-cbc"); session_start(); function get_random_iv(){ $iv=''; for($i=0;$i<16;$i++){ $iv.=chr(rand(1,255)); } return $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 show_homepage(){ if ($_SESSION["username"]==='admin'){ echo '<p>Hello admin</p>'; echo '<p>Flag is ***************************</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>'; die(); } 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!"); } } } if (isset($_POST['username'])&&isset($_POST['password'])) { $username=waf((string)$_POST['username']); $password=waf((string)$_POST['password']); if($username === 'admin'){ exit('<p>You are not real admin!</p>'); }else{ $info = array('username'=>$username,'password'=>$password); login($info); show_homepage(); } } else{ if(isset($_SESSION["username"])){ check_login(); show_homepage(); } } ?>
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>Paper login form</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div id="login">
<form action="" method="post">
<h1>Sign In</h1>
<input name='username' type="text" placeholder="Username">
<input name='password' type="password" placeholder="Password">
<button>Sign in</button>
</div>
</body>
</html>
waf 源碼
<?php function waf($str){ $array=array("'","\""," ","or","and","(",")","<","?"); for ($i=0; $i < sizeof($array); $i++) { if(strpos($str,$array[$i])){ echo "<script>alert('too young too simple,do not hack')</script>"; die(); } } return $str; } ?>
題目Hint:Only admin can see flag!
初始頁面顯示
通過測試發現存在 Injection Bypass
測試
username:
admin' or '1'='1
password:
Random:**********
Result
測試發現存在注入繞過,通過掃描網站意外發現存在網頁Bak文件
將Bak文件Down下來,進行代碼審計
<?php include 'sqlwaf.php'; define("SECRET_KEY", "Dfa5cUiJb2Xquhgv"); //密鑰key
define("METHOD", "aes-128-cbc"); //使用AES算法128bit固定分組
session_start(); function get_random_iv(){ //初始化向量IV
$iv=''; for($i=0;$i<16;$i++){ //隨機生成16字節長度的IV
$iv.=chr(rand(1,255)); } return $iv; } function login($info){ $iv=get_random_iv(); //獲取經過初始化的向量IV
$plain = serialize($info); //將用戶提交的信息進行序列化
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv); //將變量$plain的值進行加密
$_SESSION['username'] = $info['username']; //獲取用戶提交的用戶名
setcookie("iv", base64_encode($iv)); //設置cookie:iv、cipher並將其值進行base64編碼
setcookie("cipher", base64_encode($cipher)); } function show_homepage(){ //判斷網頁提交的用戶是否是"admin"
if ($_SESSION["username"]==='admin'){ //只有admin用戶才能查看Flag
echo '<p>Hello admin</p>'; echo '<p>Flag is *******************************</p>'; }else{ echo '<p>hello '.$_SESSION['username'].'</p>'; //如果不是admin用戶,網頁則會顯示"hello <username>"
echo '<p>Only admin can see flag</p>'; //查詢失敗
} echo '<p><a href="loginout.php">Log out</a></p>'; die(); } function check_login(){ //檢查cookie:iv、cipher並將其值進行base64解碼
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)){ //將cipher進行解密
$info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>"); //如果對變量$plain的值反序列化失敗則會退出整個程序的執行
$_SESSION['username'] = $info['username']; ////獲取用戶名
}else{ die("ERROR!"); } } } if (isset($_POST['username'])&&isset($_POST['password'])) { //判斷用戶的輸入
$username=waf((string)$_POST['username']); //對用戶名進行安全檢測
$password=waf((string)$_POST['password']); //對用戶密碼進行安全檢測
if($username === 'admin'){ //判斷網頁提交的用戶是否是真實的"admin"
exit('<p>You are not real admin!</p>'); }else{ $info = array('username'=>$username,'password'=>$password); login($info); show_homepage(); } } else{ if(isset($_SESSION["username"])){ check_login(); show_homepage(); } } ?>
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>Paper login form</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div id="login">
<form action="" method="post">
<h1>Sign In</h1>
<input name='username' type="text" placeholder="Username">
<input name='password' type="password" placeholder="Password">
<button>Sign in</button>
</div>
</body>
</html>
代碼審計發現用戶只能夠使用admin進行查詢Flag,但是代碼會檢測出你不是真實的admin,所以需要利用網頁代理對抓取的網頁數據包進行兩次不同的利用。
第一次數據包的利用
由於剛開始用戶的輸入不能是admin所以提交用戶名為"ddmin",但是只有是admin用戶才能查看Flag,所以只能利用代碼上的提示:define("METHOD", "aes-128-cbc"); 利用 CBC 字節反轉攻擊構造admin。
分析構造admin,明文分組16字節一組
原明文
a:2:{s:8:"username";s:5:"ddmin";s:8:"password";s:5:"12345"}
明文分組
第一組:a:2:{s:8:"userna
第二組:me";s:5:"ddmin";
第三組:s:8:"password";s
第四組::5:"12345";}
依據上述分組,通過修改第一組明文對應密文中的第10個字節來間接性修改第二組密文解密產生的明文,以此將"ddmin"修改為"admin"
python 腳本~1
import base64 import urllib.parse cipher = base64.b64decode(urllib.parse.unquote('mqyAyGTAv4dfyqwuz0mIu7HOBhqf9xStbNQgj4XKnnlIRBEc68i%2BV8hTS6IvQxYsEjzMBpMJO1s%2BFFmbw8jYxw%3D%3D')) //這里放burp放回的base64的cipher數據 x = cipher[0:9]+bytes([ord(chr(cipher[9]))^ord('d')^ord('a')])+cipher[10:] x = urllib.parse.quote(base64.b64encode(x)) print(x)
php 腳本~1
<?php header("Content-Type: text/html;charset=utf-8"); $cipher = base64_decode(urldecode('mqyAyGTAv4dfyqwuz0mIu7HOBhqf9xStbNQgj4XKnnlIRBEc68i%2BV8hTS6IvQxYsEjzMBpMJO1s%2BFFmbw8jYxw%3D%3D')); //這里放burp放回的base64的cipher數據 $temp = $cipher; $cipher[9] = chr(ord($cipher[9]) ^ ord('d') ^ ord('a')); echo urlencode(base64_encode($cipher)); ?>
運行結果
mqyAyGTAv4dfz6wuz0mIu7HOBhqf9xStbNQgj4XKnnlIRBEc68i%2BV8hTS6IvQxYsEjzMBpMJO1s%2BFFmbw8jYxw%3D%3D
第二次數據包的利用
將username和password提交的數據清空,並且將之前的 iv 和 修改過的cipher 添加到cookie字段中。 此處利用的主要代碼如下
對於發送的請求響應中看到對於密文解密出的明文反序列化失敗,為了知道是怎么回事,我們將顯示出來的base64代碼進行解碼。
從圖中發現第一組的明文出現了問題,用戶已經從"ddmin"成功修改為"admin",由於"第一次數據包利用"中修改了第一組密文,所以第一組和第二組的明文都會受到影響。要想第一組明文恢復正常,就要在"第一次數據包利用"中的基礎上修改IV使之產生正確的明文。
python 腳本~2
# -*- coding:utf8 -*-
import base64 import urllib.parse cipher = base64.b64decode('+g9Uzo1waJpEYbdiV+DYOm1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjU6IjEyMzQ1Ijt9') //這里放burp放回的base64數據 iv = base64.b64decode(urllib.parse.unquote('tWgzNB61rHp%2BzFZCLB6KQA%3D%3D')) //這里放cookie中的iv newiv = '' right = 'a:2:{s:8:"userna'
for i in range(16): newiv += chr(ord(right[i]) ^ ord(chr(iv[i])) ^ ord(chr(cipher[i]))) //產生新的向量IV newiv = newiv.encode(encoding="utf8") print(urllib.parse.quote(base64.b64encode(newiv)))
php 腳本~2
<?php #計算iv
$res = base64_decode('+g9Uzo1waJpEYbdiV+DYOm1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjU6IjEyMzQ1Ijt9'); //這里放burp放回的base64數據
$iv = base64_decode(urldecode('tWgzNB61rHp%2BzFZCLB6KQA%3D%3D')); //這里放cookie中的iv
$plaintext = 'a:2:{s:8:"userna'; $new_iv = ''; for ($i = 0; $i < 16; $i ++){ $new_iv = $new_iv . chr(ord($iv[$i]) ^ ord($res[$i]) ^ ord($plaintext[$i])); //產生新的向量IV } echo urlencode(base64_encode($new_iv)); ?>
運行結果
Ll1VwOi2%2FtgAj5RTHow8Gw%3D%3D
重新發送第二次請求
get flag
Flag is CBC{CBC is a good thing}
End,CBC 字節反轉攻擊的原理和知識以及利用過程已經講述完結,有疑惑的朋友,歡迎大家相互交流。