admin

PHP代码审计入门篇二 ——MVC结构审计
PHP代码审计入门篇二 ——MVC结构审计0x0 MVC简介在审计代码之前我们先来了解下 什么是MVC。MVC模式...
扫描右侧二维码阅读全文
16
2019/05

PHP代码审计入门篇二 ——MVC结构审计

PHP代码审计入门篇二 ——MVC结构审计

0x0 MVC简介

在审计代码之前我们先来了解下 什么是MVC。

MVC模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。

  • 模型Model – 管理大部分的业务逻辑所有的数据库逻辑。模型提供了连接和操作数据库的抽象层。
  • 控制器Controller - 负责响应用户请求、准备数据,以及决定如何展示数据。
  • 视图View – 负责渲染数据,通过HTML方式呈现给用户。

mvc的运行流程:

  1. controller获取用户的请求
  2. controller根据获取的请求调用相应的Model完成状态的读写操作
  3. controller将Model处理的数据传递给View
  4. View将数据渲染将结果呈现给用户

0x1 了解目录结构

-- YxtCMF_v6.1
    -- admin  后台静态文件
    -- application    应用目录
    -- data            数据配置文件
    -- Expand        静态储存扩展
    -- plugins        插件
    -- public        资源文件目录
    -- themes        主题
    -- ueditor        编辑器
    -- update        升级目录
    -- Uploads        上传目录
    -- yxtedu        核心目录根据Thinkphp3.2.3开发的

├─ThinkPHP 框架系统目录(可以部署在非web目录下面)
│  ├─Common       核心公共函数目录
│  ├─Conf         核心配置目录 
│  ├─Lang         核心语言包目录
│  ├─Library      框架类库目录
│  │  ├─Think     核心Think类库包目录
│  │  ├─Behavior  行为类库目录
│  │  ├─Org       Org类库包目录
│  │  ├─Vendor    第三方类库目录
│  │  ├─ ...      更多类库目录
│  ├─Mode         框架应用模式目录
│  ├─Tpl          系统模板目录
│  ├─LICENSE.txt  框架授权协议文件
│  ├─logo.png     框架LOGO文件
│  ├─README.txt   框架README文件
│  └─ThinkPHP.php 框架入口文件

mvc架构的cms通常会有URL路由

例如我们访问http://serverName/index.php/login

等同于访问http://serverName/index.php/user/login/index

对应的controller类是:application\User\Controller\IndexController.class.php

了解相应的路由规则可以有效的定位漏洞的触发点

0x2 入口文件

<?php
if (ini_get('magic_quotes_gpc'))
{
    function stripslashesRecursive(array $array)
    {
        foreach ($array as $k => $v)
        {
            if (is_string($v))
            {
                $array[$k] = stripslashes($v);
            } else
            if (is_array($v))
            {
                $array[$k] = stripslashesRecursive($v);
            }
        }
        return $array;
    }
    $_GET = stripslashesRecursive($_GET);
    $_POST = stripslashesRecursive($_POST);
}
define("APP_DEBUG",false);
define('SITE_PATH', dirname(__file__) . "/");
define('APP_PATH', SITE_PATH . 'application/');
define('SPAPP_PATH', SITE_PATH . 'yxtedu/');
define('SPAPP', './application/');
define('SPSTATIC', SITE_PATH . 'statics/');
define("RUNTIME_PATH", SITE_PATH . "data/runtime/");
define("HTML_PATH", SITE_PATH . "data/runtime/Html/");
define("THINKCMF_CORE_TAGLIBS", 'cx,Common\Lib\Taglib\TagLibSpadmin,Common\Lib\Taglib\TagLibHome');
if (!file_exists("data/install.lock"))
{
    if (strtolower($_GET['g']) != "install")
    {
        header("Location:./index.php?g=install");
        exit();
    }
}
require SPAPP_PATH . 'Core/ThinkPHP.php';

line 2-21判断了是否开启了magic_quotes_gpc ,如果开启了则会对GET、POST获得的数据进行处理。处理使用的函数就是stripslashes返回一个去除转义反斜线后的字符串(\' 转换为 ' 等等)。双反斜线(\\)被转换为单个反斜线(\)。

此处感觉多此一举,如果是开启了magic_quotes_gpc,程序直接使用了

$_GET、$_POST传输的数据则会存在安全问题。

下面的流程就是引入ThinkPHP的核心文件和判断是否已经安装

0x3 审计

因为此程序采用的mvc模式 所以 审计我们只要把重点放在controller这里即可

有些读者可能不知该如何寻找出漏洞,在拿到源码后感觉无从下手。

笔者在这里分享下自己的几个方法以供参考

  • 使用Seay源码审计根据特征扫描出来的位置进行跟进审计
  • 通读整个源码的流程层层跟进审计(这种需要耗费大量时间,但时间与收益是对等的,遗漏的东西不多)
  • 黑盒+白盒共同测试,根据前台或后台页面位置的功能,有选择的审计某个功能所对应的源码。例如:sql注入一般是需要有输入的地方,那么可以找登录模块、搜索模块、文章模块等等

选择合适的方法可以为你接下来进行的工作提高效率。

0x3.1 前台登录一处注入

application\User\Controller\LoginController.class.php

