php 简析对象,PHP白盒审计工具RIPS源码简析
RIPS是一款對PHP源碼進行風險掃描的工具,其對代碼掃描的方式是常規的正則匹配,確定sink點;還是如flowdroid構建全局數據流圖,并分析存儲全局數據可達路徑;下面就從其源碼上略探一二。
1、掃描流程
分析其源碼前,我們需要縷清其掃描的流程,方便后面的分析,下圖展示其進行掃描的主界面:
先簡單介紹下每個標簽的基本功能: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:
include('para.php');
$str = 'command';
$command = para($str);
shell_exec( $command );
?>
para.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; $itokenscanstop; $i++)
{ ...
else if( in_array($tokens[$i][1], $this->source_functions) )
{
$userinput = true;
$var_trace->marker = 4;
$mainparent->title = 'Userinput returned by function '.$tokens[$i][1].'() reaches sensitive sink.';
3、結語
上面對于RIPS的源碼進行了簡單的分析,從中可以看出,其工作的流程大致為遍歷token,發現sink點,然后對sink點的參數使用scan_parameter進行后向追蹤,如果這個參數是用戶可控制的參數,及包含在source點中,那么就存在一條從source到sink的聯通路徑,及存在一條漏洞觸發的路徑,則認為是一個風險點。
* 本文作者:nightmarelee,轉載請注明來自FreeBuf.COM
總結
以上是生活随笔為你收集整理的php 简析对象,PHP白盒审计工具RIPS源码简析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql主从和dump_MySQL主从
- 下一篇: php中一级标题和二级标题,什么是一级标