php 简析对象,PHP白盒审计工具RIPS源码简析
RIPS是一款對(duì)PHP源碼進(jìn)行風(fēng)險(xiǎn)掃描的工具,其對(duì)代碼掃描的方式是常規(guī)的正則匹配,確定sink點(diǎn);還是如flowdroid構(gòu)建全局?jǐn)?shù)據(jù)流圖,并分析存儲(chǔ)全局?jǐn)?shù)據(jù)可達(dá)路徑;下面就從其源碼上略探一二。
1、掃描流程
分析其源碼前,我們需要縷清其掃描的流程,方便后面的分析,下圖展示其進(jìn)行掃描的主界面:
先簡(jiǎn)單介紹下每個(gè)標(biāo)簽的基本功能:path/file:待掃描代碼的文件地址;
subdirs:是否對(duì)代碼的子目錄進(jìn)行掃描,勾選將會(huì)掃描子目錄,不勾選只掃描當(dāng)前目錄下的PHP文件;
verbosity level:選擇source點(diǎn),即可控制的輸入點(diǎn),定義在rips下config/sources.php中;
vuln type:選擇sink點(diǎn),即可能會(huì)觸發(fā)各種風(fēng)險(xiǎn)的函數(shù),定義在rips下config/sinks.php中;
scan:選擇好前面的選項(xiàng),點(diǎn)擊該按鈕即可開(kāi)始掃描;
code style:掃描結(jié)果的展示方式;
/regex/:要搜索內(nèi)容的正則表達(dá)式;
search:根據(jù)正則表達(dá)式對(duì)全局代碼進(jìn)行搜索;
在進(jìn)行掃描時(shí)一般將掃描文件目錄粘貼到第一欄中,點(diǎn)擊scan進(jìn)行掃描,那么這個(gè)scan就是執(zhí)行掃描的開(kāi)始點(diǎn);點(diǎn)擊scan按鈕會(huì)調(diào)用js/script.js中的scan方法進(jìn)行掃描,該方法將會(huì)獲取在主界面中獲取的參數(shù),并通過(guò)XMLHttpRequest方法傳遞給rips主目錄下的main.php中進(jìn)行處理。在main.php中主要執(zhí)行一些賦值的操作,及調(diào)用scanner.php進(jìn)行具體的掃描,下面的代碼便是其調(diào)用scanner.php的相關(guān)代碼。// scan
$scan = new Scanner($file_scanning, $scan_functions, $info_functions, $source_functions);
$scan->parse();
$scanned_files[$file_scanning] = $scan->inc_map;
其賦值對(duì)象主要是$file_scanning, $scan_functions, $info_functions, $source_functions這四個(gè)對(duì)象,四個(gè)對(duì)象的含義如下所示:$file_scanning:表示要掃描的php文件,如果掃描的對(duì)象是一個(gè)文件,那么該參數(shù)就代表這個(gè)對(duì)象本身;如果掃描對(duì)象是一個(gè)目錄,RIPS將會(huì)對(duì)目錄中的文件進(jìn)行逐個(gè)掃描,該對(duì)象就代表目錄中的每個(gè)文件。
$scan_functions:sink點(diǎn),會(huì)觸發(fā)漏洞的函數(shù)名稱的列表,根據(jù)選擇的vuln type,通過(guò)config/sinks.php進(jìn)行構(gòu)造。
$info_functions:設(shè)備信息,根據(jù)掃描文件中使用的函數(shù)特征值確定,通過(guò)config/info.php進(jìn)行構(gòu)造。
$source_functions:source點(diǎn),可控制的輸入點(diǎn),通過(guò)config/sources.php進(jìn)行構(gòu)造。
scanner的掃描可以把它大致分為兩步,第一步是初始化Scanner對(duì)象;第二步則是最關(guān)鍵的漏洞掃描,通過(guò)parse()方法進(jìn)行。
2、代碼掃描
2.1 初始化Scanner對(duì)象
此處主要通過(guò)__construct方法執(zhí)行一些初始化操作,對(duì)其中一些關(guān)鍵代碼進(jìn)行說(shuō)明: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中傳遞過(guò)來(lái)的文件賦值給類變量,這幾個(gè)變量是初始化后面一些類變量的基礎(chǔ)。下面將是初始化的關(guān)鍵步驟,為方便說(shuō)明將在代碼中直接進(jìn)行注釋說(shuō)明:$this->inc_file_stack = array(realpath($this->file_name)); ? ? ? ? ? ? ? ? ? ? ? ? ? // 待掃描文件的真實(shí)地址,存入數(shù)組中
$this->inc_map = array();
$this->include_paths = Analyzer::get_ini_paths(ini_get("include_path")); ? // 文件所包含的路徑,單個(gè)結(jié)果一般為:Array(?[0] => . ?[1] =>?),即文件的自身路徑
$this->file_pointer = end($this->inc_file_stack); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 文件地址數(shù)組中最后的元素,值為文件自身真實(shí)路徑
if(!isset($GLOBALS['file_sinks_count'][$this->file_pointer]))
$GLOBALS['file_sinks_count'][$this->file_pointer] = 0; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 初始化該文件sink點(diǎn)統(tǒng)計(jì)數(shù)目
$this->lines_stack = array();
$this->lines_stack[] = file($this->file_name); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 讀取待掃描文件內(nèi)容,存儲(chǔ)到一個(gè)數(shù)組中
$this->lines_pointer = end($this->lines_stack); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 由于文件內(nèi)容存儲(chǔ)在數(shù)組的第一個(gè)元素中,且數(shù)組長(zhǎng)度為1,此處代表將文件內(nèi)容逐行存儲(chǔ)在一個(gè)數(shù)組中
$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); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 上面幾行是整個(gè)分析的關(guān)鍵,將在下面進(jìn)行詳細(xì)的說(shuō)明
// 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);}
// 校驗(yàn)php配置文件(php.ini)中是否存在自動(dòng)包含的文件,如果存在將直接添加到$this->tokens的類變量中
此處將粗略說(shuō)明$this->tokens類變量的生成,該變量的生成主要調(diào)用lib/tokenizer.php中的方法,下面是其關(guān)鍵代碼: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;
}
通過(guò)調(diào)用ZEND引擎的token_get_all方法將PHP源碼分解成PHP tokens(參考:http://php.net/manual/en/function.token-get-all.php),并對(duì)這些tokens進(jìn)行相關(guān)處理優(yōu)化,處理優(yōu)化的過(guò)程沒(méi)有進(jìn)行仔細(xì)的研究,此處不做詳細(xì)介紹。為了讓大家對(duì)ZEND引擎生成的tokens有個(gè)更直觀的認(rèn)識(shí),這是將使用一個(gè)簡(jiǎn)單的例子分別展示源碼、token_get_all生成的原始tokens、處理后的tokens,通過(guò)后面的對(duì)比可以粗略的看出,處理后的tokens比原始生成的tokens更加簡(jiǎn)潔,去除了一些對(duì)于風(fēng)險(xiǎn)掃描無(wú)用的tokens,如<?php 、?>、空字節(jié)等。如下所示:
源碼:<?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,下一步就是進(jìn)行最關(guān)鍵的風(fēng)險(xiǎn)掃描了,風(fēng)險(xiǎn)掃描主體函數(shù)在lib/scanner.php文件中的parse()方法。該方法中會(huì)遍歷2.1中生成的tokens,對(duì)tokens進(jìn)行逐個(gè)掃描,根據(jù)每個(gè)token是否為數(shù)組(is_array)分別進(jìn)行操作,由于整體代碼比較龐雜,此處挑選處理上的幾個(gè)關(guān)鍵點(diǎn),并結(jié)合實(shí)際的代碼,對(duì)其掃描的方式進(jìn)行探究。下面先展示本次測(cè)試使用的源碼,主要包含兩個(gè)文件commond_exec.php與para.php兩個(gè)文件,源碼如下所示:commond_exec.php:
include('para.php');
$str = 'command';
$command = para($str);
shell_exec( $command );
?>
para.php:
function para($str){
return $_GET($str);
}
?>
掃描的目標(biāo)文件是commond_exec.php,此時(shí)其生成的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] => ;
)
對(duì)tokens進(jìn)行遍歷時(shí),如果該token的類型是數(shù)組,那么分別獲取該數(shù)組中的每個(gè)值,如下所示:$token_name = $this->tokens[$i][0]; ? ? // 該token的名稱,相當(dāng)于變量名稱
$token_value = $this->tokens[$i][1]; ? ?// 該token的值,相當(dāng)于變量的值
$line_nr = $this->tokens[$i][2]; ? ? ? ? ? ?// token出現(xiàn)在源碼的第幾行
2.2.1 文件包含處理
對(duì)token進(jìn)行逐個(gè)掃描時(shí),第一個(gè)出現(xiàn)的token就是便是include函數(shù),RIPS遇到這個(gè)函數(shù)時(shí)會(huì)根據(jù)文件包含出現(xiàn)的位置,獲取被包含文件的tokens,插入到原tokens語(yǔ)句的后面,其具體的操作代碼如下所示:$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如下所示,對(duì)比下會(huì)發(fā)現(xiàn)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 添加數(shù)據(jù)源(source點(diǎn))
當(dāng)掃描到第11個(gè)token return時(shí),此時(shí)會(huì)判斷返回的語(yǔ)句是否是用戶可以控制的語(yǔ)句,如果這條語(yǔ)句是用戶能夠控制的語(yǔ)句,比如此處使用$_GET進(jìn)行賦值表明是用戶可以控制的語(yǔ)句;也就是說(shuō)para()方法的返回值是用戶可以控制的,那么該方法返回的數(shù)據(jù)將被認(rèn)為是一個(gè)被污染的數(shù)據(jù)源,即source點(diǎn)并將該方法添加到source_functions的數(shù)組中。對(duì)于return返回參數(shù)是否是用戶可控制的判斷,主要是通過(guò)函數(shù)scan_parameter()實(shí)現(xiàn)的,下面抽取幾個(gè)關(guān)鍵點(diǎn)來(lái)了解判斷流程的實(shí)現(xiàn),當(dāng)遇到token為return的語(yǔ)句時(shí),會(huì)向后遍歷token,直到該語(yǔ)句結(jié)束,代碼的實(shí)現(xiàn)上是通過(guò)“;”是否出現(xiàn)進(jìn)行判斷,如下所示:while( $this->tokens[$i + $c] !== ';' )
對(duì)于每個(gè)token,判斷該token是否是一個(gè)數(shù)組,如果是一個(gè)數(shù)組則檢查數(shù)組元素是否是一個(gè)變量,如下所示:if( is_array($this->tokens[$i + $c]) )
{
if( $this->tokens[$i + $c][0] === T_VARIABLE )
如果該token是一個(gè)數(shù)組且為變量,則使用scan_parameter()函數(shù)對(duì)其進(jìn)行檢查,該函數(shù)調(diào)用形式如下。該調(diào)用的參數(shù)比較多,但是本例中實(shí)際起到判斷作用的只有第三個(gè)參數(shù),即這個(gè)token本身:$this->tokens[$i+$c],具體的值為:tokens[12],即$_GET函數(shù)。$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函數(shù)為定義的source函數(shù),因此將直接認(rèn)為返回值是用戶可輸入的,即$userinput=true。最后將此函數(shù)名添加到source_functions列中,以后的掃描該函數(shù)將作為source點(diǎn)看待。if($userinput == 1 || $GLOBALS['userfunction_taints'])
{
$this->source_functions[] = $this->function_obj->name;
}
2.2.3 添加風(fēng)險(xiǎn)點(diǎn)(sink點(diǎn))
此處實(shí)際是RIPS的一個(gè)誤報(bào),RIPS將$_GET()作為可變函數(shù)名對(duì)待,如果函數(shù)名可變那么就可以將該函數(shù)名賦值為eval,從而造成代碼執(zhí)行的漏洞,sink點(diǎn)的添加也是在scan_parameter()中進(jìn)行。由于此處是$_GET(),顯然此函數(shù)包含在source函數(shù)中,因此使用scan_parameter()函數(shù)其返回值肯定為true,那么在函數(shù)內(nèi)部將會(huì)觸發(fā)如下代碼塊的執(zhí)行。if($this->in_function && !$return_scan)
{
$this->addtriggerfunction($mainparent);
}
觸發(fā)后主要執(zhí)行的函數(shù)是addtriggerfunction(),該函數(shù)的作用主要是向$GLOBALS變量中添加該函數(shù)。$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;
最后在包含文件掃描結(jié)束時(shí),即token="}",此處是第17個(gè)token,將被全局變量合并到scan_functions中,即添加到sink點(diǎn)。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 命令執(zhí)行(shell_exec)漏洞
這個(gè)漏洞是本代碼中實(shí)際包含的一個(gè)漏洞,在上面各種準(zhǔn)備工作完成后,來(lái)看一下這個(gè)實(shí)際漏洞的掃描流程;當(dāng)token為shell_exec時(shí),由于該函數(shù)是一個(gè)危險(xiǎn)函數(shù),即包含在sink點(diǎn),那么分析將直接跳轉(zhuǎn)到TAINT ANALYSIS中進(jìn)行。同2.2.2類似,會(huì)跳轉(zhuǎn)到scan_parameter()函數(shù)中對(duì)函數(shù)的參數(shù)進(jìn)行分析,確定該參數(shù)是否是用戶可控制的,即包含在source點(diǎn)內(nèi)。該函數(shù)的參數(shù)是變量$command,該參數(shù)是一個(gè)自定義變量,RIPS對(duì)于自定義變量會(huì)進(jìn)行自動(dòng)掃描并通過(guò)函數(shù)variable_add()添加到var_declares_local、var_declares_global兩個(gè)變量中的一個(gè)。
下面先對(duì)variable_add()函數(shù)進(jìn)行簡(jiǎn)單介紹,當(dāng)遍歷到tokens[24],$command的賦值操作時(shí),會(huì)觸發(fā)該函數(shù)的執(zhí)行。該函數(shù)調(diào)用形式如下,其中比較關(guān)鍵的是第二個(gè)參數(shù),調(diào)用Analyzer::getBraceEnd()靜態(tài)方法,獲取該變量聲明的所有token,此處$command的token的序列號(hào)為24-30,將這些tokens存儲(chǔ)到一個(gè)數(shù)組中,最后將該變量的相關(guān)信息存入var_declares_global數(shù)組中。這樣就完成了對(duì)一個(gè)文件中的全局遍歷的發(fā)現(xiàn)及存儲(chǔ)。$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()
);
由于存儲(chǔ)了該變量的tokens信息,那么對(duì)于自定義變量的分析,就轉(zhuǎn)變成了對(duì)該變量的tokens的分析,遍歷該變量的tokens,如果該token來(lái)自用戶可控制的輸入,即sorce點(diǎn)數(shù)據(jù)源,那么表明自定義變量的也是可控的,此處的source點(diǎn)就是自添加的函數(shù)para(),這樣就存在一個(gè)用戶可控制的數(shù)據(jù)源(source)流向危險(xiǎn)函數(shù)(sink),形成了一個(gè)漏洞觸發(fā)的完整路徑。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、結(jié)語(yǔ)
上面對(duì)于RIPS的源碼進(jìn)行了簡(jiǎn)單的分析,從中可以看出,其工作的流程大致為遍歷token,發(fā)現(xiàn)sink點(diǎn),然后對(duì)sink點(diǎn)的參數(shù)使用scan_parameter進(jìn)行后向追蹤,如果這個(gè)參數(shù)是用戶可控制的參數(shù),及包含在source點(diǎn)中,那么就存在一條從source到sink的聯(lián)通路徑,及存在一條漏洞觸發(fā)的路徑,則認(rèn)為是一個(gè)風(fēng)險(xiǎn)點(diǎn)。
* 本文作者:nightmarelee,轉(zhuǎn)載請(qǐng)注明來(lái)自FreeBuf.COM
總結(jié)
以上是生活随笔為你收集整理的php 简析对象,PHP白盒审计工具RIPS源码简析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: mysql主从和dump_MySQL主从
- 下一篇: 比尔盖茨预测下次大流行病:20年内可能会