代碼審計:審計思路之實例解說全文通讀

 在個人新書《代碼審計:企業級web代碼安全架構》發佈之際,借用這篇文章跟你們分享下代碼審計的一些思路,目前該書已經能夠在淘寶和京東等網站購買。 本文章首發在freebuf。javascript

  根據敏感關鍵字來回溯傳入的參數,是一種逆向追蹤的思路,咱們也提到了這種方式的優缺點,實際上在須要快速尋找漏洞的狀況下用回溯參數的方式是很是有效的,但這種方式並不適合運用在企業中作安全運營時的場景,在企業中作自身產品的代碼審計時,咱們須要瞭解整個應用的業務邏輯,才能挖掘到更多更有價值的漏洞。php

  全文通讀代碼也有必定的技巧,並非隨便找文件一個個讀完就能夠了,這樣你是很難真正讀懂這套Web程序的,也很難理解代碼的業務邏輯,首先咱們要看程序的大致代碼結構,如主目錄有哪些文件,模塊目錄有哪些文件,插件目錄有哪些文件,除了關注有哪些文件,還要注意文件的大小、建立時間。咱們根據這些文件的命名就能夠大體知道這個程序實現了哪些功能,核心文件是哪些,以下是discuz的程序主目錄。如圖所示。                                            html

  在看程序目錄結構的時候,咱們要特別注意幾個文件,分別以下: java

1) 函數集文件,一般命名中包含functions或者common等關鍵字,這些文件裏面是一些公共的函數,提供給其餘文件統一調用,因此大多數文件都會在文件頭部包含到它們,尋找這些文件一個很是好用的技巧就是去打開index.php或者一些功能性文件,在頭部通常都能找到。 mysql

2) 配置文件,一般命名裏面包括config這個關鍵字,配置文件包括Web程序運行必須的功能性配置選項以及數據庫等配置信息,從這個文件裏面能夠了解程序的小部分功能,另外看這個文件的時候注意觀察配置文件中參數值是用單引號仍是用的雙引號包起來,若是是雙引號,則很大可能會存在代碼執行漏洞,例以下面kuwebs的代碼,只要咱們在修改配置的時候利用PHP可變變量的特性便可執行代碼。react

<?php/*網站基本信息配置*/$kuWebsiteURL       = "http://www.kuwebs.com";$kuWebsiteSupportEn         = "1";$kuWebsiteSupportSimplifiedOrTraditional          = "0";$kuWebsiteDefauleIndexLanguage                    = "cn";$kuWebsiteUploadFileMax                           = "2";$kuWebsiteAllowUploadFileFormat   = "swf|rar|jpg|zip|gif";
 /*郵件設置*/$kuWebsiteMailType        = "1";$kuWebsiteMailSmtpHost             = "smtp.qq.com";

3) 安全過濾文件,安全過濾文件對咱們作代碼審計相當重要,關係到咱們挖掘到的可疑點能不能利用,一般命名中有filter、safe、check等關鍵字,這類文件主要是對參數進行過濾,比較常見的是針對SQL注入和XSS過濾,還有文件路徑、執行的系統命令的參數,其餘的則相對少見。而目前大多數應用都會在程序的入口循環對全部參數使用addslashes()函數進行過濾。web

