Thinkphp5.0.2Rce分析

php菜鸟来复现一下thinkphp的rce,如果有什么分析的不对的地方欢迎指正。

一.环境搭建

php7.3.4

apache2.4.39

 

二、漏洞复现

poc:

s=whoami&_method=__construct&method=POST&filter[]=system

aaaa=whoami&_method=__construct&method=GET&filter[]=system

_method=__construct&method=GET&filter[]=system&get[]=whoami

三、漏洞分析

这里将从最开始分析如何rce的。之前这个

漏洞没分析过。

使用phpstorm+phpstudy进行debug调试

从index.php开始

首先定义APP_PATH这个常量为/../application/,就是定义web的路径位置。

然后包含thinkphp里面的start.php文件,这里就是加载框架引导文件。

让我们来看看 start.php里面是什么。也很简单的代码,两行代码。

第一个包含base.php文件,注释也写了是加载基础文件

 

下面看到一大堆define,是定义常量,版本号、路径什么的。这里我们就不一一看了,因为看了也不能记住这么多常量,反正就是定义一大堆常量。ok继续往下,哈哈。

然后是包含了loader.php,看一下注释,载入loader类。

base.php

<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2016 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------

define('THINK_VERSION', '5.0.2');
define('THINK_START_TIME', microtime(true));
define('THINK_START_MEM', memory_get_usage());
define('EXT', '.php');
define('DS', DIRECTORY_SEPARATOR);
defined('THINK_PATH') or define('THINK_PATH', __DIR__ . DS);
define('LIB_PATH', THINK_PATH . 'library' . DS);
define('CORE_PATH', LIB_PATH . 'think' . DS);
define('TRAIT_PATH', LIB_PATH . 'traits' . DS);
defined('APP_PATH') or define('APP_PATH', dirname($_SERVER['SCRIPT_FILENAME']) . DS);
defined('ROOT_PATH') or define('ROOT_PATH', dirname(realpath(APP_PATH)) . DS);
defined('EXTEND_PATH') or define('EXTEND_PATH', ROOT_PATH . 'extend' . DS);
defined('VENDOR_PATH') or define('VENDOR_PATH', ROOT_PATH . 'vendor' . DS);
defined('RUNTIME_PATH') or define('RUNTIME_PATH', ROOT_PATH . 'runtime' . DS);
defined('LOG_PATH') or define('LOG_PATH', RUNTIME_PATH . 'log' . DS);
defined('CACHE_PATH') or define('CACHE_PATH', RUNTIME_PATH . 'cache' . DS);
defined('TEMP_PATH') or define('TEMP_PATH', RUNTIME_PATH . 'temp' . DS);
defined('CONF_PATH') or define('CONF_PATH', APP_PATH); // 配置文件目录
defined('CONF_EXT') or define('CONF_EXT', EXT); // 配置文件后缀
defined('ENV_PREFIX') or define('ENV_PREFIX', 'PHP_'); // 环境变量的配置前缀

// 环境常量
define('IS_CLI', PHP_SAPI == 'cli' ? true : false);
define('IS_WIN', strpos(PHP_OS, 'WIN') !== false);

// 载入Loader类
require CORE_PATH . 'Loader.php';

// 加载环境变量配置文件
if (is_file(ROOT_PATH . '.env')) {
    $env = parse_ini_file(ROOT_PATH . '.env', true);
    foreach ($env as $key => $val) {
        $name = ENV_PREFIX . strtoupper($key);
        if (is_array($val)) {
            foreach ($val as $k => $v) {
                $item = $name . '_' . strtoupper($k);
                putenv("$item=$v");
            }
        } else {
            putenv("$name=$val");
        }
    }
}

// 注册自动加载
\think\Loader::register();

// 注册错误和异常处理机制
\think\Error::register();

// 加载惯例配置文件
\think\Config::set(include THINK_PATH . 'convention' . EXT);

