1 #include "php_snuffleupagus.h"
2 
3 static void (*orig_execute_ex)(zend_execute_data *execute_data) = NULL;
4 static void (*orig_zend_execute_internal)(zend_execute_data *execute_data,
5                                           zval *return_value) = NULL;
6 static int (*orig_zend_stream_open)(const char *filename,
7                                     zend_file_handle *handle) = NULL;
8 
9 // FIXME handle symlink
terminate_if_writable(const char * filename)10 ZEND_COLD static inline void terminate_if_writable(const char *filename) {
11   const sp_config_readonly_exec *config_ro_exec =
12       SNUFFLEUPAGUS_G(config).config_readonly_exec;
13 
14   if (0 == access(filename, W_OK)) {
15     if (config_ro_exec->dump) {
16       sp_log_request(config_ro_exec->dump,
17                      config_ro_exec->textual_representation,
18                      SP_TOKEN_READONLY_EXEC);
19     }
20     if (true == config_ro_exec->simulation) {
21       sp_log_simulation("readonly_exec",
22                         "Attempted execution of a writable file (%s).",
23                         filename);
24     } else {
25       sp_log_drop("readonly_exec",
26                   "Attempted execution of a writable file (%s).", filename);
27     }
28   } else {
29     if (EACCES != errno) {
30       // LCOV_EXCL_START
31       sp_log_err("Writable execution", "Error while accessing %s: %s", filename,
32                  strerror(errno));
33       // LCOV_EXCL_STOP
34     }
35   }
36 }
37 
is_builtin_matching(const zend_string * restrict const param_value,const char * restrict const function_name,const char * restrict const param_name,const sp_list_node * config,const HashTable * ht)38 inline static void is_builtin_matching(
39     const zend_string *restrict const param_value,
40     const char *restrict const function_name,
41     const char *restrict const param_name, const sp_list_node *config,
42     const HashTable *ht) {
43   if (!config || !config->data) {
44     return;
45   }
46 
47   should_disable_ht(
48       EG(current_execute_data), function_name, param_value, param_name,
49       SNUFFLEUPAGUS_G(config).config_disabled_functions_reg->disabled_functions,
50       ht);
51 }
52 
53 static void ZEND_HOT
is_in_eval_and_whitelisted(const zend_execute_data * execute_data)54 is_in_eval_and_whitelisted(const zend_execute_data *execute_data) {
55   const sp_config_eval *config_eval = SNUFFLEUPAGUS_G(config).config_eval;
56 
57   if (EXPECTED(0 == SNUFFLEUPAGUS_G(in_eval))) {
58     return;
59   }
60 
61   if (EXPECTED(NULL == SNUFFLEUPAGUS_G(config).config_eval->whitelist)) {
62     return;
63   }
64 
65   if (zend_is_executing() && !EG(current_execute_data)->func) {
66     return;  // LCOV_EXCL_LINE
67   }
68 
69   if (UNEXPECTED(!(execute_data->func->common.function_name))) {
70     return;
71   }
72 
73   zend_string const *const current_function = EX(func)->common.function_name;
74 
75   if (EXPECTED(NULL != current_function)) {
76     if (UNEXPECTED(false == check_is_in_eval_whitelist(current_function))) {
77       if (config_eval->dump) {
78         sp_log_request(config_eval->dump, config_eval->textual_representation,
79                        SP_TOKEN_EVAL_WHITELIST);
80       }
81       if (config_eval->simulation) {
82         sp_log_simulation(
83             "Eval_whitelist",
84             "The function '%s' isn't in the eval whitelist, logging its call.",
85             ZSTR_VAL(current_function));
86         return;
87       } else {
88         sp_log_drop(
89             "Eval_whitelist",
90             "The function '%s' isn't in the eval whitelist, dropping its call.",
91             ZSTR_VAL(current_function));
92       }
93     }
94   }
95 }
96 
97 /* This function gets the filename in which `eval()` is called from,
98  * since it looks like "foo.php(1) : eval()'d code", so we're starting
99  * from the end of the string until the second closing parenthesis. */
get_eval_filename(const char * const filename)100 zend_string *get_eval_filename(const char *const filename) {
101   int count = 0;
102   zend_string *clean_filename = zend_string_init(filename, strlen(filename), 0);
103 
104   for (int i = ZSTR_LEN(clean_filename); i >= 0; i--) {
105     if (ZSTR_VAL(clean_filename)[i] == '(') {
106       if (count == 1) {
107         ZSTR_VAL(clean_filename)[i] = '\0';
108         clean_filename = zend_string_truncate(clean_filename, i, 0);
109         break;
110       }
111       count++;
112     }
113   }
114   return clean_filename;
115 }
116 
sp_execute_ex(zend_execute_data * execute_data)117 static void sp_execute_ex(zend_execute_data *execute_data) {
118   is_in_eval_and_whitelisted(execute_data);
119   const HashTable *config_disabled_functions =
120       SNUFFLEUPAGUS_G(config).config_disabled_functions;
121 
122   if (!execute_data) {
123     return;  // LCOV_EXCL_LINE
124   }
125 
126   if (UNEXPECTED(EX(func)->op_array.type == ZEND_EVAL_CODE)) {
127     const sp_list_node *config = zend_hash_str_find_ptr(
128         config_disabled_functions, "eval", sizeof("eval") - 1);
129 
130     zend_string *filename = get_eval_filename(zend_get_executed_filename());
131     is_builtin_matching(filename, "eval", NULL, config,
132                         config_disabled_functions);
133     zend_string_release(filename);
134 
135     SNUFFLEUPAGUS_G(in_eval)++;
136     orig_execute_ex(execute_data);
137     SNUFFLEUPAGUS_G(in_eval)--;
138     return;
139   }
140 
141   if (NULL != EX(func)->op_array.filename) {
142     if (true == SNUFFLEUPAGUS_G(config).config_readonly_exec->enable) {
143       terminate_if_writable(ZSTR_VAL(EX(func)->op_array.filename));
144     }
145   }
146 
147   if (SNUFFLEUPAGUS_G(config).hook_execute) {
148     char *function_name = get_complete_function_path(execute_data);
149     zval ret_val;
150     const sp_list_node *config_disabled_functions_reg =
151         SNUFFLEUPAGUS_G(config)
152             .config_disabled_functions_reg->disabled_functions;
153 
154     if (!function_name) {
155       orig_execute_ex(execute_data);
156       return;
157     }
158 
159     // If we're at an internal function
160     if (!execute_data->prev_execute_data ||
161         !execute_data->prev_execute_data->func ||
162         !ZEND_USER_CODE(execute_data->prev_execute_data->func->type) ||
163         !execute_data->prev_execute_data->opline) {
164       should_disable_ht(execute_data, function_name, NULL, NULL,
165                         config_disabled_functions_reg,
166                         config_disabled_functions);
167     } else {  // If we're at a userland function call
168       switch (execute_data->prev_execute_data->opline->opcode) {
169         case ZEND_DO_FCALL:
170         case ZEND_DO_FCALL_BY_NAME:
171         case ZEND_DO_ICALL:
172         case ZEND_DO_UCALL:
173           should_disable_ht(execute_data, function_name, NULL, NULL,
174                             config_disabled_functions_reg,
175                             config_disabled_functions);
176         default:
177           break;
178       }
179     }
180 
181     // When a function's return value isn't used, php doesn't store it in the
182     // execute_data, so we need to use a local variable to be able to match on
183     // it later.
184     if (EX(return_value) == NULL) {
185       memset(&ret_val, 0, sizeof(ret_val));
186       EX(return_value) = &ret_val;
187     }
188 
189     orig_execute_ex(execute_data);
190 
191     should_drop_on_ret_ht(
192         EX(return_value), function_name,
193         SNUFFLEUPAGUS_G(config)
194             .config_disabled_functions_reg_ret->disabled_functions,
195         SNUFFLEUPAGUS_G(config).config_disabled_functions_ret, execute_data);
196     efree(function_name);
197 
198     if (EX(return_value) == &ret_val) {
199       EX(return_value) = NULL;
200     }
201   } else {
202     orig_execute_ex(execute_data);
203   }
204 }
205 
sp_zend_execute_internal(INTERNAL_FUNCTION_PARAMETERS)206 static void sp_zend_execute_internal(INTERNAL_FUNCTION_PARAMETERS) {
207   is_in_eval_and_whitelisted(execute_data);
208 
209   if (UNEXPECTED(NULL != orig_zend_execute_internal)) {
210     // LCOV_EXCL_START
211     orig_zend_execute_internal(INTERNAL_FUNCTION_PARAM_PASSTHRU);
212     // LCOV_EXCL_STOP
213   } else {
214     EX(func)->internal_function.handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
215   }
216 }
217 
sp_stream_open(const char * filename,zend_file_handle * handle)218 static int sp_stream_open(const char *filename, zend_file_handle *handle) {
219   zend_execute_data const *const data = EG(current_execute_data);
220 
221   if ((NULL == data) || (NULL == data->opline) ||
222       (data->func->type != ZEND_USER_FUNCTION)) {
223     goto end;
224   }
225 
226   zend_string *zend_filename = zend_string_init(filename, strlen(filename), 0);
227   const HashTable *disabled_functions_hooked =
228       SNUFFLEUPAGUS_G(config).config_disabled_functions_hooked;
229 
230   switch (data->opline->opcode) {
231     case ZEND_INCLUDE_OR_EVAL:
232       if (true == SNUFFLEUPAGUS_G(config).config_readonly_exec->enable) {
233         terminate_if_writable(filename);
234       }
235       switch (data->opline->extended_value) {
236         case ZEND_INCLUDE:
237           is_builtin_matching(
238               zend_filename, "include", "inclusion path",
239               zend_hash_str_find_ptr(disabled_functions_hooked, "include",
240                                      sizeof("include") - 1),
241               disabled_functions_hooked);
242           break;
243         case ZEND_REQUIRE:
244           is_builtin_matching(
245               zend_filename, "require", "inclusion path",
246               zend_hash_str_find_ptr(disabled_functions_hooked, "require",
247                                      sizeof("require") - 1),
248               disabled_functions_hooked);
249           break;
250         case ZEND_REQUIRE_ONCE:
251           is_builtin_matching(
252               zend_filename, "require_once", "inclusion path",
253               zend_hash_str_find_ptr(disabled_functions_hooked, "require_once",
254                                      sizeof("require_once") - 1),
255               disabled_functions_hooked);
256           break;
257         case ZEND_INCLUDE_ONCE:
258           is_builtin_matching(
259               zend_filename, "include_once", "inclusion path",
260               zend_hash_str_find_ptr(disabled_functions_hooked, "include_once",
261                                      sizeof("include_once") - 1),
262               disabled_functions_hooked);
263           break;
264           EMPTY_SWITCH_DEFAULT_CASE();  // LCOV_EXCL_LINE
265       }
266   }
267   efree(zend_filename);
268 
269 end:
270   return orig_zend_stream_open(filename, handle);
271 }
272 
hook_execute(void)273 int hook_execute(void) {
274   TSRMLS_FETCH();
275 
276   if (NULL == orig_execute_ex && NULL == orig_zend_stream_open) {
277     /* zend_execute_ex is used for "user" function calls */
278     orig_execute_ex = zend_execute_ex;
279     zend_execute_ex = sp_execute_ex;
280 
281     /* zend_execute_internal is used for "builtin" functions calls */
282     orig_zend_execute_internal = zend_execute_internal;
283     zend_execute_internal = sp_zend_execute_internal;
284 
285     /* zend_stream_open_function is used for include-related stuff */
286     orig_zend_stream_open = zend_stream_open_function;
287     zend_stream_open_function = sp_stream_open;
288   }
289 
290   return SUCCESS;
291 }
292