PHPCMSv9.6.0代码审计---后台命令执行

PHPCMSv9.6.0代码审计---后台命令执行

一月 18, 2022

后台—命令执行

modules/dbsource/data.php

后台的模块—>数据源—>添加数据源调用处:

127.0.0.1/phpcmsv9.6.0/index.php?m=dbsource&c=data&a=add

img

自定义SQL处:

img

img

image-20220118230316840

1
当选择自定义SQL的调用方法时,满足$type=1 && $dis_type=3的情况,即调用方式为自定义SQL且输出方式为js时

​ 此时传入参数存在的情况下,参数tpye data name dis_type cache num template通过post传参,如果没有caches/cahces_template/dbsource这个文件夹,就会在对应的路径下自动创建php文件,并且可写入文件的内容的参数可控($template)。

image-20220118230638397

img

可以看到内容成功写入,但是没有办法直接访问。因为没有定义常量IN_PHPCMS,直接访问文件时,输出No permission resources后代码执行结束。exit()直接退出,没有办法执行后面的代码。

因此尝试去寻找文件包含的点,如果存在文件包含,可以通过文件包含漏洞执行命令。

全局搜索:

1
CACHE_PATH.'caches_template'.DIRECTORY_SEPARATOR.'dbsource'.DIRECTORY_SEPARATOR;

因为需要找到可以包含该固定文件的点,所以全局搜索了一下这个路径:

img

在同文件夹下找到一个函数:(modules/dbsourse/)

img

该函数存在对应文件路径。当传输$id时,如果文件路不存在,则创建对应文件,并将可控参数$str写入文件中,返回文件的路径caches/cahces_template/dbsource/$id.php。

接下来看看哪些地方调用了这个函数:

img

可以看到在和一开始的modules/dbsourse/data.php同文件夹下的call.php中调用了这个函数,且存在文件包含的关键函数include,尝试跟进查看:

img

从44行开始,是调用template_url()函数的_format()函数定义:

当$type=3时,调用template_url()函数,返回我们需要的路径,通过include形成文件包含;

将文件包含执行的结果通过format_js方法的document.write()函数打印到浏览器的页面上。

这里可以查看format_js()函数的定义,查看函数的执行过程。(见补充)

​ 到这里,我们已经找到了能执行template_url()的函数_format(),再继续查看哪些地方调用了_format()函数,找到可以执行触发的点:

img

在同一文件下的第40行,存在函数的调用,向上找到定义处(第9行-第42行):

(自行加了一些注释,就不贴图了)

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
30
31
32
33
34
public function get() {
$id = isset($_GET['id']) && intval($_GET['id']) ? intval($_GET['id']) : exit(); //获取id值--get
if ($data = $this->db->get_one(array('id'=>$id))) { //将获取到的id的正行内容赋值给$data,如果存在,则进行下面语句
if (!$str = tpl_cache('dbsource_'.$id,$data['cache'])) { //tpl_cache返回false,条件成立.
if ($data['type'] == 1) { //自定义SQL调用
$get_db = pc_base::load_model("get_model");
$sql = $data['data'].(!empty($data['num']) ? " LIMIT $data[num]" : '');//如果num的值不为空则去num拼接为:data limit num;赋值给$sql
// echo $sql; //自增加代码,方便调试;输出 $data limit $num;
$r= $get_db->query($sql); //返回SQL语句查询的结果; true/false
while(($s = $get_db->fetch_next()) != false) {
$str[] = $s;
}
} else {
$filepath = PC_PATH.'modules'.DIRECTORY_SEPARATOR.$data['module'].DIRECTORY_SEPARATOR.'classes'.DIRECTORY_SEPARATOR.$data['module'].'_tag.class.php';
if (file_exists($filepath)) {
$pc_tag = pc_base::load_app_class($data['module'].'_tag', $data['module']);
if (!method_exists($pc_tag, $data['action'])) {
exit();
}
$sql = string2array($data['data']);
$sql['action'] = $data['action'];
$sql['limit'] = $data['num'];
unset($data['num']);
$str = $pc_tag->{$data['action']}($sql);

} else {
exit();
}
}
if ($data['cache']) setcache('dbsource_'.$id, $str, 'tpl_data');
}
echo $this->_format($data['id'], $str, $data['dis_type']); //输出_format($data['id'], $str, $data['dis_type']);的执行结果
}
}