看看Loader.php文件是什么,一个是Loader类,这里就啥也没干,把这个类加载进去了,就可以创建这个类了。为了文章简短,就不贴代码了。

Loader.php

接着是加载环境变量配置文件。这里的.env就是本地开发的时候配置文件,为了方便开发人员的一个东西,这边因为我们没有这个文件,所以就没有进入里面,不过和我们分析这个也没有什么关系。

接着就是注册自动加载了,我们刚刚知道他是加载了Loader类的,是在think命令空间下面。

那\think\Loader::register()的意思就是调用think命名空间下Loader类的静态register方法。

下面是这个函数,我们分析一下做了什么。先看传参,注意前面是没有传参的,也就是这里的$autoload是默认为空。

接着就是使用spl_auto_register注册一个系统自动加载。

接着就是注册命令空间定义。

后面就是加载文件,检测存在不存在,不存在跳过,存在的话就进行加载。

这里没必要去仔细研究里面是如何记载的。

 public static function register($autoload = '')
    {
        // 注册系统自动加载
        spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true);
        // 注册命名空间定义
        self::addNamespace([
            'think'    => LIB_PATH . 'think' . DS,
            'behavior' => LIB_PATH . 'behavior' . DS,
            'traits'   => LIB_PATH . 'traits' . DS,
        ]);
        // 加载类库映射文件
        if (is_file(RUNTIME_PATH . 'classmap' . EXT)) {
            self::addClassMap(__include_file(RUNTIME_PATH . 'classmap' . EXT));
        }

        // Composer自动加载支持
        if (is_dir(VENDOR_PATH . 'composer')) {
            self::registerComposerLoader();
        }

        // 自动加载extend目录
        self::$fallbackDirsPsr4[] = rtrim(EXTEND_PATH, DS);
    }

下面是注册错误和异常处理机制

think命令空间下面Error类的register静态方法。

来看看下一步,直接到了Loader类中的autoload方法,我的理解是那个自动加载生效了。传入的是think\Error,先检测命名空间别名,因为这里使用的是系统的命名空间,并不是别名,就跳过了。

 public static function autoload($class)
    {
        // 检测命名空间别名
        if (!empty(self::$namespaceAlias)) {
            $namespace = dirname($class);
            if (isset(self::$namespaceAlias[$namespace])) {
                $original = self::$namespaceAlias[$namespace] . '\\' . basename($class);
                if (class_exists($original)) {
                    return class_alias($original, $class, false);
                }
            }
        }

        if ($file = self::findFile($class)) {

            // Win环境严格区分大小写
            if (IS_WIN && pathinfo($file, PATHINFO_FILENAME) != pathinfo(realpath($file), PATHINFO_FILENAME)) {
                return false;
            }

            __include_file($file);
            return true;
        }
    }

看到有一个判断,是否存在该文件,然后进入之后判断是不是win环境,如果是win的话,严格区分大小写。然后就是包含该文件,也就是加载该类,这样做的好处就是把核心类全部放入特定文件夹,然后文件可以直接使用,他会自动记载。ok,return true,往下看。

我们刚刚讲到了,是调用Error类的register方法,他自动加载了该类,然后调用该方法,我们看看里面是什么。

error_reporting() 函数规定报告哪个错误。

set_error_handler() 函数设置用户自定义的错误处理函数。

set_exception_handler() 函数设置用户自定义的异常处理函数。

register_shutdown_function该函数是来注册⼀个会在PHP中⽌时执⾏的函数。

接着记载完注册错误和异常处理机制之后就是 加载惯例配置文件。
经过了前面的分析,那我们可以知道下面的代码是调用了think命名空间下面的Config类的静态方法set方法。他会先加载Config这个类,然后调用set方法。

 

