PHP反序列化:

关于反序列化的情况:

<?php
class test{
public $name="ghtwf01";
public $age="18";
}
$a=new test();
print_r($a);
?>

反序列化前输出结果:

img

反序列化后输出结果:

<?php
class test{
public $name="ghtwf01";
public $age="18";
}
$a=new test();
$a=serialize($a);
print_r($a);
?>

https://xzfile.aliyuncs.com/media/upload/picture/20191112144234-9c15d5ca-0517-1.png

O:4:”test”这部分是对象名字的描述

:2:这部分是有两个成员属性,然后到花括号开始描述类的内部的内容。

s就是string

如果成员不是使用public进行标记,而是使用pravite,则会出现一下的情况:

<?php
class test{
public $name="ghtwf01";
private $age="18";
protected $sex="man";
}
$a=new test();
$a=serialize($a);
print_r($a);
?>

img

其中,testage是作为age的替换出现的。

当private标注的成员属性进行反序列化的时候,会将格式改为%00类名%00成员名的格式

其中%00会占用一个字节的长度,因此,本来只有7个字符,但是实际反序列化出来之后,会显示有9个字节。

当使用protect之后,格式变为:

%00*%00成员名

就像是上面的结果变为了*sex一样,本来是sex。

image-20220309085459451

在url中也要进行%00的添加,否则利用不成功。

魔术方法:

image-20220309085817014

当没有对$_GET或是$_POST传入的数据进行过滤的时候,容易造成攻击。

特别是利用php的嵌入化特性,可以造成xss攻击。

这是一个反序列化的漏洞代码

