前言
互联网上关于分析php语言有关的文章多不胜数,但是分析大多都是根据代码逻辑进行分析,那么基于最底层的源代码进行分析与理解的文章相对较少的,也挺复杂的。因此我此篇文章也是借鉴不少大佬而来的,关于该文章主要讲phar反序列化漏洞,从最底层的源代码开始分析,在配合生动的图文并茂说明来帮助大家进一步理解phar反序列化漏洞的产生原理以及一些trick。
反序列化
关于反序列化相信大家都有过了解,目前讨论最多也是危害较大的话题都是关于反序列化的,除了php拥有反序列化漏洞以外还有java,python同样存在反序列化,甚至java反序列化漏洞出现频率非常之高,但是本篇java不是主角,php才是。而phar反序列化从2018年由国外安全研究员Sam Thomas在BlackHat上首次提出亮相。这种新型攻击方式可以在不使用unserialize函数的情况下完成数据与对象之间的转换,且使用方式极其常见,因此利用率相对较高,从而命名为phar反序列化漏洞。
源码分析
首先我们都知道php伪协议,php拥有很多常见的漏洞都来自于伪协议,其中file://,filter://,php://等等产生的漏洞不计其数。那么phar://伪协议能够在读取.phar文件中产生怎样的效果呢?那么从底层源代码进行分析来一层一层打开phar的面纱。
我们先查看unserialize函数的源代码:
PHP_FUNCTION(unserialize)
{
char *buf = NULL;
size_t buf_len;
const unsigned char *p;
php_unserialize_data_t var_hash;
zval *options = NULL, *classes = NULL;
zval *retval;
HashTable *class_hash = NULL, *prev_class_hash;
...
if (buf_len == 0) {
RETURN_FALSE;
}
p = (const unsigned char*) buf;
PHP_VAR_UNSERIALIZE_INIT(var_hash);
prev_class_hash = php_var_unserialize_get_allowed_classes(var_hash);
...
retval = var_tmp_var(&var_hash);
if (!php_var_unserialize(retval, &p, p + buf_len, &var_hash)) {
...
代码很多,我们主要还是看核心代码php_var_unserialize函数,在该版本(php-7.2.31)的php中主要通过该函数来进行反序列化数据的操作,那么我们会用到该函数以外还会用到phar_parse_metadata函数。
Session.c
Gmp.c
Phar.c
通过上述图片我们能够发现一共有很多地方调用了此函数。其中有我们最常见的session,gmp,还有许多没有截图,那么我们通过上述图片我们发现phar.c文件中phar_parse_metadata函数调用了php_var_unserialize函数来进行反序列化,通过分析此函数是用于反序列化phar中的metadata的,我们可以全局搜索一下那些地方调用了phar_parse_metadata函数。
Tar.c
Zip.c
从上述图中可以发现很多地方调用了此函数,比如tar,zip等等。那么我们还是来看和我们有关的phar吧,我们可以简单编写一个file_get_contents功能函数的php文件用于测试
Test.php
<?php
class Test{
public function __destruct(){
echo 2024722;
}
}
file_get_contents("phar:///tmp/phar_test1/phar.phar");
?>
运行该函数测试是否成功调用并打印2024722,能够正常输出说明代码没有问题测试成功。
我们再分析一下file.c文件中的file_get_contents函数,我们主要重点分析php_stream_open_wrapper_ex函数,这个函数首先调用了php_stream_locate_url_wrapper函数用于获取filename所对应的wrapper。
PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mode,
int options,
zend_string **opened_path, php_stream_context *context STREAMS_DC)
{
php_stream *stream = NULL;
php_stream_wrapper *wrapper = NULL;
const char *path_to_open;
int persistent = options & STREAM_OPEN_PERSISTENT;
zend_string *resolved_path = NULL;
char *copy_of_path = NULL;
……
path_to_open = path;
wrapper = php_stream_locate_url_wrapper(path, &path_to_open, options);
注意上述代码中的最后一行,该代码通过php_stream_locate_url_wrapper函数来获取wrapper,该函数的主要作用通过遍历文件名来解析对应的协议的方式从hash中查找出对应的wrapper,而我们所关心的phar单独注册了一个wrapper:
php_stream_ops phar_ops = {
phar_stream_write, /* write */
phar_stream_read, /* read */
phar_stream_close, /* close */
phar_stream_flush, /* flush */
"phar stream",
phar_stream_seek, /* seek */
NULL, /* cast */
phar_stream_stat, /* stat */
NULL, /* set option */
}
之后调用了如下方法,此方法通过获取到的wrapper来确定下一步骤操作并解析文件结构
stream = wrapper->wops->stream_opener(wrapper,
path_to_open, mode, options ^ REPORT_ERRORS,
opened_path, context STREAMS_REL_CC);
在上述代码中实际上是调用了phar_wrapper_open_url函数,phar_wrapper_open_url函数又调用了phar_parse_url函数,phar_parse_url函数又继续调用其它函数从而形成链式调用,最终调用流程如下:
phar_wrapper_open_url -> phar_parse_url -> phar_open_from_filename -> phar_open_from_fp -> phar_parse_pharfile -> phar_parse_metadata -> php_var_unserialize
从上述调⽤栈中可以发现,最终会调⽤ php_var_unserialize 函数对PHAR⽂件中的 meta数据进行反序列化从而导致phar反序列化漏洞产生。
从上面分析我们已经可以得出phar反序列化漏洞的具体原因在于对phar://开头的文件进行处理时会对phar文件中的metadata进行反序列化,而使用的就是如下代码进行这一步操作的:
$phar->setMetadata(new Test); //将⾃定义的meta-data存⼊manifest
不过这并不足以支撑我们完成反序列化漏洞的利用,我们还需要知道那些函数可以触发phar反序列化,这里根据互联网上查找或者也可以使用FUZZ进行模糊测试找到如下函数:
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
xmlwrite_open_uri
触发漏洞关键函数
从上述的大量分析我们能够得出一个结论,只要有操作调用了
php_stream_locate_url_wrapper 函数获取 wrapper 并调⽤了 stream_opener 的函数,都有可能触发 phar 反序列化,还有一种说法就是只要调用了_php_stream_open_wrapper_ex函数就有概率触发phar反序列化漏洞。
那么根据php的defind我们还可以发现其它函数同样可能存在
#define php_stream_open_wrapper(path, mode, options, opened)
_php_stream_open_wrapper_ex((path), (mode), (options), (opened), NULL STREAMS_CC)
#define php_stream_open_wrapper_ex(path, mode, options, opened, context)
_php_stream_open_wrapper_ex((path), (mode), (options), (opened), (context)
STREAMS_CC)
全局搜索php_stream_open_wrapper函数发现上述能够触发phar的函数很多,如下图
Fileinfo.c
Md5.c
通过上述回溯函数发现许多api都直接调用,由此以来phar反序列化成本大大减少。
实例分析
ZIP
$zip = new ZipArchive();
$res = $zip->open('test.zip');
$zip->extractTo('phar://test.phar/test');
我们通过危险调用点进行倒序分析,分析到在ZIP扩展中,其中php_zip_extract_file函数会调用php_stream_open_wrapper函数来解析文件
Php_zip.c
我们来分析一下php_zip_extract_file 这个函数会在那里被调用
通过上图可以发现在extractTo 这个⽅法中直接调用了 php_zip_extract_file函数,由此完成完整调用链,最终触发phar反序列化漏洞。
POSTGRESQL
<?php
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1",
"postgres", "psgl", "admin@123"));
@$pdo->pgsqlCopyFromFile('test', 'phar://test.phar/test');
pgsqlCopyFromFile函数直接调用了php_stream_open_wrapper_ex危险函数,因此也是可以序列化的:
pgsql_driver.c
此时是直接调用了该函数从而触发了反序列化,无需在进行倒序分析,而且除了pgsqlCopyFromFile调用了php_stream_open_wrapper_ex危险函数以外,pgsqlCopyToFile也调用了该函数
绕过安全机制
在某些情况可能会对用户提交的数据进行正则匹配过滤,因此我们需要绕过防御。
Compress.bzip2绕过
通过Compress.bzip2我们可以绕过phar://在开头的防御,我们通过源代码分析来看看到底怎么绕过的。
Payload:
compress.bzip2://phar:///home/ltm/test.phar
compress.bzip2也是在php中注册的一个wrapper,对应的源码在ext/bz2文件中,当遇到这个wrapper 时,实际上调用的是 _php_stream_bz2open 这个函数。
如上图我们可以发现,在if语句中会判断filename是否以compress.bzip2:// 开头,如果是的话那么path指针将向前移动17位,而compress.bzip2:// 刚好是17位,其实就相当于把compress.bzip2置空。当我们输入如上payload时那么通过path转换就会成为phar:///home/ltm/test.phar 从而绕过检查和过滤,而在后续的调用中会调用到php_stream_open_wrapper函数从而触发phar反序列化操作
compress.zlib绕过
通过该compress.zlib://也是同compress.bzip2同样的道理,但是从源代码中我们发现zlib也可以达到同样的效果,不过zlib没有使用php_stream_open_wrapper类似的函数,因此无法找到wrapper,所以需要在其内置函数中使用比如gzfile这样的函数来代替file_get_contents函数从而到达phar反序列化的效果,除了gzfile以外还有如下函数均可以:
gzfile
gzopen
readgzfile
php伪协议绕过
除了使用上述绕过方式以外还可以使用php伪协议进行绕过:
<?php
class Test{
public function __destruct(){
echo "very good!";
}
}
file_get_contents('php://filter/resource=phar:///Users/php_test2/phar.phar');
我们重点查看php_fopen_wrapper.c文件对于php伪协议的处理
通过上图可以发现与上述绕过方式基本一模一样,匹配成功则前移六位,最后调用php_stream_open_wrapper函数进行处理(全文通篇出现次数最多的函数就是这个,啥成分我就不多说了[dog])
if (!(stream = php_stream_open_wrapper(p + 10, mode, options, opened_path))) {
efree(pathdup);
return NULL;
}
此时传过来的 path 已经是解析完毕了。⾃然可以进行反序列化漏洞了。
__HALT_COMPILER()解析
在生成phar文件时,该文件会有特征,就如同java反序列化的文件开头以aced开头一样,而phar生成必须要如下代码:
$phar->setStub("__HALT_COMPILER();"); //设置stub
我们通过查看php是如何解析phar文件来分析为何必须存在该特征,在 phar_open_from_fp 函数中,首先会定义几个Token:
const char token[] = "__HALT_COMPILER();";
const char zip_magic[] = "PK\x03\x04";
const char gz_magic[] = "\x1f\x8b\x08";
const char bz_magic[] = "BZh";
后续操作会进行判断:
/* check for ?>\n and increment accordingly */
if (-1 == php_stream_seek(fp, halt_offset, SEEK_SET)) {
MAPPHAR_ALLOC_FAIL("cannot seek to __HALT_COMPILER(); location in phar \"%s\"")
}
这里的 halt_offset 为18,正好对应了 __HALT_COMPILER();函数的长度:
当这⾥没有匹配到这几个字符串时就会调用MAPPHAR_ALLOC_FAIL ⽅法,而该方法用于抛出异常,返回错误,此时则⽆法继续进⼊后续的反序列化流程。这也是为什么PHAR⽂件中必须设置Stub的原因,正是为了防⽌反序列化出错而存在的。
总结
源代码的分析总是那么困难却有令人着迷,所以你很模糊,懵懂的问题都变得清晰了起来,此文章也参考不少大佬的思路才一步一步地清晰并且理解。