接着来看看。包含convention文件,里面是return一个数组,那么传入的就是一个数组。

 public static function set($name, $value = null, $range = '')
    {
        $range = $range ?: self::$range;
        if (!isset(self::$config[$range])) {
            self::$config[$range] = [];
        }
        if (is_string($name)) {
            if (!strpos($name, '.')) {
                self::$config[$range][strtolower($name)] = $value;
            } else {
                // 二维数组设置和获取支持
                $name                                                 = explode('.', $name);
                self::$config[$range][strtolower($name[0])][$name[1]] = $value;
            }
            return;
        } elseif (is_array($name)) {
            // 批量设置
            if (!empty($value)) {
                self::$config[$range][$value] = isset(self::$config[$range][$value]) ?
                array_merge(self::$config[$range][$value], $name) :
                self::$config[$range][$value] = $name;
                return self::$config[$range][$value];
            } else {
                return self::$config[$range] = array_merge(self::$config[$range], array_change_key_case($name));
            }
        } else {
            // 为空直接返回 已有配置
            return self::$config[$range];
        }
    }

将$range赋值为self::$range,也就是_sys_

然后判断是否不存在_sys_这个变量,从我们传过来的数组里面,也就是那个convention.php文件里面判断,然后不存在就设置为空,注意这边是一个数组。

接着判断convention.php返回的结果是不是string类型,如果是的话就判断是否存在.,如果不存在就将其小写,然后写入到config[_sys_]数组中去,值为null。如果有点的话,就以分割,然后存储为二维数组。接着判断是否$name是否为数组,

如果不是字符串是数组的话,就先判断是不是空数组,不是的话就将其中的值放入config中,这是Config类中的config变量。设置他的值,所以我们可以知道,配置文件就是convention.php文件,因为他会从这里加载配置,ok。继续往下,有个注释,如果返回的值为空的话,就直接返回,$range为空。 

 然后回到start.php,我们回忆一下base.php里面做了什么,加载了许多常量,然后自动加载类,然后错误和异常处理机制,然后加载配置文件。

接着就是App::run()->send(),有些人看到这个可能有点害怕,这啥呀这是,不懂,没事,我们一步一步来分析,App::run()这种是不是常见,就是调用App的静态run方法,没有加命令空间罢了。然后->send()是啥意思,我们先调试看看前面第一步先干嘛。

 

 第一步不出意外,加载这个App类。

然后调用run方法,也不出意外。传入的参数为空,第一行,先判断$request是不是空,为空的话就调用Request类中的instance方法,得到的结果赋值给$request。不出意外的话就是自动记载Request类了,这边就直接跳了,直接看instance方法。

 判断自己这个类的$instance 是不是空,看到下面的调试,他是为空的,那就创建一个static对象给他,也就意味着,$instance是一个static类,不出意外的话,他会自动加载这个static类,这里也不演示了。 

 虽然不演示他的自动加载,但是他是创建一个类,会调用里面的__construct方法,这边我们分析一下干了啥。传入的参数是空,所以第一个循环就没进去了,然后判断filter是不是空,当然应该也是空,那么就调用配置文件的get方法来获取这个default_filter参数。那到底是不是获取配置文件的这个参数呢,我们来分析分析get方法就好了

 传入的参数为default_filter,第二个参数为空,先将config类中的$range赋值给$range,前面分析了是_sys_,然后判断$name,也就是我们第一个参数,是不是空,并且config[_sys_]这个参数是不是存在,如果存在这个参数,并且$name 为空的话,就直接返回该参数。

要注意的是config这数组变量里面的_sys_键里面存放了所有的配置。

接着是不是存在点,不存在的话就将$name小写,也就是 default_filter小写,还是default_filter。如果$config["_sys_"]["default_filter"]这个存在就返回该变量,否则就返回空。那如果$name是存在点的呢,就以点进行分割,然后小写,形成多维数组,然后再在里面找,之前创建config参数的时候也有这个参数,就是找对应的参数罢了,然后retrue出来

可以看到和我们想的是一样的,就是从配置里面获取对应的参数。

