Pearfect Markdown
Background
This is a page where you can edit markdown in real time.
Find vulnerabilities in services and exploit them to earn flags!
The flag format is DH {…} That's it.

- Difficulty: Easy
- This challenge has source code: Pearfect-Markdown.zip
Enumeration
Index page:

In here, we can upload a Markdown file.
We also have a pre-made markdown file name example.md:

We have a save function to change content of this example.md file.
Look at the request path: /edit.php?file=example.md. I tried to exploit path traversal through file parameter but it seems not to work.

Now we need to look through the source code.
After reading the source code, I found that we couldn't path traversal through file parameter of edit.php because it used the realpath() function:
<?php
[...]
$uploads_dir = 'uploads/';
if (isset($_GET['file'])) {
$file = $_GET['file'];
$path = realpath($uploads_dir . $file);
if (strpos($path, realpath($uploads_dir)) === 0 && file_exists($path)) {
echo htmlspecialchars(file_get_contents($path));
} else {
echo "Invalid file or file not found!";
}
} else {
echo "No file parameter provided!";
}
According to PHP documentation, the realpath() function returns the canonicalized absolute pathname. It means that we can't path traversal out of the uploads/ directory.
In Dockerfile, the flag has been moved to path /${RANDOM_STR}_flag:
[...]
RUN RANDOM_STR=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32) && \
mv /var/www/html/flag /${RANDOM_STR}_flag
This time, I thought about uploading a php file to exploit command injection.
But when I looked through the source code of upload.php. I found that we can't upload a php file because the code has filtered the extension of the uploaded file:
<?php
$uploads_dir = 'uploads/';
if ($_FILES['file']['error'] === UPLOAD_ERR_OK) {
$tmp_name = $_FILES['file']['tmp_name'];
$name = basename($_FILES['file']['name']);
if (pathinfo($name, PATHINFO_EXTENSION) === 'md') {
move_uploaded_file($tmp_name, "$uploads_dir/$name");
echo "File uploaded successfully!";
} else {
echo "Only .md files are allowed!";
}
} else {
echo "File upload error!";
}
?>
Look into post_handler.php page, it has the content of example.md file:

In post_handler.php, it will receive a file parameter and use include() to read file's content. But it doesn't have any filters for this parameter:
<?php
$uploads_dir = 'uploads/';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$file = $_GET['file'] ?? 'example.md';
$path = $uploads_dir . $file;
include($path);
} else {
echo "Use GET method!!";
}
?>
So again, I tried to exploit path traversal through this parameter:

And this time, I've succeed in reading the content of /etc/passwd.
But you know what, the flag has been named to /${RANDOM_STR}_flag, and with include() function, you can't read the flag. I tried to path traversal so hard to read the content of the flag, I've used /*_flag as a regex but include() won't treat it as a regex so it can't read the flag. (I'm sooo dumb, lol)
And then, I realized that, we have a file name post_handler.php with the extension php, and it has the content of example.md. What if we change the content of example.md to php code and then request to post_handler.php to execute it? But first, we need to check the save.php:
<?php
$uploads_dir = 'uploads/';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$file = $_POST['file'];
$content = $_POST['content'];
$path = realpath($uploads_dir . basename($file));
if (strpos($path, realpath($uploads_dir)) === 0 && file_exists($path)) {
file_put_contents($path, $content);
header('Location: edit.php?file=' . urlencode($file));
exit;
} else {
echo "Invalid file or file not found!";
}
} else {
echo "Invalid request method!";
}
?>
It doesn't have any filter when we change the content of a file.
Exploitation
Let's start to write a basic PHP shell to execute commands through cmd parameter and then save it:

Now, we request to the /post_handler.php?cmd=ls to test the ls command:

Guess what, we've succeed to execute ls command!
Change the value of cmd to cat /*_flag to read the flag:

- Flag:
DH{9a2a75682b662e873797cd3ccdd6b22fb166d43f2dddc6e57de9a6c0effc9307}
Conclusion
What we've learned:
- Command injection