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.