我们接着看,赋值给filter之后,从输入流中获取值,然后给input参数,我们看下面的图,就是 获取post的值。

 接着就是return这个$instance 这个参数是一个static类,这个类里面比较重要的一个参数就是input,是post的值,那么注意到static类,但是他调用的是Request的__construct方法,这里不是很懂。难道是static就是调用本身这个类,有空我去试试。这里接着往下分析。

 接着我们回到了App的run方法,他创建了一个$request变量,这是一个Request类,然后里面的参数input是post的值,重要的事多说几遍。 接着就是调用本身类中的initCommon函数。

我们来看看这个initConmmon函数,先判断self::$init是不是为空

,可以看到为false,不为空,然后调用init函数。那我们先暂停,看看init函数。 

传入的参数为空,然后第一行的$module也为空。判断是不是存在init.php,这个根据调试的结果,是不存在进入了else里面,如果存在就包含这个文件。

那我们接着看看else里面是啥。获取$path,这里$path就是web路径。

 然后调用Config类中的load方法。

 

 通过debug可以看到传入的值为:D:\PHPSTUDY\thinkphp\think-5.0.2\public/../application/config.php

然后继续获取$range

然后判断这个config.php文件是不是存在。分析他的拓展名,很明显是php

是php的话就调用本身的set方法。之前是分析过set方法的。这里就不分析了。

回到init方法,根据前面的分析,可以知道接着加载数据库的配置。

然后判断拓展的目录是不是存在,这里是不存在。

然后加载应用状态配置。

加载行为扩展文件自动加载加载Hook类,然后调用import函数,这里的拓展就不看了。

加载公共文件

加载当前模块语言包

 最后调用Config中的get方法。这个方法也有分析过。当参数为空时,就返回config["_sys_"]这一整数组。

 好的,接下来继续分析initCommon这个函数,前面先init初始化了一下。返回的结果是config["_sys_"]。然后将配置变量里面的class_suffix赋值给suffix变量。这一整步就是初始化应用。下面是应用调试模式。

 调用Env类中的get方法,第一个参数是app_debug,第二个参数是Config::get('app_debug'),第二个参数我们分析过了,是从配置文件中获取app_debug,这个值,我们来看看Env这个类中的get方法是干嘛的。

可以看到$name是app_debug,然后$default是true,说明配置文件中的app_debug为true,然后第一行代码,先将.替换为_,那app_debug就不变了。strtoupper是将字符串大写,因为常量一般都是大写的,环境变量也是,这是一种规范,我们看到下面那张图,ENV_PREFIX是环境变量的前缀,getenv就是获取环境变量,那就是获取PHP_APP_DEBUG的值,不是false就return $result,否则就是return $defaule那个环境变量应该就是.env里面设置,我们可以看到.env文件设置优先级高。然后才是返回配置文件的。

 

可以看到获取app_debug的值赋值给self::$debug变量,然后判断为false就把报错关了,否则就申请一个比较大的buffer。这里申请缓冲区我不懂。

接着就是 注册应用命名空间,从$config变量中获取app_namespace的值赋值给self::namespace,然后添加命名空间,后面就是如果根命名空间为空就从$config中拿根命名空间。

加载 额外文件,直接看代码吧,判断配置文件中的extra_file_list是不是为空,不为空的话就包含这些文件。然后把$file["这里面填的是额外文件的名字"]设置为true。

 

然后设置系统时区,然后监听app_init。分析一下监听app_init是啥吧。

传入的参数是app_init。看到这个static就类似于$this吧,emmm然后调用Hook类中的get方法。

 

array_key_exists() 函数判断某个数组中是否存在指定的 key,如果该 key 存在,则返回 true,否则返回 false。

判断self::$tags是不是存在app_init,存在的话就返回self::$tags[app_init],否则返回空数组。看上面的图,$tags是本类中$tags变量键名为app_init的键值。然后对其进行数组遍历,调用自身的exec方法,传入的参数最后两个为空。前两个就是$tags的键名和键值,套了好多层。看看exec方法。

 来看看exec里面是什么。先判断APP::$debug是不是true,为true才调用Debug类中的remark方法。这里就是开没开器debug的关键。又得看看remark方法了,我这边贴在下面,继续在这说了,注释是记录时间和内存使用,传入的第一个$name是behavior_start,第一个参数$value是time,然后判断$value是不是float类型,显然不是,然后就microtime(true);

