PHP魔法方法的一些总结
什么是魔术方法
一个预定义好的,在特定情况下自动触发的行为方法
魔术方法的作用
反序列化漏洞的成因:
反序列化过程中,unserialize()接收的**值 (字符串)**可控 ,通过更改这个值 (字符串),得到所需要的代码。
通过调用方法,触发代码执行。
魔术方法
__construct()
构造函数,在实例化一个对象的时候,首先会去自动执行的一个方法,如给成员属性初始化值
<?php
highlight_file(__FILE__);
class User {
public $username;
public function __construct($username) {
$this->username = $username;
echo "触发了构造函数1次" ;
}
}
$test = new User("benben"); //在此处触发构造函数
?>
#结果
触发了构造函数1次
__destruct()
析构函数,在对象的所有引用被删除或者当对象被显式销毁时执行的魔术方法
-
实例化对象结束后,代码运行完会销毁,触发析构函数
-
在反序列化过程中会触发析构函数
- 反序列化得到的是对象,用完后会销毁
<?php
highlight_file(__FILE__);
class User {
public function __destruct()
{
echo "触发了析构函数1次"."<br />" ;
}
}
$test = new User("benben"); //触发一次
$ser = serialize($test);
unserialize($ser); //触发一次
?>
#结果
触发了析构函数1次
触发了析构函数1次
__sleep()
序列化serialize() 函数会检查类中是否存在一个魔术方法 sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。
此功能可以用于清理对象(可以用来选择需要序列化的属性),并返回这些属性的数组。这些属性将被存储在序列化字符串中。
如果该方法返回一个空数组或者未返回一个数组(可能是写了其他代码,比如拿来做命令执行),那么所有属性都不会被序列化, NULL 被序列化(序列化的结果是N),并产生一个 ENOTICE级别的错误。
如果没有定义 __sleep() 方法,那么所有属性都会被序列化。
注意:__sleep()执行在前,序列化执行在后
<?php
highlight_file(__FILE__);
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password) {
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep() {
return array('username', 'nickname'); //返回了包含username,nickname的数组,不在数组中的password就不会被序列化
}
}
$user = new User('a', 'b', 'c');
echo serialize($user);
?>
#结果
O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}
可以看到,序列化后的字符串包含了 username,nickname属性的值。这些值可以在反序列化时被使用。
还可以用来执行别的命令
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password) {
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep() {
system($this->username);
}
}
$cmd = "id";
$user = new User($cmd, 'b', 'c');
echo serialize($user);
?>
#结果
uid=33(www-data) gid=33(www-data) groups=33(www-data) N; #结果后面有个N就是NULL被实例化了,应为__sleep没有返回属性数组
如果返回的数组里面不包含存在的属性呢
public $username;
public $nickname;
private $password;
public function __sleep() {
return array(1,2,3);
}
#结果
O:4:"User":0:{} #属性为空
__wakeup()
反序列化unserialize()时会检查是否存在一个__wakeup()方法。
如果存在,则会先调用_wakeup() 方法,预先准备对象需要的资源。
预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作。
注意:反序列化先执行,__wakeup()后执行
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
private $order;
public function __wakeup() {
$this->password = $this->username; #用来给password赋值
}
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}'; # 可以看到序列化字符串里没有password的值
var_dump(unserialize($user_ser));
?>
#结果
object(User)#1 (4) {
["username"]=> string(1) "a"
["nickname"]=> string(1) "b"
["password":"User":private]=> string(1) "a" //password被成功赋上username的值
["order":"User":private]=> NULL
}
用来命令执行
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
private $order;
public function __wakeup() { #在反序列化后先执行,读取username的值
system($this->username);
}c
}
$user_ser = '?benben=O:4:"User":1:{s:8:"username";s:2:"id";}'; #给username赋值
unserialize($user_ser);
?>
__toString()
当一个对象被当作字符串输出的时候,会触发__toString魔法函数,如果没有设置__toString,php会抛出致命错误,提示无法将对象转换成字符串
以下几种情况会触发
- 当使用
echo
或print
输出一个对象时,PHP会自动调用该对象的__toString()
方法,将方法返回的字符串输出到屏幕上。 - 当将一个对象转换成字符串时,PHP也会自动调用该对象的
__toString()
方法,将方法返回的字符串作为对象的字符串表示形式。 - 当使用字符串拼接运算符
.
连接一个对象和一个字符串时,PHP会自动将该对象转换成字符串,并调用其__toString()
方法,然后将字符串拼接起来。 - 当使用
sprintf
函数或printf
函数格式化字符串时,如果格式化字符串中包含一个对象占位符%s
,PHP会自动将该对象转换成字符串,并调用其__toString()
方法,然后将字符串插入到格式化字符串中
调用对象可以使用print_r
或者var_dump
示例
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
var $benben = "this is test!!";
public function __toString()
{
return '格式不对,输出不了!';
}
}
$test = new User() ;
print_r($test);
echo "<br />";
echo $test; //触发错误
?>
#结果
User Object ( [benben] => this is test!! )
格式不对,输出不了!
__invoke()
它允许一个对象像函数一样被调用。当一个对象被作为函数调用时,PHP会自动调用这个对象的__invoke()方法来处理这个调用。
以下几种方式被触发:
-
将一个对象作为函数调用:当一个对象被作为函数调用时(就是在对象变量名后加上()),PHP会自动调用这个对象的__invoke()方法来处理这个调用。
<?php highlight_file(__FILE__); error_reporting(0); class User { var $benben = "this is test!!"; public function __invoke() { echo '它不是个函数!'; } } $test = new User() ; echo $test ->benben; echo "<br />"; echo $test() ->benben; //把对象加上(),当成函数调用 ?> #结果 this is test!! 它不是个函数!
-
使用call_user_func()或call_user_func_array()函数:这两个函数可以将一个对象作为第一个参数传递,并调用该对象的__invoke()方法。
例如:
class MyObject { public function __invoke($arg1, $arg2) { return $arg1 + $arg2; } } $obj = new MyObject(); $result = call_user_func($obj, 1, 2); // 相当于调用 $obj->__invoke(1, 2) echo $result; // 输出 3
例如:
class MyObject {
public function __invoke($arg1, $arg2) {
return $arg1 + $arg2;
}
}
$obj = new MyObject();
$result = call_user_func($obj, 1, 2); // 相当于调用 $obj->__invoke(1, 2)
echo $result; // 输出 3
-
使用回调函数:当一个对象被用作回调函数时,PHP会自动调用这个对象的__invoke()方法来处理回调。
例如:
class MyObject { public function __invoke($arg1, $arg2) { return $arg1 + $arg2; } } $obj = new MyObject(); $result = array_reduce([1, 2, 3], $obj); // 相当于调用 $obj->__invoke(1, 2), $obj->__invoke(3, 3) echo $result; // 输出 9
在这个例子中,$obj
对象被用作回调函数,并作为第二个参数传递给了array_reduce()函数。这个函数会自动调用$obj
对象的__invoke()方法来处理回调,将数组中的元素两两相加。最后,$result变量将保存这个结果,并输出为9。
__call()
用于在对象调用不存在的方法时自动调用。通俗来说,就是当我们调用一个对象中不存在的方法时,PHP会自动调用
__call()
方法,并将调用的方法名和参数传递给它。
注意:__call($name,$arg)
有两个形参,其中第二个形参为数组
示例
class MyClass {
public function __call($name, $arguments) {
echo "调用的方法名为:$name,参数为:";
print_r($arguments);
}
}
$obj = new MyClass();
$obj->Method("test", 123); //调用一个不存在的函数
#结果
调用的方法名为:Method,参数为:Array ( [0] => test [1] => 123 )
__callStatic
它允许在静态上下文中调用一个不存在的静态方法。通俗来说,就是当你在一个类中调用一个不存在的静态方法时,PHP会自动调用__callStatic方法来处理这个调用。
注意:__call用于动态调用实例方法,而__callStatic用于动态调用静态方法。其它与__call一致
示例
class Test {
public static function __callStatic($name, $arguments) {
echo "调用静态方法 '$name',参数为:";
print_r($arguments);
}
}
Test::foo('bar', 'baz'); //调用不存在的静态方法
#结果
调用静态方法 'foo',参数为:Array([0] => bar [1] => baz)
知识补充
->
和::
都是PHP中用于访问类成员的运算符,但它们的作用不同:
-
->
运算符用于访问类的实例成员,即对象的属性和方法。它需要在一个实例化的对象上使用。还可以访问数组元素(仅限关联数组) -
::
运算符用于访问类的静态成员,即静态属性和静态方法。它需要在类名上使用。
__get()
用于在对象中访问一个不存在的属性时自动调用,会传递一个参数,即属性名。它的作用是在对象中获取一个不存在的属性时,可以自动执行一些代码,比如从数据库中读取数据并返回,或者返回一个默认值等。
注意:__get($arg)有一个形参
示例
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public $var1;
public function __get($arg1)
{
echo $arg1;
}
}
$test = new User() ;
$test ->var2; //这里访问了一个不存在的属性触发了__get魔法方法,并将var2这个值传给了__get
?>
#结果
var2
__set()
当我们在对象中设置一个不存在的属性时,__set()方法会被自动调用,并且会传递两个参数:属性名和属性值。
注意:只有属性名而没有属性值,那么__set()方法就无法被调用。__set()方法只有在你尝试设置一个不存在的属性时才会被调用,并且需要传递属性名和属性值两个参数。
示例
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public $var1;
public function __set($arg1 ,$arg2)
{
echo $arg1.','.$arg2;
}
}
$test = new User() ;
$test ->var2=1; //这里给不存在的属性var2赋值触发__set方法
?>
var2,1
__isset()
用于在对象中检查一个属性是否存在。当在对象中调用isset()函数或empty()函数来检查一个不存在的属性时,__isset()方法会被自动调用,并且会传递一个参数,即属性名。如果不定义__isset()方法,当尝试检查一个不存在的属性时,PHP会返回false。
示例
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var; //私有属性,无法被外面获取
public function __isset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
isset($test->var); //检查属性是否存在,var是私有属性无法被检查到,触发__isset方法,且将var传递给它
?>
#结果
var
__unset()
__unset是一个魔术方法,用于在对象中删除指定的属性。**当使用unset()函数删除一个对象的属性时,实际上是调用了对象的__unset()方法,并将要删除的属性名作为参数传递给__unset()方法。**如果没有定义__unset()方法,那么默认情况下会抛出一个警告。通过定义__unset()方法,可以在对象中自定义删除属性的行为。
示例
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __unset($arg1)
{
echo $arg1;
}
}
$test = new User() ;
unset($test->var); #触发__unset,并将属性名传给它
?>
#结果
var
这里可能会有疑问,私有属性怎么能被类外部访问并删除呢,实际上删除属性是通过类内部的__unset魔法方法来完成的
__clone()
用于在PHP中复制对象。当使用clone关键字复制一个对象时,实际上是调用了对象的__clone()方法。如果没有定义__clone()方法,那么默认情况下会创建一个浅拷贝(shallow copy)的对象,即**只复制对象的属性,而不复制属性所引用的对象:**如果原对象中某个属性引用了其他对象,那么新对象中对应的属性也会引用同一个对象。如果我们希望新对象的属性也引用新的对象,那么就需要在__clone()方法中进行处理。
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __clone( )
{
echo "__clone test";
}
}
$test = new User() ;
$newclass = clone($test) //复制新对象
?>
__clone test
此为个人学习笔记,如果错误请大佬指出,希望也对你们有帮助
参考资料