├── .gitignore ├── LICENSE ├── README.md ├── examples ├── example1.php ├── example2.php └── example3.php ├── php_loader ├── phpscan.php └── zend_opcodes.php ├── phpscan.py ├── phpscan ├── __init__.py ├── core.py ├── opcode.py ├── resolver.py ├── satisfier │ ├── __init__.py │ ├── greedy.py │ └── satisfier.py └── solver.py ├── tests └── greedy_satisfier │ └── test_satisfier.py └── zend_extension ├── config.m4 ├── php_phpscan.h └── phpscan.c /.gitignore: -------------------------------------------------------------------------------- 1 | .pylintrc 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 bartvanarnhem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPScan 2 | PHPScan is a symbolic execution inspired PHP application scanner for code-path discovery. Using a custom extension, it hooks directly into the Zend Engine to track variables and opcodes (assignments, comparisons, etc). Using this information it tries to satisfy as many branch conditions as possible. The goal is to discover valid input (in terms of *$_GET*, *$_POST* and *$_COOKIE* variables and values) that results in reaching a specfic location within the application or to maximize code coverage. 3 | 4 | **Author** 5 | Bart van Arnhem (bartvanarnhem@gmail.com). 6 | 7 | ## Disclaimer 8 | PHPScan is very much a work-in-progress and is not yet in a state to be used on larger real-world applications. If you have any suggestions or feedback please let me know. 9 | 10 | ## Usage 11 | 12 | ``` 13 | $ python phpscan.py --help 14 | usage: phpscan.py [-h] [-v {0,1,2}] entrypoint 15 | 16 | positional arguments: 17 | entrypoint the PHP application entrypoint 18 | 19 | optional arguments: 20 | -h, --help show this help message and exit 21 | -v {0,1,2}, --verbose {0,1,2} 22 | increase verbosity (0 = results only, 1 = show 23 | progress, 2 = debug 24 | ``` 25 | 26 | **example** examples/example3.php 27 | ```php 28 | 10)) 37 | { 38 | phpscan_flag('reached_greater_than'); 39 | } 40 | } 41 | ?> 42 | ``` 43 | 44 | ``` 45 | $ python phpscan.py examples/example3.php 46 | Scanning of examples/example3.php finished... 47 | - Needed 5 runs 48 | - Took 0.234709 seconds 49 | 50 | Successfully reached "reached_home" using input: 51 | _POST 52 | _GET 53 | num 54 | value: 11 (integer) 55 | page 56 | value: home (string) 57 | _COOKIE 58 | _REQUEST 59 | 60 | Successfully reached "reached_greater_than" using input: 61 | _POST 62 | _GET 63 | num 64 | value: 11 (integer) 65 | page 66 | value: home (string) 67 | _COOKIE 68 | _REQUEST 69 | ``` 70 | 71 | ## Installation 72 | ### Install dependencies 73 | #### PHP7 74 | ``` 75 | $ sudo apt-get install php7.1-cli 76 | ``` 77 | #### Runkit7 78 | Build the unofficial PHP7 runkit fork according to the instructions at https://github.com/TysonAndre/runkit7#building-and-installing-runkit7-in-unix 79 | 80 | To enable Runkit7 and allow overriding of internal PHP functions add the following lines to your php.ini configuration file: 81 | 82 | ``` 83 | extension=runkit.so 84 | runkit.internal_override=On 85 | ``` 86 | 87 | #### Z3 88 | Build Z3 and the Z3 Python bindings as per instructions on https://github.com/Z3Prover/z3. Make sure the <z3-builddir>/python path is added to your PYTHONPATH enivoronment variable: 89 | 90 | ``` 91 | $ export PYTHONPATH="/python:${PYTHONPATH}" 92 | ``` 93 | 94 | 95 | ### Download latest PHPScan 96 | ``` 97 | $ git clone https://github.com/bartvanarnhem/phpscan.git 98 | ``` 99 | 100 | ### Build the Zend Extension 101 | 102 | ``` 103 | $ cd phpscan/zend_extension 104 | $ phpize 105 | $ ./configure 106 | $ make 107 | $ sudo make install 108 | ``` 109 | 110 | To enable the PHPScan Zend Extension add the following lines to your php.ini configuration file: 111 | ``` 112 | extension=phpscan.so 113 | ``` 114 | 115 | 116 | -------------------------------------------------------------------------------- /examples/example1.php: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /examples/example2.php: -------------------------------------------------------------------------------- 1 | 100) 4 | { 5 | phpscan_flag('greater_int'); 6 | } 7 | 8 | ?> 9 | -------------------------------------------------------------------------------- /examples/example3.php: -------------------------------------------------------------------------------- 1 | 10)) 11 | { 12 | phpscan_flag('reached_greater_than'); 13 | } 14 | } 15 | 16 | ?> 17 | -------------------------------------------------------------------------------- /php_loader/phpscan.php: -------------------------------------------------------------------------------- 1 | 0) && phpscan_is_tracking($op2_zval_id) && ($op1_zval_id !== $op2_zval_id)) 47 | { 48 | $__phpscan_variable_map[$op1_zval_id] = 'assign(' . $op2_id . ':' . __phpscan_replace_uniqid('', true) . ')'; 49 | // TODO do we really need to reassign here? 50 | $op1_id = phpscan_lookup_zval_by_id($op1_zval_id); 51 | 52 | phpscan_register_transform($op1_id, 53 | 'assign', 54 | array($op2_id), 55 | array( 56 | array('type' => 'symbolic', 'id' => $op2_id, 'value' => $op2) 57 | )); 58 | } 59 | } 60 | else if (($opcode === ZendOpcodes::ZEND_ADD) || ($opcode === ZendOpcodes::ZEND_CONCAT)) 61 | { 62 | if (phpscan_is_tracking($op1_zval_id) || phpscan_is_tracking($op2_zval_id)) 63 | { 64 | $result_zval_id = phpscan_ext_get_zval_id($result); 65 | 66 | $functions = array(1 => 'add', 8 => 'concat'); 67 | 68 | $func_name = $functions[$opcode]; 69 | 70 | $args = array(); 71 | if (phpscan_is_tracking($op1_zval_id)) 72 | $args[] = array('type' => 'symbolic', 'id' => $op1_id, 'value' => $op1); 73 | else 74 | $args[] = array('type' => 'raw_value', 'value' => $op1); 75 | 76 | if (phpscan_is_tracking($op2_zval_id)) 77 | $args[] = array('type' => 'symbolic', 'id' => $op2_id, 'value' => $op2); 78 | else 79 | $args[] = array('type' => 'raw_value', 'value' => $op2); 80 | 81 | $symbolic_args = array(); 82 | foreach ($args as $arg) { 83 | if ($arg['type'] === 'symbolic') 84 | $symbolic_args[] = $arg['id']; 85 | } 86 | 87 | $__phpscan_variable_map[$result_zval_id] = $func_name . '(' . implode(';', $symbolic_args) . ';' . __phpscan_replace_uniqid() . ')'; 88 | print 'RESULT_ZVAL_ID = ' . $result_zval_id . "\n"; 89 | $result_id = phpscan_lookup_zval_by_id($result_zval_id); 90 | 91 | 92 | phpscan_register_transform($result_id, 93 | $func_name, 94 | array($op1_id, $op2_id), 95 | $args); 96 | } 97 | 98 | } 99 | else 100 | { 101 | $result_zval_id = phpscan_ext_get_zval_id($result); 102 | 103 | $op = array( 104 | 'opcode' => $opcode, 105 | 106 | 'op1_value' => $op1, 107 | 'op1_id' => $op1_id, 108 | 'op1_type' => $op1_type, 109 | 'op1_data_type' => __phpscan_replace_gettype($op1), 110 | 111 | 'op2_value' => $op2, 112 | 'op2_id' => $op2_id, 113 | 'op2_type' => $op2_type, 114 | 'op2_data_type' => __phpscan_replace_gettype($op2) 115 | ); 116 | phpscan_log_op($op); 117 | 118 | if ($opcode == ZendOpcodes::ZEND_FETCH_DIM_R) 119 | { 120 | // if (!__phpscan_replace_array_key_exists($res_zval_id, $__phpscan_variable_map)) 121 | if (phpscan_is_tracking($op1_zval_id)) 122 | { 123 | if (!__phpscan_replace_array_key_exists($op2, $op1) || !__phpscan_replace_array_key_exists($result_zval_id, $__phpscan_variable_map)) 124 | $__phpscan_variable_map[$result_zval_id] = 'fetch_dim_r(' . $op1_id . ':' . $op2 . ')'; 125 | 126 | $res_id = phpscan_lookup_zval_by_id($result_zval_id); 127 | 128 | phpscan_register_transform($res_id, 129 | 'fetch_dim_r', 130 | array($op1_id), 131 | array( 132 | array('type' => 'symbolic', 'id' => $op1_id, 'value' => $op1), 133 | array('type' => 'raw_value', 'value' => $op2) 134 | )); 135 | 136 | } 137 | } 138 | } 139 | 140 | $__phpscan_op_ignore = false; 141 | phpscan_ext_ignore_op_off(); 142 | } 143 | } 144 | 145 | function phpscan_log_op($op) 146 | { 147 | global $__phpscan_op; 148 | $__phpscan_op[] = $op; 149 | } 150 | 151 | function phpscan_initialize($state) 152 | { 153 | phpscan_ext_ignore_op(); 154 | phpscan_replace_internals(); 155 | phpscan_initialize_environment($state); 156 | phpscan_ext_ignore_op_off(); 157 | } 158 | 159 | function phpscan_initialize_environment($state) 160 | { 161 | global $__phpscan_init_complete; 162 | 163 | $state_decoded = json_decode($state); 164 | var_dump('SETTING STATE', $state_decoded); 165 | 166 | phpscan_initialize_variables($state_decoded); 167 | phpscan_initialize_register_zvals($state_decoded); 168 | 169 | $__phpscan_init_complete = true; 170 | } 171 | 172 | function phpscan_initialize_variables($state_decoded) 173 | { 174 | foreach ($state_decoded as $var_name => $var_info) 175 | { 176 | $var = phpscan_initialize_variable($var_name, $var_info); 177 | phpscan_set_toplevel_variable($var_name, $var); 178 | } 179 | } 180 | 181 | function phpscan_initialize_register_zvals($state_decoded) 182 | { 183 | foreach ($state_decoded as $var_name => $var_info) 184 | { 185 | global ${$var_name}; 186 | phpscan_register_zval(${$var_name}, $var_info); 187 | phpscan_initialize_register_zvals_rec(${$var_name}, $var_info); 188 | } 189 | } 190 | 191 | function phpscan_initialize_register_zvals_rec(&$var, $var_info) 192 | { 193 | if (($var_info->type == 'array') and (__phpscan_replace_array_key_exists('properties', $var_info))) 194 | { 195 | foreach ($var_info->properties as $prop_name => $prop_info) 196 | { 197 | phpscan_register_zval($var[$prop_name], $prop_info); 198 | phpscan_initialize_register_zvals_rec($var[$prop_name], $prop_info); 199 | } 200 | } 201 | } 202 | 203 | function phpscan_set_toplevel_variable($var_name, $var) 204 | { 205 | global ${$var_name}; 206 | ${$var_name} = $var; 207 | } 208 | 209 | 210 | function phpscan_register_zval(&$var, $var_info) 211 | { 212 | global $__phpscan_variable_map; 213 | $var_zval_id = phpscan_var_id($var); 214 | $__phpscan_variable_map[$var_zval_id] = $var_info->id; 215 | } 216 | 217 | function phpscan_is_tracking($var_zval_id) 218 | { 219 | global $__phpscan_variable_map; 220 | return __phpscan_replace_array_key_exists($var_zval_id, $__phpscan_variable_map); 221 | } 222 | 223 | function phpscan_lookup_zval($var) 224 | { 225 | return phpscan_lookup_zval_by_id(phpscan_var_id($var)); 226 | } 227 | 228 | function phpscan_lookup_zval_by_id($var_zval_id) 229 | { 230 | global $__phpscan_variable_map; 231 | global $__phpscan_op_ignore; 232 | 233 | $__phpscan_op_ignore = true; 234 | $var_id = 'untracked (zval_id=' . $var_zval_id .')'; 235 | if (phpscan_is_tracking($var_zval_id)) 236 | { 237 | $var_id = $__phpscan_variable_map[$var_zval_id]; 238 | } 239 | $__phpscan_op_ignore = false; 240 | 241 | return $var_id; 242 | } 243 | 244 | function phpscan_initialize_variable($var_name, $var_info) 245 | { 246 | $var = null; 247 | 248 | switch ($var_info->type) 249 | { 250 | case 'array': 251 | $var = array(); 252 | break; 253 | case 'string': 254 | $var = $var_info->value; 255 | break; 256 | case 'integer': 257 | $var = $var_info->value; 258 | break; 259 | case 'double': 260 | $var = $var_info->value; 261 | break; 262 | case 'unknown': 263 | // Type can be unknown if newly discovered and we did not get a typehint yet 264 | $var = $var_info->value; 265 | break; 266 | default: 267 | } 268 | 269 | $var = phpscan_separate_zval($var); 270 | 271 | if (($var_info->type == 'array') and (__phpscan_replace_array_key_exists('properties', $var_info))) 272 | { 273 | foreach ($var_info->properties as $prop_name => $prop_info) 274 | { 275 | $var[$prop_name] = phpscan_initialize_variable($prop_name, $prop_info); 276 | } 277 | } 278 | 279 | return $var; 280 | } 281 | 282 | function phpscan_separate_zval($var) 283 | { 284 | $separated_var = $var; 285 | if (is_array($separated_var)) 286 | { 287 | $separated_var['__aa__'] = rand(); 288 | unset($separated_var['__aa__']); 289 | } 290 | else if (is_string($separated_var)) { 291 | $separated_var .= ''; 292 | } 293 | else if (is_integer($separated_var) || is_double($separated_var)) { 294 | $separated_var *= 1; 295 | } 296 | else { 297 | throw new Exception('Unable to separate zval for unknown type ' . gettype($separated_var)); 298 | } 299 | 300 | return $separated_var; 301 | } 302 | 303 | function phpscan_handle_shutdown() 304 | { 305 | phpscan_ext_ignore_op(); 306 | phpscan_report_opcodes(); 307 | phpscan_report_transform(); 308 | phpscan_ext_ignore_op_off(); 309 | } 310 | 311 | function phpscan_flag($flag = 'default') 312 | { 313 | print '__PHPSCAN_FLAG__' . $flag . '__/PHPSCAN_FLAG__'; 314 | } 315 | 316 | function phpscan_report_opcodes() 317 | { 318 | global $__phpscan_op; 319 | $json = __phpscan_replace_json_encode($__phpscan_op); 320 | 321 | print '__PHPSCAN_OPS__' . $json . '__/PHPSCAN_OPS__'; 322 | 323 | global $__phpscan_variable_map; 324 | var_dump('MAP', $__phpscan_variable_map); 325 | 326 | global $__phpscan_var_id_lookup; 327 | var_dump('LOOKUP', $__phpscan_var_id_lookup); 328 | } 329 | 330 | function phpscan_report_transform() 331 | { 332 | global $__phpscan_transform; 333 | $json = __phpscan_replace_json_encode($__phpscan_transform); 334 | print '__PHPSCAN_TRANSFORMS__' . $json . '__/PHPSCAN_TRANSFORMS__'; 335 | } 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | function __phpscan_taint_hook($func, $args) 346 | { 347 | global $__phpscan_variable_map; 348 | global $__phpscan_op_ignore; 349 | global $__phpscan_transform; 350 | global $__phpscan_taint_hook; 351 | 352 | $__phpscan_op_ignore = true; 353 | 354 | $var_zval_id = null; 355 | $taint = array(); 356 | $args_symbolic = array(); 357 | for ($i = 0; $i < __phpscan_replace_count($args); ++$i) 358 | { 359 | $var = $args[$i]; 360 | $var_zval_id = phpscan_var_id($var); 361 | if (__phpscan_replace_array_key_exists($var_zval_id, $__phpscan_variable_map)) 362 | { 363 | $taint[] = $__phpscan_variable_map[$var_zval_id]; 364 | $args_symbolic[] = array( 365 | 'type' => 'symbolic', 366 | 'id' => $__phpscan_variable_map[$var_zval_id], 367 | 'value' => $var 368 | ); 369 | } 370 | else 371 | { 372 | $args_symbolic[] = array( 373 | 'type' => 'raw_value', 374 | 'value' => $var 375 | ); 376 | } 377 | } 378 | 379 | // TODO handle cases where multiple inputs are being tracked 380 | 381 | $res = __phpscan_replace_call_user_func_array($func, $args); 382 | 383 | $short_func = __phpscan_replace_str_replace('__phpscan_replace_', '', $func); 384 | if (__phpscan_replace_array_key_exists($short_func, $__phpscan_taint_hook)) 385 | { 386 | $res = __phpscan_replace_call_user_func($__phpscan_taint_hook[$short_func], $res); 387 | } 388 | 389 | if (__phpscan_replace_count($taint) > 0) 390 | { 391 | $res_zval_id = phpscan_var_id($res); 392 | $__phpscan_variable_map[$res_zval_id] = $short_func . '(' . __phpscan_replace_implode(',', $taint) . ':' . __phpscan_replace_uniqid() . ')'; 393 | 394 | phpscan_register_transform($__phpscan_variable_map[$res_zval_id], 395 | $short_func, 396 | $taint, 397 | $args_symbolic); 398 | } 399 | 400 | $__phpscan_op_ignore = false; 401 | 402 | return $res; 403 | } 404 | 405 | function phpscan_register_transform($id, $func, $ids, $args) 406 | { 407 | global $__phpscan_transform; 408 | $__phpscan_transform[$id] = array( 409 | 'function' => $func, 410 | 'ids' => $ids, 411 | 'args' => $args 412 | ); 413 | } 414 | 415 | 416 | function phpscan_replace_internals() 417 | { 418 | $hook_functions = get_defined_functions()['internal']; 419 | $hook_functions = array_filter($hook_functions, function ($f) 420 | { 421 | $r = !preg_match('/^(.*runkit.*|dl|var_dump|printf|.*phpscan.*)$/', $f); 422 | 423 | return $r; 424 | }); 425 | 426 | foreach ($hook_functions as $f) 427 | { 428 | // printf("Replacing %s ...\n", $f); 429 | runkit_function_rename($f, '__phpscan_replace_' . $f); 430 | runkit_function_add($f, '', 'return __phpscan_taint_hook("__phpscan_replace_' . $f . '", __phpscan_replace_func_get_args());'); 431 | } 432 | } 433 | 434 | 435 | 436 | function phpscan_explode_taint_hook($res) // TODO should res be a reference? 437 | { 438 | // Fixes testcase test_explode 439 | // If explode is called on a string that does not contain the split string $res[0] will point to the 440 | // same zval as the original string. Explictly separate them such that we can track $res[0] separately. 441 | if (__phpscan_replace_count($res) == 1) 442 | { 443 | $res[0] = phpscan_separate_zval($res[0]); 444 | } 445 | 446 | return $res; 447 | } 448 | 449 | $__phpscan_taint_hook['explode'] = 'phpscan_explode_taint_hook'; 450 | 451 | ?> -------------------------------------------------------------------------------- /php_loader/zend_opcodes.php: -------------------------------------------------------------------------------- 1 | 194 | -------------------------------------------------------------------------------- /phpscan.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from phpscan.core import Scan, logger, verify_dependencies 3 | from phpscan.satisfier.greedy import GreedySatisfier 4 | 5 | def parse_arguments(): 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument( 8 | '-v', '--verbose', help='increase verbosity (0 = results only, 1 = show progress, 2 = debug', type=int, choices=[0, 1, 2], default=0) 9 | parser.add_argument( 10 | 'entrypoint', help='the PHP application entrypoint', default='index.php') 11 | 12 | args = parser.parse_args() 13 | return args 14 | 15 | if __name__ == "__main__": 16 | args = parse_arguments() 17 | 18 | if verify_dependencies(): 19 | logger.verbosity = args.verbose 20 | 21 | scan = Scan(args.entrypoint) 22 | 23 | scan.satisfier = GreedySatisfier() 24 | 25 | scan.start() 26 | 27 | scan.print_results() 28 | -------------------------------------------------------------------------------- /phpscan/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartvanarnhem/phpscan/51f9b411c145e36e203487e2a8e5c06dc72647f6/phpscan/__init__.py -------------------------------------------------------------------------------- /phpscan/core.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import uuid 3 | import collections 4 | import subprocess 5 | import json 6 | import re 7 | import os 8 | from opcode import Operand 9 | from timeit import default_timer as timer 10 | 11 | INITIAL_STATE = { 12 | '_POST': { 13 | 'type': 'array' 14 | }, 15 | '_REQUEST': { 16 | 'type': 'array' 17 | }, 18 | '_GET': { 19 | 'type': 'array' 20 | }, 21 | '_COOKIE': { 22 | 'type': 'array' 23 | }, 24 | '_FILES': { 25 | 'type': 'array' 26 | } 27 | } 28 | 29 | PHP_LOADER = '%s/../php_loader/phpscan.php' % os.path.dirname(__file__) 30 | TMP_PHPSCRIPT_PATH = '/tmp/phpscan_%s.py' 31 | 32 | 33 | def verify_dependencies(): 34 | verify_ok = True 35 | 36 | if not verify_zend_extension_enabled(): 37 | print 'FATAL: PHPSCan Zend module is not properly installed' 38 | verify_ok = False 39 | 40 | return verify_ok 41 | 42 | def verify_zend_extension_enabled(): 43 | proc = subprocess.Popen(['php', '-r', 'print phpscan_enabled();'], 44 | stdout=subprocess.PIPE, 45 | stderr=subprocess.PIPE) 46 | 47 | output_stdout = proc.stdout.read() 48 | output_stderr = proc.stderr.read() 49 | 50 | return not 'Call to undefined function phpscan_enabled' in output_stdout + output_stderr 51 | 52 | class Logger(object): 53 | STANDARD = 0 54 | PROGRESS = 1 55 | DEBUG = 2 56 | 57 | def __init__(self, verbosity=STANDARD): 58 | self._verbosity = verbosity 59 | 60 | @property 61 | def verbosity(self): 62 | return self._verbosity 63 | 64 | @verbosity.setter 65 | def verbosity(self, value): 66 | self._verbosity = value 67 | 68 | def log(self, message, min_level=0): 69 | self.log('', message, min_level) 70 | 71 | def log(self, section, message, min_level=0): 72 | if self.verbosity >= min_level: 73 | if section: 74 | print section 75 | if message: 76 | print message 77 | print '' 78 | 79 | logger = Logger() 80 | 81 | 82 | class State(object): 83 | 84 | def __init__(self, state): 85 | self._state = state 86 | self._state_annotated = None 87 | self._lookup_map = None 88 | self._conditions = [] 89 | 90 | self.annotate() 91 | 92 | @property 93 | def state(self): 94 | return self._state 95 | 96 | @state.setter 97 | def state(self, value): 98 | self._state = value 99 | 100 | @property 101 | def state_annotated(self): 102 | return self._state_annotated 103 | 104 | @property 105 | def conditions(self): 106 | return self._conditions 107 | 108 | @conditions.setter 109 | def conditions(self, value): 110 | self._conditions = value 111 | 112 | 113 | @property 114 | def hash(self): 115 | 116 | hash_state = copy.deepcopy(self.state) 117 | hash_conditions = copy.deepcopy(self.conditions) 118 | 119 | def clean_state(items): 120 | for key in items.keys(): 121 | if 'value' in items[key]: 122 | del items[key]['value'] 123 | if 'properties' in items[key]: 124 | clean_state(items[key]['properties']) 125 | 126 | clean_state(hash_state) 127 | 128 | def clean_conditions(conditions): 129 | for i, condition in enumerate(conditions): 130 | if not '_cleaned' in condition: 131 | if condition['type'] == 'base_var': 132 | annotated_var = self.get_annotated_var_ref(condition['id']) 133 | condition['id'] = annotated_var['persistent_id'] 134 | if 'args' in condition: 135 | clean_conditions(condition['args']) 136 | 137 | condition['_cleaned'] = True 138 | 139 | clean_conditions(hash_conditions) 140 | 141 | return make_hash({ 142 | 'state': hash_state, 143 | 'conditions': hash_conditions 144 | }) 145 | 146 | def fork(self): 147 | state_copy = copy.deepcopy(self.state) 148 | return State(state_copy) 149 | 150 | def is_tracking(self, var_id): 151 | return var_id in self._lookup_map 152 | 153 | def get_var_ref(self, var_id): 154 | return self._lookup_map[var_id] 155 | 156 | def get_annotated_var_ref(self, var_id): 157 | return self._annotated_lookup_map[var_id] 158 | 159 | def update_guessed_type_from_value(self, var_id, value): 160 | if self.is_tracking(var_id): 161 | if isinstance(value, int): 162 | self.update_guessed_type(var_id, 'integer') 163 | else: 164 | logger.log('Warning: ignoring guessed type for untracked variable %s' % var_id, Logger.DEBUG) 165 | 166 | def update_guessed_type(self, var_id, typehint): 167 | if self.is_tracking(var_id): 168 | var = self.get_var_ref(var_id) 169 | 170 | if var['type'] == 'unknown' and typehint != var['type']: 171 | if typehint == 'integer': 172 | try: 173 | orig_val = var['value'] 174 | 175 | if (isinstance(orig_val, str) or isinstance(orig_val, unicode)) and len(orig_val.strip()) == 0: 176 | orig_val = 0 177 | 178 | int_val = int(orig_val) 179 | var['value'] = int_val 180 | var['type'] = 'integer' 181 | except ValueError: 182 | logger.log('Got op type hint for integer but could not cast \'%s\' to int.' % 183 | var['value'], Logger.DEBUG) 184 | else: 185 | logger.log('Warning: ignoring guessed type for variable %s, cannot handle typehint %s' % (var_id, typehint), Logger.DEBUG) 186 | else: 187 | logger.log('Warning: ignoring guessed type for untracked variable %s' % var_id, Logger.DEBUG) 188 | 189 | def annotate(self): 190 | self._lookup_map = dict() 191 | self._annotated_lookup_map = dict() 192 | 193 | self._state_annotated = copy.deepcopy(self.state) 194 | # self._state_annotated = self.state 195 | 196 | self.annotate_recurse(self._state_annotated, self.state) 197 | 198 | def annotate_recurse(self, state_item_copy, state_item, parent_hierarchy=[]): 199 | for key in state_item_copy.keys(): 200 | 201 | if 'id' in state_item_copy[key]: 202 | unique_id = state_item_copy[key]['id'] 203 | else: 204 | unique_id = str(uuid.uuid4()) 205 | state_item_copy[key]['id'] = unique_id 206 | state_item_copy[key]['persistent_id'] = ':'.join(parent_hierarchy + [key]) 207 | 208 | self._lookup_map[unique_id] = state_item[key] 209 | self._annotated_lookup_map[unique_id] = state_item_copy[key] 210 | 211 | if 'properties' in state_item_copy[key]: 212 | self.annotate_recurse( 213 | state_item_copy[key]['properties'], state_item[key]['properties'], 214 | parent_hierarchy + [key]) 215 | 216 | def pretty_print(self): 217 | return self.pretty_print_recurse(self._state_annotated) 218 | 219 | def pretty_print_recurse(self, state_item, level=0): 220 | output = '' 221 | 222 | for var_name, var_info in state_item.iteritems(): 223 | output += (' ' * 2 * level) + var_name + '\n' 224 | if 'properties' in var_info: 225 | output += self.pretty_print_recurse( 226 | var_info['properties'], level + 1) 227 | if 'value' in var_info: 228 | indent = ' ' * 2 * (level + 1) 229 | output += '%svalue: %s (%s, %s)\n' % (indent, 230 | var_info['value'], 231 | var_info['type'], 232 | var_info['id']) 233 | 234 | return output 235 | 236 | class Scan(object): 237 | INPUT_MODE_FILE = 1 << 0 238 | INPUT_MODE_SCRIPT = 1 << 1 239 | 240 | def __init__(self, php_file_or_script, input_mode=INPUT_MODE_FILE): 241 | self._php_file_or_script = php_file_or_script 242 | self._input_mode = input_mode 243 | self._seen = set() 244 | self._queue = collections.deque() 245 | self._reached_cases = [] 246 | self._duration = -1 247 | self._num_runs = -1 248 | 249 | 250 | self._initial_state = INITIAL_STATE 251 | self._php_loader_location = PHP_LOADER 252 | 253 | # Looks like Zend's opcode handlers are not triggered for PHP code we directly execute using -r from the CLI. 254 | # Therefore, if in INPUT_MODE_SCRIPT, wrap the passed PHP code in a temporary file and include this instead. 255 | # TODO: find out if we can remove this step 256 | self.init_tmp_script() 257 | 258 | def __del__(self): 259 | self.cleanup_tmp_script() 260 | 261 | 262 | @property 263 | def php_file(self): 264 | return self._php_file_or_script 265 | 266 | @property 267 | def initial_state(self): 268 | return self._initial_state 269 | 270 | @initial_state.setter 271 | def initial_state(self, value): 272 | self._initial_state = value 273 | 274 | @property 275 | def num_runs(self): 276 | return self._num_runs 277 | 278 | @property 279 | def satisfier(self): 280 | return self._satisfier 281 | 282 | @satisfier.setter 283 | def satisfier(self, value): 284 | self._satisfier = value 285 | 286 | @property 287 | def php_loader_location(self): 288 | return self._php_loader_location 289 | 290 | @php_loader_location.setter 291 | def php_loader_location(self, value): 292 | self._php_loader_location = value 293 | 294 | def start(self): 295 | self._queue.append(State(self.initial_state)) 296 | 297 | start = timer() 298 | self._num_runs = 0 299 | 300 | while len(self._queue) > 0: 301 | state = self._queue.popleft() 302 | 303 | if not self.is_state_seen(state): 304 | self.mark_state_seen(state) 305 | 306 | self.satisfier.start_state = state.fork() 307 | 308 | php_recorded_ops, php_recorded_transforms = self.process_state( 309 | self.satisfier.start_state) 310 | sanitized_ops = self.sanitize_ops(php_recorded_ops) 311 | 312 | for new_state in self.satisfier.process(sanitized_ops, php_recorded_transforms): 313 | self._queue.append(new_state) 314 | 315 | self._num_runs += 1 316 | 317 | end = timer() 318 | self._duration = end - start 319 | self.done() 320 | 321 | def done(self): 322 | pass 323 | 324 | def init_tmp_script(self): 325 | if self._input_mode == Scan.INPUT_MODE_SCRIPT: 326 | self._tmp_php_script = TMP_PHPSCRIPT_PATH % uuid.uuid4() 327 | 328 | with open(self._tmp_php_script, 'w') as tmp_handle: 329 | tmp_handle.write('' % self._php_file_or_script) 330 | 331 | def cleanup_tmp_script(self): 332 | if self._input_mode == Scan.INPUT_MODE_SCRIPT and os.path.exists(self._tmp_php_script): 333 | os.remove(self._tmp_php_script) 334 | 335 | def is_state_seen(self, state): 336 | return state.hash in self._seen 337 | 338 | def mark_state_seen(self, state): 339 | self._seen.add(state.hash) 340 | 341 | def process_state(self, state): 342 | logger.log( 343 | 'Running with new input', state.pretty_print(), Logger.PROGRESS) 344 | 345 | ops, transforms = self.invoke_php(state, self._php_file_or_script) 346 | 347 | logger.log('PHP OPs', json.dumps(ops, indent=4), Logger.PROGRESS) 348 | logger.log('PHP TRANSFORMs', json.dumps(transforms, indent=4), Logger.PROGRESS) 349 | 350 | return (ops, transforms) 351 | 352 | def sanitize_ops(self, ops): 353 | sanitized_ops = [] 354 | 355 | for operator in ops: 356 | op1 = Operand( 357 | operator['op1_id'], operator['op1_type'], operator['op1_data_type'], 358 | operator['op1_value']) 359 | op2 = Operand( 360 | operator['op2_id'], operator['op2_type'], operator['op2_data_type'], 361 | operator['op2_value']) 362 | 363 | sanitized_ops.append({ 364 | 'opcode': int(operator['opcode']), 365 | 'op1': op1, 366 | 'op2': op2 367 | }) 368 | 369 | return sanitized_ops 370 | 371 | def generate_php_initializer_code(self, state_json, php_file_or_script): 372 | 373 | state_json = state_json.replace('"', '\\"') 374 | 375 | php_file = php_file_or_script 376 | if self._input_mode == Scan.INPUT_MODE_SCRIPT: 377 | php_file = self._tmp_php_script 378 | 379 | code = '"include \\"%s\\"; phpscan_initialize(\'%s\'); include \\"%s\\";"' % ( 380 | self.php_loader_location, state_json, php_file) 381 | 382 | return code 383 | 384 | def filter_php_response(self, category, output): 385 | result = None 386 | matches = re.findall( 387 | r'__PHPSCAN_%s__(.*?)__/PHPSCAN_%s__' % (category, category), output) 388 | 389 | return matches 390 | 391 | def invoke_php(self, state, php_file_or_script): 392 | state_json = json.dumps(state.state_annotated) 393 | 394 | code = self.generate_php_initializer_code(state_json, php_file_or_script) 395 | 396 | logger.log('Invoking PHP with', code, Logger.DEBUG) 397 | 398 | proc = subprocess.Popen(' '.join( 399 | ['php', '-r', code]), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 400 | 401 | output_stdout = proc.stdout.read() 402 | output_stderr = proc.stderr.read() 403 | 404 | logger.log('PHP stdout', output_stdout, Logger.DEBUG) 405 | logger.log('PHP stderr', output_stderr, Logger.DEBUG) 406 | 407 | self.check_reached_cases(output_stdout, state) 408 | 409 | opcodes = self.filter_hit_ops(output_stdout) 410 | transforms = self.filter_transforms(output_stdout) 411 | 412 | return (opcodes, transforms) 413 | 414 | def check_reached_cases(self, output, state): 415 | reached_cases = self.filter_php_response('FLAG', output) 416 | 417 | for case in reached_cases: 418 | self._reached_cases.append({ 419 | 'case': case, 420 | 'state': state 421 | }) 422 | 423 | def has_reached_case(self, flag): 424 | ret = False 425 | for case in self._reached_cases: 426 | if case['case'] == flag: 427 | ret = True 428 | 429 | return ret 430 | 431 | def filter_hit_ops(self, output): 432 | return self.fetch_json_from_output('OPS', output) 433 | 434 | def filter_transforms(self, output): 435 | return self.fetch_json_from_output('TRANSFORMS', output) 436 | 437 | def fetch_json_from_output(self, key, output): 438 | items = [] 439 | json_str = self.filter_php_response(key, output) 440 | if len(json_str) > 0: 441 | items = json.loads(json_str[0]) 442 | 443 | return items 444 | 445 | def print_results(self): 446 | print 'Scanning of %s finished...' % self._php_file_or_script 447 | print ' - Needed %d runs' % self._num_runs 448 | print ' - Took %f seconds' % self._duration 449 | print '' 450 | 451 | for reached_case in self._reached_cases: 452 | print 'Successfully reached "%s" using input:' % reached_case['case'] 453 | print reached_case['state'].pretty_print() 454 | 455 | 456 | # http://stackoverflow.com/questions/5884066/hashing-a-python-dictionary 457 | def make_hash(obj): 458 | """ 459 | Makes a hash from a dictionary, list, tuple or set to any level, that contains 460 | only other hashable types (including any lists, tuples, sets, and 461 | dictionaries). 462 | """ 463 | 464 | if isinstance(obj, (set, tuple, list)): 465 | 466 | return tuple([make_hash(e) for e in obj]) 467 | 468 | elif not isinstance(obj, dict): 469 | 470 | return hash(obj) 471 | 472 | new_o = copy.deepcopy(obj) 473 | for k, val in new_o.items(): 474 | new_o[k] = make_hash(val) 475 | 476 | return hash(tuple(frozenset(sorted(new_o.items())))) 477 | -------------------------------------------------------------------------------- /phpscan/opcode.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | # See Zend/zend_vm_opcodes.h for original definition 4 | ZEND_OPCODE_LIST = [ 5 | 'ZEND_NOP', # 0 6 | 'ZEND_ADD', # 1 7 | 'ZEND_SUB', # 2 8 | 'ZEND_MUL', # 3 9 | 'ZEND_DIV', # 4 10 | 'ZEND_MOD', # 5 11 | 'ZEND_SL', # 6 12 | 'ZEND_SR', # 7 13 | 'ZEND_CONCAT', # 8 14 | 'ZEND_BW_OR', # 9 15 | 'ZEND_BW_AND', # 10 16 | 'ZEND_BW_XOR', # 11 17 | 'ZEND_BW_NOT', # 12 18 | 'ZEND_BOOL_NOT', # 13 19 | 'ZEND_BOOL_XOR', # 14 20 | 'ZEND_IS_IDENTICAL', # 15 21 | 'ZEND_IS_NOT_IDENTICAL', # 16 22 | 'ZEND_IS_EQUAL', # 17 23 | 'ZEND_IS_NOT_EQUAL', # 18 24 | 'ZEND_IS_SMALLER', # 19 25 | 'ZEND_IS_SMALLER_OR_EQUAL', # 20 26 | 'ZEND_CAST', # 21 27 | 'ZEND_QM_ASSIGN', # 22 28 | 'ZEND_ASSIGN_ADD', # 23 29 | 'ZEND_ASSIGN_SUB', # 24 30 | 'ZEND_ASSIGN_MUL', # 25 31 | 'ZEND_ASSIGN_DIV', # 26 32 | 'ZEND_ASSIGN_MOD', # 27 33 | 'ZEND_ASSIGN_SL', # 28 34 | 'ZEND_ASSIGN_SR', # 29 35 | 'ZEND_ASSIGN_CONCAT', # 30 36 | 'ZEND_ASSIGN_BW_OR', # 31 37 | 'ZEND_ASSIGN_BW_AND', # 32 38 | 'ZEND_ASSIGN_BW_XOR', # 33 39 | 'ZEND_PRE_INC', # 34 40 | 'ZEND_PRE_DEC', # 35 41 | 'ZEND_POST_INC', # 36 42 | 'ZEND_POST_DEC', # 37 43 | 'ZEND_ASSIGN', # 38 44 | 'ZEND_ASSIGN_REF', # 39 45 | 'ZEND_ECHO', # 40 46 | 'ZEND_GENERATOR_CREATE', # 41 47 | 'ZEND_JMP', # 42 48 | 'ZEND_JMPZ', # 43 49 | 'ZEND_JMPNZ', # 44 50 | 'ZEND_JMPZNZ', # 45 51 | 'ZEND_JMPZ_EX', # 46 52 | 'ZEND_JMPNZ_EX', # 47 53 | 'ZEND_CASE', # 48 54 | 'ZEND_CHECK_VAR', # 49 55 | 'ZEND_SEND_VAR_NO_REF_EX', # 50 56 | 'ZEND_MAKE_REF', # 51 57 | 'ZEND_BOOL', # 52 58 | 'ZEND_FAST_CONCAT', # 53 59 | 'ZEND_ROPE_INIT', # 54 60 | 'ZEND_ROPE_ADD', # 55 61 | 'ZEND_ROPE_END', # 56 62 | 'ZEND_BEGIN_SILENCE', # 57 63 | 'ZEND_END_SILENCE', # 58 64 | 'ZEND_INIT_FCALL_BY_NAME', # 59 65 | 'ZEND_DO_FCALL', # 60 66 | 'ZEND_INIT_FCALL', # 61 67 | 'ZEND_RETURN', # 62 68 | 'ZEND_RECV', # 63 69 | 'ZEND_RECV_INIT', # 64 70 | 'ZEND_SEND_VAL', # 65 71 | 'ZEND_SEND_VAR_EX', # 66 72 | 'ZEND_SEND_REF', # 67 73 | 'ZEND_NEW', # 68 74 | 'ZEND_INIT_NS_FCALL_BY_NAME', # 69 75 | 'ZEND_FREE', # 70 76 | 'ZEND_INIT_ARRAY', # 71 77 | 'ZEND_ADD_ARRAY_ELEMENT', # 72 78 | 'ZEND_INCLUDE_OR_EVAL', # 73 79 | 'ZEND_UNSET_VAR', # 74 80 | 'ZEND_UNSET_DIM', # 75 81 | 'ZEND_UNSET_OBJ', # 76 82 | 'ZEND_FE_RESET_R', # 77 83 | 'ZEND_FE_FETCH_R', # 78 84 | 'ZEND_EXIT', # 79 85 | 'ZEND_FETCH_R', # 80 86 | 'ZEND_FETCH_DIM_R', # 81 87 | 'ZEND_FETCH_OBJ_R', # 82 88 | 'ZEND_FETCH_W', # 83 89 | 'ZEND_FETCH_DIM_W', # 84 90 | 'ZEND_FETCH_OBJ_W', # 85 91 | 'ZEND_FETCH_RW', # 86 92 | 'ZEND_FETCH_DIM_RW', # 87 93 | 'ZEND_FETCH_OBJ_RW', # 88 94 | 'ZEND_FETCH_IS', # 89 95 | 'ZEND_FETCH_DIM_IS', # 90 96 | 'ZEND_FETCH_OBJ_IS', # 91 97 | 'ZEND_FETCH_FUNC_ARG', # 92 98 | 'ZEND_FETCH_DIM_FUNC_ARG', # 93 99 | 'ZEND_FETCH_OBJ_FUNC_ARG', # 94 100 | 'ZEND_FETCH_UNSET', # 95 101 | 'ZEND_FETCH_DIM_UNSET', # 96 102 | 'ZEND_FETCH_OBJ_UNSET', # 97 103 | 'ZEND_FETCH_LIST', # 98 104 | 'ZEND_FETCH_CONSTANT', # 99 105 | '_UNASSIGNED_', # 100 106 | 'ZEND_EXT_STMT', # 101 107 | 'ZEND_EXT_FCALL_BEGIN', # 102 108 | 'ZEND_EXT_FCALL_END', # 103 109 | 'ZEND_EXT_NOP', # 104 110 | 'ZEND_TICKS', # 105 111 | 'ZEND_SEND_VAR_NO_REF', # 106 112 | 'ZEND_CATCH', # 107 113 | 'ZEND_THROW', # 108 114 | 'ZEND_FETCH_CLASS', # 109 115 | 'ZEND_CLONE', # 110 116 | 'ZEND_RETURN_BY_REF', # 111 117 | 'ZEND_INIT_METHOD_CALL', # 112 118 | 'ZEND_INIT_STATIC_METHOD_CALL', # 113 119 | 'ZEND_ISSET_ISEMPTY_VAR', # 114 120 | 'ZEND_ISSET_ISEMPTY_DIM_OBJ', # 115 121 | 'ZEND_SEND_VAL_EX', # 116 122 | 'ZEND_SEND_VAR', # 117 123 | 'ZEND_INIT_USER_CALL', # 118 124 | 'ZEND_SEND_ARRAY', # 119 125 | 'ZEND_SEND_USER', # 120 126 | 'ZEND_STRLEN', # 121 127 | 'ZEND_DEFINED', # 122 128 | 'ZEND_TYPE_CHECK', # 123 129 | 'ZEND_VERIFY_RETURN_TYPE', # 124 130 | 'ZEND_FE_RESET_RW', # 125 131 | 'ZEND_FE_FETCH_RW', # 126 132 | 'ZEND_FE_FREE', # 127 133 | 'ZEND_INIT_DYNAMIC_CALL', # 128 134 | 'ZEND_DO_ICALL', # 129 135 | 'ZEND_DO_UCALL', # 130 136 | 'ZEND_DO_FCALL_BY_NAME', # 131 137 | 'ZEND_PRE_INC_OBJ', # 132 138 | 'ZEND_PRE_DEC_OBJ', # 133 139 | 'ZEND_POST_INC_OBJ', # 134 140 | 'ZEND_POST_DEC_OBJ', # 135 141 | 'ZEND_ASSIGN_OBJ', # 136 142 | 'ZEND_OP_DATA', # 137 143 | 'ZEND_INSTANCEOF', # 138 144 | 'ZEND_DECLARE_CLASS', # 139 145 | 'ZEND_DECLARE_INHERITED_CLASS', # 140 146 | 'ZEND_DECLARE_FUNCTION', # 141 147 | 'ZEND_YIELD_FROM', # 142 148 | 'ZEND_DECLARE_CONST', # 143 149 | 'ZEND_ADD_INTERFACE', # 144 150 | 'ZEND_DECLARE_INHERITED_CLASS_DELAYED', # 145 151 | 'ZEND_VERIFY_ABSTRACT_CLASS', # 146 152 | 'ZEND_ASSIGN_DIM', # 147 153 | 'ZEND_ISSET_ISEMPTY_PROP_OBJ', # 148 154 | 'ZEND_HANDLE_EXCEPTION', # 149 155 | 'ZEND_USER_OPCODE', # 150 156 | 'ZEND_ASSERT_CHECK', # 151 157 | 'ZEND_JMP_SET', # 152 158 | 'ZEND_DECLARE_LAMBDA_FUNCTION', # 153 159 | 'ZEND_ADD_TRAIT', # 154 160 | 'ZEND_BIND_TRAITS', # 155 161 | 'ZEND_SEPARATE', # 156 162 | 'ZEND_FETCH_CLASS_NAME', # 157 163 | 'ZEND_CALL_TRAMPOLINE', # 158 164 | 'ZEND_DISCARD_EXCEPTION', # 159 165 | 'ZEND_YIELD', # 160 166 | 'ZEND_GENERATOR_RETURN', # 161 167 | 'ZEND_FAST_CALL', # 162 168 | 'ZEND_FAST_RET', # 163 169 | 'ZEND_RECV_VARIADIC', # 164 170 | 'ZEND_SEND_UNPACK', # 165 171 | 'ZEND_POW', # 166 172 | 'ZEND_ASSIGN_POW', # 167 173 | 'ZEND_BIND_GLOBAL', # 168 174 | 'ZEND_COALESCE', # 169 175 | 'ZEND_SPACESHIP', # 170 176 | 'ZEND_DECLARE_ANON_CLASS', # 171 177 | 'ZEND_DECLARE_ANON_INHERITED_CLASS', # 172 178 | 'ZEND_FETCH_STATIC_PROP_R', # 173 179 | 'ZEND_FETCH_STATIC_PROP_W', # 174 180 | 'ZEND_FETCH_STATIC_PROP_RW', # 175 181 | 'ZEND_FETCH_STATIC_PROP_IS', # 176 182 | 'ZEND_FETCH_STATIC_PROP_FUNC_ARG', # 177 183 | 'ZEND_FETCH_STATIC_PROP_UNSET', # 178 184 | 'ZEND_UNSET_STATIC_PROP', # 179 185 | 'ZEND_ISSET_ISEMPTY_STATIC_PROP', # 180 186 | 'ZEND_FETCH_CLASS_CONSTANT', # 181 187 | 'ZEND_BIND_LEXICAL', # 182 188 | 'ZEND_BIND_STATIC', # 183 189 | 'ZEND_FETCH_THIS', # 184 190 | '_UNASSIGNED_', # 185 191 | 'ZEND_ISSET_ISEMPTY_THIS' # 186 192 | ] 193 | 194 | 195 | ZEND_OPCODE_LOOKUP = {opcode_name: opcode for opcode, opcode_name in enumerate(ZEND_OPCODE_LIST)} 196 | 197 | 198 | # See Zend/zend_compile.h for original definition 199 | class OperandType(Enum): 200 | CONST = 1 << 0 201 | TMP_VAR = 1 << 1 202 | VAR = 1 << 2 203 | UNUSED = 1 << 3 204 | CV = 1 << 4 205 | 206 | OPTYPE_LOOKUP = {optype.value: optype for optype in list(OperandType)} 207 | 208 | 209 | class Operand(object): 210 | 211 | def __init__(self, var_id, type, data_type, value): 212 | self._id = var_id 213 | self._type = OPTYPE_LOOKUP[type] 214 | self._data_type = data_type 215 | self._value = value 216 | 217 | @property 218 | def id(self): 219 | return self._id 220 | 221 | @id.setter 222 | def id(self, value): 223 | self._id = value 224 | 225 | @property 226 | def value(self): 227 | value_typed = self._value 228 | 229 | if self.data_type == 'string': 230 | pass 231 | elif self.data_type == 'integer': 232 | value_typed = int(value_typed) 233 | # elif self.data_type == 'double': 234 | # value_typed = float(value_typed) 235 | else: 236 | raise ValueError( 237 | 'Cannot handle operands of type %s' % self.data_type) 238 | 239 | return value_typed 240 | 241 | @value.setter 242 | def value(self, value): 243 | self._value = value 244 | 245 | @property 246 | def type(self): 247 | return self._type 248 | 249 | @type.setter 250 | def type(self, value): 251 | self._type = value 252 | 253 | @property 254 | def data_type(self): 255 | return self._data_type 256 | 257 | @data_type.setter 258 | def data_type(self, value): 259 | self._data_type = value 260 | -------------------------------------------------------------------------------- /phpscan/resolver.py: -------------------------------------------------------------------------------- 1 | from core import Logger, logger 2 | 3 | class Resolver(object): 4 | def __init__(self, transforms, state): 5 | self._transforms = transforms 6 | self._state = state 7 | self.register_resolvers() 8 | 9 | def register_resolvers(self): 10 | self._resolver = dict() 11 | global RESOLVERS 12 | for resolver in RESOLVERS: 13 | self.register_resolver(resolver[0], resolver[1](resolver[0], self)) 14 | 15 | def register_resolver(self, function_name, resolver): 16 | self._resolver[function_name] = resolver 17 | 18 | def is_tracking(self, var_id): 19 | return var_id in self._transforms 20 | 21 | def resolve(self, var_id, data_type): 22 | condition = {} 23 | 24 | if self._state.is_tracking(var_id): 25 | var = self._state.get_var_ref(var_id) 26 | 27 | if var['type'] == 'unknown': 28 | self._state.update_guessed_type(var_id, data_type) 29 | 30 | condition = { 31 | 'type': 'base_var', 32 | 'id': var_id 33 | } 34 | elif self.is_tracking(var_id): 35 | transform = self._transforms[var_id] 36 | function_name = transform['function'] 37 | 38 | if function_name in self._resolver: 39 | condition = self._resolver[function_name].process(data_type, transform['args']) 40 | if 'args' in condition: 41 | for i in range(len(condition['args'])): 42 | if condition['args'][i]['type'] == 'symbolic': 43 | condition['args'][i] = self.resolve(condition['args'][i]['id'], 44 | data_type) 45 | 46 | else: 47 | msg = 'Not processing %s (no resolver)' % function_name 48 | logger.log(msg, '', Logger.DEBUG) 49 | raise Exception(msg) 50 | else: 51 | raise Exception('Cannot resolve value for untracked id \'%s\'.' % var_id) 52 | 53 | return condition 54 | 55 | class TransformResolver(object): 56 | def __init__(self, function_name, resolver): 57 | self._name = function_name 58 | self._resolver = resolver 59 | 60 | def process(self, data_type, args): 61 | logger.log('Processing %s...' % self._name, '', Logger.DEBUG) 62 | return self.resolve(data_type, args) 63 | 64 | def resolve(self, data_type, args): 65 | raise Exception('resolve should be implemented in child class') 66 | 67 | 68 | class SubstrResolver(TransformResolver): 69 | def resolve(self, data_type, args): 70 | return { 71 | 'type': 'extract', 72 | 'args': args 73 | } 74 | 75 | class AssignResolver(TransformResolver): 76 | def resolve(self, data_type, args): 77 | return { 78 | 'type': 'assign', 79 | 'args': args 80 | } 81 | 82 | class DirectProxyResolver(TransformResolver): 83 | def resolve(self, data_type, args): 84 | if args[0]['type'] == 'symbolic' and args[1]['type'] == 'raw_value': 85 | self._resolver._state.update_guessed_type_from_value(args[0]['id'], args[1]['value']) 86 | if args[1]['type'] == 'symbolic' and args[0]['type'] == 'raw_value': 87 | self._resolver._state.update_guessed_type_from_value(args[1]['id'], args[0]['value']) 88 | 89 | return { 90 | 'type': self._name, 91 | 'args': args 92 | } 93 | 94 | class FetchDimResolver(TransformResolver): 95 | def resolve(self, data_type, args): 96 | (var_arg, idx_arg) = args 97 | 98 | if var_arg['type'] == 'symbolic': 99 | if self._resolver.is_tracking(var_arg['id']): 100 | transform = self._resolver._transforms[var_arg['id']] 101 | 102 | if transform['function'] == 'explode': 103 | return { 104 | 'type': 'explode', 105 | 'args': transform['args'], 106 | 'index': idx_arg['value'] 107 | } 108 | 109 | 110 | return { 111 | 'type': 'base_var', 112 | 'id': args[0] 113 | } 114 | 115 | 116 | 117 | RESOLVERS = [ 118 | ('substr', SubstrResolver), 119 | ('concat', DirectProxyResolver), 120 | ('add', DirectProxyResolver), 121 | ('assign', AssignResolver), 122 | ('fetch_dim_r', FetchDimResolver) 123 | ] 124 | -------------------------------------------------------------------------------- /phpscan/satisfier/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartvanarnhem/phpscan/51f9b411c145e36e203487e2a8e5c06dc72647f6/phpscan/satisfier/__init__.py -------------------------------------------------------------------------------- /phpscan/satisfier/greedy.py: -------------------------------------------------------------------------------- 1 | from ..opcode import ZEND_OPCODE_LIST, ZEND_OPCODE_LOOKUP 2 | from ..core import Logger, logger 3 | from satisfier import Satisfier, OpcodeHandler 4 | import json 5 | 6 | 7 | class GreedySatisfier(Satisfier): 8 | 9 | def __init__(self): 10 | global SATISFIER_HANDLERS 11 | Satisfier.__init__(self, SATISFIER_HANDLERS) 12 | 13 | self.register_handlers(SATISFIER_HANDLERS) 14 | 15 | def register_handlers(self, handlers): 16 | for handler in handlers: 17 | self.register_handler(handler[1](handler[0], self)) 18 | 19 | 20 | class UninitializedPropertyAccessHandler(OpcodeHandler): 21 | def process_op(self, compare_op, value_op, sign=-1): 22 | if self.satisfier.start_state.is_tracking(compare_op.id): 23 | state_var = self.satisfier.start_state.get_var_ref(compare_op.id) 24 | property_name = value_op.value 25 | 26 | state_var['type'] = 'array' # force this to be an array 27 | 28 | if not 'properties' in state_var: 29 | state_var['properties'] = {} 30 | 31 | if not property_name in state_var['properties']: 32 | # TODO should we name this differently? 33 | prop_id = 'fetch_dim_r(%s:%s)' % (compare_op.id, property_name) 34 | 35 | state_var['properties'][property_name] = { 36 | 'type': 'unknown', 37 | 'value': '' 38 | } 39 | 40 | self.satisfier.start_state._lookup_map[prop_id] = state_var['properties'][property_name] 41 | 42 | 43 | # TODO clean this up 44 | state_var = self.satisfier.start_state.get_annotated_var_ref(compare_op.id) 45 | property_name = value_op.value 46 | 47 | if not 'properties' in state_var: 48 | state_var['properties'] = {} 49 | 50 | if not property_name in state_var['properties']: 51 | prop_id = 'fetch_dim_r(%s:%s)' % (compare_op.id, property_name) 52 | 53 | state_var['properties'][property_name] = { 54 | 'type': 'unknown', 55 | 'value': '', 56 | 'id': prop_id, 57 | 'persistent_id': state_var['persistent_id'] + ':' + property_name 58 | } 59 | 60 | self.satisfier.start_state._annotated_lookup_map[prop_id] = state_var['properties'][property_name] 61 | 62 | 63 | 64 | class IsEqualHandler(OpcodeHandler): 65 | def process_op(self, compare_op, value_op, sign=-1): 66 | var_id = compare_op.id 67 | data_type = value_op.data_type 68 | value = value_op.value 69 | 70 | # TODO: maybe remove 71 | # self.update_guessed_type_from_value(var_id, value) 72 | 73 | self.establish_var_value(var_id, data_type, value, 'equals') 74 | 75 | class IsNotEqualHandler(OpcodeHandler): 76 | def process_op(self, compare_op, value_op, sign=-1): 77 | var_id = compare_op.id 78 | data_type = value_op.data_type 79 | value = value_op.value 80 | 81 | # TODO: maybe remove 82 | # self.update_guessed_type_from_value(var_id, value) 83 | 84 | self.establish_var_value(var_id, data_type, value, 'not_equals') 85 | 86 | class IsSmallerHandler(OpcodeHandler): 87 | def process_op(self, compare_op, value_op, sign=1): 88 | var_id = compare_op.id 89 | data_type = value_op.data_type 90 | value = value_op.value 91 | 92 | # TODO: maybe remove 93 | # self.update_guessed_type_from_value(var_id, value) 94 | 95 | self.establish_var_value(var_id, data_type, value, 'smaller' if sign == 1 else 'greater') 96 | 97 | 98 | 99 | SATISFIER_HANDLERS = [ 100 | ('ZEND_IS_EQUAL', IsEqualHandler), 101 | ('ZEND_IS_IDENTICAL', IsEqualHandler), 102 | ('ZEND_IS_NOT_EQUAL', IsNotEqualHandler), 103 | ('ZEND_CASE', IsEqualHandler), 104 | 105 | ('ZEND_ISSET_ISEMPTY_DIM_OBJ', UninitializedPropertyAccessHandler), 106 | ('ZEND_FETCH_DIM_R', UninitializedPropertyAccessHandler), 107 | ('ZEND_FETCH_DIM_FUNC_ARG', UninitializedPropertyAccessHandler), 108 | 109 | 110 | ('ZEND_IS_SMALLER', IsSmallerHandler), 111 | ('ZEND_IS_SMALLER_OR_EQUAL', IsSmallerHandler) 112 | ] 113 | -------------------------------------------------------------------------------- /phpscan/satisfier/satisfier.py: -------------------------------------------------------------------------------- 1 | from ..opcode import ZEND_OPCODE_LIST, ZEND_OPCODE_LOOKUP 2 | from ..core import Logger, logger 3 | from ..resolver import Resolver 4 | from ..solver import Solver 5 | 6 | class Satisfier: 7 | 8 | def __init__(self, handlers): 9 | self._handlers = dict() 10 | self._start_state = None 11 | 12 | @property 13 | def start_state(self): 14 | return self._start_state 15 | 16 | @start_state.setter 17 | def start_state(self, value): 18 | self._start_state = value 19 | 20 | @property 21 | def resolver(self): 22 | return self._resolver 23 | 24 | @resolver.setter 25 | def resolver(self, value): 26 | self._resolver = value 27 | 28 | def is_tracking(self, operator): 29 | return self.start_state.is_tracking(operator.id) or self.resolver.is_tracking(operator.id) 30 | 31 | def process(self, ops, transforms): 32 | state = self.start_state 33 | state.conditions = [] 34 | self.resolver = Resolver(transforms, self.start_state) 35 | 36 | for operator in ops: 37 | op1 = operator['op1'] 38 | op2 = operator['op2'] 39 | if self.is_tracking(op1) or self.is_tracking(op2): 40 | self.process_op(operator['opcode'], op1, op2) 41 | else: 42 | logger.log( 43 | 'Ignoring op on untracked operands', operator, Logger.DEBUG) 44 | 45 | solver = Solver() 46 | solver.solve(state, state.conditions) 47 | 48 | logger.log( 49 | 'Resulted in new state', state.pretty_print(), Logger.PROGRESS) 50 | 51 | yield state 52 | 53 | def process_op(self, opcode, op1, op2): 54 | if opcode in self._handlers: 55 | self._handlers[opcode].process(op1, op2) 56 | else: 57 | logger.log('Not processing %s (no handler)' % 58 | ZEND_OPCODE_LIST[opcode], '', Logger.DEBUG) 59 | 60 | def register_handler(self, handler): 61 | self._handlers[handler.opcode] = handler 62 | 63 | 64 | class OpcodeHandler: 65 | 66 | def __init__(self, opcode_name, satisfier): 67 | self.opcode_name = opcode_name 68 | self.opcode = ZEND_OPCODE_LOOKUP[opcode_name] 69 | self.satisfier = satisfier 70 | 71 | def process(self, op1, op2): 72 | logger.log('Processing %s...' % self.opcode_name, '', Logger.DEBUG) 73 | 74 | if self.satisfier.is_tracking(op1): 75 | self.process_op(op1, op2) 76 | elif self.satisfier.is_tracking(op2): 77 | self.process_op(op2, op1, -1) 78 | else: 79 | raise Exception('Got to process for untracked operand') 80 | 81 | def process_op(self, compare_op, value_op, sign=-1): 82 | raise Exception('process_op should be implemented in child class') 83 | 84 | def establish_var_value(self, var_id, data_type, value, operator): 85 | r = { 86 | 'type': operator, 87 | 'args': [ 88 | self.satisfier.resolver.resolve(var_id, data_type), 89 | { 90 | 'type': 'raw_value', 91 | 'value': value 92 | } 93 | ] 94 | } 95 | 96 | self.satisfier.start_state.conditions.append(r) 97 | 98 | def update_guessed_type_from_value(self, var_id, value): 99 | self.satisfier.start_state.update_guessed_type_from_value(var_id, value) 100 | 101 | @property 102 | def opcode(self): 103 | return self._opcode 104 | 105 | @opcode.setter 106 | def opcode(self, value): 107 | self._opcode = value 108 | 109 | @property 110 | def opcode_name(self): 111 | return self._opcode_name 112 | 113 | @opcode_name.setter 114 | def opcode_name(self, value): 115 | self._opcode_name = value 116 | 117 | @property 118 | def satisfier(self): 119 | return self._satisfier 120 | 121 | @satisfier.setter 122 | def satisfier(self, value): 123 | self._satisfier = value 124 | -------------------------------------------------------------------------------- /phpscan/solver.py: -------------------------------------------------------------------------------- 1 | from core import Logger, logger 2 | import z3 3 | import json 4 | import string 5 | import uuid 6 | 7 | class Solver(object): 8 | def __init__(self): 9 | self.register_adapters() 10 | self._base_var = {} 11 | 12 | def register_adapters(self): 13 | self._adapters = dict() 14 | global ADAPTERS 15 | for adapter in ADAPTERS: 16 | self.register_adapter(adapter[0], adapter[1](adapter[0], self)) 17 | 18 | def register_adapter(self, op_name, adapter): 19 | self._adapters[op_name] = adapter 20 | 21 | 22 | def solve(self, state, conditions): 23 | self._state = state 24 | 25 | logger.log('CONDITIONS', json.dumps(conditions, indent=4), Logger.PROGRESS) 26 | 27 | solver = z3.Solver() 28 | self._solver = solver 29 | 30 | logger.log('BASE VARS', json.dumps(state._lookup_map, indent=4), Logger.PROGRESS) 31 | 32 | for var_id, var in state._lookup_map.iteritems(): 33 | if var['type'] in ('string', 'unknown'): 34 | # Type could be unknown if newly discovered and we did not get a typehint yet 35 | self._base_var[var_id] = z3.String(var_id) 36 | elif var['type'] == 'integer': 37 | self._base_var[var_id] = z3.Int(var_id) 38 | else: 39 | logger.log('Ignoring unknown type %s while passing to Z3' % 40 | var['type'], '', Logger.DEBUG) 41 | 42 | z3_solved = False 43 | 44 | try: 45 | for condition in conditions: 46 | solver.add(self.solve_rec(condition)) 47 | 48 | solve_result = solver.check() 49 | logger.log('SOLVER RESULT', solve_result, Logger.PROGRESS) 50 | 51 | if solve_result == z3.sat: 52 | z3_solved = True 53 | model = solver.model() 54 | logger.log('SOLVER MODEL', model, Logger.PROGRESS) 55 | 56 | for var_id, var in self._base_var.iteritems(): 57 | var_ref = self._state.get_var_ref(var_id) 58 | var_ref['value'] = self.get_z3_value(model[var]) 59 | except z3.z3types.Z3Exception as e: 60 | print 'Got Z3Exception: %s' % str(e) 61 | 62 | # else: 63 | # raise Exception('SOLVER COULD NOT SOLVE CONDITIONS') 64 | 65 | def get_z3_value(self, value): 66 | if z3.is_string(value): 67 | return value.as_string()[1:-1].replace('\\x00', '?') 68 | elif z3.is_int(value): 69 | return value.as_long() 70 | elif value is None: 71 | return '' # TODO adhere to type 72 | else: 73 | raise ValueError('Got unknown Z3 type') 74 | 75 | 76 | def solve_rec(self, condition): 77 | op_name = condition['type'] 78 | if op_name in self._adapters: 79 | return self._adapters[op_name].process(condition) 80 | else: 81 | logger.log('Not processing %s (no adapter)' % 82 | op_name, '', Logger.DEBUG) 83 | raise Exception('Not processing %s (no adapter)' % op_name) 84 | 85 | 86 | class Z3Adapter(object): 87 | def __init__(self, op_name, solver): 88 | self._op_name = op_name 89 | self._solver = solver 90 | 91 | def process(self, args): 92 | logger.log('Processing %s...' % self._op_name, '', Logger.DEBUG) 93 | return self.adapt(args) 94 | 95 | def adapt(self, args): 96 | raise Exception('adapt should be implemented in child class') 97 | 98 | 99 | class EqualsAdapter(Z3Adapter): 100 | def adapt(self, condition): 101 | args = condition['args'] 102 | return self._solver.solve_rec(args[0]) == self._solver.solve_rec(args[1]) 103 | 104 | class NotEqualsAdapter(Z3Adapter): 105 | def adapt(self, condition): 106 | args = condition['args'] 107 | return self._solver.solve_rec(args[0]) != self._solver.solve_rec(args[1]) 108 | 109 | class SmallerAdapter(Z3Adapter): 110 | def adapt(self, condition): 111 | args = condition['args'] 112 | return self._solver.solve_rec(args[0]) < self._solver.solve_rec(args[1]) 113 | 114 | class GreaterAdapter(Z3Adapter): 115 | def adapt(self, condition): 116 | args = condition['args'] 117 | return self._solver.solve_rec(args[0]) > self._solver.solve_rec(args[1]) 118 | 119 | 120 | class ExtractAdapter(Z3Adapter): 121 | def adapt(self, condition): 122 | args = condition['args'] 123 | return z3.Extract(self._solver.solve_rec(args[0]), self._solver.solve_rec(args[1]), 124 | self._solver.solve_rec(args[2])) 125 | 126 | 127 | class ConcatAdapter(Z3Adapter): 128 | def adapt(self, condition): 129 | args = condition['args'] 130 | return z3.Concat(self._solver.solve_rec(args[0]), self._solver.solve_rec(args[1])) 131 | 132 | class AddAdapter(Z3Adapter): 133 | def adapt(self, condition): 134 | args = condition['args'] 135 | 136 | return self._solver.solve_rec(args[0]) + self._solver.solve_rec(args[1]) 137 | 138 | class AssignAdapter(Z3Adapter): 139 | def adapt(self, condition): 140 | args = condition['args'] 141 | 142 | return self._solver.solve_rec(args[0]) 143 | 144 | 145 | class BaseVarAdapter(Z3Adapter): 146 | def adapt(self, condition): 147 | return self._solver._base_var[condition['id']] 148 | 149 | class RawValueAdapter(Z3Adapter): 150 | def adapt(self, condition): 151 | value = condition['value'] 152 | 153 | if isinstance(value, basestring): 154 | return z3.StringVal(value) 155 | elif isinstance(value, int): 156 | return value 157 | else: 158 | raise Exception('got unknown rawvalue') 159 | 160 | 161 | def dimvar(var, idx): 162 | newvar = z3.String(str(uuid.uuid4())) 163 | # todo, split is now harcoded '.' -> this could also be a variable 164 | a = z3.parse_smt2_string('(assert (seq.in.re a (re.++ (re.loop (re.++ (re.* (re.union (re.range " " "-") (re.range "/" "~") ) ) (str.to.re ".")) %d 0 ) (str.to.re b) (re.* (re.++ (str.to.re ".") (re.* (re.range " " "~")) )) )))' % (idx), decls={'a': var, 'b': newvar}) 165 | return (a, newvar) 166 | 167 | class ExplodeAdapter(Z3Adapter): 168 | def adapt(self, condition): 169 | args = condition['args'] 170 | 171 | (explode_constraint, var) = dimvar(self._solver.solve_rec(args[1]), condition['index']) 172 | self._solver._solver.add(explode_constraint) 173 | return var 174 | 175 | 176 | ADAPTERS = [ 177 | ('equals', EqualsAdapter), 178 | ('not_equals', NotEqualsAdapter), 179 | ('smaller', SmallerAdapter), 180 | ('greater', GreaterAdapter), 181 | ('extract', ExtractAdapter), 182 | ('concat', ConcatAdapter), 183 | ('add', AddAdapter), 184 | ('base_var', BaseVarAdapter), 185 | ('raw_value', RawValueAdapter), 186 | ('assign', AssignAdapter), 187 | ('explode', ExplodeAdapter) 188 | ] 189 | 190 | -------------------------------------------------------------------------------- /tests/greedy_satisfier/test_satisfier.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import sys 4 | sys.path.append('../..') 5 | 6 | from phpscan.core import Scan, logger, verify_dependencies 7 | from phpscan.satisfier.greedy import GreedySatisfier 8 | 9 | 10 | def init_and_run_simple_scan(script): 11 | scan = Scan(script, Scan.INPUT_MODE_SCRIPT) 12 | 13 | scan.satisfier = GreedySatisfier() 14 | 15 | scan.start() 16 | 17 | return scan 18 | 19 | 20 | def test_string_comparison(): 21 | scan = init_and_run_simple_scan("if ($_GET['var'] == 'value') phpscan_flag('flag');") 22 | assert scan.has_reached_case('flag') 23 | 24 | 25 | def test_integer_comparison(): 26 | scan = init_and_run_simple_scan("if ($_GET['var'] == 10) phpscan_flag('flag');") 27 | assert scan.has_reached_case('flag') 28 | 29 | 30 | def test_greater_than(): 31 | scan = init_and_run_simple_scan("if ($_GET['var'] > 10) phpscan_flag('flag');") 32 | assert scan.has_reached_case('flag') 33 | 34 | 35 | def test_smaller_than(): 36 | scan = init_and_run_simple_scan("if ($_GET['var'] < 10) phpscan_flag('flag');") 37 | assert scan.has_reached_case('flag') 38 | 39 | 40 | def test_indirect_assign(): 41 | scan = init_and_run_simple_scan("$a = $_GET['var']; if ($a == 'value') phpscan_flag('flag');") 42 | assert scan.has_reached_case('flag') 43 | 44 | 45 | def test_substring(): 46 | scan = init_and_run_simple_scan("$a = substr($_GET['var'], 2, 3); if ($a == 'lu') phpscan_flag('flag');") 47 | assert scan.has_reached_case('flag') 48 | 49 | 50 | def test_negate(): 51 | scan = init_and_run_simple_scan("if (!($a == 'value')) phpscan_flag('flag');") 52 | assert scan.has_reached_case('flag') 53 | 54 | def test_explode(): 55 | script = """ 56 | $a = explode('.', $_GET['var']); 57 | $class = $a[0]; 58 | $action = $a[1]; 59 | 60 | if (($class == 'auth') && ($action == 'login')) { 61 | phpscan_flag('flag'); 62 | } 63 | """ 64 | scan = init_and_run_simple_scan(script) 65 | assert scan.has_reached_case('flag') 66 | 67 | def test_concat(): 68 | script = """ 69 | $a = $_GET['var']; 70 | $b = $a . 'lue'; 71 | 72 | if ($b === 'value') { 73 | phpscan_flag('flag'); 74 | } 75 | """ 76 | scan = init_and_run_simple_scan(script) 77 | assert scan.has_reached_case('flag') 78 | 79 | def test_add(): 80 | script = """ 81 | $a = $_GET['var']; 82 | $b = $a + 7; 83 | 84 | if ($b === 10) { 85 | phpscan_flag('flag'); 86 | } 87 | """ 88 | scan = init_and_run_simple_scan(script) 89 | assert scan.has_reached_case('flag') 90 | 91 | 92 | def test_concat_complex(): 93 | script = """ 94 | $a = $_GET['var']; 95 | $b = 'v' . $a . 'lue'; 96 | 97 | if ($b === 'value') { 98 | phpscan_flag('flag'); 99 | } 100 | """ 101 | scan = init_and_run_simple_scan(script) 102 | assert scan.has_reached_case('flag') 103 | 104 | def test_add_complex(): 105 | script = """ 106 | $a = $_GET['var']; 107 | $b = 2 + $a + 7; 108 | 109 | if ($b === 10) { 110 | phpscan_flag('flag'); 111 | } 112 | """ 113 | scan = init_and_run_simple_scan(script) 114 | assert scan.has_reached_case('flag') 115 | -------------------------------------------------------------------------------- /zend_extension/config.m4: -------------------------------------------------------------------------------- 1 | PHP_ARG_ENABLE(phpscan, whether to enable PHPScan support, 2 | [ --enable-phpscan Enable PHPScan support]) 3 | if test "$PHP_PHPSCAN" = "yes"; then 4 | AC_DEFINE(HAVE_PHPSCAN, 1, [Whether you have PHPScan]) 5 | PHP_NEW_EXTENSION(phpscan, phpscan.c, $ext_shared) 6 | fi 7 | -------------------------------------------------------------------------------- /zend_extension/php_phpscan.h: -------------------------------------------------------------------------------- 1 | #ifndef PHP_PHPSCAN 2 | #define PHP_PHPSCAN 1 3 | #define PHP_PHPSCAN_VERSION "1.0" 4 | #define PHP_PHPSCAN_EXTNAME "phpscan" 5 | 6 | PHP_FUNCTION(phpscan_enabled); 7 | PHP_FUNCTION(phpscan_ext_get_zval_id); 8 | PHP_FUNCTION(phpscan_ext_ignore_op); 9 | PHP_FUNCTION(phpscan_ext_ignore_op_off); 10 | 11 | extern zend_module_entry phpscan_module_entry; 12 | #define phpext_phpscan_ptr &phpscan_module_entry 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /zend_extension/phpscan.c: -------------------------------------------------------------------------------- 1 | #ifdef HAVE_CONFIG_H 2 | #include "config.h" 3 | #endif 4 | #include "php.h" 5 | #include "php_phpscan.h" 6 | 7 | #define OP_PHP_CALLBACK_FUNCTION "phpscan_ext_opcode_handler" 8 | 9 | long zval_to_id(zval* val) 10 | { 11 | long id = -1; 12 | if (Z_TYPE_P(val) == IS_REFERENCE) 13 | { 14 | id = val->value.zv; 15 | } 16 | 17 | return id; 18 | } 19 | 20 | int ignore_op = 0; 21 | int in_callback = 0; 22 | static void trigger_op_php_callback(zend_uchar opcode, 23 | zval* op1, zend_uchar op1type, 24 | zval* op2, zend_uchar op2type, 25 | zval* result, zend_uchar resulttype, 26 | int use_result) 27 | { 28 | if (!in_callback && !ignore_op) 29 | { 30 | zval* params = NULL; 31 | uint param_count = 10; 32 | 33 | params = safe_emalloc(sizeof(zval), param_count, 0); 34 | 35 | zval function_name; 36 | ZVAL_STRING(&function_name, OP_PHP_CALLBACK_FUNCTION); 37 | 38 | zval opcode_zval; 39 | ZVAL_LONG(&opcode_zval, opcode); 40 | 41 | zval op1type_zval; 42 | ZVAL_LONG(&op1type_zval, op1type); 43 | 44 | zval op1id_zval; 45 | ZVAL_LONG(&op1id_zval, zval_to_id(op1)); 46 | 47 | zval op2type_zval; 48 | ZVAL_LONG(&op2type_zval, op2type); 49 | 50 | zval op2id_zval; 51 | ZVAL_LONG(&op2id_zval, zval_to_id(op2)); 52 | 53 | zval resulttype_zval; 54 | ZVAL_LONG(&resulttype_zval, resulttype); 55 | 56 | params[0] = opcode_zval; 57 | params[1] = *op1; 58 | params[2] = op1id_zval; 59 | params[3] = op1type_zval; 60 | params[4] = *op2; 61 | params[5] = op2id_zval; 62 | params[6] = op2type_zval; 63 | 64 | // TODO: looks like result can be unitialized even though type != IS_UNUSED... use NULL for now 65 | zval resultid_zval; 66 | if (use_result && (result->u1.v.type != IS_UNDEF) && (&result->value != 0)) 67 | { 68 | params[7] = *result; 69 | ZVAL_LONG(&resultid_zval, zval_to_id(result)); 70 | } 71 | else 72 | { 73 | ZVAL_NULL(¶ms[7]); 74 | ZVAL_LONG(&resultid_zval, -1); 75 | } 76 | 77 | 78 | params[8] = resultid_zval; 79 | params[9] = resulttype_zval; 80 | 81 | zval return_value; 82 | 83 | in_callback = 1; 84 | if (call_user_function( 85 | EG(function_table), NULL /* no object */, &function_name, 86 | &return_value, param_count, params TSRMLS_CC 87 | ) == SUCCESS 88 | ) 89 | { 90 | /* do something with retval_ptr here if you like */ 91 | } 92 | in_callback = 0; 93 | 94 | efree(params); 95 | zval_ptr_dtor(&return_value); 96 | } 97 | else 98 | { 99 | // printf("Warning: ignoring opcode %d while already in callback to prevent infinite recursion.\n", opcode); 100 | } 101 | } 102 | 103 | PHP_FUNCTION(phpscan_enabled) 104 | { 105 | RETURN_BOOL(1); 106 | } 107 | 108 | PHP_FUNCTION(phpscan_ext_ignore_op) 109 | { 110 | ignore_op = 1; 111 | } 112 | 113 | PHP_FUNCTION(phpscan_ext_ignore_op_off) 114 | { 115 | ignore_op = 0; 116 | } 117 | 118 | 119 | PHP_FUNCTION(phpscan_ext_get_zval_id) 120 | { 121 | zval* var; 122 | 123 | if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &var) == FAILURE) { 124 | RETURN_NULL(); 125 | } 126 | 127 | RETURN_LONG(zval_to_id(var)); 128 | } 129 | 130 | zval *get_zval(zend_execute_data *zdata, int node_type, const znode_op *node, int *is_var) 131 | { 132 | zend_free_op should_free; 133 | zval* r = zend_get_zval_ptr(node_type, node, zdata, &should_free, BP_VAR_IS); 134 | 135 | return r; 136 | } 137 | 138 | struct OplineLag { 139 | zend_op *opline; 140 | zend_uchar opcode; 141 | zval* op1; 142 | zend_uchar op1_type; 143 | zval* op2; 144 | zend_uchar op2_type; 145 | zval* result; 146 | zend_uchar result_type; 147 | int pending; 148 | }; 149 | 150 | void force_ref(zval *val) { 151 | if (!Z_ISREF_P(val) && 152 | (Z_TYPE_P(val) != IS_INDIRECT) && 153 | (Z_TYPE_P(val) != IS_CONSTANT) && 154 | (Z_TYPE_P(val) != IS_CONSTANT_AST)) 155 | { 156 | if (Z_TYPE_P(val) == IS_UNDEF) { 157 | ZVAL_NEW_EMPTY_REF(val); 158 | Z_SET_REFCOUNT_P(val, 2); 159 | ZVAL_NULL(Z_REFVAL_P(val)); 160 | } 161 | else { 162 | ZVAL_MAKE_REF(val); 163 | } 164 | } 165 | } 166 | 167 | static int common_override_handler(zend_execute_data *execute_data) 168 | { 169 | const zend_op *opline = execute_data->opline; 170 | static struct OplineLag lag = {0, 0, 0, 0, 0, 0, 0, 0, 0}; 171 | 172 | int is_var; 173 | 174 | if (lag.pending != 0) 175 | { 176 | lag.pending = 0; 177 | 178 | trigger_op_php_callback(lag.opcode, 179 | lag.op1, lag.op1_type, 180 | lag.op2, lag.op2_type, 181 | lag.result, lag.result_type, 182 | 1 183 | ); 184 | } 185 | 186 | 187 | if (opline->opcode == ZEND_ASSIGN) 188 | { 189 | if (Z_TYPE_P(EX_VAR(opline->op1.var)) == IS_UNDEF) 190 | { 191 | force_ref(EX_VAR(opline->op1.var)); 192 | } 193 | } 194 | 195 | zval* op1 = get_zval(execute_data, opline->op1_type, &opline->op1, &is_var); 196 | zval* op2 = get_zval(execute_data, opline->op2_type, &opline->op2, &is_var); 197 | zval* result = get_zval(execute_data, opline->result_type, &opline->result, &is_var); 198 | 199 | trigger_op_php_callback(opline->opcode, 200 | op1, opline->op1_type, 201 | op2, opline->op2_type, 202 | result, opline->result_type, 203 | 0 204 | ); 205 | 206 | return ZEND_USER_OPCODE_DISPATCH; 207 | } 208 | 209 | void register_handler(zend_uchar opcode) 210 | { 211 | zend_set_user_opcode_handler(opcode, common_override_handler); 212 | } 213 | 214 | PHP_MINIT_FUNCTION(phpscan) 215 | { 216 | register_handler(ZEND_IS_EQUAL); 217 | register_handler(ZEND_IS_NOT_EQUAL); 218 | register_handler(ZEND_IS_IDENTICAL); 219 | register_handler(ZEND_CASE); 220 | 221 | register_handler(ZEND_ISSET_ISEMPTY_DIM_OBJ); 222 | register_handler(ZEND_FETCH_DIM_R); 223 | register_handler(ZEND_FETCH_DIM_FUNC_ARG); 224 | 225 | register_handler(ZEND_IS_SMALLER); 226 | register_handler(ZEND_IS_SMALLER_OR_EQUAL); 227 | 228 | register_handler(ZEND_ASSIGN); 229 | 230 | register_handler(ZEND_ADD); 231 | register_handler(ZEND_CONCAT); 232 | 233 | return SUCCESS; 234 | } 235 | 236 | 237 | ZEND_BEGIN_ARG_INFO_EX(arginfo_phpscan_ext_get_zval_id, 0, 0, 3) 238 | ZEND_ARG_INFO(1, var) 239 | ZEND_END_ARG_INFO(); 240 | 241 | 242 | zend_function_entry phpscan_functions[] = { 243 | PHP_FE(phpscan_enabled, NULL) 244 | PHP_FE(phpscan_ext_get_zval_id, arginfo_phpscan_ext_get_zval_id) 245 | PHP_FE(phpscan_ext_ignore_op, NULL) 246 | PHP_FE(phpscan_ext_ignore_op_off, NULL) 247 | {NULL, NULL, NULL} 248 | }; 249 | 250 | 251 | zend_module_entry phpscan_module_entry = { 252 | STANDARD_MODULE_HEADER, 253 | PHP_PHPSCAN_EXTNAME, 254 | phpscan_functions, 255 | PHP_MINIT(phpscan), 256 | NULL, 257 | NULL, 258 | NULL, 259 | NULL, 260 | PHP_PHPSCAN_VERSION, 261 | STANDARD_MODULE_PROPERTIES 262 | }; 263 | 264 | #ifdef COMPILE_DL_PHPSCAN 265 | ZEND_GET_MODULE(phpscan) 266 | #endif 267 | 268 | 269 | --------------------------------------------------------------------------------