Typo3 CVE-2019-12747 反序列化漏洞分析
1. 前言
TYPO3
是一個以
PHP
編寫、採用
GNU
通用公共許可證的自由、開源的內容管理系統。
2019年7月16日,
RIPS
的研究團隊公開了
Typo3 CMS
的一個關鍵漏洞
詳情,
CVE
編號為
CVE-2019-12747
,它允許後臺使用者執行任意
PHP
程式碼。
漏洞影響範圍:
Typo3 8.x-8.7.26 9.x-9.5.7
。
2. 測試環境簡述
Nginx/1.15.8 PHP 7.3.1 + xdebug 2.7.2 MySQL 5.7.27 Typo3 9.5.7
3. TCA
在進行分析之前,我們需要了解下
Typo3
的
TCA(Table Configuration Array)
,在
Typo3
的程式碼中,它表示為
$GLOBALS['TCA']
。
在
Typo3
中,
TCA
算是對於資料庫表的定義的擴充套件,定義了哪些表可以在
Typo3
的後端可以被編輯,主要的功能有
- 表示表與表之間的關係
- 定義後端顯示的欄位和佈局
- 驗證欄位的方式
這次漏洞的兩個利用點分別出在了
CoreEngine
和
FormEngine
這兩大結構中,而
TCA
就是這兩者之間的橋樑,告訴兩個核心結構該如何表現表、欄位和關係。
TCA
的第一層是表名:
$GLOBALS['TCA']['pages'] = [ ...];$GLOBALS['TCA']['tt_content'] = [ ...];
其中
pages
和
tt_content
就是資料庫中的表。
接下來一層就是一個陣列,它定義瞭如何處理表,
$GLOBALS['TCA']['pages'] = [ 'ctrl' => [ // 通常包含表的屬性 .... ], 'interface' => [ // 後端介面屬性等 .... ], 'columns' => [ .... ], 'types' => [ .... ], 'palettes' => [ .... ],];
在這次分析過程中,只需要瞭解這麼多,更多詳細的資料可以查詢 。
4. 漏洞分析
整個漏洞的利用流程並不是特別複雜,主要需要兩個步驟,第一步變數覆蓋後導致反序列化的輸入可控,第二步構造特殊的反序列化字串來寫
shell
。第二步這個就是老套路了,找個在魔術方法中能寫檔案的類就行。這個漏洞好玩的地方在於變數覆蓋這一步,而且進入兩個元件漏洞點的傳入方式也有著些許不同,接下來讓我們看一看這個漏洞吧。
4.1 補丁分析
從Typo3官方的
通告中我們可以知道漏洞影響了兩個元件——
Backend & Core API (ext:backend, ext:core)
,在GitHub上我們可以找到
:
很明顯,補丁分別禁用了
backend
的
DatabaseLanguageRows.php
和
core
中的
DataHandler.php
中的的反序列化操作。
4.2 Backend ext 漏洞點利用過程分析
根據補丁的位置,看下
Backend
元件中的漏洞點。
路徑:
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseLanguageRows.php:37
public function addData(array $result){ if (!empty($result['processedTca']['ctrl']['languageField']) && !empty($result['processedTca']['ctrl']['transOrigPointerField']) ) { $languageField = $result['processedTca']['ctrl']['languageField']; $fieldWithUidOfDefaultRecord = $result['processedTca']['ctrl']['transOrigPointerField']; if (isset($result['databaseRow'][$languageField]) && $result['databaseRow'][$languageField] > 0 && isset($result['databaseRow'][$fieldWithUidOfDefaultRecord]) && $result['databaseRow'][$fieldWithUidOfDefaultRecord] > 0 ) { // Default language record of localized record $defaultLanguageRow = $this->getRecordWorkspaceOverlay( $result['tableName'], (int)$result['databaseRow'][$fieldWithUidOfDefaultRecord] ); if (empty($defaultLanguageRow)) { throw new DatabaseDefaultLanguageException( 'Default language record with id ' . (int)$result['databaseRow'][$fieldWithUidOfDefaultRecord] . ' not found in table ' . $result['tableName'] . ' while editing record ' . $result['databaseRow']['uid'], 1438249426 ); } $result['defaultLanguageRow'] = $defaultLanguageRow; // Unserialize the "original diff source" if given if (!empty($result['processedTca']['ctrl']['transOrigDiffSourceField']) && !empty($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']]) ) { $defaultLanguageKey = $result['tableName'] . ':' . (int)$result['databaseRow']['uid']; $result['defaultLanguageDiffRow'][$defaultLanguageKey] = unserialize($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']]); } //省略程式碼 } //省略程式碼 } //省略程式碼}
很多類都繼承了
FormDataProviderInterface
介面,因此靜態分析尋找誰呼叫的
DatabaseLanguageRows
的
addData
方法根本不現實,但是根據文章中的演示影片,我們可以知道網站中修改
page
這個功能中進入了漏洞點。在
addData
方法加上斷點,然後發出一個正常的修改
page
的請求。
當程式斷在
DatabaseLanguageRows
的
addData
方法後,我們就可以得到呼叫鏈。
在
DatabaseLanguageRows
這個
addData
中,只傳入了一個
$result
陣列,而且進行反序列化操作的目標是
$result['databaseRow']
中的某個值。看命名有可能是從資料庫中獲得的值,往前分析一下。
進入
OrderedProviderList
的
compile
方法。
路徑:
typo3/sysext/backend/Classes/Form/FormDataGroup/OrderedProviderList.php:43
public function compile(array $result): array{ $orderingService = GeneralUtility::makeInstance(DependencyOrderingService::class); $orderedDataProvider = $orderingService->orderByDependencies($this->providerList, 'before', 'depends'); foreach ($orderedDataProvider as $providerClassName => $providerConfig) { if (isset($providerConfig['disabled']) && $providerConfig['disabled'] === true) { // Skip this data provider if disabled by configuration continue; } /** @var FormDataProviderInterface $provider */ $provider = GeneralUtility::makeInstance($providerClassName); if (!$provider instanceof FormDataProviderInterface) { throw new \UnexpectedValueException( 'Data provider ' . $providerClassName . ' must implement FormDataProviderInterface', 1485299408 ); } $result = $provider->addData($result); } return $result;}
我們可以看到,在
foreach
這個迴圈中,動態例項化
$this->providerList
中的類,然後呼叫它的
addData
方法,並將
$result
作為方法的引數。
在呼叫
DatabaseLanguageRows
之前,呼叫瞭如圖所示的類的
addData
方法。
經過查詢手冊以及分析程式碼,可以知道在
DatabaseEditRow
類中,透過呼叫
addData
方法,將資料庫表中資料讀取出來,儲存到了
$result['databaseRow']
中。
路徑:
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseEditRow.php:32
public function addData(array $result){ if ($result['command'] !== 'edit' || !empty($result['databaseRow'])) {// 限制功能為`edit` return $result; } $databaseRow = $this->getRecordFromDatabase($result['tableName'], $result['vanillaUid']); // 獲取資料庫中的記錄 if (!array_key_exists('pid', $databaseRow)) { throw new \UnexpectedValueException( 'Parent record does not have a pid field', 1437663061 ); } BackendUtility::fixVersioningPid($result['tableName'], $databaseRow); $result['databaseRow'] = $databaseRow; return $result;}
再後面又呼叫了
DatabaseRecordOverrideValues
類的
addData
方法。
路徑:
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseRecordOverrideValues.php:31
public function addData(array $result){ foreach ($result['overrideValues'] as $fieldName => $fieldValue) { if (isset($result['processedTca']['columns'][$fieldName])) { $result['databaseRow'][$fieldName] = $fieldValue; $result['processedTca']['columns'][$fieldName]['config'] = [ 'type' => 'hidden', 'renderType' => 'hidden', ]; } } return $result;}
在這裡,將
$result['overrideValues']
中的鍵值對儲存到了
$result['databaseRow']
中,如果
$result['overrideValues']
可控,那麼透過這個類,我們就能控制
$result['databaseRow']
的值了。
再往前,看看
$result
的值是怎麼來的。
路徑:
typo3/sysext/backend/Classes/Form/FormDataCompiler.php:58
public function compile(array $initialData){ $result = $this->initializeResultArray(); //省略程式碼 foreach ($initialData as $dataKey => $dataValue) { // 省略程式碼... $result[$dataKey] = $dataValue; } $resultKeysBeforeFormDataGroup = array_keys($result); $result = $this->formDataGroup->compile($result); // 省略程式碼...}
很明顯,透過呼叫
FormDataCompiler
的
compile
方法,將
$initialData
中的資料儲存到了
$result
中。
再往前走,來到了
EditDocumentController
類中的
makeEditForm
方法中。
在這裡,
$formDataCompilerInput['overrideValues']
獲取了
$this->overrideVals[$table]
中的資料。
而
$this->overrideVals
的值是在方法
preInit
中設定的,獲取的是透過
POST
傳入的表單中的鍵值對。
這樣一來,在這個請求過程中,進行反序列化的字串我們就可以控制了。
在表單中提交任意符合陣列格式的輸入,在後端程式碼中都會被解析,然後後端根據
TCA
來進行判斷並處理。 比如我們在提交表單中新增一個名為
a[b][c][d]
,值為
233
的表單項。
在編輯表單的控制器
EditDocumentController.php
中下一個斷點,提交之後。
可以看到我們傳入的鍵值對在經過
getParsedBody
方法解析後,變成了巢狀的陣列,並且沒有任何限制。
我們只需要在表單中傳入
overrideVals
這一個陣列即可。這個陣列中的具體的鍵值對,則需要看進行反序列化時取的
$result['databaseRow']
中的哪一個鍵值。
if (isset($result['databaseRow'][$languageField]) && $result['databaseRow'][$languageField] > 0 && isset($result['databaseRow'][$fieldWithUidOfDefaultRecord]) && $result['databaseRow'][$fieldWithUidOfDefaultRecord] > 0) { // 省略程式碼 if (!empty($result['processedTca']['ctrl']['transOrigDiffSourceField']) && !empty($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']])) { $defaultLanguageKey = $result['tableName'] . ':' . (int) $result['databaseRow']['uid']; $result['defaultLanguageDiffRow'][$defaultLanguageKey] = unserialize($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']]); } //省略程式碼}
要想進入反序列化的點,還需要滿足上面的
if
條件,動態調一下就可以知道,在
if
語句中呼叫的是
$result['databaseRow']['sys_language_uid']$result['databaseRow']['l10n_parent']
後面反序列化中呼叫的是
$result['databaseRow']['l10n_diffsource']
因此,我們只需要在傳入的表單中增加三個引數即可。
overrideVals[pages][sys_language_uid] ==> 4overrideVals[pages][l10n_parent] ==> 4overrideVals[pages][l10n_diffsource] ==> serialized_shell_data
可以看到,我們的輸入成功的到達了反序列化的點。
4.3 Core ext 漏洞點利用過程分析
看下
Core
中的那個漏洞點。
路徑:
typo3/sysext/core/Classes/DataHandling/DataHandler.php:1453
public function fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $realPid, $status, $tscPID){ // Initialize: $originalLanguageRecord = null; $originalLanguage_diffStorage = null; $diffStorageFlag = false; // Setting 'currentRecord' and 'checkValueRecord': if (strpos($id, 'NEW') !== false) { // Must have the 'current' array - not the values after processing below... $checkValueRecord = $fieldArray; if (is_array($incomingFieldArray) && is_array($checkValueRecord)) { ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray); } $currentRecord = $checkValueRecord; } else { // We must use the current values as basis for this! $currentRecord = ($checkValueRecord = $this->recordInfo($table, $id, '*')); // This is done to make the pid positive for offline versions; Necessary to have diff-view for page translations in workspaces. BackendUtility::fixVersioningPid($table, $currentRecord); } // Get original language record if available: if (is_array($currentRecord) && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'] && $GLOBALS['TCA'][$table]['ctrl']['languageField'] && $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] && (int)$currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0 ) { $originalLanguageRecord = $this->recordInfo($table, $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], '*'); BackendUtility::workspaceOL($table, $originalLanguageRecord); $originalLanguage_diffStorage = unserialize($currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]); } ......//省略程式碼
看程式碼,如果我們要進入反序列化的點,需要滿足前面的
if
條件
if (is_array($currentRecord) && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'] && $GLOBALS['TCA'][$table]['ctrl']['languageField'] && $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] && (int)$currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0 )
也就是說要滿足以下條件
-
$currentRecord
是個陣列 - 在
TCA
中$table
的表屬性中存在transOrigDiffSourceField
、languageField
、transOrigPointerField
欄位。 -
$table
的屬性languageField
和transOrigPointerField
在$currentRecord
中對應的值要大於。
查一下
TCA
表,滿足第二條條件的表有
sys_file_reference sys_file_metadata sys_file_collection sys_collection sys_category pages
但是所有
sys_*
的欄位的
adminOnly
屬性的值都是
1
,只有管理員許可權才可以更改。因此我們可以用的表只有
pages
。
它的屬性值是
[languageField] => sys_language_uid [transOrigPointerField] => l10n_parent [transOrigDiffSourceField] => l10n_diffsource
再往上,有一個對傳入的引數進行處理的
if-else
語句。
從註釋中,我們可以知道傳入的各個引數的功能:
- 陣列
$fieldArray
是預設值,這種一般都是我們無法控制的 - 陣列
$incomingFieldArray
是你想要設定的欄位值,如果可以,它會合併到$fieldArray
中。
而且如果滿足
if (strpos($id, 'NEW') !== false)
條件的話,也就是
$id
是一個字串且其中存在
NEW
字串,會進入下面的合併操作。
$checkValueRecord = $fieldArray;......if (is_array($incomingFieldArray) && is_array($checkValueRecord)) { ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);}$currentRecord = $checkValueRecord;
如果不滿足上面的
if
條件,
$currentRecord
的值就會透過
recordInfo
方法從資料庫中直接獲取。這樣後面我們就無法利用了。
簡單總結一下,我們需要
-
$table
是pages
-
$id
是個字串,而且存在NEW
字串 -
$incomingFieldArray
中要存在payload
接下來我們看在哪裡對該函式進行了呼叫。
全域性搜尋一下,只找到一處,在
typo3/sysext/core/Classes/DataHandling/DataHandler.php:954
處的
process_datamap
方法中進行了呼叫。
整個專案中,對
process_datamap
呼叫的地方就太多了,嘗試使用
xdebug
動態除錯來找一下呼叫鏈。從
RIPS
團隊的那一篇分析文章結合上面的對錶名的分析,我們可以知道,漏洞點在建立
page
的功能處。
接下來就是找從
EditDocumentController.php
的
mainAction
方法到前面我們分析的
fillInFieldArray
方法的呼叫鏈。
嘗試在網站中新建一個
page
,然後在呼叫
fillInFieldArray
的位置下一個斷點,傳送請求後,我們就拿到了呼叫鏈。
看一下
mainAction
的程式碼。
public function mainAction(ServerRequestInterface $request): ResponseInterface{ // Unlock all locked records BackendUtility::lockRecords(); if ($response = $this->preInit($request)) { return $response; } // Process incoming data via DataHandler? $parsedBody = $request->getParsedBody(); if ($this->doSave || isset($parsedBody['_savedok']) || isset($parsedBody['_saveandclosedok']) || isset($parsedBody['_savedokview']) || isset($parsedBody['_savedoknew']) || isset($parsedBody['_duplicatedoc']) ) { if ($response = $this->processData($request)) { return $response; } } ....//省略程式碼}
當滿足
if
條件是進入目標
$response = $this->processData($request)
。
if ($this->doSave || isset($parsedBody['_savedok']) || isset($parsedBody['_saveandclosedok']) || isset($parsedBody['_savedokview']) || isset($parsedBody['_savedoknew']) || isset($parsedBody['_duplicatedoc']) )
這個在新建一個
page
時,正常的表單中就攜帶
doSave == 1
,而
doSave
的值就是在方法
preInit
中獲取的。
這樣條件預設就是成立的,然後將
$request
傳入了
processData
方法。
public function processData(ServerRequestInterface $request = null): ?ResponseInterface{// @deprecated Variable can be removed in TYPO3 v10.0 $deprecatedCaller = false; ......//省略程式碼 $parsedBody = $request->getParsedBody(); // 獲取Post請求引數 $queryParams = $request->getQueryParams(); // 獲取Get請求引數 $beUser = $this->getBackendUser(); // 獲取使用者資料 // Processing related GET / POST vars $this->data = $parsedBody['data'] ?? $queryParams['data'] ?? []; $this->cmd = $parsedBody['cmd'] ?? $queryParams['cmd'] ?? []; $this->mirror = $parsedBody['mirror'] ?? $queryParams['mirror'] ?? []; // @deprecated property cacheCmd is unused and can be removed in TYPO3 v10.0 $this->cacheCmd = $parsedBody['cacheCmd'] ?? $queryParams['cacheCmd'] ?? null; // @deprecated property redirect is unused and can be removed in TYPO3 v10.0 $this->redirect = $parsedBody['redirect'] ?? $queryParams['redirect'] ?? null; $this->returnNewPageId = (bool)($parsedBody['returnNewPageId'] ?? $queryParams['returnNewPageId'] ?? false); // Only options related to $this->data submission are included here $tce = GeneralUtility::makeInstance(DataHandler::class); $tce->setControl($parsedBody['control'] ?? $queryParams['control'] ?? []); // Set internal vars if (isset($beUser->uc['neverHideAtCopy']) && $beUser->uc['neverHideAtCopy']) { $tce->neverHideAtCopy = 1; } // Load DataHandler with data $tce->start($this->data, $this->cmd); if (is_array($this->mirror)) { $tce->setMirror($this->mirror); } // Perform the saving operation with DataHandler: if ($this->doSave === true) { $tce->process_uploads($_FILES); $tce->process_datamap(); $tce->process_cmdmap(); } ......//省略程式碼}
程式碼很容易懂,從
$request
中解析出來的資料,首先儲存在
$this->data
和
$this->cmd
中,然後例項化一個名為
$tce
,呼叫
$tce->start
方法將傳入的資料儲存在其自身的成員
datamap
和
cmdmap
中。
typo3/sysext/core/Classes/DataHandling/DataHandler.php:735public function start($data, $cmd, $altUserObject = null){ ......//省略程式碼 // Setting the data and cmd arrays if (is_array($data)) { reset($data); $this->datamap = $data; } if (is_array($cmd)) { reset($cmd); $this->cmdmap = $cmd; }}
而且
if ($this->doSave === true)
這個條件也是成立的,進入
process_datamap
方法。
程式碼有註釋還是容易閱讀的,在第
985
行,獲取了
datamap
中所有的鍵名,然後儲存在
$orderOfTables
,然後進入
foreach
迴圈,而這個
$table
,在後面傳入
fillInFieldArray
方法中,因此,我們只需要分析
$table == pages
時的迴圈即可。
$fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);
大致瀏覽下程式碼,再結合前面的分析,我們需要滿足以下條件:
-
$recordAccess
的值要為true
-
$incomingFieldArray
中的payload
不會被刪除 -
$table
的值為pages
-
$id
中存在NEW
字串
既然正常請求可以直接斷在呼叫
fillInFieldArray
處,正常請求中,第一條、第三條和第四條都是成立的。
根據前面對
fillInFieldArray
方法的分析,構造
payload
,向提交的表單中新增三個鍵值對。
data[pages][NEW5d3fa40cb5ac4065255421][l10n_diffsource] ==> serialized_shell_data data[pages][NEW5d3fa40cb5ac4065255421][sys_language_uid] ==> 4 data[pages][NEW5d3fa40cb5ac4065255421][l10n_parent] ==> 4
其中
NEW*
字串要根據表單生成的值進行對應的修改。
傳送請求後,依舊能夠進入
fillInFieldArray
,而在傳入的
$incomingFieldArray
引數中,可以看到我們新增的三個鍵值對。
進入
fillInFieldArray
之後,其中
l10n_diffsource
將會進行反序列化操作。此時我們在請求中將其
l10n_diffsource
改為構造好的序列化字串,重新傳送請求即可成功
getshell
。
5. 寫在最後
其實單看這個漏洞的利用條件,還是有點雞肋的,需要你獲取到
typo3
的一個有效的後臺賬戶,並且擁有編輯
page
的許可權。
而且這次分析
Typo3
給我的感覺與其他網站完全不同,我在分析建立&修改
page
這個功能的引數過程中,並沒有發現什麼過濾操作,在後臺的所有引數都是根據
TCA
的定義來進行相應的操作,只有傳入不符合
TCA
定義的才會丟擲異常。而
TCA
的驗證又不嚴格導致了變數覆蓋這個問題。
官方的修補方式也是不太懂,直接禁止了反序列化操作,但是個人認為這次漏洞的重點還是在於前面變數覆蓋的問題上,尤其是
Backend
的利用過程中,可以直接覆蓋從資料庫中取出的資料,這樣只能算是治標不治本,後面還是有可能產生新的問題。
當然了,以上只是個人拙見,如有錯誤,還請諸位斧正。
6. 參考連結
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912109/viewspace-2652596/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 漏洞分析 | Dubbo2.7.7反序列化漏洞繞過分析
- Apache Shiro 反序列化漏洞分析Apache
- Shiro 550反序列化漏洞分析
- Java安全之Cas反序列化漏洞分析Java
- WebLogic 反序列化漏洞深入分析Web
- java RMI相關反序列化漏洞整合分析Java
- Fastjson 反序列化漏洞分析 1.2.25-1.2.47ASTJSON
- Fastjson反序列化漏洞分析 1.2.22-1.2.24ASTJSON
- Java安全之Shiro 550反序列化漏洞分析Java
- 有隙可乘 - Android 序列化漏洞分析實戰Android
- Java序列化、反序列化、反序列化漏洞Java
- JAVA反序列化漏洞完整過程分析與除錯Java除錯
- php反序列化漏洞PHP
- JMX 反序列化漏洞
- TYPO3中便捷操作
- 一次老版本jboss反序列化漏洞的利用分析
- WEB漏洞——PHP反序列化WebPHP
- fastjson反序列化漏洞ASTJSON
- python 反序列化漏洞Python
- Fastjson 反序列化漏洞史ASTJSON
- PHP反序列化漏洞總結PHP
- php xss 反序列化漏洞PHP
- common-collections中Java反序列化漏洞導致的RCE原理分析Java
- Fastjson反序列化漏洞復現ASTJSON
- Web安全之PHP反序列化漏洞WebPHP
- WebLogic XMLDecoder反序列化漏洞WebXML
- Apache Shiro 550反序列化漏洞Apache
- Apache Commons Collections反序列化漏洞Apache
- 懸鏡安全丨Java 反序列化任意程式碼執行漏洞分析與利用Java
- PHP審計之PHP反序列化漏洞PHP
- WebLogic T3反序列化漏洞Web
- OpenSSLX509Certificate反序列化漏洞(CVE-2015-3825)成因分析
- [BUG反饋]分類授權漏洞
- Fastjson1.2.24反序列化漏洞復現ASTJSON
- Python 反序列化漏洞學習筆記Python筆記
- 深入分析Java的序列化與反序列化Java
- 【漏洞分析】KaoyaSwap 安全事件分析事件
- PHP反序列化鏈分析PHP