Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to specify the allowed "php" wrapper types #490

Closed
wants to merge 10 commits into from
2 changes: 2 additions & 0 deletions config/default_php8.rules
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ sp.xxe_protection.enable();
# PHP has a lot of wrappers, most of them aren't usually useful, you should
# only enable the ones you're using.
# sp.wrappers_whitelist.list("file,php,phar");
# The "php" wrapper can be further filtered, e.g. to only allow `php://stdout`, `php://stdin` and `php://stderr`:
# sp.wrappers_whitelist.php_list("stdout,stdin,stderr");

# Prevent sloppy comparisons.
# sp.sloppy_comparison.enable();
Expand Down
11 changes: 11 additions & 0 deletions doc/source/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,17 @@ to explicitly whitelist some `stream wrappers <https://secure.php.net/manual/en/
sp.wrappers_whitelist.list("file,php,phar");


Allowlist of the php stream-wrapper
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

:ref:`The php-stream-wrapper allowlist <php-stream-wrapper-allowlist-feature>`
allows to explicitly restrict the permitted builtin `php streams <https://www.php.net/manual/en/wrappers.php.php>`__.

::

sp.wrappers_whitelist.php_list("stdout,stdin,stderr");


Eval white and blacklist
^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
18 changes: 17 additions & 1 deletion doc/source/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ and using this feature to lock this up.
Whitelist of stream-wrappers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Php comes with a `lot of different <https://secure.php.net/manual/en/wrappers.php>`__
PHP comes with a `lot of different <https://secure.php.net/manual/en/wrappers.php>`__
`stream wrapper <https://secure.php.net/manual/en/intro.stream.php>`__, and most of them
are enabled by default.

Expand All @@ -397,6 +397,22 @@ Examples of related vulnerabilities
- `Data exfiltration via stream wrapper <https://www.idontplaydarts.com/2011/02/using-php-filter-for-local-file-inclusion/>`__
- `Inclusion via zip/phar <https://lightless.me/archives/include-file-from-zip-or-phar.html>`__

.. _php-stream-wrapper-allowlist-feature:

Allowlist of php stream-wrapper
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The builtin `"php" stream wrapper <https://www.php.net/manual/en/wrappers.php.php>`__
has support for common streams, like ``stdin``, ``stdout`` or ``stderr``, but
also for the dangerous ``filter`` one.

