qiqing's Blog.

DolphinPHP 全版本后台代码执行漏洞

字数统计: 1.2k阅读时长: 4 min
2020/03/16 Share

DolphinPHP 全版本后台代码执行漏洞

DophinPHP(海豚PHP)是一个基于ThinkPHP5.1.39LTS开发的一套开源PHP快速开发框架,为开发集成了基于数据-角色的权限管理机制,集成多种灵活快速构建工具,可方便快速扩展的模块、插件、钩子、数据包,他没有前台的代码,所以的操作都在后台,所以,对这类cms的审计基本都只看后台的文件

分析
代码执行漏洞点

定位到漏洞文件 application/common.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
function action_log($action = null, $model = null, $record_id = '', $user_id = null, $details = '')
{
...
// 查询行为,判断是否执行
$action_info = model('admin/action')->where('module', $module)->getByName($action);
...
// 解析日志规则,生成日志备注
if(!empty($action_info['log'])){
if(preg_match_all('/\[(\S+?)\]/', $action_info['log'], $match)){
$log = [
'user' => $user_id,
'record' => $record_id,
'model' => $model,
'time' => request()->time(),
'data' => ['user' => $user_id, 'model' => $model, 'record' => $record_id, 'time' => request()->time()],
'details' => $details
];

$replace = [];
foreach ($match[1] as $value){
$param = explode('|', $value);
if(isset($param[1])){
$replace[] = call_user_func($param[1], $log[$param[0]]);
}else{
$replace[] = $log[$param[0]];
}
}
...
}

简单分析下这段代码,从数据库中读取数据赋值给$action_info,之后,从$action_info数组中取log下标的值,并通过正则赋值给了$match,之后,遍历$match的值,使用”|”分割并生成$param数组,将$param[1]和$log[$param[0]]带入了call_user_func函数。

$param的值是从数据库中传入的,如果能控制数据库,我们就能控制这个值,$log是一个数组,其中大多数参数都是外部传入,有可能可控,现在捋一捋思路,1、先控制数据库->将payload更新进对应的字段中->找调用action_log的控制器文件->传进action_log的参数可控->触发漏洞

先看看是否能把payload更新进数据库,这里跟进查询语句中的admin/action

image-20200206102540605

model(‘admin/action’)调用的admin模块里的action模型,这里跟进这个模型

image-20200206113135825

thinkphp5的写法,一般在controller目录下调用这个模型进行增删改查的文件,名字都相同,这里跟进controller目录下的Action.php

image-20200206114049217

但是这个类中没有编辑函数,打开浏览器查看下

image-20200206114327494

在页面上有编辑功能,这里回到源码中,跟进他的父类

image-20200206114412798

在Admin这个类,存在edit函数,跟进他的逻辑看下是怎么更新的,重点是验证函数有无过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function edit($id = '')
{
if ($id === '') $this->error('参数错误');
// 获取表单项
$cache_name = $this->request->module().'/'.parse_name($this->request->controller()).'/edit';
$cache_name = strtolower($cache_name);
$form = Cache::get($cache_name, []);
......
// 保存数据
if ($this->request->isPost()) {
// 表单数据
$data = $this->request->post();
$_pop = $this->request->get('_pop');

// 验证
if ($form['validate'] != '') {
$result = $this->validate($data, $form['validate']);
if(true !== $result) $this->error($result);
}
......
// 更新数据
if (false !== Db::name($form['table'])->update($data)) {

......

根据不同的表单项从cache中获取不同的配置,这里偷懒,直接打印配置就好,一般来讲,validate类和model类,名字也是一样的,这里跟进validate目录下的action.php查看验证规则

image-20200206123723542

回到common.php查看带入参数有无在验证规则内

image-20200206124349322

我们需要的是log字段,这个字段没做过滤,那将我们的payload带入数据库从这里看已经没问题了,接着往下看,payload要如何构造

image-20200206125025012

首先,先构造最简单的call_user_func(‘phpinfo’,’1’),从这个逆推回$action_info[‘log’],可以得到$action_info[‘log’]=[$log的下标|phpinfo],$log这个变量中,从外界传入的就4个,能用的只有最后一个,所以,这里构造的payload应该是$action_info[‘log’]=[details|phpinfo]

将payload更新进数据库中,没有过滤

image-20200206125720948

image-20200206125740175

之后就要找触发这个漏洞的文件,在attachment.php的disable函数中,刚好满足我们的要求,传进action_log中的$details是由我们输入的ids,这里需要跟进下父类的parent::setStatus函数

image-20200206150624425

1
2
3
4
5
6
7
8
9
10
11
public function setStatus($type = '', $record = [])
{
......
if (!empty($record)) {
call_user_func_array('action_log', $record);
}
$this->success('操作成功');
} else {
$this->error('操作失败');
}
}

输入的值都没过滤,直接进入action_log函数

进入后台进行验证下

先上传一张图片

image-20200206151413968

进入行为管理,编辑启动附件项

image-20200206151501819

所属模块改成系统

image-20200206151533528

日志规则改成[details|phpinfo]

image-20200206151615460

在附件管理中启动附件,触发phpinfo

image-20200206151716981

这几段代码在DophinPHP最初的版本就存在,所以应该是全版本都通用的

CATALOG
  1. 1. DolphinPHP 全版本后台代码执行漏洞
    1. 1.0.0.0.1. 分析
    2. 1.0.0.0.2. 代码执行漏洞点