Deserializando objetos Java sin los .class

En una auditoría que llevamos a cabo recientemente, surgió la necesidad de inspeccionar el contenido de ciertos ficheros en formato binario, que a todas luces se trataba de objetos Java serializados:

$ file serialized.bin 
serialized.bin: Java serialization data, version 5

En efecto, los dos primeros bytes eran el magic number de este tipo de ficheros:

0000000 edac 0500 ...
0000010 ...
0000020 ...
0000030 ...
0000040 ...

pero los objetos serializados debían ser relativamente complejos, por lo que un mero strings sobre el fichero no nos ayudaba a “descifrar” la información.

Lo primero que intentamos fue su deserializado directamente mediante la API estándar de Java ObjectInputStream.readObject():

import java.io.*;

class Deserialize {
    public static void main(String[] args) throws Exception {
        FileInputStream fis = new FileInputStream("serialized.bin");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Object object = (Object) ois.readObject();
        ois.close();
    }
}

Sin embargo, como enseguida pudimos comprobar, este método requiere que las clases a partir de las cuáles se han instanciado los objetos serializados estén presentes en el CLASSPATH de la JVM para poder llevar a cabo la correspondiente deserialización:

$ java Deserialize
Exception in thread "main" java.lang.ClassNotFoundException: CENSURADO ;-)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
[...]

En efecto, si tomamos una clase de prueba:

class Account implements Serializable {
    private String user;
    private String password;
    
    public String getUser() {
        return this.user;
    }
    public void setUser(String user) {
        this.user = user;
    }
    
[...]
    
    public Account(String user, String password) {
        setUser(user);
        setPassword(password);
    }
}

y generamos un fichero con su serialización:

import java.io.*;

class Serialize {
    public static void main(String[] args) throws Exception {
        Account account = new Account("pablo", "secreto");
        FileOutputStream fos = new FileOutputStream("serialized.bin");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(account);
        oos.flush();
        oos.close();
    }
}

no podremos deserializarlo con readObject() si no tenemos la clase Account en el CLASSPATH:

$ java Deserialize
Exception in thread "main" java.lang.ClassNotFoundException: Account
	at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
	at java.security.AccessController.doPrivileged(Native Method)
[...]
$ ls ../serialize
Account.class  Account.java  Serialize.class  serialized.bin  Serialize.java
$ CLASSPATH=.:../serialize java Deserialize
...

Lo usual, cuando estamos auditando por ejemplo applets o aplicaciones Android, es disponer de los .jar y .class correspondientes a los objetos serializados, por lo que sí que seríamos capaces de deserializarlos, además de obtener el bytecode de sus clases asociadas con decompiladores como JAD, Jdec, y muchos otros… Esto nos facilitaría en gran medida el análisis.

Sin embargo, en este caso tan sólo disponíamos de los ficheros, y en principio no había nada que hacer. Pero la información estaba ahí; un strings sobre nuestro fichero serializado de pruebas mostraba perfectamente los nombres de las clases, los campos y sus tipos:

$ strings serialized.bin 
Account
passwordt
Ljava/lang/String;L
userq
secretot
pablo

Tras buscar un poco, dimos con una de las conferencias de la BlackHat EU 2010, Attacking Java Serialized Communication, de Manish Saindane, y el plugin DSer para Burp Suite, que permite manipular objetos Java serializados presentes en peticiones HTTP. Sin embargo, según nuestras pruebas (y algún comentario del autor) esta herramienta también necesita que las clases correspondientes estén presentes en su directorio de librerías (¿alguno de nuestros lectores ha conseguido hacerla funcionar sin las clases?).

Escarbando en la documentación de Java al respecto, confirmamos nuestra creencia de que la información necesaria para llevar a cabo la deserialización está presente en los ficheros, y cuando estábamos a punto de escribir nuestro propio parser en Python según la especificación, ¡por fin! dimos con la herramienta: jdeserialize (lo peor de todo es que habíamos buscado por “java deserialization”, “java serialization decompile”, etc. pero no probamos lo jobvio ;-)

El uso de la herramienta es harto sencillo, está escrita en Java puro, y además no tiene dependencias más allá de las librerías estándar. Aplicada a nuestro objeto Account de prueba, ésta es la salida que arroja:

$ java -jar jdeserialize-1.2.jar serialized.bin 
read: Account _h0x7e0002 = r_0x7e0000;  
//// BEGIN stream content output
Account _h0x7e0002 = r_0x7e0000;  
//// END stream content output

//// BEGIN class declarations (excluding array classes)
class Account implements java.io.Serializable {
    java.lang.String password;
    java.lang.String user;
}

//// END class declarations

//// BEGIN instance dump
[instance 0x7e0002: 0x7e0000/Account
  field data:
    0x7e0000/Account:
        user: r0x7e0004: [String 0x7e0004: "pablo"]
        password: r0x7e0003: [String 0x7e0003: "secreto"]
]
//// END instance dump

Como se observa, indica la estructura de las clases, con los respectivos tipos, y la información contenida en las instancias serializadas. Su utilidad se aprecia mejor si se aplica sobre clases con estructuras algo más complicadas. Por ejemplo, si los objetos serializados fueran instancias de esta clase, que contiene una lista enlazada de objetos Account:

