PHP白盒审计工具RIPS源码简析

栏目: PHP · 发布时间: 9年前

内容简介:PHP白盒审计工具RIPS源码简析

RIPS是一款对 PHP 源码进行风险扫描的工具,其对代码扫描的方式是常规的正则匹配,确定sink点;还是如flowdroid构建全局数据流图,并分析存储全局数据可达路径;下面就从其源码上略探一二。

1、扫描流程

分析其源码前,我们需要缕清其扫描的流程,方便后面的分析,下图展示其进行扫描的主界面:

PHP白盒审计工具RIPS源码简析

先简单介绍下每个标签的基本功能:

path/file:待扫描代码的文件地址;

subdirs:是否对代码的子目录进行扫描,勾选将会扫描子目录,不勾选只扫描当前目录下的PHP文件;

verbosity level:选择source点,即可控制的输入点,定义在rips下config/sources.php中;

vuln type:选择sink点,即可能会触发各种风险的函数,定义在rips下config/sinks.php中;

scan:选择好前面的选项,点击该按钮即可开始扫描;

code style:扫描结果的展示方式;

/regex/:要搜索内容的正则表达式;

search:根据正则表达式对全局代码进行搜索;

在进行扫描时一般将扫描文件目录粘贴到第一栏中,点击scan进行扫描,那么这个scan就是执行扫描的开始点;点击scan按钮会调用js/script.js中的scan方法进行扫描,该方法将会获取在主界面中获取的参数,并通过XMLHttpRequest方法传递给rips主目录下的main.php中进行处理。在main.php中主要执行一些赋值的操作,及调用scanner.php进行具体的扫描,下面的代码便是其调用scanner.php的相关代码。

// scan

$scan = new Scanner($file_scanning, $scan_functions, $info_functions, $source_functions);
$scan->parse();
$scanned_files[$file_scanning] = $scan->inc_map;

其赋值对象主要是 $file_scanning, $scan_functions, $info_functions, $source_functions这四个对象,四个对象的含义如下所示:

$file_scanning:表示要扫描的php文件,如果扫描的对象是一个文件,那么该参数就代表这个对象本身;如果扫描对象是一个目录,RIPS将会对目录中的文件进行逐个扫描,该对象就代表目录中的每个文件。

$scan_functions:sink点,会触发漏洞的函数名称的列表,根据选择的 vuln type,通过 config/sinks.php进行构造。

$info_functions:设备信息,根据扫描文件中使用的函数特征值确定,通过config/info.php进行构造。

$source_functions:source点, 可控制的输入点,通过config/sources.php进行构造。

scanner的扫描可以把它大致分为两步,第一步是初始化Scanner对象;第二步则是最关键的漏洞扫描,通过parse()方法进行。

2、代码扫描

2.1 初始化Scanner对象

此处主要通过__construct方法执行一些初始化操作,对其中一些关键代码进行说明:

function __construct($file_name, $scan_functions, $info_functions, $source_functions)

