Home  >  Article  >  Operation and Maintenance  >  Analysis of the principle of remote code execution vulnerability caused by java deserialization

Analysis of the principle of remote code execution vulnerability caused by java deserialization

王林
王林forward
2019-11-30 17:50:382737browse

Analysis of the principle of remote code execution vulnerability caused by java deserialization

Mainly consists of 3 parts:

1. Java’s reflection mechanism

2. Java’s serialization processing

3. Java's remote code execution

Java's reflection and code execution

Let's look at a simple example first, using Java to call the calculator Program:

import java.io.IOException;
import java.lang.Runtime;
public class Test {
    public static void main(String[] args) {
        Runtime env = Runtime.getRuntime();
        String cmd = "calc.exe";        
    try {
            env.exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

We import the Runtime class from the java.lang package, and then call its getRuntime method to get a Runtime object, which can be used to process the running status of the JVM virtual machine. Then we call its exec method and pass in a string as a parameter.

At this point, the calculator program on the local computer will be launched.

Let’s rewrite the above code through Java’s reflection mechanism. Through Java's introspection mechanism, the code can be called dynamically and escape the processing of some server-side blacklists:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test {

    public static void main(String[] args) {
        try {
            Class<?> cls = Class.forName("java.lang.Runtime");            
            String cmd = "calc.exe";
            try {
                Method getRuntime = cls.getMethod("getRuntime", new Class[] {});                
                Object runtime = getRuntime.invoke(null);
                Method exec = cls.getMethod("exec", String.class);
                exec.invoke(runtime, cmd);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (SecurityException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        } catch (ClassNotFoundException e1) {
            e1.printStackTrace();
        }
    }
}

The above code seems very cumbersome, but in fact it is not difficult. First, pass in a string as a parameter through Class.forName, which returns an instance of Class. Its function is to find the corresponding class based on the corresponding name.

Then we use the getMethod method of the Class instance to obtain the getRuntime method of the corresponding class. Since the class has no parameters, it can be set to null or use an anonymous class to handle it.

Method getRuntime = cls.getMethod("getRuntime", new Class[] {});

Then call the corresponding class method through the invoke method of the obtained method instance. Since there are no parameters, just pass in null. In the same way, we get the exec method.

Java serialization processing

For serialization processing in Java, the corresponding class needs to implement the Serializable interface, for example:

import java.io.Serializable;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class Reader implements Serializable {
    private static final long serialVersionUID = 10L;    
    private void readObject(ObjectInputStream stream) {
        System.out.println("foo...bar...");
    }    public static byte[] serialize(Object obj) {        //序列化对象
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream output = null;        
    try {
            output = new ObjectOutputStream(out);
            output.writeObject(obj);
            output.flush();
            output.close();

        } catch (IOException e) {
            e.printStackTrace();
        }        return out.toByteArray();

    }    public static Object deserialize(byte[] bytes) {        //反序列化处理
        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
        ObjectInputStream input;
        Object obj = null;        
    try {
            input = new ObjectInputStream(in);
            obj = input.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }        return obj;

    }    
    public static void main(String[] args) {        
    byte[] data = serialize(new Reader()); //对类自身进行序列化
        Object response = deserialize(data);
        System.out.println(response);
    }
}

Here we override the readObject method of this class to read objects for testing. The two most important functions are serialize and deserialize, which are used for serialization and deserialization respectively.

Among them, the serialize method needs to pass in an object as a parameter, and its output result is a byte array. In this class, the object output stream ObjectOutputStream is mainly used for packaging ByteArrayOutputStream, and then uses its writeObject method to write the object into it. Finally, we get the byte array through the toByteArray method of the ByteArrayOutputStream instance.

In the deserialize method, a byte array needs to be passed in, and the return value is an Object object. Similar to the previous serialize function, at this time we use ByteArrayInputStream to receive the byte array, then use ObjectInputStream to wrap the ByteArrayInputStream, and then call its readObject method to get an Object object and return it.

When we run this class, we will get the following results:

Analysis of the principle of remote code execution vulnerability caused by java deserialization

##Java Remote Communication and Transmission

In order to achieve remote transmission and remote code execution of Java code, we can use RMI, RPC and other methods. Here we use Socket for server-side and client-side processing.

The first is the server side, which listens to the local port 8888. The code is:

import java.net.Socket;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
public class Server {
    public static void main(String[] args) throws ClassNotFoundException {        
    int port = 8888;        
    try {
            ServerSocket server = new ServerSocket(port);
            System.out.println("Server is waiting for connect");
            Socket socket = server.accept();
            InputStream input = socket.getInputStream();            
            byte[] bytes = new byte[1024];
            int length = 0;            
            while((length=input.read(bytes))!=-1) {
                String out = new String(bytes, 0, length, "UTF-8");
                System.out.println(out);
            }
            input.close();
            socket.close();
            server.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

We instantiate the ServerSocket class by passing in a port, and at this time we get a server socket. Then call its accept method to receive the client's request. At this time, a socket object is obtained, and the input stream is obtained through the getInputStream method of the socket object, and a byte array with a length of 1024 is specified.

Then call the read method of the socket to read the byte sequence of the specified length, and then convert the byte array into a string through the String constructor and output it. In this way we get the content transmitted by the client.

For the client, the code is similar to the following:

import java.io.IOException;
import java.net.Socket;
import java.io.OutputStream;
public class Client {
    public static void main(String[] args) {
        String host = "192.168.1.108";        
        int port = 8888;
        try {
            Socket socket = new Socket(host, port);
            OutputStream output = socket.getOutputStream();
            String message = "Hello,Java Socket Server";
            output.write(message.getBytes("UTF-8"));
            output.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

On the client, we pass the IP address and port to be connected through the Socket object, and then obtain it through the getOutputStream method of the socket object Output stream, used to send output to the server. Since this is just a demonstration, the local host IP is used. In practical applications, if we know the IP and open ports of a certain external network host, and if the current host has corresponding vulnerabilities, we can also use a similar method to achieve this.

Here we set the content to be transmitted to a UTF-8 encoded string. In the write method of the output stream, we specify its encoding through the getBytes of the string, thereby converting it into the corresponding byte array. to send.

Under normal circumstances, after we run the server and then run the client, we can get the following output on the server side:

Server is waiting for connect
Hello,Java Socket Server

Java反序列化与远程代码执行

下面我们通过Java反序列化的问题来实现远程代码执行,为了实现远程代码执行,我们首先在Reader类中添加1个malicious方法,其代码为:

public Object malicious() throws IOException {
        Runtime.getRuntime().exec("calc.exe");
        System.out.println("Hacked the Server...");        
        return this;
    }

在该方法中我们使用之前的介绍调用宿主机器上的计算器程序,然后输出1个相关信息,最后返回当前类。

之后是对服务器端的代码进行如下的修改:

while((length=input.read(bytes))!=-1) {
    Reader obj = (Reader) Reader.deserialize(bytes);
    obj.malicious();
}

我们在接收到客户端对应的字符串后对其进行反序列处理,之后调用某个指定的函数,从而实现远程代码的执行。而在客户端,我们需要对其进行序列化处理:

Reader reader = new Reader();
byte[] bytes = Reader.serialize(reader);
String message = new String(bytes);
output.write(message.getBytes());

下面我们在宿主机器上运行服务器端程序,之后在本地机器上运行客户端程序,当客户端程序执行时,可以看到类似如下的结果:

Analysis of the principle of remote code execution vulnerability caused by java deserialization

可以看到,我们成功的在宿主机器上执行了对应的命令执行。

总结

为了实现通过Java的反序列问题来实现远程代码执行的漏洞,我们需要编写1个有恶意代码注入的序列化类。之后在客户端将恶意代码序列化后发送给服务器端,而服务器端需要调用我们期望的方法,从而触发远程代码执行。

为了避免服务器端进行一些安全处理,我们可以采用反射的方式来逃逸其处理。

这里只是1个简化的过程,更加实用的过程可以参考Apache Common Collections的问题导致的Weblogic漏洞CVE-2015-4852及Jboss的漏洞CVE-2015-7501

推荐相关文章教程:web安全教程

The above is the detailed content of Analysis of the principle of remote code execution vulnerability caused by java deserialization. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:freebuf.com. If there is any infringement, please contact admin@php.cn delete