1. 程式人生 > 其它 >「程式碼審計」那些程式碼審計的思路

「程式碼審計」那些程式碼審計的思路

  前言

  程式碼審計工具的實現都是基於程式碼審計經驗開發出來用於優化工作效率的工具,我們要學好程式碼審計就必須要熟悉程式碼審計的思路。而且程式碼審計是基於PHP語言基礎上學習的,學習程式碼審計最基本的要求就是能讀懂程式碼。常見的程式碼審計思路有以下四種:

  根據敏感關鍵字回溯引數傳遞過程;

  查詢可控變數,正向追蹤變數傳遞過程;

  尋找敏感功能點,通讀功能點程式碼;

  直接通讀全文程式碼。

  敏感函式回溯引數過程

  根據敏感函式來逆向追蹤引數的傳遞過程,是目前使用的最多的一種方式,因為大多數漏洞是由於函式的使用不當造成的。另外非函式使用不當的漏洞,如SQL注入,等以後學習再詳細介紹。這種方式的優缺點如下:

  優點:只需搜尋相應敏感關鍵字,即可快速挖掘想要的漏洞,可定向挖掘,高效、高質量;

  缺點:由於沒有通讀程式碼,對程式整體架構瞭解不夠深入,在挖掘漏洞時定位利用會花點時間,另外對邏輯漏洞挖掘覆蓋不到。

  espcms注入挖掘案例:

  開啟seay原始碼審計系統,點選左上角新建專案,選擇下載的espcms資料夾,點選自動審計,開始審計,得到可能存在漏洞,漏洞檔案的路徑,和漏洞程式碼列表。

  我們挑選其中的一條程式碼

  雙擊直接定位到這行程式碼,選中該變數後,可以看到變數的傳遞過程,在左側點選parentid函式,在下面詳細資訊的地方可以看到parentid函式,在下面詳細資訊的地方可以看到parentid變數獲得。

  右鍵選中這行程式碼,定位函式主體accept,點選右鍵,選擇定位函式

  可以看到跳轉到了class_function.php檔案,程式碼如下:

  可以看到這是一個獲取GET、POST、COOKIE引數值得函式,我們傳入的變數是parentid和R,則代表在POST、GET中都可以獲取parentid引數,最後經過一個daddslashes()函式,實際上是包裝的addslashes()函式,對單引號等字元進行過濾。看前面的SQL語句是這樣的:

  $sql=“select * from db_table where parentid=dbtablewhereparentid=parentid”;

  並不需要單引號來閉合,可以直接注入。

  在citylist.php檔案看到oncitylist()函式在important類中,選中該類名右鍵點選,選擇全域性搜尋

  可以看到index.php檔案有例項化該類,程式碼如下:

  $archive=indexget(‘archive’, ‘R’);

  $archive=empty($archive) ? ‘adminuser’ : $archive;

  $action=indexget(‘action’, ‘R’);

  $action=empty($action) ? ‘login’ : $action;

  $soft_MOD=array(‘admin’, ‘public’, ‘product’, ‘forum’, ‘filemanage’, ‘basebook’, ‘member’, ‘order’, ‘other’, ‘news’, ‘inc’, ‘cache’, ‘bann’, ‘logs’, ‘template’);

  if (in_array($point, $soft_MOD)) {

  include admin_ROOT . adminfile . “/control/$archive.php”;

  $control=new important();

  $action=‘on’ . $action;

  if (method_exists($control, $action)) {

  $control->$action();

  } else {

  exit(‘錯誤:系統方法錯誤!’);

  }

  這裡可以看到一個include檔案的操作,可惜經過了addslashes()函式無法進行階段使其包含任意檔案,只能包含本地的PHP檔案,往下是例項化類並且呼叫函式的操作,根據程式碼可以構造出利用EXP:

  127.0.0.1/espcms/upload/adminsoft/index.php?archive=citylist&action=citylist&parentid=-1 union select 1,2,user(),4,5

  通讀全文程式碼

  通讀全文程式碼也有一定的技巧,否則很難讀懂Web程式的,也很難理解程式碼的業務邏輯。首先我們要看程式的大體結構,如主目錄有哪些檔案,模組目錄有哪些檔案,外掛目錄有哪些檔案,另外還要注意檔案的大小,建立時間,就可以大概知道這個程式實現了那些功能,核心檔案有哪些。

  如discuz的主目錄如下圖所示:

  在看目錄結構的時候,特別注意以下幾個檔案:

  函式集檔案

  函式集檔案通常命名中包含functions或者common等關鍵字,這些檔案裡面是一些公共的函式,提供給其他檔案統一呼叫,所以大多數檔案都會在檔案頭部包含到其他檔案。尋找這些檔案的一個技巧就是開啟index.php或者一些功能性檔案。配置檔案

  配置檔案通常命名中包含config關鍵字,配置檔案包括Web程式執行必須的功能性配置選項以及資料庫等配置資訊。從這個檔案可以瞭解程式的小部分功能,另外看這個檔案的時候注意觀察配置檔案中引數是用單引號還是雙引號,如果是雙引號,則很可能會存在程式碼執行漏洞。安全過濾檔案

  安全過濾檔案對我們做程式碼審計至關重要,通常命名中有filter、safe、check等關鍵字,這類檔案主要是對引數進行過濾,比較常見的是針對SQL注入和XSS過濾,還有檔案路徑、執行的系統命令的引數。index檔案

  index是一個程式的入口檔案,所以我們只要讀一遍index檔案就可以大致瞭解整個程式的架構、執行的流程、包含到的檔案。

  騎士cms通讀審計案例

  (1)檢視應用檔案結構

  首先看看有哪些檔案和資料夾,尋找名稱裡有沒有帶api、admin、manage、include一類關鍵字的檔案和資料夾。可以看到有一個include資料夾,一般比較核心的檔案都會放在這個資料夾中。

  (2)檢視關鍵檔案程式碼

  在這個資料夾裡可以看多多個數十K的檔案,弱common.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(‘&’, ‘"’, ‘<’, ‘>’), 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','outfile','dumpfile');

  $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 .='(???)?';

  $pattern .='|(?([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即可驗證是否有許可權,被用在註冊、找回密碼等驗證過程中,程式碼如下:

  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”這樣的關鍵字,我們只要搜尋帶有這個關鍵字的檔名即可:

  在搜尋結果中我們可以看到搜尋到多個檔案,結合經驗可以判斷config.php以及cache_config.php才是真正的配置檔案,開啟config.php檢視程式碼:

  

  $dbhost=“localhost”;

  $dbname=”74cms”;

  $dbuser=”root”;

  $dbpass=”123456”;

  $pre=”qs_”;

  $QS_cookiedomain=‘’;

  $QS_cookiepath=“/74cms/“;

  $QS_pwdhash=“K0ciF:RkE4xNhu@S”;

  define(‘QISHI_CHARSET’,’gb2312’);

  define(‘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.”, character_set_results=”.$dbcharset.”, character_set_client=binary”, $this->linkid);

  也就是說在MySQL版本小於5的情況下是不會執行這行程式碼的,

  但是執行了”set names gbk”,我們在之前介紹過”set names gbk”其實幹了三件事,等同於:

  SET character_set_connection=’gbk’, character_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.php’);

  require_once(QISHI_ROOT_PATH.’include/74cms_version.php’);

  \include\common.inc.php檔案在開頭包含了三個檔案,data\config.php為資料庫配置檔案,include\common.php檔案為基礎函式庫檔案,include\74cms_version.php為應用版本檔案。

  再看下面的程式碼:

  f (!empty($_GET))

  {

  $_GET=addslashes_deep($_GET);

  }

  if (!empty($_POST))

  {

  $_POST=addslashes_deep($_POST);

  }

  $_COOKIE=addslashes_deep($_COOKIE);

  $_REQUEST=addslashes_deep($_REQUEST);

  這段程式碼呼叫了include\common.php檔案裡面的addslashes_deep()函式對GET、POST、COOKIE引數進行了過濾。

  再往下可以看到有一個包含檔案的操作:

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

  包含了include pl.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 -> force_compile=false;

  $smarty -> assign(‘_PLUG’, $_PLUG);

  $smarty -> assign(‘QISHI’, $_CFG);

  $smarty -> assign(‘page_select’,$page_select);

  首先看到包含了include emplate_lite\class.template.php檔案,這是一個對映程式模板的類,繼續往下看,可以看到這段程式碼例項化了這個類物件賦值給¥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檔案一樣跟進其他功能入口檔案即可完成程式碼通讀。

  根據功能點定向審計

  根據經驗我們簡單介紹幾個功能點會出現的漏洞:

  檔案上傳功能

  這裡說的檔案上傳在很多功能點都會出現,比如像文章編輯、資料編輯、頭像上傳、附件上傳,這個功能最常見的漏洞就是任意檔案上傳了,後端程式沒有嚴格地限制上傳的格式,導致可以上傳或者存在繞過的情況,而除了檔案上傳功能外,還經常發生SQL注入漏洞。檔案管理功能

  在檔案管理功能中,如果程式將檔名或者檔案路徑直接在引數中傳遞,則很有可能會存在任意檔案的操作漏洞,比如任意檔案讀取等,利用的方法是在路徑中使用…/或者…\跳轉目錄。

  除了任意檔案操作漏洞外,還可能會存在XSS漏洞,程式會在頁面中輸出檔名,而通常會疏忽對檔名進行過濾,導致可以在資料庫中存入帶有尖括號等特殊符號的檔名,最後在頁面顯示的時候就會被執行。登入認證功能

  登入認證功能不是指一個過程,而是整個操作過程中的認證,目前的認證方式大多是基於Cookie和Session,不少程式會把當前登陸的使用者賬號等認證資訊放到Cookie中,或許是加密方式。進行操作的時候直接從Cookie中讀取當前使用者資訊,這裡就存在一個演算法可信的問題,如果這段Cookie資訊沒有加salt一類的東西,就可以導致任意使用者登入漏洞,只要知道使用者的不扥資訊,即可生成認證令牌,甚至有的程式會直接把使用者名稱放到Cookie中,操作的時候直接讀取這個使用者名稱的資料,這也是常說的越權漏洞。找回密碼功能

  找回密碼雖然看起來不像任意檔案上傳這種可以危害到伺服器安全的漏洞,但是如果可以重置管理員的密碼,也是可以間接控制業務許可權甚至拿到服務許可權的。找回密碼功能的漏洞有很多利用場景,最常見的是驗證碼爆破。目前特別是APP應用,請求後端驗證碼的時候大多是4位,並且沒有限制驗證碼的錯誤次數和有效時間,於是就出現了爆破的漏洞。