LIne 116-140

 //登录验证
    function dologin(){
        if(!sp_check_verify_code()){
            $this->error("验证码错误!");
        }
        $users_model=M("Users");
        $rules = array(
                //array(验证字段,验证规则,错误提示,验证条件,附加规则,验证时间)
                array('username', 'require', '手机号/邮箱/用户名不能为空!', 1 ),
                array('password','require','密码不能为空!',1),
        
        );
        if($users_model->validate($rules)->create()===false){
            $this->error($users_model->getError());
        }
        
        $username=$_POST['username'];
        
        if(preg_match('/^\d+$/', $username)){//手机号登录
            $this->_do_mobile_login();
        }else{
            $this->_do_email_login(); // 用户名或者邮箱登录
        }
         
    }
  1. 首先验证了验证码是否正确(默认是没有开验证码功能的)
  2. 验证输入的内容是否满足 $rules的条件
  3. 132行直接接收了 $_POST传入的username
  4. 判断是以那种方式,手机号或者用户名登录的

问题出在了

因为是直接接受的Post传过来的内容,在Thinkphp 3.2.3where处存在缺陷如果没有经过I函数接受数据则会导致SQL注入

这个缺陷因为执行流程篇幅过长,分析会在后面的文章进行详细分析。

_do_mobile_login这里也存在,两个一样的类型。

0x3.2 后台广告编辑一处注入

application\Admin\Controller\AdController.class.php

Line 37-42

function edit(){
        $id=I("get.id");
        $ad=$this->ad_model->where("ad_id=$id")->find();
        $this->assign($ad);
        $this->display();
    }
    

where出直接拼接了参数,I函数默认使用的是htmlspecialchars

并不会转义单引号所以这里存在了注入

0x3.3 后台友情连接处注入

application\Admin\Controller\LinkController.class.php

//第一处注入
function edit(){
        $id=I("get.id");//下方使用了拼接字符
        $link=$this->link_model->where("link_id=$id")->find();
        $this->assign($link);
        $this->assign("targets",$this->targets);
        $this->display();
    }
function toggle(){
        if(isset($_POST['ids']) && $_GET["display"]){
            $ids = implode(",", $_POST['ids']);
            $data['link_status']=1;
            if ($this->link_model->where("link_id in ($ids)")->save($data)!==false) {
                $this->success("显示成功!");
            } else {
                $this->error("显示失败!");
            }
        }
        if(isset($_POST['ids']) && $_GET["hide"]){
            $ids = implode(",", $_POST['ids']);
            $data['link_status']=0;
            if ($this->link_model->where("link_id in ($ids)")->save($data)!==false) {
                $this->success("隐藏成功!");
            } else {
                $this->error("隐藏失败!");
            }
        }
    }

这几处注入都是因为自己拼接了字符串 或者使用原生的POST、GET获得数据没有任何过滤引起的注入

这种注入在此程序中有多处就不再一一列举

0x3.4 Getshell

没有getshell的审计,怎么能满足各位呢:)

application\Admin\Controller\RouteController.class.php

function index(){
        
        $routes=$this->route_model->order("listorder asc")->select();
        sp_get_routes(true);
        $this->assign("routes",$routes);
        $this->display();
    }

首先获取了数据库中route表内容的数据,跟进sp_get_routes

先从数据库中取出status为1的路由规则,然后对取出来的路由数组进行htmlspecialchars_decode解码

在Line 1225-1227

F("routes",$cache_routes);
    $route_dir=SITE_PATH."/data/conf/";
    if(!file_exists($route_dir)){
        mkdir($route_dir);
    }
        
    $route_file=$route_dir."route.php";
        
    file_put_contents($route_file, "<?php\treturn " . stripslashes(var_export($all_routes, true)) . ";");

这里判断了/data/conf/文件夹是否存在不存在会创建,接着往下看可以看到使用file_put_contents函数进行了文件操作。将从数据表获取的数据以键值的形式存进route.php。这里要注意的是stripslashes

,如果没有这个函数的存在,数据中的'会被var_export全部转义。

因为多了一个stripslashes去除转义符,那么getshell的思路就有了:

  1. 首先在后台添加路由到数据库内,不必担心添加数据时是否会被过滤.
  2. 添加后转到路由规则设置的首页
  3. 首页执行index()将数据库里的内容写入route.php

保存后访问route.php看看是否成功

成功getshell

0x4 篇外

代码审计需要有足够的耐心和细心,此处的getshell处,笔者也是找了很久才发现。

多多阅读各位前辈公开的代码审计案例,以及乌云1000php,可以学习到一些不错的思路。

笔者已经搭建了乌云1000php,在这里贴上地址:http://php.evi1s.com/

当然此程序还有其他的漏洞,例如变量覆盖,xss等这里笔者就留给各位学习代码审计的道友去自行审计学习了

如果内容有错误处,请联系笔者,避免带偏其他人。

Last modification:May 16th, 2019 at 01:37 pm
If you think my article is useful to you, please feel free to appreciate

2 comments

  1. 咸鱼

    大佬能给php1000案例加一个搜索么。

    1. admin
      @咸鱼

      感觉搜索没有什么必要啊,如果是看特定的可以在乌云漏洞镜像直接搜索了,这个也是我简单的用js分了下页

Leave a Comment