summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/default.rules12
-rw-r--r--doc/source/config.rst2
-rw-r--r--doc/source/features.rst6
-rw-r--r--src/snuffleupagus.c52
-rw-r--r--src/sp_config_utils.c5
-rw-r--r--src/sp_upload_validation.c5
-rw-r--r--src/sp_utils.c7
-rw-r--r--src/tests/disable_function/disabled_function_echo_2.phpt6
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
36sp.cookie.name("PHPSESSID").samesite("lax"); 36sp.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;
74sp.disable_function.function("extract").pos("0").value_r("^_").drop() 86sp.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
153it's `possible to modify php's logging system via php 153it'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
155heavily recommended to use the ``syslog`` option instead. The ``file:` option 155heavily recommended to use the ``syslog`` option instead. The ``file:`` option
156might be useful if you're using Snuffleupagus to fuzz or audit a codebase. 156might be useful if you're using Snuffleupagus to fuzz or audit a codebase.
157 157
158log_max_len 158log_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
312The intent is to make post-exploitation process (such as backdooring of legitimate code, or RAT usage) a lot harder for the attacker. 312The intent is to make post-exploitation process (such as backdooring of
313legitimate code, or RAT usage) a lot harder for the attacker.
314
315Note that an attacker able to run arbitrary PHP code can likely bypass some virtual-patching
316by (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
30ZEND_DECLARE_MODULE_GLOBALS(snuffleupagus) 63ZEND_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--
6sp.configuration_file={PWD}/config/disabled_function_echo.ini 6sp.configuration_file={PWD}/config/disabled_function_echo.ini
7opcache.optimization_level=0
7--FILE-- 8--FILE--
8<?php 9<?php
9echo "qwe"; 10echo "qwe";
10echo "1", "oops"; 11echo "1";
12echo "oops";
11?> 13?>
12--EXPECTF-- 14--EXPECTF--
13qwe1 15qwe1
14Fatal 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 16Fatal 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