<?php
/**
RIPS - A static source code analyser for vulnerabilities in PHP scripts
by Johannes Dahse (hide@address.com)
Open source under the BSD License.
Copyright (c) 2010, Johannes Dahse
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**/
// get all files from directory, including all subdirectories
function read_recursiv($path, $scan_subdirs)
{
$result = array();
$handle = opendir($path);
if ($handle)
{
while (false !== ($file = readdir($handle)))
{
if ($file !== '.' && $file !== '..')
{
$name = $path . '/' . $file;
if (is_dir($name) && $scan_subdirs)
{
$ar = read_recursiv($name, true);
foreach ($ar as $value)
{
$result[] = $value;
}
} else
{
$result[] = $name;
}
}
}
}
closedir($handle);
return $result;
}
// traces recursivly parameters and adds them as child to parent
// returns true if a parameter is tainted by userinput
function scan_parameter($file_name, $mainparent, $parent, $var_name, $var_declares, $last_token_id, $var_declares_global, $function_params, $function_obj, $userinput, $F_SECURES, $return_scan=false)
{
$vardependent = false;
$ignore_var = '';
$arrayname = explode('[', $var_name);
// if $array[key] was not declared explicitly scan for $array
if(isset($arrayname[1]))
{
if ($arrayname[0] == '$GLOBALS'
&& !isset($var_declares[$var_name]) && !empty($arrayname[1]) )
$var_name = '$'. str_replace(array('"', "'", ']'), '', $arrayname[1]);
else if(!isset($var_declares[$var_name]) )
$var_name = $arrayname[0];
}
// check if var declaration could be found for this var
if( isset($var_declares[$var_name]) )
{
foreach($var_declares[$var_name] as $var_declare)
{
$line_nr = $var_declare->lines[0];
$line = $var_declare->value;
$token_id = $var_declare->id;
if( $token_id < $last_token_id )
{
// add line to output
if(count($mainparent->lines) < $GLOBALS['maxtrace'])
{
$foundvalue = $line;
if( count($mainparent->dependencies) <
count($var_declare->dependencies))
{
foreach($var_declare->dependencies as $deplinenr=>$dependency)
{
if( !isset($mainparent->dependencies[$deplinenr]) && $deplinenr != $line_nr )
{
$vardependent = true;
$foundvalue = $foundvalue.' // '.trim($dependency);
}
}
}
$mainparent->lines[] = $line_nr;
$var_trace = new VarDeclare( highlightline($foundvalue, $line_nr) );
$parent->children[] = $var_trace;
} else
{
$stop = new VarDeclare('... Trace stopped.');
$parent->children[] = $stop;
return $userinput;
}
// find other variables in this line
$tokens = token_get_all('<?'.trim($line).'?>');
$tokens = prepare_tokens($tokens, $GLOBALS['T_IGNORE']);
$last_scanned = '';
$last_userinput = false;
$in_arithmetic = false;
$in_securing = false;
$parentheses_open = 0;
$parentheses_save = -1;
for($i=1, $maxtokens=count($tokens); $i<$maxtokens; $i++)
{
if( is_array($tokens[$i]) )
{
// if token is variable
if( $tokens[$i][0] === T_VARIABLE
&& $tokens[$i][1] !== $ignore_var )
{
$new_token_trace = $tokens[$i][1];
// trace $var['keyname'] (if available) not only $var
if($tokens[$i+1] === '['
&& isset($tokens[$i+2][1])
&& (isset($var_declares[$new_token_trace.'['.$tokens[$i+2][1].']'])
|| in_array($new_token_trace, $GLOBALS['V_USERINPUT'])
|| $new_token_trace === '$GLOBALS'))
{
$new_token_trace = $new_token_trace.'['.$tokens[$i+2][1].']';
}
// global $varname
if( is_array($tokens[$i-1])
&& $tokens[$i-1][0] === T_GLOBAL )
{
// scan in global scope
$userinput = scan_parameter($file_name, $mainparent, $var_trace,
$new_token_trace, $var_declares_global, $token_id,
$var_declares_global, $function_params, $function_obj, $userinput,
$F_SECURES, $return_scan);
// scan in local scope
} else
{
$userinput = scan_parameter($file_name, $mainparent, $var_trace,
$new_token_trace, $var_declares, $token_id,
$var_declares_global, $function_params, $function_obj, $userinput,
$F_SECURES, $return_scan);
}
// check if typecast or securing function wrapped
if((is_array($tokens[$i-1])
&& in_array($tokens[$i-1][0], $GLOBALS['T_CASTS']))
|| (is_array($tokens[$i+1])
&& in_array($tokens[$i+1][0], $GLOBALS['T_CASTS']))
|| $in_securing)
{
// mark user function as a securing user function
$GLOBALS['userfunction_secures'] = true;
$var_trace->marker = 2;
if( $GLOBALS['verbosity'] < 3 && !$last_userinput )
{
$userinput = false;
}
}
// check for automatic typecasts by arithmetic
if(in_array($tokens[$i-1], $GLOBALS['T_ARITHMETIC'])
|| in_array($tokens[$i+1], $GLOBALS['T_ARITHMETIC'])
|| $in_arithmetic)
{
// mark user function as a securing user function
$GLOBALS['userfunction_secures'] = true;
$in_arithmetic = true;
$var_trace->marker = 2;
if( $GLOBALS['verbosity'] < 3 && !$last_userinput )
{
$userinput = false;
}
}
}
// if in foreach($bla as $key=>$value) dont trace $key, $value back
else if( $tokens[$i][0] === T_AS )
{
break;
}
// if tokens is mathematical assignment like $a.=$b, trace $a again
else if( in_array($tokens[$i][0], $GLOBALS['T_ASSIGNMENT']) )
{
$tokens = array_merge(
array_slice($tokens, 0, $i),
array('='), array($tokens[$i-1]),
array_slice($tokens, $i+1)
);
$maxtokens = count($tokens);
}
// also check for userinput from functions defined as userinput
// depending on verbosity level
else if( in_array($tokens[$i][1], $GLOBALS['F_USERINPUT']) )
{
$userinput = true;
$var_trace->marker = 4;
if($return_scan)
{
$GLOBALS['userfunction_taints'] = true;
}
// userinput received in function, just needs a trigger
else if($function_obj !== null)
{
addtriggerfunction($mainparent, $function_obj, $file_name);
}
return $userinput;
}
// detect securing functions
else if(in_array($tokens[$i][1], $F_SECURES)
|| (isset($tokens[$i-2][1]) &&
in_array($tokens[$i-2][1], $GLOBALS['F_SECURING_STRING'])) )
{
$parentheses_save = $parentheses_open;
$in_securing = true;
}
// if this is a vuln line, it has already been scanned -> return
else if( in_array($tokens[$i][0], $GLOBALS['T_FUNCTIONS'])
&& isset($GLOBALS['scan_functions'][$tokens[$i][1]])
&& !isset($GLOBALS['F_CODE'][$tokens[$i][1]]) )
{
$var_trace->value = $var_trace->value.' <span class=\"phps-t-comment\">// stopped, already traced</span>';
return $userinput;
}
}
// string concat disables arithmetic
else if($tokens[$i] === '.')
{
$in_arithmetic = false;
}
// watch opening parentheses
else if($tokens[$i] === '(')
{
$parentheses_open++;
}
// watch closing parentheses
else if($tokens[$i] === ')')
{
$parentheses_open--;
if($parentheses_open === $parentheses_save)
{
$parentheses_save = -1;
$in_securing = false;
}
}
// special case for var declaration in constructs
else if( is_array($tokens[$i-1]) )
{
// assignments in a if()/while() need to skip the var declaring name
if( $tokens[$i-1][0] === T_IF || $tokens[$i-1][0] === T_WHILE)
{
// if($h = fopen($asd)) , $h should not be traced back
$i+=2;
}
// ignore first variable in for($i=0;...)
else if( $tokens[$i-1][0] === T_FOR )
{
$ignore_var = $tokens[$i+1][1];
}
}
// save userinput (true|false) for vars in same line
$last_userinput = $userinput;
}
// we only need the last var declaration, other declarations have been overwritten
if( $userinput || !$vardependent )
break;
}
}
}
// if var comes from function parameter
if( in_array($arrayname[0], $function_params) )
{
// add child with function declaration
$func_name = $function_obj->name;
$mainparent->lines[] = $function_obj->lines[0];
makefunclink($function_obj);
$parent->children[] = $function_obj;
// add function to scanlist
$key = array_search($arrayname[0], $function_params);
$mainparent->funcdepend = $func_name;
// with potential parameters
$GLOBALS['user_functions'][$file_name][$func_name][0][$key] = $key+1;
// and with according securing functions from original find
$GLOBALS['user_functions'][$file_name][$func_name][1] = isset($GLOBALS['scan_functions'][$mainparent->name]) ?
$GLOBALS['scan_functions'][$mainparent->name][1] : $GLOBALS['user_functions'][$file_name][$mainparent->name][1];
$userinput = true;
}
// if var is userinput, return true directly
if( in_array($arrayname[0], $GLOBALS['V_USERINPUT']) )
{
$userinput = true;
$parent->marker = 1;
// add userinput markers to mainparent object
if(isset($arrayname[1]))
$parameter_name = str_replace(array('"', "'", ']'), '', $arrayname[1]);
if(!empty($parameter_name))
{
switch($arrayname[0])
{
case '$_GET': $mainparent->get[] = $parameter_name; break;
case '$HTTP_GET_VARS': $mainparent->get[] = $parameter_name; break;
case '$_REQUEST': $mainparent->get[] = $parameter_name; break;
case '$HTTP_REQUEST_VARS': $mainparent->get[] = $parameter_name; break;
case '$_POST': $mainparent->post[] = $parameter_name; break;
case '$HTTP_POST_VARS': $mainparent->post[] = $parameter_name; break;
case '$HTTP_RAW_POST_DATA': $mainparent->post[] = $parameter_name; break;
case '$_COOKIE': $mainparent->cookie[] = $parameter_name; break;
case '$HTTP_COOKIE_VARS': $mainparent->cookie[] = $parameter_name; break;
case '$_FILES': $mainparent->files[] = $parameter_name; break;
case '$HTTP_POST_FILES': $mainparent->files[] = $parameter_name; break;
}
}
// userinput received in function, just needs a trigger
if($function_obj !== null && !$return_scan)
{
addtriggerfunction($mainparent, $function_obj, $file_name);
}
}
return $userinput;
}
// add function to output that triggers something by call
function addtriggerfunction($mainparent, $function_obj, $file_name)
{
// add dependency and mark this as interesting function
$func_name = $function_obj->name;
$mainparent->dependencies[$function_obj->lines[0]] = $function_obj->value;
// add function to scanlist
$mainparent->funcdepend = $func_name;
// with all parameters as valuable since userinput comes from inside the func
$GLOBALS['user_functions'][$file_name][$func_name][0][0] = 0;
// no securings
$GLOBALS['user_functions'][$file_name][$func_name][1] = array();
// doesnt matter if with userinput called or not
$GLOBALS['user_functions'][$file_name][$func_name][3] = true;
}
// traces values of variables for dynamic file includes
function get_var_value($var_name, $var_declares, $var_declares_global, $last_token_id)
{
$var_value = '';
if(isset($var_declares['$'.$var_name]))
{
$var_name = '$'.$var_name;
}
// check if var declaration could be found for this var
if( isset($var_declares[$var_name]) )
{
foreach($var_declares[$var_name] as $var_declare)
{
$token_id = $var_declare->id;
if( $token_id < $last_token_id )
{
$line = $var_declare->value;
// find other variables in this line
$tokens = token_get_all('<?'.trim($line).'?>');
$tokens = prepare_tokens($tokens, $GLOBALS['T_IGNORE']);
for($i=($tokens[1] === '[') ? 3:1, $max=count($tokens); $i<$max; $i++)
{
if( is_array($tokens[$i]) )
{
$token_name = $tokens[$i][0];
$token_value = $tokens[$i][1];
// if token is variable trace again
if( $token_name === T_VARIABLE )
{
$var_trace = $token_value;
// trace $var['keyname'] (if available) not only $var
if($tokens[$i+1] === '['
&& isset($var_declares[$var_trace.'['.$tokens[$i+2][1].']'])
|| $var_trace === '$GLOBALS' )
{
$var_trace = $var_trace.'['.$tokens[$i+2][1].']';
$i=$i+2;
}
// global $varname -> global scope
if( is_array($tokens[$i-1]) && $tokens[$i-1][0] === T_GLOBAL )
{
$var_value.= get_var_value($var_trace,
$var_declares_global, $var_declares_global, $token_id);
}
// local scope
else
{
$var_value.= get_var_value($var_trace,
$var_declares, $var_declares_global, $token_id);
}
}
// if token is string add string to output
// except first string of define('var', 'value')
else if( $token_name === T_CONSTANT_ENCAPSED_STRING
&& !($tokens[$i-2][0] === T_STRING
&& $tokens[$i-2][1] === 'define'))
{
$var_value.= str_replace(array('"', "'"), '', $token_value);
}
// try to guess constants
else if( $token_name === T_STRING
&& !($tokens[$i-2][0] === T_STRING
&& $tokens[$i-2][1] === 'define'))
{
if ($token_value == 'DIRECTORY_SEPARATOR' || $token_value == 'PATH_SEPARATOR')
{
$var_value.='/';
}
else if( isset($var_declares['$'.$token_value]) )
{
$var_value.= get_var_value('$'.$token_value,
$var_declares, $var_declares_global, $token_id);
}
}
}
}
}
break;
}
}
return $var_value;
}
// fetches a line from the sourcecode and checks for commands written over several lines
function getmultiline($lines_pointer, $linenr)
{
$line = trim($lines_pointer[$linenr]);
$i = strlen($line)-1;
if($i>0 && $line[$i] != ';' && $line[$i] != ')' && $line[$i] != '('
&& $line[$i] != '{' && $line[$i] != '}')
{
$line .= getmultiline($lines_pointer, $linenr+1);
}
return $line;
}
// scans tokens of php file for function calls and watches dependencies
function scan_file($file_name, $scan_functions,
$T_FUNCTIONS, $T_ASSIGNMENT, $T_IGNORE, $T_INCLUDES, $T_XSS, $T_IGNORE_STRUCTURE)
{
$var_declares_global = array();
$var_declares_local = array();
$put_in_global_scope = array();
$globals_from_function = array();
$dependencies = array();
$exit_functions = array();
$vuln_classes = array();
$class_vars = array();
$braces_open = 0;
$brace_save_func = -1;
$brace_save_class = -1;
$ignore_requirement = false;
$in_function = false;
$in_class = false;
$comment = '';
$inc_file = '';
$inc_file_stack = array();
$lines_stack = array();
$lines_stack[] = file($file_name);
// pointer to current lines set
$lines_pointer =& end($lines_stack);
$code = implode('',$lines_pointer);
$tokens = token_get_all($code);
$tokens = prepare_tokens($tokens, $T_IGNORE);
$tokens = fix_tokens($tokens);
$GLOBALS['counterlines']+=count($lines_pointer);
// scan all tokens of file
for($i=0,$tokencount=count($tokens); $i<$tokencount; $i++)
{
$token = $tokens[$i];
if( is_array($token) )
{
$token_name = $token[0];
$token_value = $token[1];
$line_nr = $token[2];
# debug
#echo "file:".$file_name.",line:".$line_nr.",token:".token_name($token_name).",";
#echo "value:".htmlentities($token_value).",";
#echo "in_function:".$in_function.",in_class:".$in_class."<br>";
// include tokens from included files, only static includes supported
if( in_array($token_name, $T_INCLUDES) && !$in_function)
{
// save found
$found_line = trim($lines_pointer[$line_nr-1])."\t".$comment;
$last_inc_file = $inc_file;
// include('xxx')
if ( (($tokens[$i+1] === '('
&& $tokens[$i+2][0] === T_CONSTANT_ENCAPSED_STRING
&& $tokens[$i+3] === ')')
// include 'xxx'
|| (is_array($tokens[$i+1])
&& $tokens[$i+1][0] === T_CONSTANT_ENCAPSED_STRING
&& $tokens[$i+2] === ';' )) )
{
// include('file')
if($tokens[$i+1] === '(')
{
$inc_file = substr($tokens[$i+2][1], 1, -1);
$skip = 5;
}
// include 'file'
else
{
$inc_file = substr($tokens[$i+1][1], 1, -1);
$skip = 3;
}
}
// dynamic include
else
{
$inc_file = '';
$c = 1;
// check all tokens until include statement ends
while( $tokens[$i +$c] !== ';' )
{
if( is_array($tokens[$i +$c]) )
{
// trace variables for its values
if( $tokens[$i +$c][0] === T_VARIABLE
|| ($tokens[$i +$c][0] === T_STRING
&& $tokens[$i +$c +1] !== '(' ) )
{
$var_trace = $tokens[$i +$c][1];
// trace $var['keyname'] (if available) not only $var
if($tokens[$i +$c +1] === '[')
{
$var_trace = $var_trace.'['.$tokens[$i +$c +2][1].']';
$i=$i+2;
}
if(!$in_function)
$inc_file .= get_var_value($var_trace,
$var_declares_global, $var_declares_global, $i);
else
$inc_file .= get_var_value($var_trace,
$var_declares_local, $var_declares_global, $i);
}
// add strings to include file name
else if( $tokens[$i + $c][0] === T_CONSTANT_ENCAPSED_STRING )
{
$inc_file .= str_replace(array('"', "'"), '', $tokens[$i + $c][1]);
}
}
if($c>50)break;
$c++;
}
$skip = $c+1;
}
// if file name has not been included
if( !in_array($inc_file, $inc_file_stack) )
{
$try_file = dirname($file_name). '/' . $inc_file;
// try to open include file name
if ( $inc_lines = @file( $try_file ) )
{
$include = '// successfully analysed';
$GLOBALS['counterlines']+=count($inc_lines);
$inc_code = implode('',$inc_lines);
$inc_tokens = token_get_all($inc_code);
$inc_tokens = prepare_tokens($inc_tokens, $T_IGNORE);
// insert included tokens in current tokenlist and mark end
$tokens = array_merge(
array_slice($tokens, 0, $i), // before include
$inc_tokens, // included tokens
array(array(T_INCLUDE_END, 0, $inc_file)), // extra END-identifier
array_slice($tokens, $i+$skip) // after include
);
$tokencount = count($tokens);
// set lines pointer to included lines, save last pointer
// (the following tokens will be the included ones)
$lines_stack[] = $inc_lines;
$lines_pointer =& end($lines_stack);
$comment = '// '.basename($inc_file);
$inc_file_stack[] = $inc_file;
}
// included file name could not be reversed
// (probably dynamic with function calls)
else
{
$include = "// could not analyse file, tried: $try_file";
$inc_file = $last_inc_file;
}
}
else
{
$include = "// $inc_file has already been included";
$inc_file = $last_inc_file;
}
// add information about include success
if( $GLOBALS['verbosity'] >= 4 )
{
// add include command to output
$found_value = highlightline(trim($found_line)."\t".$include, $line_nr, $token_value);
$new_find = new InfoTreeNode($found_value);
$new_find->line = $line_nr;
// if in included code, set file name
if(!empty($last_inc_file))
$new_find->filename = dirname($file_name) . '/' . $last_inc_file;
$id = (isset($GLOBALS['output'][$file_name])) ?
count($GLOBALS['output'][$file_name]) : 0;
$GLOBALS['output'][$file_name][$id] = $new_find;
}
// if successfully included, dont check further for file inclusion vulnerability
if($include == '// successfully analysed')
{
continue;
}
}
// check for XSS vulns
if( in_array($token_name, $T_XSS)
&& ($_POST['vector'] == 'client' || $_POST['vector'] == 'all') )
{
if($token_name === T_OPEN_TAG_WITH_ECHO)
$token_value = 'echo';
// build new find
$new_find = new VulnTreeNode();
$new_find->name = $token_value;
$new_find->lines[] = $line_nr;
// if in included code, set file name
if(!empty($inc_file))
$new_find->filename = dirname($file_name) . '/' . $inc_file;
// add dependencies
foreach($dependencies as $deplinenr=>$dependency)
{
$new_find->dependencies[$deplinenr] = $dependency;
}
$c = 1;
$has_vuln_parameters = false;
$parameter_has_userinput = false;
$secured_by_start = false;
while( $tokens[$i + $c] !== ';' )
{
$this_one_is_secure = false;
if( $tokens[$i + $c][0] === T_VARIABLE )
{
if( (is_array($tokens[$i + $c -2])
&& (in_array($tokens[$i + $c -2][1], $GLOBALS['F_SECURING_STRING'])
|| in_array($tokens[$i + $c -2][1], $GLOBALS['F_SECURING_XSS'])))
|| in_array($tokens[$i + $c -1][0], $GLOBALS['T_CASTS']) )
{
$secured_by_start = true;
$this_one_is_secure = true;
}
$has_vuln_parameters = true;
$trace_par_var = $tokens[$i + $c][1];
// $var['keyname'] should be directly traced, not $var
if($tokens[$i + $c +1] === '[')
{
$trace_par_var = $trace_par_var.'['.$tokens[$i + $c +2][1].']';
}
// trace back parameters and look for userinput
if($in_function)
{
$userinput = scan_parameter($file_name, $new_find, $new_find,
$trace_par_var, $var_declares_local, $i+$c,
$var_declares_global, $function_params, $function_obj,
false, $GLOBALS['F_SECURING_XSS']);
} else
{
$userinput = scan_parameter($file_name, $new_find, $new_find,
$trace_par_var, $var_declares_global, $i+$c,
null, array(), null, false, $GLOBALS['F_SECURING_XSS']);
}
if($userinput && (!$this_one_is_secure || $GLOBALS['verbosity'] >= 3) )
$parameter_has_userinput = true;
}
if($c>50)break;
$c++;
}
// add find to output if function call has variable parameters (With userinput)
if( ($has_vuln_parameters && $parameter_has_userinput) || $GLOBALS['verbosity'] >= 5 )
{
$new_find->value = highlightline(getmultiline($lines_pointer, $line_nr-1)."\t".$comment,
$line_nr, $token_value);
if($secured_by_start)
$new_find->marker = 2;
$id = (isset($GLOBALS['output'][$file_name])) ?
count($GLOBALS['output'][$file_name]) : 0;
$GLOBALS['output'][$file_name][$id] = $new_find;
// mark function in class as vuln
if($in_function && $in_class)
{
$vuln_classes[$class_name][] = $function_name;
}
}
}
// switch lines pointer back to original code if included tokens end
else if( $token_name === T_INCLUDE_END)
{
array_pop($lines_stack);
$lines_pointer =& end($lines_stack);
array_pop($inc_file_stack);
$inc_file = end($inc_file_stack);
$comment = '';
}
// build list of all variable declarations
else if( $token_name === T_VARIABLE
&& ( $tokens[$i+1][0] === '=' || // normal assignment
(in_array($tokens[$i+1][0], $T_ASSIGNMENT)) // mathematical assignment
|| ($tokens[$i-1][0] === T_AS // foreach($var as $key=>$value)
|| ($tokens[$i-1][0] === T_DOUBLE_ARROW
&& $tokens[$i-2][0] === T_VARIABLE))
|| ($tokens[$i+1] === '[' // $foo['a'], hard to check all keys and assignments
// example: $a[0][$i+$k] &= $_GET['a'];
// easier: the last token was an ending statement or beginning of the file
&& ($tokens[$i-1] === '}' || $tokens[$i-1] === '{'
|| $tokens[$i-1] === ';' || !isset($tokens[$i-1][0])))
)
)
{
// add variable declaration to beginning of varlist
$new_var = new VarDeclare(getmultiline($lines_pointer, $line_nr-1)."\t".$comment);
$new_var->lines[] = $line_nr;
$new_var->id = $i;
$new_token_value = $token_value;
// add dependencies
foreach($dependencies as $deplinenr=>$dependency)
{
$new_var->dependencies[$deplinenr] = $dependency;
}
// save $var['keyname'] not only $var
if($tokens[$i+1] === '['
&& is_array($tokens[$i+2])
&& ($tokens[$i+2][0] === T_CONSTANT_ENCAPSED_STRING
|| $tokens[$i+2][0] === T_LNUMBER)
&& $tokens[$i+3] === ']')
{
// global varlist or local (in function) varlist
if($in_function)
{
if(!isset($var_declares_local[$new_token_value]))
$var_declares_local[$new_token_value] = array($new_var);
else
array_unshift($var_declares_local[$new_token_value], $new_var);
} else
{
if(!isset($var_declares_global[$new_token_value]))
$var_declares_global[$new_token_value] = array($new_var);
else
array_unshift($var_declares_global[$new_token_value], $new_var);
}
$new_token_value = $token_value.'['.$tokens[$i+2][1].']';
}
// global varlist or local (in function) varlist
if($in_function)
{
if(!isset($var_declares_local[$new_token_value]))
$var_declares_local[$new_token_value] = array($new_var);
else
array_unshift($var_declares_local[$new_token_value], $new_var);
if(in_array($new_token_value, $put_in_global_scope))
{
if(!isset($globals_from_function[$function_name][$new_token_value]))
$globals_from_function[$function_name][$new_token_value] = array($new_var);
else
array_unshift($globals_from_function[$function_name][$new_token_value], $new_var);
}
} else
{
if(!isset($var_declares_global[$new_token_value]))
$var_declares_global[$new_token_value] = array($new_var);
else
array_unshift($var_declares_global[$new_token_value], $new_var);
}
$i++;
}
// add user input variables to global list
else if($token_name === T_VARIABLE && in_array($token_value, $GLOBALS['V_USERINPUT']))
{
$file_finding = (!empty($inc_file)) ? dirname($file_name) . '/' . $inc_file : $file_name;
if($tokens[$i+1] === '[' && $tokens[$i+2][0] === T_CONSTANT_ENCAPSED_STRING)
$GLOBALS['user_input'][$token_value.'['.$tokens[$i+2][1].']'][$file_finding][] = $line_nr;
else
$GLOBALS['user_input'][$token_value][$file_finding][] = $line_nr;
}
// add globaled variables (global $a, $b, $c;) to var list
else if($token_name === T_GLOBAL && $in_function)
{
$globals_from_function[$function_name] = array();
// get all globaled variables
$b=1;
while($tokens[$i + $b] !== ';')
{
if( $tokens[$i + $b][0] === T_VARIABLE )
{
$var_value = $tokens[$i + $b][1];
$put_in_global_scope[] = $var_value;
// add variable declaration to beginning of varlist
$new_var = new VarDeclare("global $var_value;\t".$comment);
$new_var->lines[] = $line_nr;
$new_var->id = $i;
$var_declares_local[$var_value] = array($new_var);
}
if($b>50)break;
$b++;
}
}
// define("FOO", $_GET['asd']);
else if($token_name === T_STRING && $token_value === 'define')
{
// add variable declaration to beginning of varlist
$new_var = new VarDeclare(getmultiline($lines_pointer, $line_nr-1)."\t".$comment);
$new_var->lines[] = $line_nr;
$new_var->id = $i;
// add dependencies
foreach($dependencies as $deplinenr=>$dependency)
{
$new_var->dependencies[$deplinenr] = $dependency;
}
$new_token_value = '$'.str_replace(array('"', "'"), '', $tokens[$i+2][1]);
// global varlist or local (in function) varlist
if($in_function)
{
if(!isset($var_declares_local[$new_token_value]))
$var_declares_local[$new_token_value] = array($new_var);
else
array_unshift($var_declares_local[$new_token_value], $new_var);
} else
{
if(!isset($var_declares_global[$new_token_value]))
$var_declares_global[$new_token_value] = array($new_var);
else
array_unshift($var_declares_global[$new_token_value], $new_var);
}
}
// list($drink, $color, $power) = $info;
else if($token_name === T_LIST)
{
$c=2;
while( $tokens[$i + $c] !== ')' )
{
if( is_array($tokens[$i + $c])
&& $tokens[$i + $c][0] === T_VARIABLE )
{
$token_value = $tokens[$i + $c][1];
// add variable declaration to beginning of varlist
$new_var = new VarDeclare(getmultiline($lines_pointer, $tokens[$i + $c][2]-1));
$new_var->lines[] = $tokens[$i + $c][2];
$new_var->id = $i;
// global varlist or local (in function) varlist
if($in_function)
{
if(!isset($var_declares_local[$token_value]))
$var_declares_local[$token_value] = array($new_var);
else
array_unshift($var_declares_local[$token_value], $new_var);
} else
{
if(!isset($var_declares_global[$token_value]))
$var_declares_global[$token_value] = array($new_var);
else
array_unshift($var_declares_global[$token_value], $new_var);
}
}
if($c>50)break;
$c++;
}
$i=$i+$c+2;
}
// add interesting function calls to output (info gathering)
else if( isset($GLOBALS['F_INTEREST'][$token_value]) && $GLOBALS['verbosity'] >= 4 )
{
// add include command to output
$new_find = new VulnTreeNode(
highlightline(getmultiline($lines_pointer, $line_nr-1)."\t$comment // ".$GLOBALS['F_INTEREST'][$token_value], $line_nr, $token_value)
);
$new_find->lines[] = $line_nr;
// if in included code, set file name
if(!empty($inc_file))
$new_find->filename = dirname($file_name) . '/' . $inc_file;
// add dependencies
foreach($dependencies as $deplinenr=>$dependency)
{
$new_find->dependencies[$deplinenr] = $dependency;
}
$id = (isset($GLOBALS['output'][$file_name])) ?
count($GLOBALS['output'][$file_name]) : 0;
$GLOBALS['output'][$file_name][$id] = $new_find;
}
// check if token is a function call and a function to scan
else if( in_array($token_name, $T_FUNCTIONS)
&& isset($scan_functions[$token_value]) )
{
// prevent alerts with wrong classes (same function name in classes)
// $classvar->func();
if($tokens[$i-1][0] === T_OBJECT_OPERATOR)
{
$classvar = $tokens[$i-2][1];
if(substr($classvar,0,1) !== '$')
$classvar = '$'.$classvar;
$class = $class_vars[$classvar];
if(!($in_function && in_array($classvar, $function_params))
&& !@in_array($token_value, $vuln_classes[$class]) )
{
continue;
}
}
// build new find
$new_find = new VulnTreeNode();
$new_find->name = $token_value;
$new_find->lines[] = $line_nr;
// if in included code, set file name
if(!empty($inc_file))
$new_find->filename = dirname($file_name) . '/' . $inc_file;
// add dependencies
foreach($dependencies as $deplinenr=>$dependency)
{
$new_find->dependencies[$deplinenr] = $dependency;
}
$c = 2;
$parameter=1;
$has_vuln_parameters = false;
$parameter_has_userinput = false;
$secured_by_start = false;
$newbraceopen = ($tokens[$i+1] === '(') ? 1 : 0;
// get all variables in parameter list between (...)
// not only until ';' because: system(get($a),$b,strstr($c));
while( !($newbraceopen === 0 || $tokens[$i + $c] === ';') )
{
$this_one_is_secure = false;
if( is_array($tokens[$i + $c])
&& $tokens[$i + $c][0] === T_VARIABLE
// scan only potential vulnerable parameters of function call
&& (in_array($parameter, $scan_functions[$token_value][0])
|| (isset($scan_functions[$token_value][0][0])
&& $scan_functions[$token_value][0][0] === 0) // all parameters accepted
) )
{
if( (is_array($tokens[$i + $c -2])
&& (in_array($tokens[$i + $c -2][1], $GLOBALS['F_SECURING_STRING'])
|| in_array($tokens[$i + $c -2][1], $scan_functions[$token_value][1])))
|| in_array($tokens[$i + $c -1][0], $GLOBALS['T_CASTS']) )
{
$secured_by_start = true;
$this_one_is_secure = true;
}
$has_vuln_parameters = true;
$trace_par_var = $tokens[$i + $c][1];
// $var['keyname'] should be directly traced, not $var
if($tokens[$i + $c +1] === '[')
{
$trace_par_var = $trace_par_var.'['.$tokens[$i + $c +2][1].']';
}
// trace back parameters and look for userinput
if($in_function)
{
$userinput = scan_parameter($file_name, $new_find, $new_find,
$trace_par_var, $var_declares_local, $i+$c,
$var_declares_global, $function_params, $function_obj,
false, $scan_functions[$token_value][1]);
} else
{
$userinput = scan_parameter($file_name, $new_find, $new_find,
$trace_par_var, $var_declares_global, $i+$c,
null, array(), null, false, $scan_functions[$token_value][1]);
}
if($userinput && (!$this_one_is_secure || $GLOBALS['verbosity'] >= 3) )
$parameter_has_userinput = true;
}
// count parameters
else if( $newbraceopen === 1 && $tokens[$i + $c] === ',' )
{
$parameter++;
}
// watch function calls in function call
else if( $tokens[$i + $c] === '(' )
{
$newbraceopen++;
}
else if( $tokens[$i + $c] === ')' )
{
$newbraceopen--;
}
if($c>50)break;
$c++;
}
// add find to output if function call has variable parameters (With userinput)
if( ($has_vuln_parameters && $parameter_has_userinput) || $GLOBALS['verbosity'] >= 5
|| isset($scan_functions[$token_value][3]) )
{
if(isset($GLOBALS['user_functions'][$file_name][$token_value]))
{
$found_line = '<A NAME="'.$token_value.'_call"></A>';
$found_line.= highlightline(getmultiline($lines_pointer, $line_nr-1)."\t".$comment,
$line_nr, false, $token_value);
} else
{
$found_line = highlightline(getmultiline($lines_pointer, $line_nr-1)."\t".$comment,
$line_nr, $token_value);
}
$new_find->value = $found_line;
if($secured_by_start)
$new_find->marker = 2;
// only show vuln user defined functions
// if call with userinput has been found
if( isset($GLOBALS['user_functions'][$file_name][$token_value]) )
$GLOBALS['user_functions'][$file_name][$token_value]['called'] = true;
// add to output
$id = (isset($GLOBALS['output'][$file_name])) ?
count($GLOBALS['output'][$file_name]) : 0;
$GLOBALS['output'][$file_name][$id] = $new_find;
// mark function in class as vuln
if($in_function && $in_class)
{
$vuln_classes[$class_name][] = $function_name;
}
// putenv with userinput --> getenv is treated as userinput
if($token_value == 'putenv')
{
$GLOBALS['F_USERINPUT'][] = 'getenv';
}
else if($token_value == 'apache_setenv')
{
$GLOBALS['F_USERINPUT'][] = 'apache_getenv';
}
}
// if classvar depends on function parameter, add this parameter to list
if( isset($classvar) && $in_function && in_array($classvar, $function_params) )
{
$param = array_search($classvar, $function_params);
$GLOBALS['user_functions'][$file_name][$function_name][0][$param] = $param+1;
}
// if securing was inside the function parameter trace
if($in_function && $GLOBALS['userfunction_secures'] && $GLOBALS['verbosity'] < 3)
{
unset($GLOBALS['user_functions'][$file_name][$function_name]);
$GLOBALS['userfunction_secures'] = false;
}
}
// check if token is a function declaration
else if($token_name === T_FUNCTION)
{
$in_function = true;
// the next token is the "function name()"
$i++;
$function_name = isset($tokens[$i][1]) ? $tokens[$i][1] : $tokens[$i+1][1];
// write to user_functions offset list for referencing in output
$ref_name = ($in_class ? $class_name.'::' : '') . $function_name;
$GLOBALS['user_functions_offset'][$ref_name][0] = !empty($inc_file) ? dirname($file_name).'/'.$inc_file : $file_name;
$GLOBALS['user_functions_offset'][$ref_name][1] = $line_nr-1;
// save function as object
$function_obj = new FunctionDeclare(getmultiline($lines_pointer, $line_nr-1));
$function_obj->lines[] = $line_nr;
$function_obj->name = $function_name;
// save all function parameters
$function_params = array();
$e=1;
// until function test(...) {
// OR
// interface test { public function test(...); }
while( $tokens[$i+$e] !== '{' && $tokens[$i+$e] !== ';' )
{
if( is_array($tokens[$i + $e]) && $tokens[$i + $e][0] === T_VARIABLE )
{
$function_params[] = $tokens[$i + $e][1];
}
if($e>50)break;
$e++;
}
// now skip the params from rest of scan,
// or function test($a=false, $b=false) will be detected as var declaration
$i+=$e-1; // -1, because '{' must be evaluated again
// just for fun
if($function_name === '__destruct')
{
$scan_functions['unserialize'] = array(array(1), array());
}
}
// check if token is a class declaration
else if($token_name === T_CLASS)
{
$i++;
$class_name = $tokens[$i][1];
$vuln_classes[$class_name] = array();
$in_class = true;
}
// build list of vars that are associated with a class
// $var = new Classname()
else if( $token_name === T_NEW && $tokens[$i-2][0] === T_VARIABLE )
{
$class_vars[ $tokens[$i-2][1] ] = $tokens[$i+1][1];
}
// $var = Classname($constructor_param);
else if( $token_name === T_STRING && isset($vuln_classes[$token_value]) )
{
$class_vars[ $tokens[$i-2][1] ] = $token_value;
}
// add exit(),die() or throw exception() as requirements to watch out for
else if(($token_name === T_EXIT || $token_name === T_THROW) && $GLOBALS['verbosity'] >= 4)
{
// build new find
$new_find = new InfoTreeNode();
$new_find->name = $token_value;
$new_find->line = $line_nr;
$new_find->value = highlightline(
getmultiline($lines_pointer, $line_nr-1)."\t".$comment.' // call my cause exit',
$line_nr, $token_value
);
// if in included code, set file name
if(!empty($inc_file))
$new_find->filename = dirname($file_name) . '/' . $inc_file;
// add dependencies
foreach($dependencies as $deplinenr=>$dependency)
{
$new_find->dependencies[$deplinenr] = $dependency;
}
// add to output
$id = (isset($GLOBALS['output'][$file_name])) ?
count($GLOBALS['output'][$file_name]) : 0;
$GLOBALS['output'][$file_name][$id] = $new_find;
// if exit in function, add this function as "exit function"
if($in_function)
$GLOBALS['F_INTEREST'][$function_name] = 'call may cause exit';
}
// ignore requirements: do, while, for, foreach
else if( in_array($token_name, $T_IGNORE_STRUCTURE) )
{
$ignore_requirement = true;
}
// watch returns before vuln function gets called
else if($in_function && $token_name === T_RETURN)
{
$GLOBALS['userfunction_taints'] = false;
$GLOBALS['userfunction_secures'] = false;
$c = 1;
// get all variables in parameter list
while( $tokens[$i + $c] !== ';' && $c < 10)
{
if( is_array($tokens[$i + $c]) )
{
if( $tokens[$i + $c][0] === T_VARIABLE )
{
// check if returned var is secured --> securing function
$new_find = new VulnTreeNode();
$userinput = scan_parameter($file_name, $new_find, $new_find,
$tokens[$i + $c][1],
$var_declares_local, $i+$c,
$var_declares_global, array(), $function_obj,
false, $GLOBALS['F_SECURES_ALL'], TRUE);
// add function to securing functions
if($GLOBALS['userfunction_secures'])
{
$GLOBALS['F_SECURING_STRING'][] = $function_name;
}
// add function to userinput functions if userinput
// is fetched in the function and then returned
if($userinput || ($GLOBALS['userfunction_taints'] && $GLOBALS['verbosity'] < 1))
{
$GLOBALS['F_USERINPUT'][] = $function_name;
}
}
// add function to securing functions if return value is secured
else if( in_array($tokens[$i + $c][1], $GLOBALS['F_SECURING_STRING']) )
{
$GLOBALS['F_SECURING_STRING'][] = $function_name;
break;
}
}
if($c>50)break;
$c++;
}
}
// check if token is function call that affects variable scope (global)
if($token_name === T_STRING && isset($globals_from_function[$token_value]) )
{
foreach($globals_from_function[$token_value] as $var_name=>$new_vars)
{
foreach($new_vars as $new_var)
{
$new_var->value = $new_var->value . "// put in global scope by $token_value()";
if(!isset($var_declares_global[$var_name]))
$var_declares_global[$var_name] = array($new_var);
else
array_unshift($var_declares_global[$var_name], $new_var);
}
}
}
// keep track of { program blocks }
} else
{
// get current dependencies in program flow
if($token === '{'
&& ($tokens[$i-1] === ')' || $tokens[$i-1] === ':'
|| (is_array($tokens[$i-1])
&& ($tokens[$i-1][0] === T_DO // do {
|| $tokens[$i-1][0] === T_ELSE // else {
|| $tokens[$i-1][0] === T_STRING)) ) ) // class bla {
{
// save brace amount at start of function
if($in_function && $brace_save_func < 0)
{
$brace_save_func = $braces_open;
}
// save brace amount at start of class
if($in_class && $brace_save_class < 0)
{
$brace_save_class = $braces_open;
}
if(empty($e))
{
$k=1;
// line_nr of the token before '{'
while( !is_numeric($line_nr) )
{
$line_nr = $tokens[$i-$k][2];
if($k>50)break;
$k++;
}
$dependency = '';
if(!$ignore_requirement)
{
$dependency = getmultiline($lines_pointer, $line_nr-1);
// if dependency is 'else' we want the 'if'
if( preg_match('/else\s*[^\w]*$/i', $dependency) )
$dependency = trim($last_dependency).'else';
} else
{
$ignore_requirement = false;
}
// add dependency (even add empty dependency, it will get poped again)
$dependencies[$line_nr] = $dependency;
} else
{
unset($e);
}
$braces_open++;
}
// before block ending "}" there must be a ";" or another "}". otherwise curly syntax
else if( $token === '}'
&& ($tokens[$i-1] === ';' || $tokens[$i-1] === '}' || $tokens[$i-1] === '{') )
{
$braces_open--;
// delete current dependency
$last_dependency = array_pop($dependencies);
// end of function found if brace amount = amount before function start
if($in_function && $brace_save_func === $braces_open)
{
// write ending to user_function list for referencing functions in output
$ref_name = ($in_class ? $class_name.'::' : '') . $function_name;
$GLOBALS['user_functions_offset'][$ref_name][2] = $line_nr;
// reset vars for next function declaration
$brace_save_func = -1;
$in_function = false;
$function_params = array();
$var_declares_local = array();
$put_in_global_scope = array();
// load new found vulnerable user functions to current scanlist
if(isset($GLOBALS['user_functions'][$file_name]))
{
$scan_functions = array_merge($scan_functions,
$GLOBALS['user_functions'][$file_name]);
}
}
// end of class found
if($in_class && $brace_save_class === $braces_open)
{
$brace_save_class = -1;
$in_class = false;
}
}
}
// token scanned. next.
}
// all tokens scanned.
}
?>