赋值给self::$info["behavior_start"]

如果$value不是time,就继续赋值,看完之后,算了不管了,就如注释所说,把时间和内存使用存储一下。接着分析, is_callable() 函数用于检测函数在当前环境中是否可调用,这里呢就是检查他是不是可以执行的函数,然后执行命令,然后判断是不是object,里面是一样的功能,最后记录一下运行的时间,然后记录到日志中去。返回里面执行命令的结果,这里的值都不能改变,所以就不多看了。

exec就看完了,我们接着看,$results[$key]为执行命令的结果,如果执行成功了,就返回结果,否则就break。中断行为执行。

那这边就结束了,把$config赋值给self::$init,然后返回,这是initCommon函数,

 回到run方法。返回了配置信息,那个$config还是配置信息,然后判断BIND_MODULE是否定义,这边debug跳过去了,。 

然后是检查多语言机制,否则读取默认语言。接着加载语言包。

 获取调度信息,如果dispatch为空的话,就调用routeCheck方法,第一个参数为Request类,第二个参数为配置信息

那我们接着分析一下他是如何进行路由检测的。 

 我们又从run方法跑到了routeCheck方法,然后第一行看到request类的path方法,继续先分析path方法。

 判断$this->path是不是空,为空的话从配置文件中获取url_html_suffix,然后赋值给$suffix,然后调用pathinfo方法。我们又从path方法跳到pathinfo方法。那就一个一个来吧。不能急。

 判断$this->pathinfo是不是空,判断配置文件var_pathinfo的值,有没有这个变量,这里是字符s。存在s这个变量的话,就将s的值赋值给$_SERVER['PATH_INFO'],然后把_GET数组中s这个变量unset掉。

 然后分析这个pathinfo的信息,然后返回pathinfo,这里就是检查?s的路由方式,然后我们debug的时候什么也没有,所以就为“/”。

回到path函数,得到pathinfo为“\”,然后进入去掉正常的url后缀这一行,这里大概就是后缀是html,伪静态访问,最后一个else写了就是什么后缀都可以,第如果设置为fasle,就是禁止伪静态访问了。然后返回return $this->path;

然后回到routeCheck这个函数,我们知道path获取了路径信息,然后从配置信息中找到pathinfo_depr,他的值为“/”,然后把$result设置为false。$check为true,包含route.php,接着判断$rules为数组不,然后就进入else了。

 

配置信息中获取route_config_file,他是一个数组,里面一个参数为route。然后包含route.php,返回一个数组,然后调用Route类的import方法。

 检查各种规则,然后注册规则。导入路由配置。

 接着调用Route类的check方法。

将/替换成|,这里的$url为/。所以$url就是|了。然后调用request的method方法。

 这个方法就是从post获取_method这个变量,然后赋值给$this->method,然后执行$this->{$this->method}($_POST);看到这,我的天,这里好像两个都可控。不过$this->method只能在Request中的方法。最终得到$this->method为GET。

紧接着是检测域名部署、检测URL绑定,检查域名部署为空,然后检查url绑定直接false了。再将|改回/。

 

 

 然后直接到了检查路由这个地方。

传入的$rules 为hello=>true,然后item变量就是true,然后调用本身的getRouteExpress方法,传入的参数为hello,我们接着看看这个函数吧。

里面是判断自身的domainRule是不是true,为true的话就是返回self::$domainRule['*']['hello'],这个变量,否则返回self::$rules['*']['hello']这个变量。

接着分析吧,然后接着就将里面的数据赋值出来。

然后是检查参数有效性,进入函数分析。

里面是判断参数是不是存在,都存在的话就return true,只要有一个不存在就是返回false

