diff options
| -rw-r--r-- | config/default.rules | 12 | ||||
| -rw-r--r-- | doc/source/config.rst | 2 | ||||
| -rw-r--r-- | doc/source/features.rst | 6 | ||||
| -rw-r--r-- | src/snuffleupagus.c | 52 | ||||
| -rw-r--r-- | src/sp_config_utils.c | 5 | ||||
| -rw-r--r-- | src/sp_upload_validation.c | 5 | ||||
| -rw-r--r-- | src/sp_utils.c | 7 | ||||
| -rw-r--r-- | src/tests/disable_function/disabled_function_echo_2.phpt | 6 |
8 files changed, 80 insertions, 15 deletions
diff --git a/config/default.rules b/config/default.rules index 3e82ae3..818e73d 100644 --- a/config/default.rules +++ b/config/default.rules | |||
| @@ -35,6 +35,10 @@ sp.xxe_protection.enable(); | |||
| 35 | # https://snuffleupagus.readthedocs.io/features.html#protection-against-cross-site-request-forgery | 35 | # https://snuffleupagus.readthedocs.io/features.html#protection-against-cross-site-request-forgery |
| 36 | sp.cookie.name("PHPSESSID").samesite("lax"); | 36 | sp.cookie.name("PHPSESSID").samesite("lax"); |
| 37 | 37 | ||
| 38 | # Note that an attacker with arbitrary PHP code execution | ||
| 39 | # can bypass some virtual-patching, by (as)using PHP feature. | ||
| 40 | # A clever example would be to declare a class with a __toString method. | ||
| 41 | |||
| 38 | # Harden the `chmod` function (0777 (oct = 511, 0666 = 438) | 42 | # Harden the `chmod` function (0777 (oct = 511, 0666 = 438) |
| 39 | @condition PHP_VERSION_ID < 80000; | 43 | @condition PHP_VERSION_ID < 80000; |
| 40 | sp.disable_function.function("chmod").param("mode").value("438").drop(); | 44 | sp.disable_function.function("chmod").param("mode").value("438").drop(); |
| @@ -69,6 +73,14 @@ sp.cookie.name("PHPSESSID").samesite("lax"); | |||
| 69 | sp.disable_function.function("putenv").param("assignment").value_r("GCONV_").drop() | 73 | sp.disable_function.function("putenv").param("assignment").value_r("GCONV_").drop() |
| 70 | @end_condition; | 74 | @end_condition; |
| 71 | 75 | ||
| 76 | # https://github.com/php/php-src/issues/22035 | ||
| 77 | # CURLOPT_SSLENGINE = 10089 | ||
| 78 | @condition PHP_VERSION_ID < 80000; | ||
| 79 | sp.disable_function.function("curl_setopt").param("option").value("10089").drop() | ||
| 80 | @condition PHP_VERSION_ID >= 80000; | ||
| 81 | sp.disable_function.function("curl_setopt").param("option").value("10089").drop() | ||
| 82 | @end_condition; | ||
| 83 | |||
| 72 | # Since people are stupid enough to use `extract` on things like $_GET or $_POST, we might as well mitigate this vector | 84 | # Since people are stupid enough to use `extract` on things like $_GET or $_POST, we might as well mitigate this vector |
| 73 | @condition PHP_VERSION_ID < 80000; | 85 | @condition PHP_VERSION_ID < 80000; |
| 74 | sp.disable_function.function("extract").pos("0").value_r("^_").drop() | 86 | sp.disable_function.function("extract").pos("0").value_r("^_").drop() |
diff --git a/doc/source/config.rst b/doc/source/config.rst index 2053c2f..a84bb60 100644 --- a/doc/source/config.rst +++ b/doc/source/config.rst | |||
| @@ -152,7 +152,7 @@ least astonishment | |||
| 152 | <https://en.wikipedia.org/wiki/Principle_of_least_astonishment>`__. But since | 152 | <https://en.wikipedia.org/wiki/Principle_of_least_astonishment>`__. But since |
| 153 | it's `possible to modify php's logging system via php | 153 | it's `possible to modify php's logging system via php |
| 154 | <https://www.php.net/manual/en/errorfunc.configuration.php>`__, it's | 154 | <https://www.php.net/manual/en/errorfunc.configuration.php>`__, it's |
| 155 | heavily recommended to use the ``syslog`` option instead. The ``file:` option | 155 | heavily recommended to use the ``syslog`` option instead. The ``file:`` option |
| 156 | might be useful if you're using Snuffleupagus to fuzz or audit a codebase. | 156 | might be useful if you're using Snuffleupagus to fuzz or audit a codebase. |
| 157 | 157 | ||
| 158 | log_max_len | 158 | log_max_len |
diff --git a/doc/source/features.rst b/doc/source/features.rst index adb8779..517bbec 100644 --- a/doc/source/features.rst +++ b/doc/source/features.rst | |||
| @@ -309,7 +309,11 @@ of dangerous functions, dropping them everywhere else: | |||
| 309 | :language: php | 309 | :language: php |
| 310 | 310 | ||
| 311 | 311 | ||
| 312 | The intent is to make post-exploitation process (such as backdooring of legitimate code, or RAT usage) a lot harder for the attacker. | 312 | The intent is to make post-exploitation process (such as backdooring of |
| 313 | legitimate code, or RAT usage) a lot harder for the attacker. | ||
| 314 | |||
| 315 | Note that an attacker able to run arbitrary PHP code can likely bypass some virtual-patching | ||
| 316 | by (ab)using some PHP features. | ||
| 313 | 317 | ||
| 314 | 318 | ||
| 315 | .. _global-strict-feature: | 319 | .. _global-strict-feature: |
diff --git a/src/snuffleupagus.c b/src/snuffleupagus.c index 4d5fa09..6b0a327 100644 --- a/src/snuffleupagus.c +++ b/src/snuffleupagus.c | |||
| @@ -25,6 +25,39 @@ static inline void sp_op_array_handler(zend_op_array *const op) { | |||
| 25 | op->fn_flags |= ZEND_ACC_STRICT_TYPES; | 25 | op->fn_flags |= ZEND_ACC_STRICT_TYPES; |
| 26 | } | 26 | } |
| 27 | } | 27 | } |
| 28 | #if PHP_VERSION_ID >= 80500 | ||
| 29 | /* Prevent opcache from inlining user functions that have return-value | ||
| 30 | * monitoring rules, otherwise zend_execute_ex is never called and the | ||
| 31 | * hook never fires. ZEND_ACC_HAS_TYPE_HINTS is checked by | ||
| 32 | * zend_try_inline_call() and blocks inlining. For functions without | ||
| 33 | * actual type hints the only runtime effect is that ZEND_RECV opcodes | ||
| 34 | * are executed instead of skipped; for 0-arg functions (the common | ||
| 35 | * inlineable case) there are no RECV opcodes so the impact is zero. */ | ||
| 36 | if (op->function_name && ZEND_USER_CODE(op->type) && | ||
| 37 | ((SPCFG(disabled_functions_ret) && zend_hash_num_elements(SPCFG(disabled_functions_ret))) || | ||
| 38 | SPCFG(disabled_functions_reg_ret).disabled_functions)) { | ||
| 39 | char *fname = NULL; | ||
| 40 | if (op->scope) { | ||
| 41 | const size_t len = ZSTR_LEN(op->scope->name) + 2 + ZSTR_LEN(op->function_name) + 1; | ||
| 42 | fname = emalloc(len); | ||
| 43 | snprintf(fname, len, "%s::%s", ZSTR_VAL(op->scope->name), ZSTR_VAL(op->function_name)); | ||
| 44 | } else { | ||
| 45 | fname = estrdup(ZSTR_VAL(op->function_name)); | ||
| 46 | } | ||
| 47 | bool has_ret_rule = false; | ||
| 48 | if (SPCFG(disabled_functions_ret) && | ||
| 49 | zend_hash_str_find_ptr(SPCFG(disabled_functions_ret), fname, strlen(fname))) { | ||
| 50 | has_ret_rule = true; | ||
| 51 | } | ||
| 52 | if (!has_ret_rule && SPCFG(disabled_functions_reg_ret).disabled_functions) { | ||
| 53 | has_ret_rule = true; /* regex rules require runtime matching */ | ||
| 54 | } | ||
| 55 | if (has_ret_rule) { | ||
| 56 | op->fn_flags |= ZEND_ACC_HAS_TYPE_HINTS; | ||
| 57 | } | ||
| 58 | efree(fname); | ||
| 59 | } | ||
| 60 | #endif | ||
| 28 | } | 61 | } |
| 29 | 62 | ||
| 30 | ZEND_DECLARE_MODULE_GLOBALS(snuffleupagus) | 63 | ZEND_DECLARE_MODULE_GLOBALS(snuffleupagus) |
| @@ -246,7 +279,7 @@ PHP_MINFO_FUNCTION(snuffleupagus) { | |||
| 246 | php_info_print_table_start(); | 279 | php_info_print_table_start(); |
| 247 | php_info_print_table_row( | 280 | php_info_print_table_row( |
| 248 | 2, "snuffleupagus support", | 281 | 2, "snuffleupagus support", |
| 249 | SPG(is_config_valid) ? "enabled" : "disabled"); | 282 | SPG(is_config_valid) == SP_CONFIG_VALID ? "enabled" : "disabled"); |
| 250 | php_info_print_table_row(2, "Version", PHP_SNUFFLEUPAGUS_VERSION); | 283 | php_info_print_table_row(2, "Version", PHP_SNUFFLEUPAGUS_VERSION); |
| 251 | php_info_print_table_row(2, "Valid config", valid_config); | 284 | php_info_print_table_row(2, "Valid config", valid_config); |
| 252 | php_info_print_table_end(); | 285 | php_info_print_table_end(); |
| @@ -582,12 +615,25 @@ static PHP_INI_MH(OnUpdateConfiguration) { | |||
| 582 | 615 | ||
| 583 | sp_hook_register_server_variables(); | 616 | sp_hook_register_server_variables(); |
| 584 | 617 | ||
| 585 | if (SPCFG(global_strict).enable) { | 618 | bool need_op_array_handler = SPCFG(global_strict).enable; |
| 619 | |||
| 620 | #if PHP_VERSION_ID >= 80500 | ||
| 621 | /* Register as zend extension to get op_array_handler callbacks, which we | ||
| 622 | * use to prevent opcache from inlining monitored functions. */ | ||
| 623 | if (SPCFG(disabled_functions_ret) && zend_hash_num_elements(SPCFG(disabled_functions_ret))) { | ||
| 624 | need_op_array_handler = true; | ||
| 625 | } | ||
| 626 | if (SPCFG(disabled_functions_reg_ret).disabled_functions) { | ||
| 627 | need_op_array_handler = true; | ||
| 628 | } | ||
| 629 | #endif | ||
| 630 | |||
| 631 | if (need_op_array_handler) { | ||
| 586 | if (!zend_get_extension(PHP_SNUFFLEUPAGUS_EXTNAME)) { | 632 | if (!zend_get_extension(PHP_SNUFFLEUPAGUS_EXTNAME)) { |
| 587 | zend_extension_entry.startup = NULL; | 633 | zend_extension_entry.startup = NULL; |
| 588 | zend_register_extension(&zend_extension_entry, NULL); | 634 | zend_register_extension(&zend_extension_entry, NULL); |
| 589 | } | 635 | } |
| 590 | // This is needed to implement the global strict mode | 636 | // This is needed to enable the op_array_handler callback |
| 591 | CG(compiler_options) |= ZEND_COMPILE_HANDLE_OP_ARRAY; | 637 | CG(compiler_options) |= ZEND_COMPILE_HANDLE_OP_ARRAY; |
| 592 | } | 638 | } |
| 593 | 639 | ||
diff --git a/src/sp_config_utils.c b/src/sp_config_utils.c index 84b1f30..415e434 100644 --- a/src/sp_config_utils.c +++ b/src/sp_config_utils.c | |||
| @@ -11,8 +11,9 @@ sp_list_node *parse_functions_list(const char *const value) { | |||
| 11 | sp_list_node *list = NULL; | 11 | sp_list_node *list = NULL; |
| 12 | char *tmp = strdup(value); | 12 | char *tmp = strdup(value); |
| 13 | const char *function_name; | 13 | const char *function_name; |
| 14 | char *next_token = tmp; | 14 | char *next_token = NULL; |
| 15 | while ((function_name = strtok_r(NULL, sep, &next_token))) { | 15 | for (function_name = strtok_r(tmp, sep, &next_token); function_name; |
| 16 | function_name = strtok_r(NULL, sep, &next_token)) { | ||
| 16 | list = sp_list_prepend(list, strdup(function_name)); | 17 | list = sp_list_prepend(list, strdup(function_name)); |
| 17 | } | 18 | } |
| 18 | free(tmp); | 19 | free(tmp); |
diff --git a/src/sp_upload_validation.c b/src/sp_upload_validation.c index e24149e..b5babed 100644 --- a/src/sp_upload_validation.c +++ b/src/sp_upload_validation.c | |||
| @@ -64,8 +64,7 @@ static int sp_rfc1867_callback(unsigned int event, void *event_data, void **extr | |||
| 64 | if ((pid = fork()) == 0) { | 64 | if ((pid = fork()) == 0) { |
| 65 | if (execve(ZSTR_VAL(config_upload->script), cmd, env) == -1) { | 65 | if (execve(ZSTR_VAL(config_upload->script), cmd, env) == -1) { |
| 66 | sp_log_warn("upload_validation", "Could not call '%s' : %s", ZSTR_VAL(config_upload->script), strerror(errno)); | 66 | sp_log_warn("upload_validation", "Could not call '%s' : %s", ZSTR_VAL(config_upload->script), strerror(errno)); |
| 67 | EFREE_3(env); | 67 | _exit(1); |
| 68 | exit(1); | ||
| 69 | } | 68 | } |
| 70 | } else if (pid == -1) { | 69 | } else if (pid == -1) { |
| 71 | // LCOV_EXCL_START | 70 | // LCOV_EXCL_START |
| @@ -77,7 +76,7 @@ static int sp_rfc1867_callback(unsigned int event, void *event_data, void **extr | |||
| 77 | 76 | ||
| 78 | EFREE_3(env); | 77 | EFREE_3(env); |
| 79 | int waitstatus; | 78 | int waitstatus; |
| 80 | wait(&waitstatus); | 79 | waitpid(pid, &waitstatus, 0); |
| 81 | if (WEXITSTATUS(waitstatus) != 0) { // Nope | 80 | if (WEXITSTATUS(waitstatus) != 0) { // Nope |
| 82 | char *uri = getenv("REQUEST_URI"); | 81 | char *uri = getenv("REQUEST_URI"); |
| 83 | int sim = config_upload->simulation; | 82 | int sim = config_upload->simulation; |
diff --git a/src/sp_utils.c b/src/sp_utils.c index e6efcc6..10beb8b 100644 --- a/src/sp_utils.c +++ b/src/sp_utils.c | |||
| @@ -393,11 +393,12 @@ bool sp_match_array_key(const zval* zv, const zend_string* to_match, const sp_re | |||
| 393 | char* idx_str = NULL; | 393 | char* idx_str = NULL; |
| 394 | spprintf(&idx_str, 0, ZEND_ULONG_FMT, idx); | 394 | spprintf(&idx_str, 0, ZEND_ULONG_FMT, idx); |
| 395 | zend_string* tmp = zend_string_init(idx_str, strlen(idx_str), 0); | 395 | zend_string* tmp = zend_string_init(idx_str, strlen(idx_str), 0); |
| 396 | if (sp_match_value(tmp, to_match, rx)) { | 396 | bool match = sp_match_value(tmp, to_match, rx); |
| 397 | efree(idx_str); | 397 | zend_string_release(tmp); |
| 398 | efree(idx_str); | ||
| 399 | if (match) { | ||
| 398 | return true; | 400 | return true; |
| 399 | } | 401 | } |
| 400 | efree(idx_str); | ||
| 401 | } | 402 | } |
| 402 | } | 403 | } |
| 403 | ZEND_HASH_FOREACH_END(); | 404 | ZEND_HASH_FOREACH_END(); |
diff --git a/src/tests/disable_function/disabled_function_echo_2.phpt b/src/tests/disable_function/disabled_function_echo_2.phpt index c1d9817..ce3488e 100644 --- a/src/tests/disable_function/disabled_function_echo_2.phpt +++ b/src/tests/disable_function/disabled_function_echo_2.phpt | |||
| @@ -4,11 +4,13 @@ Echo hooking | |||
| 4 | <?php if (!extension_loaded("snuffleupagus")) print "skip"; ?> | 4 | <?php if (!extension_loaded("snuffleupagus")) print "skip"; ?> |
| 5 | --INI-- | 5 | --INI-- |
| 6 | sp.configuration_file={PWD}/config/disabled_function_echo.ini | 6 | sp.configuration_file={PWD}/config/disabled_function_echo.ini |
| 7 | opcache.optimization_level=0 | ||
| 7 | --FILE-- | 8 | --FILE-- |
| 8 | <?php | 9 | <?php |
| 9 | echo "qwe"; | 10 | echo "qwe"; |
| 10 | echo "1", "oops"; | 11 | echo "1"; |
| 12 | echo "oops"; | ||
| 11 | ?> | 13 | ?> |
| 12 | --EXPECTF-- | 14 | --EXPECTF-- |
| 13 | qwe1 | 15 | qwe1 |
| 14 | Fatal error: [snuffleupagus][0.0.0.0][disabled_function][drop] Aborted execution on call of the function 'echo' in %a/disabled_function_echo_2.php on line 3 | 16 | Fatal error: [snuffleupagus][0.0.0.0][disabled_function][drop] Aborted execution on call of the function 'echo' in %a/disabled_function_echo_2.php on line 4 |