{
$this->file_name = $file_name;
$this->scan_functions = $scan_functions;
$this->info_functions = $info_functions;
$this->source_functions = $source_functions;

此处主要是将main.php中传递过来的文件赋值给类变量,这几个变量是初始化后面一些类变量的基础。下面将是初始化的关键步骤,为方便说明将在代码中直接进行注释说明:

$this->inc_file_stack = array(realpath($this->file_name));                           // 待扫描文件的真实地址,存入数组中

$this->inc_map = array();

$this->include_paths = Analyzer::get_ini_paths(ini_get("include_path"));   // 文件所包含的路径,单个结果一般为:Array( [0] => .  [1] => ),即文件的自身路径

$this->file_pointer = end($this->inc_file_stack);                                          // 文件地址数组中最后的元素,值为文件自身真实路径

if(!isset($GLOBALS['file_sinks_count'][$this->file_pointer]))

$GLOBALS['file_sinks_count'][$this->file_pointer] = 0;                              // 初始化该文件sink点统计数目

$this->lines_stack = array();

$this->lines_stack[] = file($this->file_name);                                             // 读取待扫描文件内容,存储到一个数组中

$this->lines_pointer = end($this->lines_stack);                                        // 由于文件内容存储在数组的第一个元素中,且数组长度为1,此处代表将文件内容逐行存储在一个数组中

$this->tif = 0; // tokennr in file

$this->tif_stack = array();

// preload output

echo $GLOBALS['fit'] . '|' . $GLOBALS['file_amount'] . '|' . $this->file_pointer . ' (tokenizing)|' . $GLOBALS['timeleft'] . '|' . "\n";

@ob_flush();

flush();

// tokenizing

$tokenizer = new Tokenizer($this->file_pointer);

$this->tokens = $tokenizer->tokenize(implode('',$this->lines_pointer));

unset($tokenizer);                                                                                     // 上面几行是整个分析的关键,将在下面进行详细的说明

// add auto includes from php.ini

if(ini_get('auto_prepend_file')) { $this->add_auto_include(ini_get('auto_prepend_file'), true);}

if(ini_get('auto_append_file')) { $this->add_auto_include(ini_get('auto_append_file'), false);}

// 校验php配置文件(php.ini)中是否存在自动包含的文件,如果存在将直接添加到 $this->tokens的类变量中

此处将粗略说明 $this->tokens类变量的生成,该变量的生成主要调用lib/tokenizer.php中的方法,下面是其关键代码:

public function tokenize($code) {
$this->tokens = token_get_all($code);
$this->prepare_tokens();
$this->array_reconstruct_tokens();
$this->fix_tokens();
$this->fix_ternary();
#die(print_r($this->tokens));
return $this->tokens;
}

通过调用ZEND引擎的 token_get_all方法将PHP源码分解成PHP tokens(参考: http://php.net/manual/en/function.token-get-all.php ),并对这些tokens进行相关处理优化,处理优化的过程没有进行仔细的研究,此处不做详细介绍。为了让大家对ZEND引擎生成的tokens有个更直观的认识,这是将使用一个简单的例子分别展示源码、 token_get_all生成的原始tokens、处理后的tokens,通过后面的对比可以粗略的看出,处理后的tokens比原始生成的tokens更加简洁,去除了一些对于风险扫描无用的tokens,如<?php、?>、空字节等。如下所示:

源码:

<?php echo $_GET('info'); ?>

原始tokens:

Array
(
    [0] => Array
        (
            [0] => 374
            [1] => <?php
            [2] => 1
        )
    [1] => Array
        (
            [0] => 317
            [1] => echo
            [2] => 2
        )
    [2] => Array
        (
            [0] => 377
            [1] =>  
            [2] => 2
        )
    [3] => Array
        (
            [0] => 310
            [1] => $_GET
            [2] => 2
        )
    [4] => (
    [5] => Array
        (
            [0] => 316
            [1] => 'info'
            [2] => 2
        )
    [6] => )
    [7] => ;
    [8] => Array
        (
            [0] => 377
            [1] => 
            [2] => 2
        )
    [9] => Array
        (
            [0] => 376
            [1] => ?>
            [2] => 3
        )
)

处理后的tokens:

Array
(
    [0] => Array
        (
            [0] => 317
            [1] => echo
            [2] => 2
        )
    [1] => Array
        (
            [0] => 310
            [1] => $_GET
            [2] => 2
        )
    [2] => (
    [3] => Array
        (
            [0] => 316
            [1] => 'info'
            [2] => 2
        )
    [4] => )
    [5] => ;
    [6] => ;
)

2.2 parse扫描

获取了需要扫描的PHP tokens,下一步就是进行最关键的风险扫描了,风险扫描主体函数在lib/scanner.php文件中的parse()方法。该方法中会遍历2.1中生成的tokens,对tokens进行逐个扫描,根据每个token是否为数组(is_array)分别进行操作,由于整体代码比较庞杂,此处挑选处理上的几个关键点,并结合实际的代码,对其扫描的方式进行探究。下面先展示本次测试使用的源码,主要包含两个文件commond_exec.php与para.php两个文件,源码如下所示:

commond_exec.php:
<?php
include('para.php');
$str = 'command';
$command = para($str);
shell_exec( $command );
?>
para.php:
<?php
function para($str){
return $_GET($str);
}
?>

扫描的目标文件是commond_exec.php,此时其生成的tokens如下所示:

Array
(
    [0] => Array
        (
            [0] => 262
            [1] => include
            [2] => 2
        )
    [1] => (
    [2] => Array
        (
            [0] => 318
            [1] => 'para.php'
            [2] => 2
        )
    [3] => )
    [4] => ;
    [5] => Array
        (
            [0] => 312
            [1] => $str
            [2] => 4
        )
    [6] => =
    [7] => Array
        (
            [0] => 318
            [1] => 'command'
            [2] => 4
        )
    [8] => ;
    [9] => Array
        (
            [0] => 312
            [1] => $command
            [2] => 5
        )
    [10] => =
    [11] => Array
        (
            [0] => 310
            [1] => para
            [2] => 5
        )
    [12] => (
    [13] => Array
        (
            [0] => 312
            [1] => $str
            [2] => 5
        )
    [14] => )
    [15] => ;
    [16] => Array
        (
            [0] => 310
            [1] => shell_exec
            [2] => 6
        )
    [17] => (
    [18] => Array
        (
            [0] => 312
            [1] => $command
            [2] => 6
        )
    [19] => )
    [20] => ;
    [21] => ;
)

对tokens进行遍历时,如果该token的类型是数组,那么分别获取该数组中的每个值,如下所示:

$token_name = $this->tokens[$i][0];     // 该token的名称,相当于变量名称
$token_value = $this->tokens[$i][1];    // 该token的值,相当于变量的值
$line_nr = $this->tokens[$i][2];            // token出现在源码的第几行

2.2.1 文件包含处理

对token进行逐个扫描时,第一个出现的token就是便是include函数,RIPS遇到这个函数时会根据文件包含出现的位置,获取被包含文件的tokens,插入到原tokens语句的后面,其具体的操作代码如下所示:

$tokenizer = new Tokenizer($try_file);
$inc_tokens = $tokenizer->tokenize(implode('',$inc_lines));
unset($tokenizer);
// if(include('file')) { - include tokens after { and not into the condition :S
if($this->in_condition)
{
$this->tokens = array_merge(
array_slice($this->tokens, 0, $this->in_condition+1), // before include in condition
$inc_tokens, // included tokens
array(array(T_INCLUDE_END, 0, 1)), // extra END-identifier
array_slice($this->tokens, $this->in_condition+1) // after condition
);
} else
{
// insert included tokens in current tokenlist and mark end
$this->tokens = array_merge(
array_slice($this->tokens, 0, $i+$skip), // before include
$inc_tokens, // included tokens
array(array(T_INCLUDE_END, 0, 1)), // extra END-identifier
array_slice($this->tokens, $i+$skip) // after include
);
}

最后生成的包含include文件的tokens如下所示,对比下会发现5-19的token是新添加的,为被包含文件para.php的tokens。

Array
(
    [0] => Array
        (
            [0] => 262
            [1] => include
            [2] => 2
        )
    [1] => (
    [2] => Array
        (
            [0] => 318
            [1] => 'para.php'
            [2] => 2
        )
    [3] => )
    [4] => ;
    [5] => Array
        (
            [0] => 337
            [1] => function
            [2] => 2
        )
    [6] => Array
        (
            [0] => 310
            [1] => para
            [2] => 2
        )
    [7] => (
    [8] => Array
        (
            [0] => 312
            [1] => $str
            [2] => 2
        )
    [9] => )
    [10] => {
    [11] => Array
        (
            [0] => 339
            [1] => return
            [2] => 3
        )
    [12] => Array
        (
            [0] => 312
            [1] => $_GET
            [2] => 3
        )
    [13] => (
    [14] => Array
        (
            [0] => 312
            [1] => $str
            [2] => 3
        )
    [15] => )
    [16] => ;
    [17] => }
    [18] => ;
    [19] => Array
        (
            [0] => 380
            [1] => 0
            [2] => 1
        )
    [20] => Array
        (
            [0] => 312
            [1] => $str
            [2] => 4
        )
    [21] => =
    [22] => Array
        (
            [0] => 318
            [1] => 'command'
            [2] => 4
        )
    [23] => ;
    [24] => Array
        (
            [0] => 312
            [1] => $command
            [2] => 5
        )
    [25] => =
    [26] => Array
        (
            [0] => 310
            [1] => para
            [2] => 5
        )
    [27] => (
    [28] => Array
        (
            [0] => 312
            [1] => $str
            [2] => 5
        )
    [29] => )
    [30] => ;
    [31] => Array
        (
            [0] => 310
            [1] => shell_exec
            [2] => 6
        )
    [32] => (
    [33] => Array
        (
            [0] => 312
            [1] => $command
            [2] => 6
        )
    [34] => )
    [35] => ;
    [36] => ;
)

2.2.2 添加数据源(source点)

当扫描到第11个token return时,此时会判断返回的语句是否是用户可以控制的语句,如果这条语句是用户能够控制的语句,比如此处使用$_GET进行赋值表明是用户可以控制的语句;也就是说para()方法的返回值是用户可以控制的,那么该方法返回的数据将被认为是一个被污染的数据源,即source点并将该方法添加到source_functions的数组中。对于return返回参数是否是用户可控制的判断,主要是通过函数scan_parameter()实现的,下面抽取几个关键点来了解判断流程的实现,当遇到token为return的语句时,会向后遍历token,直到该语句结束,代码的实现上是通过“;”是否出现进行判断,如下所示:

while( $this->tokens[$i + $c] !== ';' )

对于每个token,判断该token是否是一个数组,如果是一个数组则检查数组元素是否是一个变量,如下所示:

if( is_array($this->tokens[$i + $c]) )
{
if( $this->tokens[$i + $c][0] === T_VARIABLE )

如果该token是一个数组且为变量,则使用scan_parameter()函数对其进行检查,该函数调用形式如下。该调用的参数比较多,但是本例中实际起到判断作用的只有第三个参数,即这个token本身:$this->tokens[$i+$c],具体的值为:tokens[12],即$_GET函数。

$new_find = new VulnTreeNode();
$userinput = $this->scan_parameter(
$new_find, 
$new_find, 
$this->tokens[$i+$c], 
$this->tokens[$i+$c][3],
$i+$c,
$this->var_declares_local,  
$this->var_declares_global,
false, 
$GLOBALS['F_SECURES_ALL'], 
TRUE
);

由于$_GET函数为定义的source函数,因此将直接认为返回值是用户可输入的,即$userinput=true。最后 将此函数名添加到source_functions列中,以后的扫描该函数将作为source点看待。

if($userinput == 1 || $GLOBALS['userfunction_taints'])
{
$this->source_functions[] = $this->function_obj->name;
}

2.2.3 添加风险点(sink点)

此处实际是RIPS的一个误报,RIPS将$_GET()作为可变函数名对待,如果函数名可变那么就可以将该函数名赋值为eval,从而造成代码执行的漏洞,sink点的添加也是在scan_parameter()中进行。由于此处是 $_GET(),显然此函数包含在source函数中,因此使用 scan_parameter()函数其返回值肯定为true,那么在函数内部将会触发如下代码块的执行。

if($this->in_function && !$return_scan)

{
$this->addtriggerfunction($mainparent);
}

触发后主要执行的函数是addtriggerfunction(),该函数的作用主要是向$GLOBALS变量中添加该函数。

$GLOBALS['user_functions'][$this->file_name][$this->function_obj->name][0][0] = 0;
// no securings
$GLOBALS['user_functions'][$this->file_name][$this->function_obj->name][1] = array();
// doesnt matter if called with userinput or not
$GLOBALS['user_functions'][$this->file_name][$this->function_obj->name][3] = true;

最后在包含文件扫描结束时,即token=”}”,此处是第17个token,将被全局变量合并到scan_functions中,即添加到sink点。

if(isset($GLOBALS['user_functions'][$this->file_name]))
{
$this->scan_functions = array_merge($this->scan_functions, $GLOBALS['user_functions'][$this->file_name]);
}

2.2.4 命令执行(shell_exec)漏洞

这个漏洞是本代码中实际包含的一个漏洞,在上面各种准备工作完成后,来看一下这个实际漏洞的扫描流程;当token为 shell_exec时, 由于该函数是一个危险函数,即包含在sink点,那么分析将直接跳转到TAINT ANALYSIS中进行。同2.2.2类似,会跳转到scan_parameter()函数中对函数的参数进行分析,确定该参数是否是用户可控制的,即包含在source点内。该函数的参数是变量$command,该参数是一个自定义变量,RIPS对于自定义变量会进行自动扫描并通过函数variable_add()添加到var_declares_local、var_declares_global两个变量中的一个。

下面先对variable_add()函数进行简单介绍,当遍历到tokens[24], $command的赋值操作时,会触发该函数的执行。该 函数调用形式如下,其中比较关键的是第二个参数,调用Analyzer::getBraceEnd()静态方法,获取该变量声明的所有token,此处$command的token的序列号为24-30,将这些tokens存储到一个数组中,最后将该变量的相关信息存入var_declares_global数组中。这样就完成了对一个文件中的全局遍历的发现及存储。

$this->variable_add(
$token_value, 
array_slice($this->tokens, $i-$c, $c+Analyzer::getBraceEnd($this->tokens, $i)), 
'', 
0, 0, 
$line_nr, 
$i, 
isset($this->tokens[$i][3]) ? $this->tokens[$i][3] : array()
);

由于存储了该变量的tokens信息,那么对于自定义变量的分析,就转变成了对该变量的tokens的分析,遍历该变量的tokens,如果该token来自用户可控制的输入,即sorce点数据源,那么表明自定义变量的也是可控的,此处的source点就是自添加的函数para(),这样就存在一个用户可控制的数据源(source)流向危险函数(sink),形成了一个漏洞触发的完整路径。

for($i=$var_declare->tokenscanstart; $i<$var_declare->tokenscanstop; $i++)
{ ...
else if( in_array($tokens[$i][1], $this->source_functions) )
{
$userinput = true;
$var_trace->marker = 4;
$mainparent->title = 'Userinput returned by function <i>'.$tokens[$i][1].'()</i> reaches sensitive sink.';

3、结语

上面对于RIPS的源码进行了简单的分析,从中可以看出,其工作的流程大致为遍历token,发现sink点,然后对sink点的参数使用 scan_parameter进行后向追踪,如果这个参数是用户可控制的参数,及包含在source点中,那么就存在一条从source到sink的联通路径,及存在一条漏洞触发的路径,则认为是一个风险点。

* 本文作者:nightmarelee,转载请注明来自FreeBuf.COM


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Ajax Design Patterns

Ajax Design Patterns

Michael Mahemoff / O'Reilly Media / 2006-06-29 / USD 44.99

Ajax, or Asynchronous JavaScript and XML, exploded onto the scene in the spring of 2005 and remains the hottest story among web developers. With its rich combination of technologies, Ajax provides a s......一起来看看 《Ajax Design Patterns》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具