import java.io.*;
import java.util.*;

class Accounts implements Serializable {
    private LinkedList accounts =
        new LinkedList();

    public void addAccount(Account account) {
        this.accounts.add(account);
    }
}

Si serializamos varios objetos:

import java.io.*;

class Serialize {
    public static void main(String[] args) throws Exception {
        Accounts accounts = new Accounts();
        accounts.addAccount(new Account("62774889", "aqdauw)ZLCz:"));
        accounts.addAccount(new Account("23354642", "Jf!<Tkc9LWSC"));
        accounts.addAccount(new Account("98388213", "{rp9r[C4WY@u"));
        accounts.addAccount(new Account("75627389", "b54KXH)Uh)Z!"));
        FileOutputStream fos = new FileOutputStream("serialized2.bin");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(accounts);
        oos.flush();
        oos.close();
    }
}

un strings empieza a ser un poco más difícil de interpretar (pese a que el nombre de los campos, user y password, es totalmente revelador; en nuestro caso los campos no tenían nombres tan “sugerentes” ;-):

$ strings serialized2.bin 
Accountsw#E7!Y
accountst
Ljava/util/LinkedList;xpsr
java.util.LinkedList
)S]J`
Account
passwordt
Ljava/lang/String;L
userq
aqdauw)ZLCz:t
62774889sq
Jf!<Tkc9LWSCt
23354642sq
{rp9r[C4WY@ut
98388213sq
b54KXH)Uh)Z!t
75627389x

mientras que jdeserialize nos facilitaría en gran medida la ingeniería inversa:

$ java -jar jdeserialize-1.2.jar serialized2.bin 
read: Accounts _h0x7e0002 = r_0x7e0000;  
//// BEGIN stream content output
Accounts _h0x7e0002 = r_0x7e0000;  
//// END stream content output

//// BEGIN class declarations (excluding array classes)
class Account implements java.io.Serializable {
    java.lang.String password;
    java.lang.String user;
}

class java.util.LinkedList implements java.io.Serializable {
}

class Accounts implements java.io.Serializable {
    java.util.LinkedList accounts;
}

//// END class declarations

//// BEGIN instance dump
[instance 0x7e0010: 0x7e0005/Account
  field data:
    0x7e0005/Account:
        user: r0x7e0012: [String 0x7e0012: "75627389"]
        password: r0x7e0011: [String 0x7e0011: "b54KXH)Uh)Z!"]
]
[instance 0x7e0007: 0x7e0005/Account
  field data:
    0x7e0005/Account:
        user: r0x7e0009: [String 0x7e0009: "62774889"]
        password: r0x7e0008: [String 0x7e0008: "aqdauw)ZLCz:"]
]
[instance 0x7e0004: 0x7e0003/java.util.LinkedList
  object annotations:
    java.util.LinkedList
        [blockdata 0x00: 4 bytes]
        Account _h0x7e0007 = r_0x7e0005;  
        Account _h0x7e000a = r_0x7e0005;  
        Account _h0x7e000d = r_0x7e0005;  
        Account _h0x7e0010 = r_0x7e0005;  

  field data:
    0x7e0003/java.util.LinkedList:
]
[instance 0x7e0002: 0x7e0000/Accounts
  field data:
    0x7e0000/Accounts:
        accounts: r0x7e0004: java.util.LinkedList _h0x7e0004 = r_0x7e0003;  
]
[instance 0x7e000d: 0x7e0005/Account
  field data:
    0x7e0005/Account:
        user: r0x7e000f: [String 0x7e000f: "98388213"]
        password: r0x7e000e: [String 0x7e000e: "{rp9r[C4WY@u"]
]
[instance 0x7e000a: 0x7e0005/Account
  field data:
    0x7e0005/Account:
        user: r0x7e000c: [String 0x7e000c: "23354642"]
        password: r0x7e000b: [String 0x7e000b: "Jf!<Tkc9LWSC"]
]
//// END instance dump

Esperamos que el post os haya resultado ameno, y que como mínimo la información os ahorre el proceso de búsqueda y pruebas hasta dar con la herramienta adecuada si os veis en la misma situación. Y por supuesto, si conoceis alguna otra forma (que seguro que la hay), os animamos a que la compartais con nosotros y el resto de lectores.

Comments

  1. Antonio Villalon says

    Buen post Pablo. Menos técnico que los míos, pero buen post ;)

  2. Nice find. Have you tried this on java based webapps that might serialize objects into their viewstate?

  3. Not yet, but very good idea! ;-)

    It would be very interesting to research how the different frameworks (e.g. JSF2, Facelets..) implement the view state when it’s stored client-side and it’s nor authenticated nor encrypted, so we could play with it as with ASP.NET ViewState.

    jdeserialize is Java, so it shouldn’t be very difficult to integrate it with Burp Suite or the like.

    Also, it would be nice to try to deserialize Java objects present in plain text Remote Method Invocation (RMI) messages. Unfortunately, it seems that wireshark doesn’t support yet this protocol, and we couldn’t find any sniffer for it. But the search continues! ;-)

    Thanks for the comment!