From 0 to 100: Innocuous Source Code to Web Server Compromise

Antonio Sánchez, Lead Consultant

In a recent web application penetration test I was challenged with figuring out how to fully compromise a client’s website. The site was using the latest version of WordPress, and although they had a few plugins installed, they seemed to be patched as well. However, I did find an interesting web directory that was anonymously browseable:

https://www.example.com/.hg

This directory is used by Mercurial, a source code version control platform that eases the task of software development, to keep track of files throughout the development stages. Like all version control platforms, it is possible to browse earlier versions of stored code, as shown in the Mercurial documentation referenced below:

https://www.mercurial-scm.org/wiki/FileFormats

4.7. data/
Revlogs for each file in the project history. Names are escaped in various increasingly-complex ways:

  • old (see mercurial/filelog.py:encodedir()):
  • directory names ending in .i or .d have .hg appended
  • store (see mercurial/store.py:encodedstore):
  • uppercase is escaped: ‘FOO’ -> ‘_f_o_o’
  • character codes outside of 32-126 are converted to ‘~XX’ hex format
  • fncache (see mercurial/store.py:hybridencode):
  • windows reserved filename prefixes are ~XX-encoded
  • very long filenames and stored by hash

https://www.mercurial-scm.org/wiki/Revlog 

A revlog, for example .hg/data/somefile.d, is the most important data structure and represents all versions of a file in a repository. Each version is stored compressed in its entirety or stored as a compressed binary delta (difference) relative to the preceeding version in the revlog. Whether to store a full version is decided by how much data would be needed to reconstruct the file. This system ensures that Mercurial does not need huge amounts of data to reconstruct any version of a file, no matter how many versions are stored.

While it would be straightforward to manually download the content of the “.hg” directory and then use a local instance of Mercurial to retrieve the source code, the web application was relatively large, and doing so was likely to impact on the client’s agreement with their hosting provider. Instead, I used an alternative approach, based on a readily available tool, currently hosted on Github, called DVCS-Pillage: https://github.com/evilpacket/DVCS-Pillage.

In short, this tool automatically downloads the latest version of each file in the target repository or web application by calling the Mercurial “revert” command. The end result of performing this action against the target website was a complete local copy of the website, including configuration files.

The configuration files included credentials for a locally hosted MySQL database instance but the web server was configured to permit access only over HTTP and HTTPS (TCP ports 80 and 443), so these credentials were not under any immediate threat. There was, however, a custom “File Upload” page included in the web application that was not linked from any area of the public website.

This meant the original source code, including PHP, SQL, and configuration files were vulnerable.

Now what? Although the configuration files revealed credentials for the MySQL database used by the application, the web server only allowed access to ports 80 and 443, and therefore although this information was valuable it was of no use.  There was no password reuse for the WordPress admin area. After having another look at the website, the page below caught my eye:

As far as I know, WordPress does not come with a default file upload functionality, so the developers either installed a plugin or they wrote their own custom code. After having a look at the source code, I found out they actually had implemented their own upload functionality, reason enough to have a closer inspection of the source code for the file upload:

The image above shows the source code fragment that takes the processes and stores the uploaded files. There are four important details within this fragment we analysed in detail to understand how the uploading functionality works, and therefore enabled opportunities for further exploitation.

  • The first red arrow points to the “validateForm” function. The application checks the client made a POST request, and then passes the values of the form to this function, which is shown below:
<?php
[...]
private function validateForm() {
    if ((!isset($this->request->post['expense_id']) && empty($this->request->post['expense_id'])) && (!isset($_FILES) || empty($_FILES['file1']['size']))) {
        $this->error['file1'] = 'You must upload a document!';
    }
    if ($_FILES['file1']['size'] > 18000000) {
        $this->error['file1'] = 'You must upload a smaller document!';
    }
    if(count($this->error)) {
        $this->error['errors'] = 'Please check the form for errors!';
    }
    if (!$this->error) {
        return true;
    } else {
        return false;
    }
}
[...]
?>

The validation is quite simple, and it just involves a check to verify that all the necessary parameters have been supplied and the file size does not exceed 18000000 MB (that’s big!). So far, so good.

The second red arrow points to the portion of code that sets the path where the files will be stored. Of particular relevance is the value of the “DIR_UPLOADS” folder, which after a quick search was found to be set as a static variable:

define('DIR_UPLOADS','/var/www/repos/******/application/uploads/');

Verifying that I had access to this directory was as simple as force browsing to https://www.example.com/application/uploads/, and although directory listing was disabled and therefore an error was returned by the server, it did mean they were uploading the files to a browseable directory!

  • The next red arrow points to another element used to construct the path where the files will be stored, in this case an application ID. I completed the main application form the web application provided, after which I received the following email:

As observed in the image above, the parameter “token” could potentially be used as an application ID.

  • Finally, the last red arrow points to the function that creates the file on the server. The code corresponding to this function is shown below:
if (isset($data['filename'])) {
    $ext = pathinfo($data['filename'], PATHINFO_EXTENSION);
}
$sql .= " `generated_filename` = '" . $this->db->escape(substr(md5(date('ymdhis').$data['filename']),0,35).'.'.$ext) . "',";
$sql .= " date_added = NOW()";
$this->db->query($sql);
$id = $this->db->getLastId();
return $this->getId($id);

The most important line has been highlighted. It crafts a SQL query with the final name of the file. To create the name, the developers decided to keep the original extension (!), and append it to the MD5 hash of the current date linked to the filename. This is a major issue which can allow a malicious attacker to control or guess all the variables in play which are in charge of generating the final filename.

Are we now in the place of exploiting the file upload functionality? Below shows what we found:

  1. Upload directory: […]/application/uploads/applications/<application_id>/<filename>.<ext>
  2. Application_id: 980353d4-8d0b-11e5-8429-feff000051bc
  3. Filename + ext:

The only piece missing in the puzzle is the final filename  the application generates, as we have knowledge of all the other key values. Fortunately enough, creating a function that does exactly what the original application does is trivial, as shown below:

And after we run the previous PHP script…

Now the million dollar question, did this work?!

https://www.example.com/application/uploads/applications/980353d4-8d0b-11e5-8429-feff000051bc/7ebc5ec71a17ebd52e2f6bc25e32711c.php

Indeed! Quite an interesting find that highlights the importance of paying attention to the small details and not assuming an issue may look like something minor.

Find out how we can help with your cyber challenge

Please enter your contact details using the form below for a free, no obligation, quote and we will get back to you as soon as possible. Alternatively, you can email us directly at [email protected]