该函数的执行过程为:

1
2
3
4
5
6
7
当获取到的id值存在的话,将获取到的对应id值的整行数据赋值给$data数组,tpl_cache返回false,条件成立向下执行。

当$data[type']等于1时,调用方式为自定义SQL调用,当num的值不为空的时候拼接成$data limit $num赋值为$sql;

然后输出_format($data['id'], $str, $data['dis_type']);的执行结果。

因此,当我们通过get方式传入存在的id值时,执行get()函数,就能调用_format()函数,因此$type即dis_type,满足case等于3的情况,通过format_js()输出文件包含后的结果。

​ 这里再通过template_url($id)进行追踪值理清前后关系:

回到一开始的modules/dbsourse/data.php中解释参数的含义:

1
2
3
4
5
6
7
8
9
CACHE_PATH.'caches_template'.DIRECTORY_SEPARATOR.'dbsource'.DIRECTORY_SEPARATOR.$id.'.php';

data.php中的$id即为数据库中的id值,对应生成的文件的名字,前后含义一致(前:data.php;后:call.php-->get());

data.php中的$dis_type=3即输出方式为js,满足前后利用的$dis_type=3的条件;

data.php中的$str即为$template,如果写入命令执行的代码,可以通过format_js()中的document.weite()将执行的结果返回在浏览器页面中;

$data即为接收到的自定义SQL的内容值,对应get()函数用到的$data[data]值。

(补充)这里补充说明format_js()执行的过程:

img

format_js()函数将传入的内容$html通过document.write()返回在页面上。

document.write($string);

会将页面清空,输出变量$string的值。

img

因此在该函数的调用中,会将文件包含的文件中的执行结果($html)返回到页面中。

但是这里存在一个问题,在get()函数的执行中,

1
2
3
4
$r= $get_db->query($sql);  //返回SQL语句查询的结果; true/false
$sql=$data limit $num;

必须绕过$data limit $num,否则的话语句执行错误会返回一个报错界面,代码无法执行。(报错代码的显示,我们可以构造报错语句执行报错注入)

img

img

绕过的方法就是构造正确的SQL语句:

img

构造select 语句:

img

因此我们可以将data的值构造成带有select即可:

select 1等。

补充说明:

select直接加数字时,可以不写后面的表名,那么它输出的内容就是我们select后的数字。

这时如果我们写的一串数字就是一个数组(或1个行向量),这时select实际上没有向任何一个数据库查询数据,即查询命令不指向任何数据库的表。返回值就是我们输入的这个数组,这时它是个1行n列的表,表的属性名和值都是我们输入的数组:(内容来自:https://www.cnblogs.com/xxzl20171025/p/12624682.html)

img

通过data.php页面的写入:每次写入的时候$name都需要改

img

命令执行结果:(call.php—>get()函数触发)

img

还可以查看目录信息:

img


后台—SQL注入

这里补充一下上面提到的SQL注入问题,从报错的语句中我们可以尝试进行报错注入:

通过$data构造SQL语句:

image-20220118231713947

可以看到成功执行了语句。

也可以查到表名等信息。

img

参考文章

https://blog.csdn.net/Zlirving_/article/details/113572457

后序

​ phpcms的代码相对来说会更加复杂的一点,符合MVC的架构,也有尝试根据功能点和抓到的数据包找对应的文件和函数,但是自己能想到的点还是很少的,因此文章也是参照别人的博客进行了一个自己的梳理和分析。