From 096e7faa6a5e21e5416a7c8c484e27acd4636a66 Mon Sep 17 00:00:00 2001 From: xXx-caillou-xXx Date: Mon, 27 Aug 2018 13:56:44 +0200 Subject: Add whitelist support for php's wrappers --- src/config.m4 | 2 +- src/php_snuffleupagus.h | 1 + src/snuffleupagus.c | 15 +++++ src/sp_config.c | 21 +++++++ src/sp_config.h | 12 +++- src/sp_config_keywords.c | 20 +++++-- src/sp_config_keywords.h | 1 + src/sp_wrapper.c | 70 ++++++++++++++++++++++ src/sp_wrapper.h | 8 +++ src/tests/config/config_stream_wrapper.ini | 1 + .../config/config_stream_wrapper_register.ini | 1 + src/tests/stream_wrapper.phpt | 31 ++++++++++ src/tests/stream_wrapper_register.phpt | 25 ++++++++ src/tests/stream_wrapper_restore.phpt | 17 ++++++ 14 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 src/sp_wrapper.c create mode 100644 src/sp_wrapper.h create mode 100644 src/tests/config/config_stream_wrapper.ini create mode 100644 src/tests/config/config_stream_wrapper_register.ini create mode 100644 src/tests/stream_wrapper.phpt create mode 100644 src/tests/stream_wrapper_register.phpt create mode 100644 src/tests/stream_wrapper_restore.phpt (limited to 'src') diff --git a/src/config.m4 b/src/config.m4 index 0165f87..52b6d04 100644 --- a/src/config.m4 +++ b/src/config.m4 @@ -6,7 +6,7 @@ sources="$sources sp_unserialize.c sp_utils.c sp_disable_xxe.c sp_list.c" sources="$sources sp_disabled_functions.c sp_execute.c sp_upload_validation.c" sources="$sources sp_cookie_encryption.c sp_network_utils.c tweetnacl.c" sources="$sources sp_config_keywords.c sp_var_parser.c sp_var_value.c sp_tree.c" -sources="$sources sp_pcre_compat.c sp_crypt.c sp_session.c sp_sloppy.c" +sources="$sources sp_pcre_compat.c sp_crypt.c sp_session.c sp_sloppy.c sp_wrapper.c" PHP_ARG_ENABLE(snuffleupagus, whether to enable snuffleupagus support, [ --enable-snuffleupagus Enable snuffleupagus support]) diff --git a/src/php_snuffleupagus.h b/src/php_snuffleupagus.h index 96f9dd4..41d9b77 100644 --- a/src/php_snuffleupagus.h +++ b/src/php_snuffleupagus.h @@ -44,6 +44,7 @@ #include "sp_crypt.h" #include "sp_session.h" #include "sp_sloppy.h" +#include "sp_wrapper.h" extern zend_module_entry snuffleupagus_module_entry; #define phpext_snuffleupagus_ptr &snuffleupagus_module_entry diff --git a/src/snuffleupagus.c b/src/snuffleupagus.c index ff0c6c3..1a92f11 100644 --- a/src/snuffleupagus.c +++ b/src/snuffleupagus.c @@ -95,6 +95,7 @@ PHP_GINIT_FUNCTION(snuffleupagus) { SP_INIT(snuffleupagus_globals->config.config_cookie); SP_INIT(snuffleupagus_globals->config.config_session); SP_INIT(snuffleupagus_globals->config.config_eval); + SP_INIT(snuffleupagus_globals->config.config_wrapper); snuffleupagus_globals->config.config_disabled_functions_reg ->disabled_functions = NULL; @@ -103,6 +104,7 @@ PHP_GINIT_FUNCTION(snuffleupagus) { snuffleupagus_globals->config.config_cookie->cookies = NULL; snuffleupagus_globals->config.config_eval->blacklist = NULL; snuffleupagus_globals->config.config_eval->whitelist = NULL; + snuffleupagus_globals->config.config_wrapper->whitelist = NULL; #undef SP_INIT #undef SP_INIT_HT @@ -160,12 +162,14 @@ PHP_MSHUTDOWN_FUNCTION(snuffleupagus) { sp_list_free(SNUFFLEUPAGUS_G(config).config_cookie->cookies); sp_list_free(SNUFFLEUPAGUS_G(config).config_eval->blacklist); sp_list_free(SNUFFLEUPAGUS_G(config).config_eval->whitelist); + sp_list_free(SNUFFLEUPAGUS_G(config).config_wrapper->whitelist); #undef FREE_LST_DISABLE pefree(SNUFFLEUPAGUS_G(config.config_disabled_functions_reg), 1); pefree(SNUFFLEUPAGUS_G(config.config_disabled_functions_reg_ret), 1); pefree(SNUFFLEUPAGUS_G(config.config_cookie), 1); + pefree(SNUFFLEUPAGUS_G(config.config_wrapper), 1); UNREGISTER_INI_ENTRIES(); @@ -176,6 +180,14 @@ PHP_RINIT_FUNCTION(snuffleupagus) { #if defined(COMPILE_DL_SNUFFLEUPAGUS) && defined(ZTS) ZEND_TSRMLS_CACHE_UPDATE(); #endif + + // We need to disable wrappers loaded by extensions loaded after SNUFFLEUPAGUS. + if (SNUFFLEUPAGUS_G(config).config_wrapper->enabled && + zend_hash_num_elements(php_stream_get_url_stream_wrappers_hash()) != + SNUFFLEUPAGUS_G(config).config_wrapper->num_wrapper) { + sp_disable_wrapper(); + } + if (NULL != SNUFFLEUPAGUS_G(config).config_snuffleupagus->encryption_key) { if (NULL != SNUFFLEUPAGUS_G(config).config_cookie->cookies) { zend_hash_apply_with_arguments( @@ -243,6 +255,9 @@ static PHP_INI_MH(OnUpdateConfiguration) { if (SNUFFLEUPAGUS_G(config).config_disable_xxe->enable == 0) { hook_libxml_disable_entity_loader(); } + if (SNUFFLEUPAGUS_G(config).config_wrapper->enabled) { + hook_stream_wrappers(); + } hook_disabled_functions(); hook_execute(); diff --git a/src/sp_config.c b/src/sp_config.c index c652984..e5cb3ce 100644 --- a/src/sp_config.c +++ b/src/sp_config.c @@ -23,6 +23,7 @@ sp_config_tokens const sp_func[] = { {.func = parse_eval_whitelist, .token = SP_TOKEN_EVAL_WHITELIST}, {.func = parse_session, .token = SP_TOKEN_SESSION_ENCRYPTION}, {.func = parse_sloppy_comparison, .token = SP_TOKEN_SLOPPY_COMPARISON}, + {.func = parse_wrapper_whitelist, .token = SP_TOKEN_ALLOW_WRAPPERS}, {NULL, NULL}}; /* Top level keyword parsing */ @@ -61,6 +62,26 @@ int parse_empty(char *restrict line, char *restrict keyword, void *retval) { return 0; } +int parse_list(char *restrict line, char *restrict keyword, void *list_ptr) { + zend_string *value = NULL; + sp_list_node **list = list_ptr; + char *token, *tmp; + + size_t consumed = 0; + value = get_param(&consumed, line, SP_TYPE_STR, keyword); + if (!value) { + return -1; + } + + tmp = ZSTR_VAL(value); + while ((token = strtok_r(tmp, ",", &tmp))) { + *list = sp_list_insert(*list, zend_string_init(token, strlen(token), 1)); + } + + pefree(value, 1); + return consumed; +} + int parse_php_type(char *restrict line, char *restrict keyword, void *retval) { size_t consumed = 0; zend_string *value = get_param(&consumed, line, SP_TYPE_STR, keyword); diff --git a/src/sp_config.h b/src/sp_config.h index d2fa64f..9d58359 100644 --- a/src/sp_config.h +++ b/src/sp_config.h @@ -77,6 +77,12 @@ typedef struct { bool simulation; } sp_cookie; +typedef struct { + sp_list_node *whitelist; + bool enabled; + size_t num_wrapper; // Used to verify if wrappers were added. +} sp_config_wrapper; + typedef struct { bool encrypt; bool simulation; @@ -166,6 +172,7 @@ typedef struct { sp_config_global_strict *config_global_strict; sp_config_disable_xxe *config_disable_xxe; sp_config_eval *config_eval; + sp_config_wrapper *config_wrapper; sp_config_session *config_session; bool hook_execute; @@ -204,6 +211,7 @@ typedef struct { #define SP_TOKEN_EVAL_BLACKLIST ".eval_blacklist" #define SP_TOKEN_EVAL_WHITELIST ".eval_whitelist" #define SP_TOKEN_SLOPPY_COMPARISON ".sloppy_comparison" +#define SP_TOKEN_ALLOW_WRAPPERS ".wrappers_whitelist" // common tokens #define SP_TOKEN_ENABLE ".enable(" @@ -256,8 +264,7 @@ typedef struct { // upload_validator #define SP_TOKEN_UPLOAD_SCRIPT ".script(" -// eval blacklist -#define SP_TOKEN_EVAL_LIST ".list(" +#define SP_TOKEN_LIST ".list(" int sp_parse_config(const char *); int parse_array(sp_disabled_function *); @@ -267,6 +274,7 @@ int parse_regexp(char *restrict, char *restrict, void *); int parse_empty(char *restrict, char *restrict, void *); int parse_cidr(char *restrict, char *restrict, void *); int parse_php_type(char *restrict, char *restrict, void *); +int parse_list(char *restrict, char *restrict, void *); // cleanup void sp_disabled_function_list_free(sp_list_node *); diff --git a/src/sp_config_keywords.c b/src/sp_config_keywords.c index fb20a43..b86b9d9 100644 --- a/src/sp_config_keywords.c +++ b/src/sp_config_keywords.c @@ -166,12 +166,11 @@ int parse_global(char *line) { } static int parse_eval_filter_conf(char *line, sp_list_node **list) { - char *token, *tmp; zend_string *rest = NULL; sp_config_eval *eval = SNUFFLEUPAGUS_G(config).config_eval; sp_config_functions sp_config_funcs[] = { - {parse_str, SP_TOKEN_EVAL_LIST, &rest}, + {parse_list, SP_TOKEN_LIST, list}, {parse_empty, SP_TOKEN_SIMULATION, &(SNUFFLEUPAGUS_G(config).config_eval->simulation)}, {parse_str, SP_TOKEN_DUMP, &(SNUFFLEUPAGUS_G(config).config_eval->dump)}, @@ -184,16 +183,25 @@ static int parse_eval_filter_conf(char *line, sp_list_node **list) { return ret; } - tmp = ZSTR_VAL(rest); - while ((token = strtok_r(tmp, ",", &tmp))) { - *list = sp_list_insert(*list, zend_string_init(token, strlen(token), 1)); - } if (rest != NULL) { pefree(rest, 1); } return SUCCESS; } +int parse_wrapper_whitelist(char *line) { + SNUFFLEUPAGUS_G(config).config_wrapper->enabled = true; + sp_config_functions sp_config_funcs[] = { + {parse_list, SP_TOKEN_LIST, + &SNUFFLEUPAGUS_G(config).config_wrapper->whitelist}, + {0}}; + int ret = parse_keywords(sp_config_funcs, line); + if (0 != ret) { + return ret; + } + return SUCCESS; +} + int parse_eval_blacklist(char *line) { return parse_eval_filter_conf( line, &SNUFFLEUPAGUS_G(config).config_eval->blacklist); diff --git a/src/sp_config_keywords.h b/src/sp_config_keywords.h index 36c66a5..ab58456 100644 --- a/src/sp_config_keywords.h +++ b/src/sp_config_keywords.h @@ -16,5 +16,6 @@ int parse_eval_blacklist(char *line); int parse_eval_whitelist(char *line); int parse_session(char *line); int parse_sloppy_comparison(char *line); +int parse_wrapper_whitelist(char *line); #endif // __SP_CONFIG_KEYWORDS_H diff --git a/src/sp_wrapper.c b/src/sp_wrapper.c new file mode 100644 index 0000000..d9cd296 --- /dev/null +++ b/src/sp_wrapper.c @@ -0,0 +1,70 @@ +#include "php_snuffleupagus.h" +#include "sp_config.h" + +ZEND_DECLARE_MODULE_GLOBALS(snuffleupagus) + +static bool wrapper_is_whitelisted(const zend_string *zs) { + const sp_list_node *list = SNUFFLEUPAGUS_G(config).config_wrapper->whitelist; + + if (!zs) { + return false; + } + + while (list) { + if (zend_string_equals_ci(zs, (const zend_string*)list->data)) { + return true; + } + list = list->next; + } + return false; +} + +void sp_disable_wrapper() { + HashTable *orig = php_stream_get_url_stream_wrappers_hash(); + HashTable *orig_complete = pemalloc(sizeof(*orig_complete), 1); + zval *zv; + zend_string *zs; + + // Copy the original hashtable into a temporary one, as I'm not sure about + // the behaviour of ZEND_HASH_FOREACH when element are removed from the + // hashtable used in the loop. + zend_hash_init(orig_complete, zend_hash_num_elements(orig), NULL, NULL, 1); + zend_hash_copy(orig_complete, orig, NULL); + zend_hash_clean(orig); + + ZEND_HASH_FOREACH_STR_KEY_VAL(orig_complete, zs, zv) { + if (wrapper_is_whitelisted(zs)) { + zend_hash_add(orig, zs, zv); + } + } + ZEND_HASH_FOREACH_END(); + + zend_hash_destroy(orig_complete); + pefree(orig_complete, 1); + SNUFFLEUPAGUS_G(config).config_wrapper->num_wrapper = zend_hash_num_elements(orig); +} + +PHP_FUNCTION(sp_stream_wrapper_register) { + void (*orig_handler)(INTERNAL_FUNCTION_PARAMETERS); + zend_string *protocol_name = NULL; + + ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 2, EX_NUM_ARGS()); + Z_PARAM_STR(protocol_name); + ZEND_PARSE_PARAMETERS_END_EX((void)0); + + if (wrapper_is_whitelisted(protocol_name)) { + orig_handler = zend_hash_str_find_ptr( + SNUFFLEUPAGUS_G(sp_internal_functions_hook), + "stream_wrapper_register", sizeof("stream_wrapper_register") - 1); + orig_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); + } +} + +int hook_stream_wrappers() { + TSRMLS_FETCH(); + + HOOK_FUNCTION("stream_wrapper_register", sp_internal_functions_hook, + PHP_FN(sp_stream_wrapper_register)); + + return SUCCESS; +} diff --git a/src/sp_wrapper.h b/src/sp_wrapper.h new file mode 100644 index 0000000..f57513a --- /dev/null +++ b/src/sp_wrapper.h @@ -0,0 +1,8 @@ +#ifndef SP_WRAPPER_H +#define SP_WRAPPER_H +#include "php_snuffleupagus.h" + +void sp_disable_wrapper(); +int hook_stream_wrappers(); + +#endif diff --git a/src/tests/config/config_stream_wrapper.ini b/src/tests/config/config_stream_wrapper.ini new file mode 100644 index 0000000..0cd7f77 --- /dev/null +++ b/src/tests/config/config_stream_wrapper.ini @@ -0,0 +1 @@ +sp.wrappers_whitelist.list("https,FTP,does_not_exist"); diff --git a/src/tests/config/config_stream_wrapper_register.ini b/src/tests/config/config_stream_wrapper_register.ini new file mode 100644 index 0000000..ee273a1 --- /dev/null +++ b/src/tests/config/config_stream_wrapper_register.ini @@ -0,0 +1 @@ +sp.wrappers_whitelist.list("php,lelel"); diff --git a/src/tests/stream_wrapper.phpt b/src/tests/stream_wrapper.phpt new file mode 100644 index 0000000..b4e49a0 --- /dev/null +++ b/src/tests/stream_wrapper.phpt @@ -0,0 +1,31 @@ +--TEST-- +Stream wrapper +--SKIPIF-- + +--INI-- +sp.configuration_file={PWD}/config/config_stream_wrapper.ini +--FILE-- + +--EXPECTF-- +Warning: Unknown: Unable to find the wrapper "php" - did you forget to enable it when you configured PHP? in Unknown on line 0 + +Warning: Unknown: Unable to find the wrapper "php" - did you forget to enable it when you configured PHP? in Unknown on line 0 + +Warning: Unknown: Unable to find the wrapper "php" - did you forget to enable it when you configured PHP? in Unknown on line 0 + +Warning: file_get_contents(): Unable to find the wrapper "http" - did you forget to enable it when you configured PHP? in %a/tests/stream_wrapper.php on line %d + +Warning: file_get_contents(): php_network_getaddresses: getaddrinfo failed: Name or service not known in %a/tests/stream_wrapper.php on line %d + +Warning: file_get_contents(https://qweqwezxc): failed to open stream: php_network_getaddresses: getaddrinfo failed: Name or service not known in %a/tests/stream_wrapper.php on line %d + +Warning: file_get_contents(ftp://qweqwezxc): failed to open stream: operation failed in %a/tests/stream_wrapper.php on line %d + +Warning: file_get_contents(): Unable to find the wrapper "lelel" - did you forget to enable it when you configured PHP? in %a/tests/stream_wrapper.php on line %d + +Warning: file_get_contents(lelel://qweqwezxc): failed to open stream: No such file or directory in %a/tests/stream_wrapper.php on line %d diff --git a/src/tests/stream_wrapper_register.phpt b/src/tests/stream_wrapper_register.phpt new file mode 100644 index 0000000..31e53ea --- /dev/null +++ b/src/tests/stream_wrapper_register.phpt @@ -0,0 +1,25 @@ +--TEST-- +Stream wrapper +--SKIPIF-- + +--INI-- +sp.configuration_file={PWD}/config/config_stream_wrapper_register.ini +--FILE-- + +--EXPECTF-- +Warning: fopen(): Unable to find the wrapper "lolol" - did you forget to enable it when you configured PHP? in %a/tests/stream_wrapper_register.php on line %d + +Warning: fopen(): file:// wrapper is disabled in the server configuration in %a/tests/stream_wrapper_register.php on line %d + +Warning: fopen(lolol://asdasd): failed to open stream: no suitable wrapper could be found in %a/tests/stream_wrapper_register.php on line %d diff --git a/src/tests/stream_wrapper_restore.phpt b/src/tests/stream_wrapper_restore.phpt new file mode 100644 index 0000000..c1bce3b --- /dev/null +++ b/src/tests/stream_wrapper_restore.phpt @@ -0,0 +1,17 @@ +--TEST-- +Stream wrapper +--SKIPIF-- + +--INI-- +sp.configuration_file={PWD}/config/config_stream_wrapper_register.ini +--FILE-- + +--EXPECTF-- +Notice: stream_wrapper_restore(): file:// was never changed, nothing to restore in %a/tests/stream_wrapper_restore.php on line %d + +Warning: fopen(): Unable to find the wrapper "file" - did you forget to enable it when you configured PHP? in %a/tests/stream_wrapper_restore.php on line %d + +Warning: fopen(file://asdasd): failed to open stream: No such file or directory in %a/tests/stream_wrapper_restore.php on line %d -- cgit v1.3