既然有源码可以debug,那么直接跑测试用例,来跟踪处理逻辑感觉比直接看代码理逻辑更快一些,尤其是涉及到了扫描阶段,不然不容易弄清某刻某个变量的取值。
对于所有漏洞而言,都是由sink点到source点检测是否有过滤函数,那么sink和source之间到底隔了多远,中间函数之间,类之间有何种复杂的联系也决定了当前静态检测算法提供的功能能不能正确实现正确的回溯,对测试用例列举的越详细,就能够提升阅读源码的效率。
搭配简单例子进行扫描分析:
那么对于source和sink,通过其定义位置,首先对其进行一个预先分类,方便写测试用例:
①.sink定义在函数内部不在类里:
sink in function and not in class
②.sink定义在函数外
sink in function
③.sink定义在函数内在类里:
sink in function and in class
那么对于source,此时选择$_GET,暂时不加过滤函数
第一种情况:
此时debug整个过程:
此时遍历$a=$_GET['tr1ple'];时,这种情况涉及到变量的赋值,分为以下几种情况:
1.赋值一个非数组变量
非数组变量又分为:
a.赋值为一个函数调用的返回值
b.一个类的实例
c.普通字符串、int等基本数据类型
2.赋值一个数组变量
数组变量又分为:
a.array(1,2,3,4,5)
b.array("a"=>1,"b"=>2)
即有键和无键两种
函数定义有固定的function标识的token,通过scanner的成员变量in_function来标识此时进入函数内部
此时rips识别出的source即为$_GET[tr1ple]
此时rips识别出来是代码执行,并且已经能够识别出用户的输入被传递到函数的入口参数中,并且用户的输入作为动态的函数名,即可以导致任意函数调用
对$a=$_GET[tr1ple]的扫描规则如下:
首先在this->tokens数组中,$a所在的token数组index=0已经标识其为变量,那么接下来在rips中判断如下,进入如下逻辑:
对于当前为变量又分为以下几种情况:
a.如果下一个token为(,则可能为任意函数调用
b.如果前一个token为左花括号{,并且前前token为$,也就是${$xxx}的形式,那么这种情况可能存在变量覆盖的风险
c.如果前一个是as或者前一个是=>再前一个是变量,再前一个是as,那么就是foreach结构
d.对于在for()循环中的话,只需要关心是否前前token为for,并切下一个token为赋值运算符
e.当前是变量定义
那么在这个例子中即进入了这个if体,这里又回到之前所说,对于变量的定义要区分是数组还是非数组两大类,如果当前定义的是一个数组
那么对于非数组,就是anything,即其它类型的变量了
此时调用variable_add函数,该函数需要:
a.当前的token_value 即$a变量名
b.一个array_slice处理后的数组
其中返回的是当前tokens数组的一个子集,其中调用Analyzer的getBraceEnd函数
那么这个函数就是针对扫描当前被赋值的变量是到哪个token结束,首先定义一个游标c,然后再定义一个$newbraceopen来寻找圆括号,这里实际上对于普通的变量分为上面说过的3种:
(1).函数返回值
即 $a=func();
(2).类实例
$a=new classa();
(3).普通变量:
$a="tr1ple";
那么下面的循环实际上就是去找到;分号,也就是php语句的结束符,这里的找()在这貌似并无太大作用,因为最终返回的游标是落在;分号处的,
所以这里getBraceEnd返回+1就是分号的下一个token,即对于当前被赋值的变量,用vardeclare的end键存储它的变量声明结束的地方,这里实际就是一个游标距离,当前变量的token位置+游标距离就是;分号,所以这里这里要取的token数组包括游标从1开始到游标结束的所有,外层返回后再+1即包括当前变量所代表的token,所以variable_add的第二个变量的值即为要赋值的变量的到赋值结束的token数组,包括结束;分号。
c.comment,这里为空
d.tokenscanstart 也就是判断当前是不是赋值开始,即判断当前变量是否在常见的赋值运算符中
e.tokenscanend token扫描结束
f.line_nr 赋值变量所在的行号
g.$i 当前变量索引
h.当前变量索引为3处是否设置,没设置则初始化为数组
在该函数中,首先要完成一个VarDeclare的初始化过程,这里传入的有变量包含的tokens子集,以及comment,对于该类还要保存这个被赋值变量的行号以及当前被赋值变量的token索引
function variable_add($var_name, $tokens, $comment='', $tokenscanstart, $tokenscanstop, $linenr, $id, $array_keys=array(), $additional_keys=array())
{
// add variable declaration to beginning of varlist
$new_var = new VarDeclare($tokens,$this->comment . $comment);
$new_var->line = $linenr; //token行号
$new_var->id = $id; //token索引
if($tokenscanstart)
$new_var->tokenscanstart = $tokenscanstart;
if($tokenscanstop)
$new_var->tokenscanstop = $tokenscanstop;
// add dependencies
foreach($this->dependencies as $deplinenr=>$dependency) //如果有依赖则为当前变量添加依赖
{
if(!empty($dependency))
$new_var->dependencies[$deplinenr] = $dependency;
}
// if $GLOBALS['x'] is used outside a function its the same as using var $x, rewrite
if($var_name === '$GLOBALS' && !empty($array_keys) && !$this->in_function)
{
$var_name = '$'.array_shift($array_keys); //对于全局变量的处理,如果当前是global,那么在token重构时已经放到了global的索引为3的数组中,那么此时返回的就是某个全局变量的名字,并且如果此时不在
函数内部,则将当前该全局变量赋值给$var_name,也就是起到重新赋值的作用,这么做还是要依靠好之前的对数组重构的过程,因为global也是个数组~
}
// add additional array keys //暂时没想到这个是干啥的,不过要是不为空的话,就要和array_keys合并
if(!empty($additional_keys))
{
if(empty($array_keys))
$array_keys[] = $additional_keys;
else
$array_keys = array_merge($array_keys, array($additional_keys));
}
// add/resolve array keys
if(!empty($array_keys)) //接着解析array_keys,若其不为空
{
foreach($array_keys as $key)
{
if(!is_array($key)) //key非数组,也就是非变量的键名
$new_var->array_keys[] = $key; //取出其中的每个key,赋值给new_var的array_keys
else
{ //对于变量型的键名,这里调用get_tokens_value来拿到该token的value
$recstring = Analyzer::get_tokens_value(
$this->file_pointer,
$key,
$this->in_function ? $this->var_declares_local : $this->var_declares_global,
$this->var_declares_global,
$id
);
if(!empty($recstring))
$new_var->array_keys[] = $recstring;
else
$new_var->array_keys[] = '*';
}
}
}
//如果当前变量定义实在函数中
if($this->in_function)
{
if(!isset($this->var_declares_local[$var_name])) //如果局部变量列表中没有该值
$this->var_declares_local[$var_name] = array($new_var); //将当前被赋值变量所代表的VarDeclares放进局部变量列表中
else
array_unshift($this->var_declares_local[$var_name], $new_var); //否则将VarDeclare放入该变量所代表数组的第一位置处
// if variable was put in global scope, save assignments
// later they will be pushed to the global var list when function is called
if(in_array($var_name, $this->put_in_global_scope))
{
if(!isset($this->globals_from_function[$this->function_obj->name][$var_name]))
$this->globals_from_function[$this->function_obj->name][$var_name] = array($new_var);
else
array_unshift($this->globals_from_function[$this->function_obj->name][$var_name], $new_var);
}
} else
{//没有在函数中则说明当前变量是全局变量
if(!isset($this->var_declares_global[$var_name]))
$this->var_declares_global[$var_name] = array($new_var); //和局部一样,全局没有则放到全局变量列表中
else
array_unshift($this->var_declares_global[$var_name], $new_var); //有则放到该变量对应数组的第一位置
}
}
所以综上所述,variable_add要完成的功能即将当前被赋值变量的相关信息,包括tokens数组子集、行号、当前变量token索引等信息放到VarDeclare这个类中,然后再根据当前变量是否在函数内,决定把当前的VarDeclare实例放到当前扫描文件全局Scanner类的局部变量列表中还是全局列表中,比如这里就将$a所关联的VarDeclare最终放到了全局变量列表中
接着回到variable_add的调用处,调用结束后将保存该变量的开始和结束范围
这里要vardeclare减1是因为之前array_slice的时候第三个值+1所以要减1使范围缩小到变量定义的那一行的;分号结束处
接着就是判断当前被赋值的变量是否在Source定义的userinput中,因为当前$a不在userinput中,因此暂时跳过分析
扫描完$a后,等号直接跳过,然后到了$_GET的扫描:
这里直接到$_GET是否在Source::v_userinput的判断中,也就是下面的if判断代码
// add user input variables to global finding list
if( in_array($token_value, Sources::$V_USERINPUT) ) //如果是在userinput中
{
if(isset($this->tokens[$i][3])) //对于数组型变量,此时第三个里面保存的就是数组的键名
{
if(!is_array($this->tokens[$i][3][0])) //如果键名不是变量,是常量,则直接将当前变量以及键名保存在全局的user_input中,以及保存行号和文件名
$GLOBALS['user_input'][$token_value.'['.$this->tokens[$i][3][0].']'][$this->file_pointer][] = $line_nr;
else
//否则说明键名是个变量,则调用get_tokens_value取到该变量的值,然后再保存到user_input中
$GLOBALS['user_input'][$token_value.'['.Analyzer::get_tokens_value(
$this->file_pointer,
$this->tokens[$i][3][0],
$this->in_function ? $this->var_declares_local : $this->var_declares_global,
$this->var_declares_global,
$i
).']'][$this->file_pointer][] = $line_nr;
}
else
$GLOBALS['user_input'][$token_value][$this->file_pointer][] = $line_nr;
// count found userinput in function for graphs
if($this->in_function) //如果在函数中
{
$GLOBALS['user_functions_offset'][$this->function_obj->name][5]++;
} else
{ //如果不在函数中,则另user_function_offset的__main__的索引5加1
$GLOBALS['user_functions_offset']['__main__'][5]++;
}
}
接着继续扫描到了分号;,因为之前在扫描到$a时,已经在vardeclare中保存了$a的扫描范围,所以这里end即分号所在的索引,因此此时unset掉$vardeclare
接着扫描到function的定义:
else if($token_name === T_FUNCTION) #当前是函数标识
{
if($this->in_function) #是否已经在函数内部
{
#addError('New function declaration in function declaration of '.$this->function_obj->name.'() found. This is valid PHP syntax but not supported by RIPS now.', array_slice($this->tokens, $i, 10), $this->tokens[$i][2], $this->file_pointer);
}
else
{
$this->in_function++; #不是则标识此时进入函数内部
// the next token is the "function name()"
$i++;
$function_name = isset($this->tokens[$i][1]) ? $this->tokens[$i][1] : $this->tokens[$i+1][1]; #获取函数名
$ref_name = ($this->in_class ? $this->class_name.'::' : '') . $function_name; #如果在类内部,则拿到类名和函数名
// add POP gadgets to info
if(isset($this->info_functions[$function_name])) //info_functions是否存在该函数名
{
$GLOBALS['info'][] = $ref_name; //存在则将当前该函数的完整路径存在Global的info数组中
// add gadget to output
$found_line = highlightline(array_slice($this->tokens,$i-1,4),$this->comment,
$line_nr, $function_name, false, $function_name); //存储该函数的位置,highlightline用于输出展示
$new_find = new InfoTreeNode($found_line); //将该函数的位置存储到InfoTreeNode实例中
$new_find->title = "POP gadget $ref_name"; //title就是该函数的完整名
$new_find->lines[] = $line_nr; //存储函数开始所在行号
$new_find->filename = $this->file_pointer; //存储当前文件名
if(isset($GLOBALS['output'][$this->file_name]['gadgets'])) //如果设置了输出当前文件的gadget,则将当前函数的信息保存在treenodes中
$GLOBALS['output'][$this->file_name]['gadgets']->treenodes[] = $new_find; //treenodes是VulnBlock的实例
else
{
//new一个VulnBlock的实例,其中包括tif(当前token的循环索引,为啥不用$i,因为$i实际会变化,比如这里$i指向函数名,而$tif指向function标志)
$block = new VulnBlock($this->tif.'_'.$this->tokens[$i][2].'_'.basename($this->file_pointer), 'POP gadgets');
$block->vuln = true;
$block->treenodes[] = $new_find;
$GLOBALS['output'][$this->file_name]['gadgets'] = $block;
}
}
$c=3;
//这里声明游标来找左花括号,和分号,因为对于函数定义有两种
a.在函数定义时变写好函数体
b.只声明,不实现,也就是抽象函数
此时默认是忽略了函数的入口参数的扫描
while($this->tokens[$i+$c] !== '{' && $this->tokens[$i+$c] !== ';')
{
$c++;
}
// abstract functions ended
if($this->tokens[$i+$c] === ';') #如果是扫描到分号,则说明当前是抽象函数,则退出函数体
$this->in_function--;
// write to user_functions offset list for referencing in output
//此时记录该函数所在的文件和开始行号到user_func_offset中
$GLOBALS['user_functions_offset'][$ref_name][0] = $this->file_pointer;
$GLOBALS['user_functions_offset'][$ref_name][1] = $line_nr-1;
// save function as object
//将当前函数保存为FunctionDeclare的实例,其中传入的this->dependencytokens就为函数的左花括号或者是函数声明分号之间的所有token数组,作为该函数依赖的token
即对于函数的声明块,肯定要把它的入口参数也和它绑定保存
$this->function_obj = new FunctionDeclare($this->dependencytokens = array_slice($this->tokens,$i-1,$c+1));
$this->function_obj->lines[] = $line_nr; //函数行号
$this->function_obj->name = $function_name; //函数名
// save all function parameters
$this->function_obj->parameters = array();
$e=1;
// until function test(...) {
// OR
// interface test { public function test(...); }
//直到扫到{或者;才说明当前函数扫描结束
while( $this->tokens[$i+$e] !== '{' && $this->tokens[$i+$e] !== ';' )
{
if( is_array($this->tokens[$i + $e]) && $this->tokens[$i + $e][0] === T_VARIABLE )
{
$this->function_obj->parameters[] = $this->tokens[$i + $e][1]; //存储该函数定义的所有入口参数
}
$e++;
}
//因为已经将函数的入口参数放到函数的para变量中了,所以这里跳过对接下来函数入口参数token的扫描
// 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
}
}
接着要对函数体进行扫描:
接着继续扫描左花括号,在此例中也就是函数体,这里对于花括号解析又分为以下几种情况:
a. ) 和{
b.:和{ 即 case x:{
c.case x;{
d.do {
e.else {
f.class bla {
g.try {
h. catch{
这里就包含了php语法中所有可能的左花括号和前一种token的搭配情况
if($this->tokens[$i] === '{'
&& ($this->tokens[$i-1] === ')' || $this->tokens[$i-1] === ':' || $this->tokens[$i-1] === ';' // case x:{ or case x;{
|| (is_array($this->tokens[$i-1])
&& ($this->tokens[$i-1][0] === T_DO // do {
|| $this->tokens[$i-1][0] === T_ELSE // else {
|| $this->tokens[$i-1][0] === T_STRING // class bla {
|| $this->tokens[$i-1][0] === T_TRY // try {
|| $this->tokens[$i-1][0] === T_CATCH)) ) ) // catch{
{
// save brace amount at start of function
if($this->in_function && $this->brace_save_func < 0) //因为当前已经扫过function标志时将in_function置1,并且brace_save_func默认是-1
{
$this->brace_save_func = $this->braces_open; //将brace_save_func置0
}
// save brace amount at start of class
if($this->in_class && $this->brace_save_class < 0) //如果是在类中,则将brace_save_class置0
{
$this->brace_save_class = $this->braces_open;
}
$this->in_condition = 0;
if(empty($e))
{
if(!$this->ignore_requirement)
{
if(!empty($this->dependencytokens)
&& $this->dependencytokens[0][0] === T_ELSE && $this->dependencytokens[1][0] !== T_IF )
{
$this->dependencytokens = $this->last_dependency;
$this->dependencytokens[] = array(T_ELSE, 'else', $this->dependencytokens[0][2]);
}
} else
{
$this->ignore_requirement = false;
}
// add dependency (even push empty dependency on stack, it will get poped again)
$this->dependencies[$line_nr] = $this->dependencytokens;
$this->dependencytokens = array();
} else
{
unset($e);
}
$this->braces_open++;
}
get_token_value的入口参数包括:
(1).filename 当前被扫描的文件指针,也就是文件名
(2).tokens 当前数组键名
(3).当前变量赋值是在函数内部还是在函数外部
(4).当前被赋值的变量的token索引
(5).start
(6).stop
(7).source_functions 输入源
function get_tokens_value($file_name, $tokens, $var_declares, $var_declares_global, $tokenid, $start=0, $stop=0, $source_functions=array())
{
$value = '';
if(!$stop) $stop = count($tokens); //默认stop赋值为1
// check all tokens until instruction ends
for($i=$start; $i<$stop; $i++) //对传入的token进行遍历
{
if( is_array($tokens[$i]) )
{
// trace variables for its values
if( $tokens[$i][0] === T_VARIABLE
|| ($tokens[$i][0] === T_STRING
&& $tokens[$i+1] !== '(' ) )
{
if(!in_array($tokens[$i][1], Sources::$V_USERINPUT))
{
// constant CONSTANTS
if ($tokens[$i][1] === 'DIRECTORY_SEPARATOR')
$value .= '/';
else if ($tokens[$i][1] === 'PATH_SEPARATOR')
$value .= ';';
// global $varname -> global scope, CONSTANTS
else if( (isset($tokens[$i-1]) && is_array($tokens[$i-1]) && $tokens[$i-1][0] === T_GLOBAL) || $tokens[$i][1][0] !== '$' )
$value .= self::get_var_value($file_name, $tokens[$i], $var_declares_global, $var_declares_global, $tokenid);
// local scope
else
$value .= self::get_var_value($file_name, $tokens[$i], $var_declares, $var_declares_global, $tokenid);
} else
{
if(isset($tokens[$i][3]))
$parameter_name = str_replace(array("'",'"'), '', $tokens[$i][3][0]);
else
$parameter_name = '';
// mark userinput for quote analysis
if( ($tokens[$i][1] !== '$_SERVER' || (empty($parameter_name) || in_array($parameter_name, Sources::$V_SERVER_PARAMS) || substr($parameter_name,0,5) === 'HTTP_'))
&& !((is_array($tokens[$i-1])
&& in_array($tokens[$i-1][0], Tokens::$T_CASTS))
|| (is_array($tokens[$i+1])
&& in_array($tokens[$i+1][0], Tokens::$T_ARITHMETIC))) )
$value.='$_USERINPUT';
else
$value.='1';
}
}
// add strings
// except first string of define('var', 'value')
else if( $tokens[$i][0] === T_CONSTANT_ENCAPSED_STRING
&& !($tokens[$i-2][0] === T_STRING && $tokens[$i-2][1] === 'define'))
{
// add string without quotes
$value .= substr($tokens[$i][1], 1, -1);
}
// add directory name dirname(__FILE__)
else if( $tokens[$i][0] === T_FILE
&& ($tokens[$i-2][0] === T_STRING && $tokens[$i-2][1] === 'dirname'))
{
// overwrite value because __FILE__ is absolute
// add slash just to be sure
$value = dirname($file_name).'/';
}
// add numbers
else if( $tokens[$i][0] === T_LNUMBER || $tokens[$i][0] === T_DNUMBER || $tokens[$i][0] === T_NUM_STRING )
{
$value .= round($tokens[$i][1]);
}
else if( $tokens[$i][0] === T_ENCAPSED_AND_WHITESPACE )
{
$value .= $tokens[$i][1];
}
// if in foreach($bla as $key=>$value) dont trace $key, $value back
else if( $tokens[$i][0] === T_AS )
{
break;
}
// function calls
else if($tokens[$i][0] === T_STRING && $tokens[$i+1] === '(')
{
// stop if strings are fetched from database/file (otherwise SQL query will be added)
if (in_array($tokens[$i][1], Sources::$F_DATABASE_INPUT) || in_array($tokens[$i][1], Sources::$F_FILE_INPUT) || isset(Info::$F_INTEREST[$tokens[$i][1]]))
{
break;
}
// add userinput for functions that return userinput
else if(in_array($tokens[$i][1], $source_functions))
{
$value .= '$_USERINPUT';
}
}
}
}
return $value;
}
具体扫描php文件的逻辑是在Sanner这个类中,通过parse方法来实现:
此时开始解析php文件中的token,进行污点分析
1.第一个if(当前token为数组)
此时首先保存当前token的标识,token值以及token所在行号
1.1当前tocken为变量类型
1.1.1如果当前为变量+左花括号则为动态函数调用
此时理论上应该回溯该变量,调用variable_scan方法,传入当前token的索引i,以及偏移量0,当前类型为eval型,以及对应的title(理论上应该输出的漏洞描述信息)
function variable_scan($i, $offset, $category, $title)
{
if(isset($this->scan_functions[$category])) #如果待扫描的函数类型指定了eval
{
// build new find
$new_find = new VulnTreeNode(); # tree code ,就是把当前节点设为一个漏洞树节点,该类是定义在lib/constructor.php中
$new_find->name = $category; #存入eval类型
$new_find->lines[] = $this->tokens[$i][2]; #当前变量的行数
// count sinks
$GLOBALS['file_sinks_count'][$this->file_pointer]++; #当前文件的sink点+1 (最后要输出这个数量,所以用个全局变量保存)
if($this->in_function) #此时判断当前检测的变凉是不是在函数内部定义的,也就是之前有没有扫到function标识,扫到就标识此时该变量在函数内部
{
$GLOBALS['user_functions_offset'][$this->function_obj->name][6]++;
} else
{
$GLOBALS['user_functions_offset']['__main__'][6]++;
}
// add dependencies
foreach($this->dependencies as $deplinenr=>$dependency) #找依赖
{
if(!empty($dependency))
$new_find->dependencies[$deplinenr] = $dependency;
}
// trace back parameters and look for userinput
$userinput = $this->scan_parameter(
$new_find,
$new_find,
$this->tokens[$i],
$this->tokens[$i][3],
$i,
$this->in_function ? $this->var_declares_local : $this->var_declares_global,
$this->var_declares_global,
false,
array()
);
// add find to output if function call has variable parameters (With userinput)
if( $userinput || $GLOBALS['verbosity'] == 4 )
{
$new_find->filename = $this->file_pointer;
$new_find->value = highlightline(array_slice($this->tokens, $i-$offset, $offset+3+Analyzer::getBraceEnd($this->tokens, $i+2)), $this->comment, $this->tokens[$i][2], $this->tokens[$i][1], false, array(1));
// add to output
$new_find->title = $title;
$block = new VulnBlock($this->tif.'_'.$this->tokens[$i][2].'_'.basename($this->file_pointer), getVulnNodeTitle($category), $this->tokens[$i][1]);
$block->treenodes[] = $new_find;
if($userinput == 1 || $GLOBALS['verbosity'] == 4)
{
$block->vuln = true;
increaseVulnCounter($category);
}
$GLOBALS['output'][$this->file_name][] = $block;
if($this->in_function)
{
$this->ignore_securing_function = true;
// mark function in class as vuln
if($this->in_class)
{
$this->vuln_classes[$this->class_name][] = $this->function_obj->name;
}
}
// add register_globals implementation
if($category === 'extract')
{
$this->variable_add(
'register_globals',
array_merge(array_slice($this->tokens, $i-$offset, ($end=$offset+3+Analyzer::getBraceEnd($this->tokens, $i+2))),array(array(T_COMMENT,'// is like ',0),array(T_STRING,'import_request_variables',0),'(',')')),
'see above',
1, $end+2,
$this->tokens[$i][2],
$i,
isset($this->tokens[$i][3]) ? $this->tokens[$i][3] : array()
);
}
}
}
}