[Web security] php://filter Shallow bottom layer analysis

Posted by homer.favenir on Tue, 02 Nov 2021 06:58:08 +0100

Create a new PHP file: a.php

<?php
$a = "a.txt";
include("php://filter/resource=" . $a);

Create a new file in the same directory: a.txt (the content is <? PHP phpinfo();? > base64 encoding)

PD9waHAgcGhwaW5mbygpOz8+

Next breakpoint in the related function of the corresponding file:

[data]

Post key codes:

php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *path, const char *mode, int options,
                                     zend_string **opened_path, php_stream_context *context STREAMS_DC) /* {{{ */
{
    ...
    if (!strncasecmp(path, "php://", 6)) {
    }
    if (!strncasecmp(path, "temp", 4)) {     
    }
    if (!strcasecmp(path, "memory")) {
    }
    if (!strcasecmp(path, "output")) {
    }
    if (!strcasecmp(path, "input")) {
    }
    if (!strcasecmp(path, "stdin")) {
    } else if (!strcasecmp(path, "stdout")) {
    } else if (!strcasecmp(path, "stderr")) {
    } else if (!strncasecmp(path, "fd/", 3)) {
    } else if (!strncasecmp(path, "filter/", 7)) {
        /* Save time/memory when chain isn't specified */
        if (strchr(mode, 'r') || strchr(mode, '+')) {
            mode_rw |= PHP_STREAM_FILTER_READ;
        }
        if (strchr(mode, 'w') || strchr(mode, '+') || strchr(mode, 'a')) {
            mode_rw |= PHP_STREAM_FILTER_WRITE;
        }
        pathdup = estrndup(path + 6, strlen(path + 6));
        p = strstr(pathdup, "/resource=");
        if (!p) {
            zend_throw_error(NULL, "No URL resource specified");
            efree(pathdup);
            return NULL;
        }

        if (!(stream = php_stream_open_wrapper(p + 10, mode, options, opened_path))) {
            efree(pathdup);
            return NULL;
        }

        *p = '\0';

        p = php_strtok_r(pathdup + 1, "/", &token);
        while (p) {
            if (!strncasecmp(p, "read=", 5)) {
                php_stream_apply_filter_list(stream, p + 5, 1, 0);
            } else if (!strncasecmp(p, "write=", 6)) {
                php_stream_apply_filter_list(stream, p + 6, 0, 1);
            } else {
                php_stream_apply_filter_list(stream, p, mode_rw & PHP_STREAM_FILTER_READ, mode_rw & PHP_STREAM_FILTER_WRITE);
            }
            p = php_strtok_r(NULL, "/", &token);
        }
        efree(pathdup);

        if (EG(exception)) {
            php_stream_close(stream);
            return NULL;
        }

        return stream;
    } else {
        /* invalid php://thingy */
        php_error_docref(NULL, E_WARNING, "Invalid php:// URL specified");
        return NULL;
    }
    ...
    return stream;
}

Let's test its logic first. We set the variable a to a /... / a.txt, which is equivalent to including one php://filter/resource=a/ ... / a.txt, make a break point in the filter judgment and gradually adjust it:

The first is to judge whether to read or write. Don't worry here. Continue to look down.

Here are two string operations, which are assigned to pathdup and p respectively. By the way, path is the value we passed in include at the beginning, that is php://filter/resource=a/ ... / a.txt, there is a judgment on whether PHP: / / exists. If it exists, move the path pointer back 6 bits. Here, move the path pointer back 6 bits to pathdup, That is, assign the address of the next bit after the filter to pathdup, and p uses the strstr function to obtain the first address of / resource =, and then judge whether p is assigned. If not, it will report an error, and if so, continue to go down.

Next is the first key point, where PHP is called_ stream_ open_ Wrapper is used to judge whether the file exists. The judgment of the file here is to move the p pointer backward by 10 bits, that is, the section after / resource =, that is, a /... / a.txt here. Although we did not create the folder a, we can directly go back through the directory. Therefore, the file has been read out and the stream has been assigned to the stream variable.

Then * p = '0' clears p, which is the most interesting paragraph and the second key point.

Let's talk about the meaning here. It will move the pathdup pointer back one bit, and the string after it will be divided by /. Each divided part will be thrown into PHP_ stream_ apply_ filter_ In the list function, that is to say, the pathdup here is / resource=a /... / a.txt, which will be divided into resource=a &... & a.txt, and each will be thrown into php_stream_apply_filter_list, so what does this function do? We can follow up:


Let's put forward the most important paragraph in the middle separately:

if (read_chain) {
            if ((temp_filter = php_stream_filter_create(p, NULL, php_stream_is_persistent(stream)))) {
                php_stream_filter_append(&stream->readfilters, temp_filter);
            } else {
                php_error_docref(NULL, E_WARNING, "Unable to create filter (%s)", p);
            }
        }

It is not difficult to find that it will try to create this filter with this string as the filter name. If it is created successfully, it will perform filter related operations on the file stream we read before. If it does not exist, a warning will pop up:

So here we understand how it works, and it's not difficult to construct it php://filter/resource=a/convert.base64-decode/…/…/a.txt:

Not limited to base64 coding, you can also try something else.

PS: during the previous test, it seems that this is true from 5.x to 8.x
reference

Topics: PHP network Programmer security Cyber Security