private static function _do_query_safe($sql) {
              $sql = str_replace(array('\\\\', '\\\'', '\\"', '\'\''), '', $sql);
              $mark = $clean = '';
              if (strpos($sql, '/') === false && strpos($sql, '#') === false && strpos($sql, '-- ') === false && strpos($sql, '@') === false && strpos($sql, '`') === false) {
                     $clean = preg_replace("/'(.+?)'/s", '', $sql);
              } else {

4) index文件,index是一個程序的入口文件,因此一般咱們只要讀一遍index文件就能夠大體的瞭解整個程序的架構,運行的流程,包含到的文件,其中核心的文件又有哪些,而不一樣目錄的index文件也有不一樣的實現方式,建議最好是先把幾個核心目錄的index文件都簡單讀一遍。 sql

  上面介紹了咱們應該注意的部分文件,能夠幫助咱們更有方向的去讀所有的代碼,實際上在咱們真正作的代碼審計的時候,常常會遇到各類框架,這時候就會被搞的暈頭轉向,因此在學習代碼審計的前期建議不要去讀開源框架或者使用開源框架的應用,先去chinaz、admin5一類的源碼下載網站下載一些小應用來讀一下,而且必定要多找幾套程序通讀全文代碼,這樣咱們才能總結經驗,等總結了必定的經驗,會PHP也比較熟悉的時候,再去讀一些像thinkphp、Yii、Zend Framework等開源框架,才能快速的挖掘高質量的漏洞。 thinkphp

  通讀全文代碼的好處顯而易見,能夠更好的瞭解程序的架構以及業務邏輯,可以挖掘到更多更高質量的邏輯漏洞,通常老手會比較喜歡這種方式。而缺點就是花費的時間比較多,若是程序比較大,讀起來也會比較累。 數據庫

騎士cms通讀審計案例


  咱們已經介紹了代碼審計中通讀全文代碼審計方式的思路,下面咱們來用這種方式進行一個大體的案例說明。

爲了方便你們理解,筆者找了一款相對簡單容易看懂的應用騎士cms來介紹,版本是3.5.1,具體的審計思路咱們在上文中已經有過介紹。

1. 查看應用文件結構

首先來看一下騎士cms的大體文件目錄結構,如圖所示:

  首先須要看看有哪些文件和文件夾,尋找名稱裏有沒有帶有api、admin、manage、include一類關鍵字的文件和文件夾,一般這些文件比較重要,在這個程序裏,能夠看到並無什麼php文件,就一個index.php,看到有一個名爲include的文件夾,通常比較核心的文件都會放在這個文件夾,咱們進行看看大概有哪些文件,如圖所示:

2. 查看關鍵文件代碼

  在這個文件夾裏面咱們看到了多個數十K的PHP文件,好比common.fun.php就是本程序的核心文件,基礎函數基本在這個文件中實現,咱們來看看這個文件裏有哪些關鍵函數,一打開這個文件,立馬就看到一大堆過濾函數,這是咱們最應該關心的地方,首先是一個SQL注入過濾函數。

function addslashes_deep($value){
    if (empty($value))
    {
        return $value;
    }
    else
    {
              if (!get_magic_quotes_gpc())
              {
              $value=is_array($value) ? array_map('addslashes_deep', $value) : mystrip_tags(addslashes($value));
              }
              else
              {
              $value=is_array($value) ? array_map('addslashes_deep', $value) : mystrip_tags($value);
              }
              return $value;
    }}

  該函數將傳入的變量使用addslashes()函數進行過濾,也就過濾掉了單引號、雙引號、NULL字符以及斜槓,如今咱們要記住,在挖掘SQL注入等漏洞時,只要參數在拼接到sql語句前,除非有寬字節注入或者其餘特殊狀況,不然使用了這個函數就不能注入了。

再往下走是一個XSS過濾的函數mystrip_tags(),代碼以下:

function mystrip_tags($string){
       $string = new_html_special_chars($string);
       $string = remove_xss($string);
       return $string;}

這個函數調用了new_html_special_chars()和remove_xss()兩個函數來過濾XSS,就在該函數下方,代碼以下

function new_html_special_chars($string) {
       $string = str_replace(array('&amp;', '&quot;', '&lt;', '&gt;'), array('&', '"', '<', '>'), $string);
       $string = strip_tags($string);
       return $string;}function remove_xss($string) {
    $string = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S', '', $string);
 
    $parm1 = Array('javascript', 'union','vbscript', 'expression', 'applet', 'xml', 'blink', 'link', 'script', 'embed', 'object', 'iframe', 'frame', 'frameset', 'ilayer', 'layer', 'bgsound', 'title', 'base');
 
    $parm2 = Array('onabort', 'onactivate', 'onafterprint', 'onafterupdate', 'onbeforeactivate', 'onbeforecopy', 'onbeforecut', 'onbeforedeactivate', 'onbeforeeditfocus', 'onbeforepaste', 'onbeforeprint', 'onbeforeunload', 'onbeforeupdate', 'onblur', 'onbounce', 'oncellchange', 'onchange', 'onclick', 'oncontextmenu', 'oncontrolselect', 'oncopy', 'oncut', 'ondataavailable', 'ondatasetchanged', 'ondatasetcomplete', 'ondblclick', 'ondeactivate', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'onerror', 'onerrorupdate', 'onfilterchange', 'onfinish', 'onfocus', 'onfocusin', 'onfocusout', 'onhelp', 'onkeydown', 'onkeypress', 'onkeyup', 'onlayoutcomplete', 'onload', 'onlosecapture', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onmove', 'onmoveend', 'onmovestart', 'onpaste', 'onpropertychange', 'onreadystatechange', 'onreset', 'onresize', 'onresizeend', 'onresizestart', 'onrowenter', 'onrowexit', 'onrowsdelete', 'onrowsinserted', 'onscroll', 'onselect', 'onselectionchange', 'onselectstart', 'onstart', 'onstop', 'onsubmit', 'onunload','style','href','action','location','background','src','poster');
      
       $parm3= Array('alert','sleep','load_file','confirm','prompt','benchmark','select','update','insert','delete','alter','drop','truncate','script','eval');
 
    $parm = array_merge($parm1, $parm2, $parm3);
 
       for ($i = 0; $i < sizeof($parm); $i++) {
              $pattern = '/';
              for ($j = 0; $j < strlen($parm[$i]); $j++) {
                     if ($j > 0) {
                            $pattern .= '(';
                            $pattern .= '(&#[x|X]0([9][a][b]);?)?';
                            $pattern .= '|(&#0([9][10][13]);?)?';
                            $pattern .= ')?';
                     }
                     $pattern .= $parm[$i][$j];
              }
              $pattern .= '/i';
              $string = preg_replace($pattern, '****', $string);
       }
       return $string;}

  在new_html_special_chars()函數中能夠看到,這個函數對&符號、雙引號以及尖括號進行了html實體編碼,而且使用strip_tags()函數進行了二次過濾。而remove_xss()函數則是對一些標籤關鍵字、事件關鍵字以及敏感函數關鍵字進行了替換。 

再往下走有一個獲取ip地址的函數getip() 是能夠僞造IP地址的。 

function getip(){
       if (getenv('HTTP_CLIENT_IP') and strcasecmp(getenv('HTTP_CLIENT_IP'),'unknown')) {
              $onlineip=getenv('HTTP_CLIENT_IP');
       }elseif (getenv('HTTP_X_FORWARDED_FOR') and strcasecmp(getenv('HTTP_X_FORWARDED_FOR'),'unknown')) {
              $onlineip=getenv('HTTP_X_FORWARDED_FOR');
       }elseif (getenv('REMOTE_ADDR') and strcasecmp(getenv('REMOTE_ADDR'),'unknown')) {
              $onlineip=getenv('REMOTE_ADDR');
       }elseif (isset($_SERVER['REMOTE_ADDR']) and $_SERVER['REMOTE_ADDR'] and strcasecmp($_SERVER['REMOTE_ADDR'],'unknown')) {
              $onlineip=$_SERVER['REMOTE_ADDR'];
       }
       preg_match("/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/",$onlineip,$match);
       return $onlineip = $match[0] ? $match[0] : 'unknown';}

不少應用都會在獲取IP這裏沒有驗證IP格式,致使存在注入漏洞,不過這裏還只是能夠僞造IP。

再往下看能夠看到一個值得關注的地方,sql查詢統一操做函數inserttable()以及updatetable()函數,大多數SQL語句執行都會通過這裏,因此咱們要關注這個地方是是否還有過濾等問題。

function inserttable($tablename, $insertsqlarr, $returnid=0, $replace = false, $silent=0) {
       global $db;
       $insertkeysql = $insertvaluesql = $comma = '';
       foreach ($insertsqlarr as $insert_key => $insert_value) {
              $insertkeysql .= $comma.'`'.$insert_key.'`';
              $insertvaluesql .= $comma.'\''.$insert_value.'\'';
              $comma = ', ';
       }
       $method = $replace?'REPLACE':'INSERT';
       // echo $method." INTO $tablename ($insertkeysql) VALUES ($insertvaluesql)", $silent?'SILENT':'';die;
       $state = $db->query($method." INTO $tablename ($insertkeysql) VALUES ($insertvaluesql)", $silent?'SILENT':'');
       if($returnid && !$replace) {
              return $db->insert_id();
       }else {
           return $state;
       }}

再往下走則是wheresql()函數,是sql語句查詢的where條件拼接的地方,咱們能夠看到參數都使用了單引號進行包裹起來,代碼以下:


function wheresql($wherearr=''){
       $wheresql="";
       if (is_array($wherearr))
              {
              $where_set=' WHERE ';
                     foreach ($wherearr as $key => $value)
                     {
                     $wheresql .=$where_set. $comma.$key.'="'.$value.'"';
                     $comma = ' AND ';
                     $where_set=' ';
                     }
              }
       return $wheresql;}

還有一個訪問令牌生成的函數asyn_userkey(),拼接用戶名、密碼salt以及密碼進行一次md5,訪問的時候只要在GET參數key的值裏面加上生成的這個key便可驗證是否有權限,被用在註冊、找回密碼等驗證過程當中,也就是咱們能看到的找回密碼連接裏面的key,代碼以下:

function asyn_userkey($uid){
       global $db;
       $sql = "select * from ".table('members')." where uid = '".intval($uid)."' LIMIT 1";
       $user=$db->getone($sql);
       return md5($user['username'].$user['pwd_hash'].$user['password']);}

同目錄下的文件如圖所示:

則是具體功能的實現代碼,咱們這時候還不須要看,先再瞭解下程序的其餘結構。

3. 查看配置文件


  接下里咱們找找看配置文件,上面咱們有介紹到配置文件的文件名一般都帶有」 config」這樣的關鍵字,咱們只要搜索帶有這個關鍵字的文件名便可,如圖所示:

在搜索結果中咱們能夠看到有搜索出來多個文件,結合文件所在目錄這個經驗能夠判斷出data目錄下面的config.php以及cache_config.php纔是真正的配置文件,打開/data/config.php看看代碼,以下:

<?php
$dbhost   = "localhost";$dbname   = "1850pxs";$dbuser   = "root";$dbpass   = "123456";$pre    = "qs_";$QS_cookiedomain = '';$QS_cookiepath =  "/1850pxs/";$QS_pwdhash = "K0ciF:RkE4xNhu@S";define('QISHI_CHARSET','gb2312');define('QISHI_DBCHARSET','GBK');?>

很明顯的能夠看到這裏,頗有可能存在咱們以前說過的雙引號解析代碼執行的問題,一般這個配置是在安裝系統的時候設置的,或者後臺也有設置的地方,另外咱們還應該記住的一個點是QISHI_DBCHARSET常量,這裏配置的數據庫編碼是GBK,也就可能存在寬字節注入,不過須要看數據庫鏈接時設置的編碼,不妨找找看,找到騎士cms鏈接mysql的代碼在include\mysql.class.php文件的connect()函數,代碼以下:

function connect($dbhost, $dbuser, $dbpw, $dbname = '', $dbcharset = 'gbk', $connect=1){
       $func = empty($connect) ? 'mysql_pconnect' : 'mysql_connect';
       if(!$this->linkid = @$func($dbhost, $dbuser, $dbpw, true)){
              $this->dbshow('Can not connect to Mysql!');
       } else {
              if($this->dbversion() > '4.1'){
                     mysql_query( "SET NAMES gbk");
                     if($this->dbversion() > '5.0.1'){
                            mysql_query("SET sql_mode = ''",$this->linkid);
                            mysql_query("SET character_set_connection=".$dbcharset.", character_set_results=".$dbcharset.", character_set_client=binary", $this->linkid);
                     }
              }
       }
       if($dbname){
              if(mysql_select_db($dbname, $this->linkid)===false){
                     $this->dbshow("Can't select MySQL database($dbname)!");
              }
       }}

這段代碼裏面加粗部分有一個存在安全隱患的地方,代碼中首先判斷mysql版本是否大於4.1,若是是則執行:

mysql_query( "SET NAMES gbk");

執行這個語句以後再判斷,若是大於5則執行:

mysql_query("SET character_set_connection=".$dbcharset.", haracter_set_results=".$dbcharset.", character_set_client=binary", $this->linkid);

也就是說在mysql版本小於mysql5的狀況下是不會執行這行代碼的,可是執行了」set names gbk」,咱們在以前有介紹過」set names gbk」其實幹了三件事,等同於:

SET character_set_connection=’ gbk’, haracter_set_results=’ gbk’, character_set_client=’gbk’

所以在mysql版本大於4.1小於5的狀況下基本全部跟數據庫有關的操做都存在寬字節注入。

4. 跟讀首頁文件

  經過對系統文件大概的瞭解,咱們對這套程序的總體架構已經有了必定的瞭解,可是還不夠了解,因此咱們得跟讀一下index.php文件,看看程序運行的時候會調用哪些文件和函數。

打開首頁文件index.php能夠看到以下代碼:

if(!file_exists(dirname(__FILE__).'/data/install.lock'))  header("Location:install/index.php");define('IN_QISHI', true);$alias="QS_index";require_once(dirname(__FILE__).'/include/common.inc.php');

首先判斷安裝鎖文件是否存在,若是不存在則跳轉到install/index.php,接下里是包含/include/common.inc.php 文件,跟進該文件查看,代碼以下:

require_once(QISHI_ROOT_PATH.'data/config.php');header("Content-Type:text/html;charset=".QISHI_CHARSET);require_once(QISHI_ROOT_PATH.'include/common.fun.php');require_once(QISHI_ROOT_PATH.'include/1850pxs_version.php');

/include/common.inc.php文件在開頭包含了三個文件,data/config.php爲數據庫配置文件,include/common.fun.php文件爲基礎函數庫文件,include/1850pxs_version.php爲應用版本文件,接着往下看:

if (!empty($_GET)){
    $_GET  = addslashes_deep($_GET);}if (!empty($_POST)){
    $_POST = addslashes_deep($_POST);}$_COOKIE   = addslashes_deep($_COOKIE);$_REQUEST  = addslashes_deep($_REQUEST);

這段代碼調用了include/common.fun.php文件裏面的addslashes_deep() 函數對GET/POST/COOKIE參數進行了過濾,再往下走能夠看到又有一個包含文件的操做:

require_once(QISHI_ROOT_PATH.'include/tpl.inc.php');

包含了include/tpl.inc.php文件,跟進看看這個文件作了什麼,代碼以下

include_once(QISHI_ROOT_PATH.'include/template_lite/class.template.php');$smarty = new Template_Lite;$smarty -> cache_dir = QISHI_ROOT_PATH.'temp/caches/'.$_CFG['template_dir'];$smarty -> compile_dir =  QISHI_ROOT_PATH.'temp/templates_c/'.$_CFG['template_dir'];$smarty -> template_dir = QISHI_ROOT_PATH.'templates/'.$_CFG['template_dir'];$smarty -> reserved_template_varname = "smarty";$smarty -> left_delimiter = "{#";$smarty -> right_delimiter = "#}";$smarty -> force_compile = false;$smarty -> assign('_PLUG', $_PLUG);$smarty -> assign('QISHI', $_CFG);$smarty -> assign('page_select',$page_select);

首先看到包含了include/template_lite/class.template.php文件,這是一個映射程序模板的類,由Paul Lockaby paul和Mark Dickenson編寫,因爲該文件較大,咱們這裏不在仔細分析,繼續往下跟進,能夠看到這段代碼實例化了這個類對象賦值給$smarty變量,進行跟進則迴轉到index.php文件,代碼以下:

if(!$smarty->is_cached($mypage['tpl'],$cached_id)){require_once(QISHI_ROOT_PATH.'include/mysql.class.php');$db = new mysql($dbhost,$dbuser,$dbpass,$dbname);unset($dbhost,$dbuser,$dbpass,$dbname);$smarty->display($mypage['tpl'],$cached_id);}else{$smarty->display($mypage['tpl'],$cached_id);}

判斷是否已經緩存,而後調用display()函數輸出頁面,審計到這裏是否對整個程序的框架比較熟悉了?接下來只要像審計index.php文件同樣跟進其餘功能入口文件便可完成代碼通讀。

  整個全文通讀的思路大體如上所述,不一樣的程序有不一樣的實現方式,代碼審計須要很是強的邏輯能力,若是你對這方面比較感興趣,能夠參考書籍《代碼審計:企業級web代碼安全架構》繼續研究。

相關文章
相關標籤/搜索