Tag: directory traversal
2011
10.03

Summary

Scripts using PHP 5.3 that accept multiple file uploads in a single request are potentially vulnerable to a directory traversal attack. Information about the mechanism for attack (corrupting array indices in $_FILES) has been publicly available since at least March 2011 June 2009. [1] [2] [3] [4] I submitted Sec Bug #55500 to point out the potential for directory traversal on August 24th, 2011.

[Note: I’ve been informed that a similar attack using the same vector was mentioned in the PHP Bug Tracker in September 2009. [5]]

[Update: As of January 1st 2012, a fix for this issue has been committed for PHP 5.4 and trunk in SVN r321664]

How does it work

The NYU Poly ISIS Lab blog has a wonderful, detailed writeup discussing how it’s possible to cause PHP to mangle indices in the $_FILES array. In short, multi-part POST requests containing variables with extra opening square brackets will cause PHP to build a malformed $_FILES array.

All of the previous writeups on this topic have focused on attacks where the target script is using copy instead of move_uploaded_file to handle uploads. While that assumption does allow for some interesting exploitation, it’s a less realistic scenario. I decided to focus exclusively on the implications of an attack if a target uses the move_uploaded_file function.

Consider the following script, based on code from the PHP documentation: [6] [7]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

print_r($_FILES);

if (!empty($_FILES['pictures']))
{
    // Modified slightly from http://php.net/manual/en/function.move-uploaded-file.php
    $uploads_dir = '.';
    foreach ($_FILES["pictures"]["error"] as $key => $error) {
        if ($error == UPLOAD_ERR_OK) {
            $tmp_name = $_FILES["pictures"]["tmp_name"][$key];
            $name = $_FILES["pictures"]["name"][$key];
            echo "move_uploaded_file($tmp_name, \"$uploads_dir/$name\");";
        }
    }
}
?>
<form action="" method="POST" enctype="multipart/form-data" >
<input type="hidden" name="MAX_FILE_SIZE" value="10000000">
<input type="file" name="pictures[[type]">
<input type="file" name="pictures[[name]">
<input type="file" name="pictures[name][">
<input type="submit" value="submit">
</form>

If you play around with sending requests to this script, you’ll see that it wants to move the contents of the file uploaded as pictures[[type] into a location specified by pictures[name][‘s Content-Type. Since an uploaded file’s Content-Type is not sanitized by PHP, any attacker can send a request with any value, allowing for directory traversal.

But you shouldn’t accept an unsanitized filename from users! The bug is in the script, not the language!

Yes. And no.

Yes, leaving file uploads completely unsanitized is a bad idea. The code sample I posted above is vulnerable to a number of different attacks in its current state, the most glaring of which is that an attacker can upload arbitrary PHP files.

However, under normal circumstances PHP sanitizes the name parameter of the $_FILES array to prevent directory traversal attacks. That’s a language feature which developers may be relying on for security and which PHP’s maintainers have released security patches for in the past. Just a few months ago, Krzysztof Kotowicz discovered an off-by-one error in PHP’s file upload sanitization routine which made it possible for an attacker to write to the root of a drive. That bug, #54939, was fixed in PHP 5.3.7.

Workarounds

If you have a vulnerable script, you can mitigate the attack by calling basename on the user-provided filename. It should also be possible to detect malicious requests because of the mangled index names.

Edit: Adam pointed out an older bug report referencing the array corruption issue and potential for security concerns. I’ve updated the post to reflect the new information.

2011
02.10

Summary

addons.mozilla.org was vulnerable to a directory traversal / local file inclusion vulnerability. As a result, it was possible for an attacker to load webserver-readable files from the local filesystem (and to execute PHP stored on the server).

This vulnerability is filed as Bug #628697.

How did it work?

In the PHP code for the addons website, there’s a controller called pages_controller.php that is used to load static / semi-static pages. The exact name of the page to be loaded is determined by the query string: for example, https://addons.mozilla.org/en-US/firefox/pages/credits loads the Site Credits page, which is stored as a template in the system. In older, vulnerable versions of the code, the method for displaying a page looked like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
function display() {
    if (!func_num_args()) {
        $this->redirect('/');
    }

    $path = func_get_args();
    $path_string = join('/', $path);

    if (!count($path) || ($path[0] == 'home')) {
        $this->redirect('/');
    }

    // ...snip...

    $this->render($path_string);
}

This code is vulnerable to a directory traversal attack: the $path_string, which is used to load a template, is directly tied to user input (the arguments to the function here are the elements of the query string). By sending URL encoded slashes (%252F), it was possible to break out of the current directory and traverse via a relative path to any directory in the system. It was also possible to convince CakePHP (the framework used here) to load files without the .thtml file extension associated with templates by including a URL-encoded null byte (%2500) at the end of the URL.

To give one example of a possible traversal, here is the proof of concept that I included with the bug. It displayed the contents of the /etc/passwd file of the addons server (As Michael Coates properly notes, no password hashes were disclosed due to this vulnerability):

https://addons.mozilla.org/en-US/firefox/pages/..%252f..%252f..%252f..%252f..%252f..%252f..%252fetc/passwd%2500

The vulnerability was resolved by using CakePHP’s own built-in parameter handling, which precludes an attacker from including slashes in a parameter passed via the query string. The relevant code from the latest revision looks something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
function display($page) {
    if (empty($page) || $page == 'home') {
        $this->redirect('/');
        exit;
    }

    // ...snip...

    $this->render("/$page");
}

More Information

The vulnerability mentioned here has been confirmed fixed by Mozilla.

I’d like to thank Wil Clouser and Michael Coates for handling this issue. I’d especially like to applaud them for the speed with which they handled this report: the vulnerability was patched and the fix was deployed in production about an hour and a half after my initial report.

Interested readers are encouraged to take a look at other vulnerabilities I’ve reported as a part of Mozilla’s Web Security Bug Bounty.