當前位置:
首頁 > 最新 > PHP白盒審計工具RIPS源碼簡析

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 ] . | . "";

@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,如、空位元組等。如下所示:

源碼:

原始tokens:

Array ( [0] => Array ( [0] => 374 [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: para.php:

掃描的目標文件是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的聯通路徑,及存在一條漏洞觸發的路徑,則認為是一個風險點。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 推酷 的精彩文章:

C#7.1先睹為快
國內Foscam製造的IP攝像頭被曝出大量漏洞
動態添加Redis密碼認證
史玉柱、雷軍是如何從定位理論中找到營銷爆點的?
開發者分享如何與媒介合作進行恰當的產品營銷

TAG:推酷 |

您可能感興趣

PHP源碼分析之parse
OS_SEM.c源碼分析4 OSSemPend
APT組織Carbanak源碼泄露
MFC TabSheet 源碼
小米MIX 3 Android P內核源碼公布
輕輕鬆鬆看懂Spring AOP源碼
AutoLine源碼分析之靜態頁面模板及對應API介紹
LG V40 ThinQ公布內核源碼:第三方ROM可期
Drill-on-YARN之源碼解析
HashMap源碼分析
《跟隨霄,LAMMPS源碼學習06》Atom:grow
LinkedList源碼分析
TinyHttpd源碼分析
AutoLine源碼分析之調度管理器
如何實現一個HTTP請求庫——axios源碼閱讀與分析
Storm源碼分析之Trident源碼分析
源碼級剖析PHP 7.2.x GD拒絕服務漏洞
Vue + ElementUI 後台管理網站基本框架之創建項目(附源碼)
史上最大源碼泄露事件:iOS 關鍵源代碼被匿名公布在 GitHub 上
RocketMQ源碼:通信協議設計及編解碼