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