Examples of related vulnerabilities
"""""""""""""""""""""""""""""""""""

- `CNEXT exploits <https://github.com/ambionics/cnext-exploits/>`__
- Synacktiv's `php_filter_chain_generator <https://github.com/synacktiv/php_filter_chain_generator>`__ tool
- Ambionic's `wrapwrap <https://github.com/ambionics/wrapwrap>`__ tool

.. _eval-feature:

White and blacklist in ``eval``
Expand Down
3 changes: 3 additions & 0 deletions src/snuffleupagus.c
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ static PHP_GINIT_FUNCTION(snuffleupagus) {
SP_INIT_NULL(config_eval.blacklist);
SP_INIT_NULL(config_eval.whitelist);
SP_INIT_NULL(config_wrapper.whitelist);
SP_INIT_NULL(config_wrapper.php_stream_allowlist);
#undef SP_INIT_NULL
}

Expand Down Expand Up @@ -175,6 +176,7 @@ static PHP_GSHUTDOWN_FUNCTION(snuffleupagus) {
FREE_LST(config_eval.blacklist);
FREE_LST(config_eval.whitelist);
FREE_LST(config_wrapper.whitelist);
FREE_LST(config_wrapper.php_stream_allowlist);
#undef FREE_LST


Expand Down Expand Up @@ -388,6 +390,7 @@ static void dump_config(void) {
add_assoc_bool(&arr, SP_TOKEN_SLOPPY_COMPARISON "." SP_TOKEN_ENABLE, SPCFG(sloppy).enable);

ADD_ASSOC_SPLIST(&arr, SP_TOKEN_ALLOW_WRAPPERS "." SP_TOKEN_LIST, SPCFG(wrapper).whitelist);
ADD_ASSOC_SPLIST(&arr, SP_TOKEN_ALLOW_WRAPPERS "." SP_TOKEN_ALLOW_PHP_STREAMS, SPCFG(wrapper).php_stream_allowlist);

#undef ADD_ASSOC_SPLIST

Expand Down
2 changes: 2 additions & 0 deletions src/sp_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ typedef struct {

typedef struct {
sp_list_node *whitelist;
sp_list_node *php_stream_allowlist;
bool enabled;
size_t num_wrapper; // Used to verify if wrappers were added.
} sp_config_wrapper;
Expand Down Expand Up @@ -214,6 +215,7 @@ typedef struct {
#define SP_TOKEN_EVAL_WHITELIST "eval_whitelist"
#define SP_TOKEN_SLOPPY_COMPARISON "sloppy_comparison"
#define SP_TOKEN_ALLOW_WRAPPERS "wrappers_whitelist"
#define SP_TOKEN_ALLOW_PHP_STREAMS "php_list"
#define SP_TOKEN_INI_PROTECTION "ini_protection"
#define SP_TOKEN_INI "ini"

Expand Down
1 change: 1 addition & 0 deletions src/sp_config_keywords.c
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ SP_PARSE_FN(parse_wrapper_whitelist) {

sp_config_keyword config_keywords[] = {
{parse_list, SP_TOKEN_LIST, &cfg->whitelist},
{parse_list, SP_TOKEN_ALLOW_PHP_STREAMS, &cfg->php_stream_allowlist},
{0, 0, 0}};

SP_PROCESS_CONFIG_KEYWORDS_ERR();
Expand Down
154 changes: 154 additions & 0 deletions src/sp_wrapper.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "php_snuffleupagus.h"

#define LOG_FEATURE "wrappers_whitelist"

static bool wrapper_is_whitelisted(const zend_string *const zs) {
const sp_list_node *list = SPCFG(wrapper).whitelist;

Expand All @@ -16,6 +18,132 @@ static bool wrapper_is_whitelisted(const zend_string *const zs) {
return false;
}

static bool sp_php_stream_is_filtered(void) {
const sp_list_node *list = SPCFG(wrapper).php_stream_allowlist;

return list != NULL;
}

static bool sp_php_stream_is_whitelisted(const char *const kind) {
const sp_list_node *list = SPCFG(wrapper).php_stream_allowlist;

while (list) {
if (!strcasecmp(kind, ZSTR_VAL((const zend_string *)list->data))) {
return true;
}
list = list->next;
}
return false;
}

/*
* Adopted from
* https://github.com/php/php-src/blob/8896bd3200892000d8aaa01595d6c64b926a26f7/ext/standard/php_fopen_wrapper.c#L176
*/
static php_stream * sp_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)) {
path += 6;
}

if (!strncasecmp(path, "temp", 4)) {
if (!sp_php_stream_is_whitelisted("temp")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"temp\" dropped");
return NULL;
}
} else if (!strcasecmp(path, "memory")) {
if (!sp_php_stream_is_whitelisted("memory")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"memory\" dropped");
return NULL;
}
} else if (!strcasecmp(path, "output")) {
if (!sp_php_stream_is_whitelisted("output")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"output\" dropped");
return NULL;
}
} else if (!strcasecmp(path, "input")) {
if (!sp_php_stream_is_whitelisted("input")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"input\" dropped");
return NULL;
}
} else if (!strcasecmp(path, "stdin")) {
if (!sp_php_stream_is_whitelisted("stdin")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"stdin\" dropped");
return NULL;
}
} else if (!strcasecmp(path, "stdout")) {
if (!sp_php_stream_is_whitelisted("stdout")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"stdout\" dropped");
return NULL;
}
} else if (!strcasecmp(path, "stderr")) {
if (!sp_php_stream_is_whitelisted("stderr")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"stderr\" dropped");
return NULL;
}
} else if (!strncasecmp(path, "fd/", 3)) {
if (!sp_php_stream_is_whitelisted("fd")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"fd\" dropped");
return NULL;
}
} else if (!strncasecmp(path, "filter/", 7)) {
if (!sp_php_stream_is_whitelisted("filter")) {
sp_log_warn(LOG_FEATURE, "Call to not allowed php stream type \"filter\" dropped");
return NULL;
}
} else {
sp_log_warn(LOG_FEATURE, "Call to unknown php stream type dropped");
return NULL;
}

extern PHPAPI const php_stream_wrapper php_stream_php_wrapper;

return php_stream_php_wrapper.wops->stream_opener(wrapper, path, mode, options, opened_path, context STREAMS_DC);
}

/*
* Adopted from
* https://github.com/php/php-src/blob/8896bd3200892000d8aaa01595d6c64b926a26f7/ext/standard/php_fopen_wrapper.c#L428-L446
*/
static const php_stream_wrapper_ops sp_php_stdio_wops = {
sp_php_stream_url_wrap_php,
NULL, /* close */
NULL, /* fstat */
NULL, /* stat */
NULL, /* opendir */
"PHP",
NULL, /* unlink */
NULL, /* rename */
NULL, /* mkdir */
NULL, /* rmdir */
NULL
};
static const php_stream_wrapper sp_php_stream_php_wrapper = {
&sp_php_stdio_wops,
NULL,
0, /* is_url */
};

static void sp_reregister_php_wrapper(void) {
if (!sp_php_stream_is_filtered()) {
return;
}

if (php_unregister_url_stream_wrapper("php") != SUCCESS) {
sp_log_warn(LOG_FEATURE, "Failed to unregister stream wrapper \"php\"");
return;
}

if (php_register_url_stream_wrapper("php", &sp_php_stream_php_wrapper) != SUCCESS) {
sp_log_warn(LOG_FEATURE, "Failed to register custom stream wrapper \"php\"");
return;
}

sp_log_debug(LOG_FEATURE, "Stream \"php\" successfully re-registered");
}

void sp_disable_wrapper() {
HashTable *orig = php_stream_get_url_stream_wrappers_hash();
HashTable *orig_complete = pemalloc(sizeof(HashTable), 1);
Expand Down Expand Up @@ -50,16 +178,42 @@ PHP_FUNCTION(sp_stream_wrapper_register) {
zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "S*", &protocol_name, &params, &param_count);
// ignore proper arguments here and just let the original handler deal with it
if (!protocol_name || wrapper_is_whitelisted(protocol_name)) {

// reject manual loading of "php" wrapper
if (!strcasecmp(ZSTR_VAL(protocol_name), "php") && sp_php_stream_is_filtered()) {
return;
}

orig_handler = zend_hash_str_find_ptr(SPG(sp_internal_functions_hook), ZEND_STRL("stream_wrapper_register"));
orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
}
}

PHP_FUNCTION(sp_stream_wrapper_restore) {
zif_handler orig_handler;
zend_string *protocol_name = NULL;
zval *params = NULL;
uint32_t param_count = 0;

zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "S*", &protocol_name, &params, &param_count);
orig_handler = zend_hash_str_find_ptr(SPG(sp_internal_functions_hook), ZEND_STRL("stream_wrapper_restore"));
orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);

if (protocol_name && !strcasecmp(ZSTR_VAL(protocol_name), "php")) {
sp_reregister_php_wrapper();
}
}

int hook_stream_wrappers() {
TSRMLS_FETCH();

HOOK_FUNCTION("stream_wrapper_register", sp_internal_functions_hook,
PHP_FN(sp_stream_wrapper_register));

HOOK_FUNCTION("stream_wrapper_restore", sp_internal_functions_hook,
PHP_FN(sp_stream_wrapper_restore));

sp_reregister_php_wrapper();

return SUCCESS;
}
2 changes: 2 additions & 0 deletions src/tests/stream_wrapper/config/config_stream_wrapper_php.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
sp.wrappers_whitelist.list("php");
sp.wrappers_whitelist.php_list("stdin,stderr,stdout");
78 changes: 78 additions & 0 deletions src/tests/stream_wrapper/stream_wrapper_php.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
--TEST--
Stream wrapper (php)
--SKIPIF--
<?php
if (!extension_loaded("snuffleupagus")) print "skip snuffleupagus extension missing";
?>
--INI--
sp.configuration_file={PWD}/config/config_stream_wrapper_php.ini
--FILE--
<?php
echo file_get_contents('php://input');
file_put_contents('php://output', "Hello from stdout\n");
file_put_contents('php://stderr', "Hello from stderr #1\n");

file_put_contents('php://memory', "Bye from memory\n");
echo file_get_contents('php://memory');

file_put_contents('php://temp', "Bye from temp\n");
echo file_get_contents('php://temp');

file_put_contents('php://stderr', "Hello from stderr #2\n");

file_put_contents('php://filter/write=string.toupper/resource=output.tmp', "Hello from stdout filtered\n");
echo file_get_contents('php://filter/read=string.toupper/resource=output.tmp');

$foo = stream_wrapper_unregister("php");
fwrite(STDERR, $foo);
file_put_contents('php://stderr', "Hello from stderr #3\n");

stream_wrapper_restore("php");
file_put_contents('php://stderr', "Hello from stderr #4\n");
file_put_contents('php://memory', "Bye from memory\n");
?>
--EXPECTF--
Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "input" dropped in %a/stream_wrapper_php.php on line %d

Warning: file_get_contents(php://input): %s to open stream: operation failed in %a/stream_wrapper_php.php on line %d

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "output" dropped in %a/stream_wrapper_php.php on line %d

Warning: file_put_contents(php://output): %s to open stream: operation failed in %a/stream_wrapper_php.php on line %d
Hello from stderr #1

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "memory" dropped in %a/stream_wrapper_php.php on line %d

Warning: file_put_contents(php://memory): %s to open stream: operation failed in %a/stream_wrapper_php.php on line %d

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "memory" dropped in %a/stream_wrapper_php.php on line %d

Warning: file_get_contents(php://memory): %s to open stream: operation failed in %a/stream_wrapper_php.php on line %d

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "temp" dropped in %a/stream_wrapper_php.php on line %d

Warning: file_put_contents(php://temp): %s to open stream: operation failed in %a/stream_wrapper_php.php on line %d

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "temp" dropped in %a/stream_wrapper_php.php on line %d

Warning: file_get_contents(php://temp): %s to open stream: operation failed in %a/stream_wrapper_php.php on line %d
Hello from stderr #2

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "filter" dropped in %a/stream_wrapper_php.php on line %d

Warning: file_put_contents(php://filter/write=string.toupper/resource=output.tmp): %s to open stream: operation failed in %a/stream_wrapper_php.php on line %d

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "filter" dropped in %a/stream_wrapper_php.php on line %d

Warning: file_get_contents(php://filter/read=string.toupper/resource=output.tmp): %s to open stream: operation failed in %a/stream_wrapper_php.php on line %d
1
Warning: file_put_contents(): Unable to find the wrapper "php" - did you forget to enable it when you configured PHP? in %a/stream_wrapper_php.php on line %d

Warning: file_put_contents(): file:// wrapper is disabled in the server configuration in %a/stream_wrapper_php.php on line %d

Warning: file_put_contents(php://stderr): %s to open stream: no suitable wrapper could be found in %a/stream_wrapper_php.php on line %d
Hello from stderr #4

Warning: [snuffleupagus][0.0.0.0][wrappers_whitelist][log] Call to not allowed php stream type "memory" dropped in %a/stream_wrapper_php.php on line %d

Warning: file_put_contents(php://memory): %s to open stream: operation failed in %a/stream_wrapper_php.php on line %d
Loading