不过里面是contine;所以这里一点都不影响好吧。

判断系统配置的伪静态后缀参数,让后将后缀去掉。

 

 继续向下看,判断rule是不是数组,debug过后是数组的,先将<替换成:,然后取:的个数。我们看看$key是什么。他是hello。如果存在:就截取,不存在就直接赋值。

判断str变量是不是字符串,将url变量的/替换为|,然后这里就这一行代码有用,继续下面。

继续检查路由,类似于递归吧,然后如果结果为true就退出递归。

 

分析一下else if,,如果route存在,就把$item赋值给${$var},如果group存在就将group连接后面的$rule、然后判断bind_model是不是存在。接着里面也是递归。

 

 如果auto变量存在,就调用parseUrl方法。看看这个方法吧。

存在module参数的话,就将其/替换成/,然后赋值给$bind。 这里调试了一下,不会到这,我就先不分析了。继续看下面。

 $result是false,$must是false,所以那个if就不进去了,后面是进的,调用那个方法,我靠,还是逃不掉。

第一个if直接跳过了,url本来是/的,然后替换成了|,来了一个parseUrlPath函数,看看里面是什么。

 第一行又改成/了,晕乎乎,然后去空,直接到$path=[$url],return一个数组,第一个是空, 第二个也是空。

route是一个数组,里面三个空,然后判断path存不存在。path存在,不过为空,module也为空,因为我访问的时候啥也没有所以是空,当我访问index.php/index/index/index的时候,module就是index了。然后$controller也是index,$action也是,然后将模块控制器动作写入route这个变量中, 然后我直接到最后面返回,返回了一个数组,里面是type和module,module是route里面包含模块,控制器,动作。然后再return 一个result,所以$dispatch这个变量就是返回的结果。

 记录当前调度信息,里面就是赋值,将App的$dispatch赋值给request类的dispatch变量,

然后记录路由和请求信息,这个就不看了。接着就是监听app_degin了,之前也有一个差不多的。然后判断$dispatch里面的type,然后是module。

里面我们 先不看了,看第二张图,清空类的实例化,将self::$instance清空,然后输出数据到客户端,data是html代码。然后进行处理,最后监听app_end,return了response,也就是run方法最终返回的是response类,那个再调用里面的send发送给客户端,客户端收到html代码。大致明白了流程。其中还有很多函数我没调试的。

接着我们分析之前我们找到的利用点吧。

之前分析了我们可以调用request里面的所有方法,我们注意到__construct方法。

里面传入的值是POST,我们可控,然后里面是有一个$this->$name=$item,就是把传入的值判断是不是自己得属性,然后赋值给他,也就是说,我们可以对任意的 自己的属性赋值,我们先看看返回什么。

 

 他return的是method,我们就将这个改一下。也就是构造_method=__construct&method=[一个值],那么我们就可以构造method了,再继续往下分析。

可以看到他找自己的rules中的method这个参数,如果不存在的话,就报错。那么我们看一下里面有些什么。那我们随便赋值一个东西,例如POST。然后接着一行一行代码往下调试。

 

存在一个filterValue的方法,然后里面会进行一个循环执行命令,这个filter是传入的值,

看到是$this->filter,我们记得我们可以控制request类中的任意变量,然后再在post里面加入filter[]=system。接着这个value是什么,是传入的值,那我们随便构造一个参数例如a=whoami就好了那最终的payload就是

 _method=__construct&method=POST&filter[]=system&a=whoami

我们来试试。

最终我们就分析成功了,一个变量覆盖导致的rce。

四、总结

这一整个系统代码分析持续了很久,以为一天可以搞定的,因为一些事情耽误了,最终还是分析出来了,里面的分析挺乱的,还有很多函数没有分析,自己也没有那么多耐心了,算是知道这个版本有漏洞然后来找这个漏洞,属于是零基础分析了吧。

分析完这个thinkphp的运行原理,只能说简单的分析了,然后后面肯定会轻松一点的,加油加油。

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值