企業級自動化程式碼安全掃描實戰
轉載請註明出處並附上源連結
版權所有,侵權必究
我們專注漏洞檢測方向:danenmao、arnoxia、sharker、lSHANG、KeyKernel、BugQueen、zyl、隱形人真忙、oxen(不分先後)
Author :隱形人真忙、sharker、 arnoxia
一、概述
原始碼安全檢測是安全開發流程( SDL )中舉足輕重的一部分,一般通過人工審計或者自動化工具來進行檢測。在大型企業中,業務線情況較為複雜,專案開發往往使用不同的程式語言、開發框架,編碼風格也大不相同。此外,存量的程式碼有上億行、每天又會有大量的新增程式碼與專案,這些因素導致在大型企業安全實踐中是無法通過人工審計程式碼的方式來進行檢測的。因此,在人力緊張以及工作量龐大的情況下, 最優的選擇是依賴自動化檢測工具了 。
本篇文章中,主要介紹我們自主研發的靜態程式碼安全檢測平臺的整體技術原理、研發部署方案與架構、總體檢出效果,以及一些具體生產環境的檢出場景。
二、自動化程式碼漏洞挖掘技術
2.1 基於汙點傳播分析的自動化挖掘技術
2.1.1 檢測模型
自動化程式碼安全檢測是一種自動化挖掘、發現原始碼漏洞的安全檢測。我們在人工形式下的程式碼審計中,通常關注以下三點:
-
輸入源
即使用者輸入可進入程式碼邏輯的入口,如 PHP 語言中的 GPC , JAVA 語言中的 HttpServletRequest 等等。此外,在對框架開發的工程進行程式碼審計的時候,我們還會關注框架自身封裝的一些入口方式,比如 SpingMVC 中的 @RequestParam 註解。
-
淨化操作
淨化操作包括兩個部分:系統自帶的淨化操作函式以及使用者自定義的淨化操作函式。
這些淨化操作函式通常可以針對某種漏洞型別將傳入的引數進行一定程度的字串變化,從而消除某種漏洞的威脅。比如 htmlspecialchars 函式,被廣泛認為是用於消除 XSS 漏洞影響的,雖然在某些場景 j 下一樣可以被繞過。
-
危險函式
危險函式是觸發漏洞的位置,一般按照漏洞型別進行分類別討論。比如 SQL 注入漏洞的 PHP 危險函式為 mysql_query ,呼叫該方法執行查詢,如果一旦危險引數可控,則會造成 SQL 注入漏洞。
現在我們來將這三個要素串聯起來,實際上每次程式碼審計判斷一個可疑點是否有漏洞(注意這裡是指的常規的 Web 安全漏洞,不包含邏輯漏洞),都是運用的以下模型:
本章節就是要討論如何將上述過程進行自動化,且能夠嵌入到網際網路公司的 SDL 流程中。
目前市面上有 基於正則表示式和基於語義分析 的兩種檢測方式,基於正則表示式的傳統程式碼安全掃描方案的缺陷在於其無法很好的“理解”程式碼的語義,而是僅僅把程式碼檔案當作純字串處理。目前較為成功的靜態掃描商用產品都運用了語義分析、語法分析等程式分析技術,如開源的 Rips 中,使用的是 PHP 內建的 Tokenizer 系列 API 來獲取 token 流。
${RIPS_HOME}/lib/scanner.php 檔案:
// tokenizing
$tokenizer = newTokenizer($this->file_pointer);
$this->tokens = $tokenizer->tokenize(implode('',$this->lines_pointer));
unset($tokenizer);
在 token 流的基礎上進行分析,可以通過每個節點的解析器代號對程式碼進行一定程度的理解,如 T_ABSTRACT 表示抽象類、 T_AND_EQUAL 代表賦值運算子。
除了在 token 流的基礎上執行程式碼分析,市面上越來越多的程式碼漏洞掃描器選擇了從抽象語法樹層面入手執行程式碼分析,相比冗長的 token 流,語法樹層面的分析顯得層次分明、容易理解,這對於平臺開發人員來說是個好訊息。
通過更加高階的語義、語法分析,我們可以從待分析的程式碼中獲取更多的資訊,通過這些資訊,就能夠大大提高漏洞檢測的準確度,這也是為什麼目前絕大多數商用軟體都選擇在語義、語法分析的基礎上執行程式碼安全檢測。
2.1.2 系統層次
接下來,我們將會以 PHP 語言為例,介紹一個自動化白盒掃描系統中所需要的各個方面,以及給出從靜態分析層面構建一個自動化程式碼安全檢測系統的思路。
我們先有一個整體的認識,一個基於程式靜態分析的自動化審計系統大致應該有以下幾個部分:
(1) 靜態分析層
靜態分析層負責對程式碼檔案進行“理解”,完成語義、語法層面的分析,比如生成抽象語法樹、生成程式的控制流圖等基礎的分析結構。同時,針對 PHP 語言的一些變數型別進行分類抽象,用於後續的資料流分析中。
(2) 資料流分析層
資料流分析主要負責收集程式碼中的變數傳遞流向,同時收集變數的淨化情況。資料流分析是汙點傳播分析的基礎,其收集的資訊越全面越準確,後續的汙點傳播分析才會準確。
(3) 汙點分析層
汙點傳播分析是依照資料流分析過程獲取的一系列程式資訊來進行漏洞判定的模組。
(4) 其他分析層
其他分析包含針對 PHP 多個程式碼檔案的聯合檢測、結合程式的上下文資訊進行更加細化的漏洞判定分析、過程內和過程間分析等等。
2.1.2.1 語義語法分析
抽象語法樹( Abstract syntaxtree , AST )是將原始碼按照一定的語法結構,並且將一些冗餘的細節抽象為樹狀結構進行表示。市面上有很多語義語法分析工具,如 ANTLR , Yacc , Lex , PHP-Parser 等等。
這裡著重介紹一下 PHP-Parser ( https://github.com/nikic/PHP-Parser ),作為 PHP 語言的語法分析工具,其本身就是使用 PHP 語言進行實現的,且支援節點遍歷等功能。使用 composer 可以很方便地安裝 PHP-Parser :
// 安裝composer: curl -s http://getcomposer.org/installer | php // 安裝PHP-Parser: php composer.phar require nikic/php-parser
具體使用可以參考官方文件。
2.1.2.2 控制流分析
僅僅在抽象語法樹的基礎上進行分析是不夠的,因為抽象語法樹無法很好的獲取程式中流程控制語句的資訊。比如分析以下示例程式碼時,如果不考慮程式中的分支判斷,則很容易發生誤判:
<?php $username = $_GET['username'] ; $data = $_GET['data'] ; if($username == 'alice'){ $data= "[data]" . $data ; }else{ $data= intval($data) ; } eval($data) ;
當分析到程式呼叫了 eval 函式,則向上回溯 $data 變數,結果發現在 else 分支中呼叫了 intval ,如果不考慮控制流程,即不考慮其他的程式碼分支,這時審計程式就會認為該程式碼片段不存在問題直接返回,顯然這是錯誤。因為在 if 分支中,出現了另外的賦值,如果只進入這個 if 分支,那麼就會觸發程式碼注入漏洞。
因此我們需要在抽象語法樹的基礎上構建控制流程圖,將 PHP 中的分支跳轉進行整理,以基本塊的形式編排抽象語法樹的節點。比如針對上文的程式碼片段生成控制流圖:
在程式分析的時候,必須要對每個分支進行獨立的分析。
生成新基本塊的依據是一些控制流程語句,這些語法結構都是固定,大致有以下幾種:
-
條件分支
If 語句、 Switch 語句、 Try catch 塊、三目運算子、 Or 語句
-
迴圈結構
For 語句、 Foreach 語句、 While 語句、 Do…While 語句
-
終止結構
Break 、 Continue 、 Throw
-
返回結構
Return
生成控制流圖的方法是當遇到條件分支和迴圈結構時,需要生成一個新的基本塊,將這些程式碼塊中的語法樹節點插入進去;當遇到終止結構時,停止當前基本塊的生成;當遇到返回語句時,停止圖的構建。
2.1.2.3 抽象內建函式
在任何程式碼中,都會有 PHP 內建函式的呼叫,如果不對這些內建函式進行處理,或者說自動化分析不“理解”這些函式的含義和效果,可能會造成大量的誤報,比如分析下面的程式碼片段:
<?php $comm = “touch ” ; $name = $_GET[‘name’] ; $hash = md5 ($name) ; system($comm . $hash) ;
由於 PHP 內建函式眾多,每個函式都人工進行處理是不現實的,最好的方式是對這些內建函式進行分類。按照功能和對程式分析的影響,大致可分為:
(1) 返回常量值
比如 strlen() 或者 md5() ,一旦引數進入這些內建函式將會返回常量型別的結
果,比如返回純數字、布林值、純字串,這種處理需要考慮為針對引數的一種淨化方法。
(2) 返回引數的一部分
某些內建函式如 trim() 或者 array_keys() 會將引數的一部分或者全部進行返回。針對返回值,該分類可以分為返回 array 、返回 array 的單個元素、返回 string 型別。
(3) 淨化操作
針對某種漏洞型別進行引數淨化操作的內建函式,比較典型的有 addslashes ,
htmlspecialchars 等函式。
(4) 字串切割變換
比如內建函式 substr() 或者 chunk_split() ,這些內建函式會返回傳入引數的一個子串。由於這種操作會破壞引數原有的結構,如果不追求絕對精確分析的話,實際上完全可以將這類函式的呼叫視為某種程度的淨化處理。
(5) 編碼和解碼函式
-
編碼函式
如 urlencode() 或者 base64_encode() 等執行某種編碼操作的內建函式。
-
解碼函式
如 urldecode() 或者 base64_decode() 等執行某種解碼操作的內建函式。注意,如果在分析中遇到了解碼函式,通常認為該函式呼叫前引數所受到的一切淨化都是無效的,如果直接進入危險函式就會觸發漏洞。
(6) 回撥函式
一些內建函式會執行引數指定的其他函式,比如 array_walk(),array_map(), set_error_handler() 。如果標識其他函式名的引數可以直接獲取到,那麼將該呼叫資訊進行進一步分析,比如以下程式碼片段 :
$NAMES = array( ‘1’=> $_GET[‘1’], ‘2’=> $_GET[‘2’] ) ; $arr = array_map(‘intval’, $NAMES) ; system($arr[‘1’]) ;
第五行程式碼呼叫了 array_map 函式,並且陣列中每個元素都進行了 intval 從操作,因此需要收集這裡的一個淨化資訊,為後續汙點分析提供依據。
(7) 獲取檔案控制代碼
一些檔案操作型別的危險函式的檔案引數是以 resource 傳遞進來的,因此如果僅僅去判斷是否受汙染是不可行的,所以需要對獲取檔案控制代碼的函式進行分類。
(8) 白名單機制
在條件分支中遇到這些函式如 in_array,array_key_exists 等函式,可以預設是
一種基於白名單的淨化處理。
(9) 正則表示式校驗
比如 preg_match 和 ereg 函式,這裡不可能去分析具體的正則表示式,因此
為了減少誤報,可以認為呼叫這些函式進行校驗都是有效的過濾。當然如果選擇犧牲誤報而減少漏報,也可以認為這些函式的呼叫是不起作用的,可以直接報警。
(10) 其他函式
其他不屬於上述類別但是需要額外分析的內建函式。
2.1.2.4 資料流分析
資料流分析層所做的工作是收集程式中的變數值傳遞、特殊函式呼叫等資訊。可以理解為資料流分析就是一個綜合的程式資訊蒐集,目的是為了給後續的汙點分析提供詳細的參考資訊。
對於資料流向的識別和蒐集,應該考慮到以下兩種情況:
-
針對程式碼中的賦值語句
-
PHP 內建函式導致的隱藏資料流,比如呼叫 list 、 func_get_args 等函式。
執行分析的流程圖如下:
2.1.2.5 汙點傳播分析
汙點分析過程是在程式分析中,一旦發現危險函式的呼叫則啟動分析。對傳入危險函式的危險引數進行分析,結合資料流分析時該危險引數的一些程式資訊,如淨化資訊、內建函式處理資訊等進行判斷,如果一旦發現該變數可以被使用者控制並且沒有進行有效過濾,則判定為漏洞。
-
定義危險函式
首先我們來考慮如何定義危險函式。在人工程式碼審計的時候,我們會按照漏洞的型別著重關注某一批函式,比如 mysql_query, file_get_contents, eval 等等。然後我們會找到這些函式的某些引數,然後判斷這些引數是否經過程式處理後還是會存在漏洞。因此,汙點分析時,只需要關注危險函式的名稱和危險引數的位置即可,配置示例如下:
print => array(array(1),$F_SECURING_XSS)
該配置的含義是: print 函式是一個可能引發 XSS 漏洞的危險函式,並且其引數 1 是一個危險引數。
-
定義淨化操作
定義一個引數是否受到了有效淨化是汙點分析中比較重要的環節,這關係到後續漏洞判定的準確性。根據人工程式碼審計的經驗,我們可以抽象總結出一個 PHP 實現的淨化操作可以有以下方式:
(1) 使用 PHP 內建的或者使用者自定義的淨化函式執行淨化,比如呼叫 addslashes 等函式
(2) 使用 PHP 內建的一些校驗型別的函式,比如型別判斷、正則表示式校驗、字串切割、回撥函式以及編碼解碼等操作。這些操作都會進行一定程度的淨化,如果在實踐中,我們期望有較高的精確度,則可以認為凡是呼叫這些函式,就認為是有效的淨化;如果我們期望降低漏報率,則可以忽視這些內建函式影響或者執行更加細緻的分析。
( 3 )使用邏輯判斷進行校驗,如使用了 == 或者 === 與某些靜態常量進行了比較操作,則認定為該變數接受了淨化。
-
執行汙點分析
有了上述基礎,我們可以很清晰地執行汙點分析判斷漏洞了,大致的過程如下:
( 1 )在執行資料流分析過程中,如果發現了敏感函式的呼叫,則啟動汙點分析。
( 2 )查詢危險函式配置列表,獲取到需要判斷的危險引數列表。
( 3 )向上找到連線的基本塊資訊,關注一個基本塊內所有的資料流記錄,找到資料流記錄右邊的值,提取出該變數。
( 4 )如果該變數進入到了內建函式,則按照之前章節中整理的內建函式的作用判斷是否受到了有效的淨化。
( 5 )當遍歷時找不到基本塊中相關的賦值語句,或者賦值的值為字串、數字或者布林值,則停止汙點分析。
此外,我們需要格外注意編碼和解碼的影響,:
( 1 )一個回溯變數被進行解碼操作後( base64/hex/zlib… ),該變數向上的所有淨化操作都可以認為是無效的。
( 2 )如果一個變數被編碼和解碼多次,則進行抵消分析操作,並由抵消之後的結果進行判斷。
2.1.3 其他分析層
在整個語法樹遍歷、控制流圖生成的過程中,可能還會伴隨著以下幾種情況的細緻分析:
( 1 )過程內、過程間分析
當我們分析程式碼時,會遇到方法內部的變數傳遞與程式資訊蒐集;或者會遇到一個變數在多個方法之間通過引數進行傳遞。支援過程內、過程間分析的重點是作用域之間的對應和轉換。此外,當我們遇到某個函式中執行了對某個引數的過濾,我們可以將其加入到淨化函式列表,比如分析以下程式碼:
<?php function escapeCmd($cmd){ returnescapeshellcmd($cmd) ; } system("ls -l ".escapeCmd($_GET['cmd'])) ; ?>
這裡在執行分析時,遇到 escapeCmd 這個使用者自定義函式的呼叫,因此需要在上下文中尋找該方法的方法體(定義)並分析該方法的程式段。當發現其呼叫了安全函式 escapeshellcmd 後,我們就可以把這個 escapeCmd 加入到命令注入漏洞的安全函式中。
( 2 )多檔案分析
執行多檔案分析時,很多系統僅僅只是依賴 use 、 include 、 require 等關鍵詞來尋找當前檔案所包含的其他檔案,但是很多程式實現中,往往會用到 autoload 技術,或者在某個統一入口執行檔案包含的操作。所以我們有很大的機率會遇到某個檔案中呼叫了一個其他檔案的方法,但是在該檔案中卻找不到該方法的定義語法結構的情景。
因此多檔案分析時,我們最好在分析初始化階段中獲取到所有類定義、方法定義、通用函式定義以及其所在的路徑位置,將這些資訊儲存在記憶體中。當我們後續分析時,可以直接操作該記憶體結構,動態提取需要的方法定義。
三、企業級研發與部署方案
3.1 企業級實際場景和挑戰
或許很多人會認為,原始碼安全掃描只需要把程式碼拿來扔給掃描器掃描,然後產生列印結果給業務線就行了。可事實上在大型網際網路企業中部署並不是那麼簡單的一件事,主要考慮一下問題:
-
非本地工具,而是一個平臺
在大型公司,可能有成千上萬的產品線,涉及到的程式碼庫數量龐大並且程式語言繁雜,面對這種狀況,本地化工具過於鬆散,不好管理,接入效率低的缺點,而統一的掃描平臺可以提供一整套的自動化接入方案。統一的原始碼安全掃描平臺可以區分多場景的任務型別,提供多種接入方式,可以更高效、自動化地提供安全掃描能力。
-
軟體開發關鍵環節,中流砥柱
SDL(securitydevelopment lifecycle) 是微軟提出的從安全形度指導軟體開發過程的管理模式,原始碼安全掃描是 SDL 方法中的重要環節。從軟體的開發流程上看,原始碼安全掃描處於軟體開發流程上下游,在軟體上線之前需要通過安全掃描。
在我們的實踐中,原始碼安全掃描是以外掛的形式,嵌入到軟體研發的流程中,為業務線程式碼保駕護航 。
這將要求平臺必須具有高可用性,高響應及時度和完善的使用者反饋機制。
-
戰略地位,支撐安全紅線
企業有一條安全紅線,業務必須遵守的最基本安全要求。原始碼安全掃描也可以用於在系統上線前發現漏洞與違規的內容,禁止漏洞帶到線上,一定程度保護企業業務安全。
3.2 整體架構
從分層的角度,原始碼安全掃描平臺大致分為五層,每一層都有明確的職責劃分。
3.2.1 介面層
介面層是平臺對外輸出能力的入口,主要對介面請求進行合理性校驗,許可權校驗,引數校驗,以確保請求是合法的,非惡意的。功能上主要分掃描介面和查詢介面,掃描介面用來接收發起掃描任務,查詢介面用來查詢掃描任務的結果資料。
3.2.2 任務管理層
每一次掃描就是一個任務,任務管理層是整個平臺的中心部分,管理所有任務的掃描狀態。主要包括三個職責:
1) 狀態跟蹤。任務管理層跟蹤和記錄所有任務的掃描狀態。
2) 策略下發。原始碼掃描平臺根據業務的實際場景,針對不同任務實施不同的掃描策略,比如根據程式碼的變更情況使用快取策略,根據程式碼庫的特性調整超時時間等。
3) 超時監控。每個任務都有一個超時時間,當任務掃描時間超過期望值,任務會結束並返回超時異常。
3.2.3 引擎管理層
引擎管理層是對引擎呼叫的一層封裝,在企業裡面涉及到多種計算機語言,不同語言可能使用不同的原始碼分析引擎。引擎管理層向上抽象出掃描介面和報告介面,掃描介面接收掃描資料,下層實現具體的引擎呼叫策略,報告介面獲取不同引擎的掃描結果,統一生成掃描分析報告資料。
3.2.4 原始碼管理層
原始碼管理層主要關注目標原始碼獲取、原始碼儲存和原始碼安全。
1) 原始碼獲取。 原始碼獲取主要支援兩種方式,第一是使用者上傳原始碼的方式,這種方式需要使用者自己把原始碼打包,上傳到 web 伺服器。第二種是和企業級程式碼託管平臺打通,原始碼管理層根據託管平臺提供的安全協議拉取原始碼。
2) 原始碼儲存。 原始碼儲存主要包括兩個方面,其一是需要掃描的原始碼,這部分原始碼儲存時間不長,在掃描結束後的一段時間內會被清除;其二是有漏洞的原始碼,這部分原始碼會被永久儲存,用來後續分析漏洞報告使用。
3) 原始碼安全。 原始碼安全掃描涉及到業務線的原始碼下載與分析,對於企業來說原始碼具有最高 保密性,需要防止原始碼洩露的問題發生。這裡我們有兩個層面措施:第一是伺服器隔離,所有對原始碼操作和原始碼儲存都放在指定的伺服器上,並且由原始碼託管平臺負責,這種物理隔離大大減少平臺自身的管理成本。第二是所有原始碼儲存傳輸都採用了加密,這裡採用常用的方式,使用 AES 加密原始碼, RSA 加密 AES 的 key 。
3.2.5 掃描引擎
引擎主要利用靜態程式碼分析技術分析原始碼中可能存在的漏洞,具體參考 “ 自動化程式碼挖掘技術 ” 章節。
3.3 SDL 中的實踐
研發工程師( RD )提交程式碼,進行原始碼安全掃描,編譯,持續整合到部署上線,這一系列步驟是全自動化的過程,而原始碼安全掃描嵌入到整個流程中,必定牽扯到各個平臺。這裡我們分享這個過程遇到的問題。
3.3.1 介面可用性
一般平臺的介面可用性要求是四個 9 ,即 SLA 為 99.99% ,即使企業內部使用,也要達到 99.9% 。 SLA 指標很重要,因為一旦某段時間內介面不可用導致軟體持續整合阻塞,會影響到正常的產品釋出的流程,導致產品上線出問題。比如企業內某個產品緊急修復了 bug 需要上線,但是卻在安全掃描這一步卡住,產品線的同學可能會抓狂了。
為了保障我們介面的可用性,以及及時發現故障,我們做了以下策略:
-
慢啟動 。介面的服務邏輯應該儘可能少的計算和 IO 訪問,這裡我們僅僅將請求的原始資料做一次 redis 寫入,然後就返回,其他的任務掃描邏輯之後才被後續的邏輯“慢”啟動起來。
-
介面監控 。我們監控每個介面每次接收請求到做出響應的時間,在響應時間超出一定範圍(一般與呼叫平臺約定),則進行郵件告警。
-
降級策略。在遇到故障無法在短時間內恢復的情況,我們啟動降級策略,捨棄一定功能的情況下,保障介面能夠及時返回,防止流程卡住。
3.3.2 “ 慢 ” 是永恆話題
原始碼安全掃描作為軟體整合釋出的一個步驟,一定程度上影響軟體釋出的時間。如果安全掃描耗時過長,將會大大拖累的整個上線流程,安全部本身也會承受著很大的壓力。
3.3.2.1 此增量非彼增量
或許有人會講, RD 大多數時候可能只會修改幾個程式碼庫的檔案,這樣怎麼會有掃描速度慢的問題呢?這樣的想法是基於兩個前提:第一,掃描是單檔案;第二,檔案與檔案之間是沒有關聯的。但事實上,前面漏洞分析技術也有提及,白盒程式碼掃描過程其實會對相關聯的多個檔案進行掃描,可能修改了一個檔案,但是有很多其他的檔案引用了這個檔案,為了保險起見,所以會把相關聯的檔案一併執行掃描。
所以一開始的做法是:不管程式碼庫如何變化,都使用全量掃描,而這將導致掃描速度特別“慢”,經常會收到業務線同學的反饋,對運營造成很大的壓力。
3.3.2.2 增量掃描的實踐方法
為了提高掃描的效能,我們嘗試做了增量掃描策略。
大致思路是,如果我們能夠對本次變更的程式碼進行分析,找出當前程式碼庫中變更檔案引用的檔案和被引用的檔案,將這些檔案進行掃描,最後跟全量合併結果。這樣可以大大減少掃描的檔案數,提高掃描速度。
具體做法是,在原始碼管理層嵌入增量策略層,利用簡單的原始碼分析技術,得出檔案的引用流向圖。通過對圖的遍歷獲取到增量涉及到的檔案。
3.3.2.3 其他優化策略
除了增量掃描,我們還有如下優化策略:
-
無變更檔案使用歷史結果 。相同程式碼庫前後兩次掃描,如果檔案沒有任何修改,則認為兩次掃描結果一致。
-
基於無關檔案變更的快取策略 。一個程式碼庫,前後兩次掃描,如果前後兩次變更的檔案與安全掃描無關,則複用第一次掃描結果。
-
閒時掃描。很多時候掃描慢也可能是資源緊缺有關,一個平臺的訪問量也可能集中在某個時間段,因此,閒時掃描作為一種快取策略,可以大大提高掃描的速度。
四、平臺檢測效果
4.1 總體檢出效果
靜態程式碼安全掃描嵌入開發流程以來,接入程式碼庫數量達到 3000+ ,其中每天由程式碼託管平臺觸發的增量任務有 1700+ 個,由上線平臺觸發的任務有 2500+ ,根據我們的優化方案, 95% 的任務能夠在 10 分鐘內完成。我們的漏洞檢測規則加入人工運營和自定義開發,漏洞準確率達到 90% 左右,其中不同的語言漏洞的分佈情況不一樣。
目前我們 php 檢測覆蓋 13 中型別漏洞, java 語言掃描覆蓋 30+ 種類型漏洞,其中除了 web 常見漏洞外,還覆蓋部分 android , python , nodejs 漏洞。
4.2 具體檢測場景與能力
4.2.1 PHP 漏洞檢測
關於 PHP 的漏洞檢測手段詳見第二章節。
4.2.2 java 漏洞檢測
java 白盒檢測需要覆蓋 owasp 主要 web 漏洞,同時也需要對一些第三方開源庫已知漏洞進行覆蓋 , 因此針對通用漏洞和第三方開源庫 nday ,我們採取了不同的檢測方法。
-
第三方開源庫已知漏洞
java 通過載入第三方庫,由於第三方庫可能存在已知高危漏洞,例如 struts2 存在 s2-056 , spring-boot 存在表示式注入等漏洞。針對這一類由於老版本元件使用導致的漏洞,可以通過版本檢查進行覆蓋,大多數版本均可在 pom.xml 或者 build.gradle 檔案中查詢到,例如 struts2:
<dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>2.5.16</version> </dependency>
此時可以通過檢測 struts2-core 版本是否在漏洞範圍以內,即可判斷是否存在漏洞。但這種方法無法檢測業務線同學通過修改程式碼或者新增過濾器修補漏洞,而這需要通過資料流分析來解決。
-
通用漏洞
通用漏洞主要覆蓋一些高危或者中危漏洞,例如 sql 注入、命令執行、程式碼執行、 SSRF 、任意檔案上傳等。對於任意一種漏洞,都需要首先獲取漏洞的輸入點與觸發點以及漏洞的資料流,以簡單的 sql 注入為例:
protected void processRequest(HttpServletRequestrequest, HttpServletResponse response) throws ServletException, IOException { String user = request.getParameter("user"); Statement st = conn.createStatement(); String query = "SELECT * FROM User where userid='" + user +"'"; ResultSet res = st.executeQuery(query); }
首先需要獲取到輸入方法 HttpServletRequest::getParameter() ,觸發點 executeQuery() ,然後通過資料流進行分析,這裡的資料流可以確定為:
通過跟蹤資料流即可確定是否存在漏洞,當然這屬於最簡單模式,實際檢測漏洞中為了減少誤報,我們還需要處理以下幾種情況:
-
包含對某些漏洞的全部過濾機制,例如針對 sql 注入的全域性 filter
-
資料流中是否包含 if 、過濾函式等過濾節點
-
某些資料流是否已經中斷,例如檔案上傳中,檔案字尾在資料流圖中已經被更改,實際上已不可控,此時需要單獨處理類似資料流
-
框架的處理
java 由於擁有眾多開源框架,而大多數開源框架均有一定的安全機制,因此某些漏洞在特定的框架中可能並不符合通用模式。以 mybatis 注入為例:
<select id="orderBlog" resultType="Blog"parameterType=”map”> SELECTid,title,author,content FROM blog ORDER BY ${orderParam}(#{orderParam}) </select>
對於 mybatis 來說,如果 sql 語句變數的引入採用 #{} 的方式,則 sql 語句底層直接採用的預編譯的形式,此時不需要再檢測 sql 注入漏洞;而如果採用 ${} 形式,則需要按照一般的 sql 注入流程檢測。因此 java 的大多數通用漏洞檢測,還需要對使用量較大的框架進行適配,此時在輸入和觸發點的獲取上適配即可。上面的例子,就可以在檢測輸出時,首先判斷是否為 mybatis 應用,如果未 mybatis 應用,則直接檢測是否在 sql 語句中引入了 ${} 型別變數,然後就可以按照通用方式檢測了。
4.2.3 Python/Nodejs 漏洞檢測
python 和 nodejs 的漏洞主要是命令執行、程式碼執行、 sql 注入、 XSS 與 SSRF ,兩者的檢測思路大致相同。首先根據不同的框架確定輸入, python 的主要 web 框架有 python-cgi (原生)、 Django 、 Flask 、 webpy ,而 nodejs 主要有 express 、 trails 等。
例如:
class index: def GET(self): data = web.input()
在檢測 python 時,首先需要通過正則判斷是否為 web 應用,然後根據不同框架獲取不同輸入函式即可。而 python 的主要有: eval 、 exec 、 execfile 、 pickle.* 、 cPicke.* 、 timeit.timeit 等。
簡單的 python 漏洞示例:
classindex: def GET(self): data= web.input() code= data.get('code','') return eval(code)
這個程式碼片段中, web.input()->data->code->eval 能夠形成完成的漏洞資料流,同時包含了可控制輸入與漏洞觸發點,因此就可以確認為一個漏洞。而為了準確的檢測漏洞,某些安全使用方式需要過濾,例如
eval(code,{"__builtins__":None},safe_dict)
這裡通過將 __builtins__ 置為空,以及設定安全白名單函式,可以較好防禦程式碼執行,因此對於這一類需要通過 eval 的引數數量進行判斷。
五、總結
基於語義分析、語法分析技術,並且將各個語言和框架特性進行整合和識別而構建的靜態程式碼安全掃描器,無論是誤報率還是檢出率都遠遠低於傳統的商用掃描器。
此外,將自動化的靜態程式碼安全掃描嵌入到開放與上線流程中,可以有效地在上線前階段就發現程式碼中的安全漏洞,並將原始碼漏洞報告第一時間推送給業務線的開發同學進行修復,做到防患於未然。
而安全部門需要做的就是持續運營安全掃描規則,處理誤報反饋與安全能力增強上,可以在人工程式碼審計上節省很多人力。
對於大型網際網路公司,擁有可以與 CI 流程進行結合的原始碼安全掃描平臺是非常必要的,原始碼安全掃描不僅可以對安全漏洞進行掃描,還可以配合一些原始碼指紋識別技術與安全開發規範,對內部的一些高危開源框架、不良的編碼習慣進行有效治理,可謂一舉多得。
百度安全應急響應中心
歡迎關注獲取更多安全知識