<?php
class A{
public $test = "demo";
function __destruct(){
echo $this->test;
}
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>

这里可以看见的是,变量$a没有对get方法传入的值进行过滤,一旦传入的值是一个序列化后的特殊构造值,则会产生错误的后果。

主要是因为在删除对象之前会调用_destruct(),然后进行一个echo,经过字符的传输,会产生一个简易的XSS。

整个反序列化的流程:

首先创建一个类,建立一个类对应的对象,这个时候会首先调用_construct(),然后再将对象进行序列化,在进行序列化的时候,会首先调用_sleep(),然后再进行反序列化,然后在进行反序列化的时候,会直接调用_wakeup()这个函数,然后当消灭对象的时候再会调用_destruct()这个方法。

__sleep()方法不需要接受任何参数,但是需要返回一个数组。在数组中包含需要串行化的属性。没有被包含在数组中的属性将会在串行化的时候被忽略。如果没有在类中声明__sleep()方法,对象中的所有属性都将被串行化(就是相当于是没有做出任何的改变)。

所返回的数组需要自己进行定义,其中的每一个名字都可以是一个属性,返还这个数组之后,就会自动的只将数组中的属性进行串行化。

在调用unserialize()函数将对象反串行化的时候,将会自动的调用对象中存在的__wakeup()方法,用来在二进制串重新组成一个对象时,为新对象中的成员属性重新初始化。

__wakeup()方法将会在使用unserialize()函数的时候被自动调用(这两个魔术方法都是写在对象里面,不是单独写的)

可以在__wakeup()的内部直接为对象中的属性赋值。

PHP 7.1+版本:

在PHP7.1版本以上,对于序列化中的成员变量类型不敏感,当有对不可见字符串进行过滤的时候,可以将public类型的变量输入,然后进行绕过。

CVE-2016-7124 __wakeup绕过

__wakeup魔法函数简介
unserialize()会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup() 方法,预先准备对象需要的资源
反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup()的执行

漏洞影响版本:
php5 < 5.6.25
php7 < 7.0.10

https://xzfile.aliyuncs.com/media/upload/picture/20191112144234-9c15d5ca-0517-1.png

也就是这里的对象成员个数,如果说这里的个数多了,就会直接跳过对于_wakeup()的执行。

常见于绕过_wakeup()中对于成员属性的再次赋值。

session反序列化漏洞:

session英文翻译为”会话”,两个人聊天从开始到结束就构成了一个会话。PHP里的session主要是指客户端浏览器与服务端数据交换的对话,从浏览器打开到关闭,一个最简单的会话周期

cookie与session的区别:

https://www.zhihu.com/question/19786827

PHP session工作流程

会话的工作流程很简单,当开始一个会话时,PHP会尝试从请求中查找会话 ID (通常通过会话 cookie),如果发现请求的CookieGetPost中不存在session id,**PHP 就会自动调用php_session_create_id函数创建一个新的会话**,并且在http response中通过set-cookie头部发送给客户端保存,例如登录如下网页Cokkie、Get、Post都不存在session id,于是就使用了set-cookieimg有时候浏览器用户设置会禁止 cookie,当在客户端cookie被禁用的情况下,php也可以自动将session id添加到url参数中以及formhidden字段中,但这需要将php.ini中的session.use_trans_sid设为开启,也可以在运行时调用ini_set来设置这个配置项.

会话开始之后,PHP 就会将会话中的数据设置到 $_SESSION 变量中,如下述代码就是一个在 $_SESSION 变量中注册变量的例子:

<?php
session_start();
if (!isset($_SESSION['username'])) {
$_SESSION['username'] = 'ghtwf01' ;
}
?>
//就是判断是否设置了username,如果没有设置的话,就把username这个session变量设置为所需的内容

img

主要的一个重点还是session相较于cookie来说的话,是存储在服务器中的。因此,可以直接通过改写session文件进行多种攻击。

php.ini配置

php.ini里面有如下六个相对重要的配置

session.save_path=""      --设置session的存储位置
session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.auto_start --指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.serialize_handler --定义用来序列化/反序列化的处理器名字,默认使用php
session.upload_progress.enabled --启用上传进度跟踪,并填充$ _SESSION变量,默认启用
session.upload_progress.cleanup --读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用

phpstudy下上述配置如下:

session.save_path = "/tmp"      --所有session文件存储在/tmp目录下
session.save_handler = files --表明session是以文件的方式来进行存储的
session.auto_start = 0 --表明默认不启动
session.serialize_handler = php --表明session的默认(反)序列化引擎使用的是php(反)序列化引擎
session.upload_progress.enabled on --表明允许上传进度跟踪,并填充$ _SESSION变量
session.upload_progress.cleanup on --表明所有POST数据(即完成上传)后,立即清理进度信息($ _SESSION变量)

对于session文件来说,文件名的格式都是:

sess_xxxxx

一般来说是默认使用文件的形式进行存储。

这种类似的形式,后面部分的填充是自动生成的,也就是session id部分。

img

session.serialize_handler,它定义的引擎有三种

|处理器名称|存储格式|
|php| 键名 + 竖线 + 经过serialize()函数序列化处理的值|
|php_binary| 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值|
|php_serialize(php>5.5.4)|经过serialize()函数序列化处理的数组|

php处理器

首先来看看session.serialize_handler等于php时候的序列化结果,代码如下

<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

(这里是序列化和反序列化的引擎语言使用PHP)

可以直接看见sessionid的,使用f12打开控制台,然后在存储选项可以看见。

session是$_SESSION['session']的键名,在|之后是传入的GET参数,经过序列化之后的值。

键名 + 竖线 + 经过serialize()函数序列化处理的值

img

图片中键名是session,后面的部分是序列化的值,表示string类型,有7位,内容

php_binary处理器:

再来看看session.serialize_handler等于php_binary时候的序列化结果

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sessionsessionsessionsessionsession'] = $_GET['session'];
?>

为了更能直观的体现出格式的差别,因此这里设置了键值长度为 3535 对应的 ASCII 码为#,所以最终的结果如下

键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值

img

这部分会在键名前加一个和键名长度相等的ASCII码表示的字符。

php_serialize 处理器

最后就是session.serialize_handler等于php_serialize时候的序列化结果,代码如下

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

结果如下

|php_serialize(php>5.5.4)|经过serialize()函数序列化处理的数组|

img

其中a:1表示$_SESSION中有一个元素,同时在花括号里面的内容是传入的GET的值。

利用:

php处理器和php_serialize处理器这两个处理器生成的序列化格式本身是没有问题的,但是如果这两个处理器混合起来用,就会造成危害。形成的原理就是在用session.serialize_handler = php_serialize存储的字符可以引入 | , 再用session.serialize_handler = php格式取出$_SESSION的值时, |会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。

键名 + 竖线 + 经过serialize()函数序列化处理的值

|php_serialize(php>5.5.4)|经过serialize()函数序列化处理的数组|

也就是说,可以通过php_serialize来引入|,然后当php进行解析的时候,就会将其中的|前判断为键名。

遇到’|’时会将之看做键名与值的分割符,从而造成了歧义,导致其在解析session文件时直接对’|’后的值进行反序列化处理。
这里可能会有一个小疑问,为什么在解析session文件时直接对’|’后的值进行反序列化处理,这也是处理器的功能?这个其实是因为session_start()这个函数,可以看下官方说明:

img

直接看这个博客:https://www.cnblogs.com/hello-py/articles/13501786.html

简单的总结一下,这里主要是有两个部分的问题:

1、首先是对于session文件使用了两种不同的处理器来进行存储和读取,一个是php_serialize,这里存储的方式是使用a:1{}这种格式,而另外一个是php处理器,这里使用的是键名|值这种方式。当传入的值经过php_serialize处理器进行存储的时候,可以传入|这个符号,当再次使用php处理器进行读取的时候,会直接将|前面的部分读取为键名,然后将后面的部分读取为内容。

2、看上面的截图,当上述部分成立了之后,会自动的对值的部分进行反序列化。

对于是否有session反序列化漏洞,最好可以通过查看phpinfo()配置来进行了解。

Phar拓展反序列化攻击面:

phar文件简介

概念

一个php应用程序往往是由多个文件构成的,如果能把他们集中为一个文件来分发和运行是很方便的,这样的列子有很多,比如在window操作系统上面的安装程序、一个jquery库等等,为了做到这点php采用了phar文档文件格式,这个概念源自javajar,但是在设计时主要针对 PHP 的 Web 环境,与 JAR 归档不同的是Phar归档可由 PHP 本身处理,因此不需要使用额外的工具来创建或使用,使用php脚本就能创建或提取它。phar是一个合成词,由PHPArchive构成,可以看出它是php归档文件的意思(简单来说phar就是php压缩文档,不经过解压就能被 php 访问并执行)

phar组成结构

stub:它是phar的文件标识,格式为xxx<?php xxx; __HALT_COMPILER();?>;
manifest:也就是meta-data,压缩文件的属性等信息,以序列化存储
contents:压缩文件的内容
signature:签名,放在文件末尾

eg:

php内置了一个Phar类来处理相关操作

<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='hu3sky';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。

php Version>=5.3.0

这里有两个关键点:

一是文件标识,必须以__HALT_COMPILER();?>结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者其它文件来绕过一些上传限制;

二是反序列化,phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化,而这样的文件操作函数有很多

漏洞成因:

phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化

毕竟通过setmetadata()这个方法传入的是一个对象。

既然有序列化,自然会有反序列化。

有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化
在网上扒了一张图img

将phar伪造成其他格式的文件

在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件

<?php
class TestObject {
}

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$o = new TestObject();
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

利用条件

phar文件要能够上传到服务器端。

file_exists()fopen()file_get_contents()file()等文件操作的函数

要有可用的魔术方法作为“跳板”。

文件操作函数的参数可控,且:/phar等特殊字符没有被过滤。

漏洞验证

环境准备

upload_file.php`,后端检测文件上传,文件类型是否为gif,文件后缀名是否为gif
`upload_file.html` 文件上传表单
`file_un.php` 存在`file_exists()`,并且存在`__destruct()

文件内容

upload_file.php

<?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.html

<body>
<form action="http://localhost/upload_file.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" name="Upload" />
</form>
</body>

file_un.php

<?php
$filename=$_GET['filename'];
class AnyClass{
var $output = 'echo "ok";';
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename);

直接将文件头和文件名改成gif格式的,然后在通过phar://这个伪协议进行访问,就能够造成攻击。