序列化是什么?
要想了解反序列化,就先要知道序列化是什么。下面是是一串序列化数组:
a:2:{s:4:"name";s:6:"cike_y";s:3:"age";i:18;}
- a表示array(数组),2表示这个数组有两个元素。
- 第一个元素s代表字符串,长度为4,键值是name,然后分号分隔;接下来name的值为cike_y,它的长度为六,也是字符串。
- 第二个元素age键名,类型为字符串,键名的值是18,i代表的是整数。
构造序列化函数使用serialize函数,将一个可读的字符串打包成难理解的序列化数组
<?php
// 构造数组
$data = array(
"name" => "cike_y",
"age" => 18
);
// 序列化数组
$serializedData = serialize($data);
// 输出序列化字符串
echo $serializedData;
反序列化
将序列化数组进行逆转过来,就是反序列化,简而言之就是一串很难理解的字符串反序列化为很直观的形式,如前面的示例:
<?php
$serialize_array = 'a:2:{s:4:"name";s:6:"cike_y";s:3:"age";i:18;}';
var_dump (unserialize($serialize_array)); //unserialize函数为反序列化函数
访问控制
php对属性或者方法控制,是通过在前面添加关键字来进行实现的。
- public (公有):公有的类成员可以在任何地方被访问。
- protected (受保护): 受保护的类成员则可以被其自身以及其子类和父类访问。
- private (私有) :私有的类成员则只能被其定义所在的类访问。
演示代码:
<?php
class Myclass {
public $name1;
protected $name2;
private $age;
function __construct($name1,$name2,$age) {
$this->name1 = $name1;
$this->name2 = $name2;
$this->age = $age;
}
}
// 序列化
$a = new Myclass();
$a->name1 = "cike";
#$a->name2 = "_y";
#$a->age = "18";
$str = serialize($a);
$str2 = urlencode($str);
echo "序列化的内容:<br>";
echo "没有url编码:".$str."<br>";
echo "有url编码的:".$str2."<br>";
// 反序列化
$a2 = unserialize($str);
echo "<br>反序列化的内容:<br>";
echo var_dump($a2)."<br>";
print_r($a2);
?>
运行php文件后发现只有cike进行输出了,其他的值为空,可能觉得认为是注释掉了,所以才没有进行回显,但并不是
public变量是公共变量,可以使用属性赋值,而其他成员却是不能直接使用属性赋值的方法,这时候取消剩下两个不同成员的属性赋值注释:
<?php
class Myclass {
public $name1;
protected $name2;
private $age;
function __construct($name1,$name2,$age) {
$this->name1 = $name1;
$this->name2 = $name2;
$this->age = $age;
}
}
// 序列化
#$a = new Myclass("cike", "_y" , 18);
$a = new Myclass();
$a->name1 = "cike";
$a->name2 = "_y";
$a->age = "18";
$str = serialize($a);
$str2 = urlencode($str);
echo "序列化的内容:<br>";
echo "没有url编码:".$str."<br>";
echo "有url编码的:".$str2."<br>";
// 反序列化
$a2 = unserialize($str);
echo "<br>反序列化的内容:<br>";
echo var_dump($a2)."<br>";
print_r($a2);
?>
可以看见出现了报错,中文意思为以下
致命错误:无法访问 test.php在第18行中的受保护属性myclass :: $ name2
说明了不同的成员变量的访问性质也不同
不同成员变量的编码
<?php
class Myclass {
public $name1;
protected $name2;
private $age;
function __construct($name1,$name2,$age) {
$this->name1 = $name1;
$this->name2 = $name2;
$this->age = $age;
}
}
// 序列化
$a = new Myclass("cike", "_y" , 18);
$str = serialize($a);
$str2 = urlencode($str);
echo "序列化的内容:<br>";
echo "没有url编码:".$str."<br>";
echo "有url编码的:".$str2."<br>";
// 反序列化
$a2 = unserialize($str);
echo "<br>反序列化的内容:<br>";
echo var_dump($a2)."<br>";
print_r($a2);
?>
可以看见 protected成员和private成员如果是直接输出会略过特殊的字符串,因为不会显示%00。而public变量的成员则没有特殊字符
序列化的内容:
没有url编码:O:7:"Myclass":3:{s:5:"name1";s:4:"cike";s:8:"*name2";s:2:"_y";s:12:"Myclassage";i:18;}
有url编码的:O%3A7%3A%22Myclass%22%3A3%3A%7Bs%3A5%3A%22name1%22%3Bs%3A4%3A%22cike%22%3Bs%3A8%3A%22%00%2A%00name2%22%3Bs%3A2%3A%22_y%22%3Bs%3A12%3A%22%00Myclass%00age%22%3Bi%3A18%3B%7D
反序列化的内容:
object(Myclass)#2 (3) { ["name1"]=> string(4) "cike" ["name2":protected]=> string(2) "_y" ["age":"Myclass":private]=> int(18) }
Myclass Object ( [name1] => cike [name2:protected] => _y [age:Myclass:private] => 18 )
O:7:“Myclass”:3 // O表示对象,类名长度为7,类名为Myclass,有三个成员,大括号里面是三个成员变量的信息
-
public成员变量的名字直接写
-
protected成员变量的名字前需要加%00类名*%00
-
private成员变量等待名字前需要加%00类名%00
其中%00为一个字符
常见的绕过
十六进制绕过
这是一个简单的例子:
<?php
class test{
public $username;
public function __construct(){
$this->username = 'admin';
}
}
function waf($data){
if(stristr($data, 'username')!==False){
echo("hcaker!");
}
else{
echo "flag{@_@}";
}
}
$a = $_GET['source'];
$a = waf($a);
unserialize($a);
可以看见如果想要输入username,就会被过滤掉
当表示字符串的s变成大写时,会被当成十六进制进行解析
O:4:"test":1:{s:8:"username";s:5:"admin";}
//改成以下
O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}
//u的十六进制就是\\75
引用绕过
比如题目对 c o n r r e c t 变量进行 b a s e 64 解密什么的,必须要 conrrect变量进行base64解密什么的,必须要 conrrect变量进行base64解密什么的,必须要input变量等于$correct 才能获得flag
<?php
class BUU {
public $correct = "";
public $input = "";
}
$a = new BUU();
/*主要的绕过方式*/
$a->correct = ""; //先让correct的值为空
$a->input = &$a->correct;
#在 PHP 中,使用 & 符号表示引用赋值,意味着两个变量将指向同一个数据空间,从而使他们两个相等
echo serialize($a);
?>
具体可以参考我之前的刷题文章
https://blog.csdn.net/weixin_53912233/article/details/128105583
__wakeup绕过
绕过的版本范围:
- PHP5 < 5.6.25
- PHP7 < 7.0.10
具体可以参考我之前这篇文章
https://blog.csdn.net/weixin_53912233/article/details/128069937
php反序列化常见的魔术方法
__invoke()方法
实例化该方法的类后,再以函数的方式调用类,就可以触发__invoke方法
<?php
class Myclass
{
public function __invoke()
{
echo '__invoke方法调用成功';
}
}
$a = new Myclass();
$a(); //实例化类后,以函数的形式进行调用,可以触发__invoke方法
;?>
可以发现当$a()执行之后就会成功调用invoke方法
__wakeup()方法
该方法在反序列化对象的时候会进行调用,在php中unserialize函数为反序列化函数
<?php
class Myclass{
public function __wakeup(){
echo "我已经被反序列化了,可以执行苏醒函数了";
}
}
$obj = new Myclass();
$serialize_obj = serialize($obj);
unserialize($serialize_obj); //反序列化对象
当unserialize函数执行完毕之后,可以看见成功调用了__weakeup()魔术方法
__sleep()方法
当该对象执行serialize函数的时候,进行序列化的时候会进行调用,如下
<?php
class Myclass{
public function __sleep(){
echo "我已经被序列化了,可以执行睡觉函数了";
}
}
$obj = new Myclass();
serialize($obj); //序列化对象
可以看见执行serialize函数的时候,会调用__sleep()方法
__construct()方法
构造方法,实例化对象的时候自动调用的方法,如下:
<?php
class MyClass {
public function __construct() {
echo "对象创建成功!";
}
}
$obj = new MyClass(); //实例化MyClass对象
可以看见实例化的时候,会调用__construct()魔术方法。
__destruct()方法
销毁函数,当脚本执行结束的时候就会运行该方法,如下:
<?php
class Myclass {
function __construct() {
echo "aa ";
}
function __destruct() {
print "bb";
}
}
$obj = new Myclass();
创建 $obj 对象时,调用 __construct() 方法,输出 aa。执行完上述之后,就会调用销毁函数,最后输出bb
__toString()方法
当该方法的对象时被当作字符串输出,会返回__toString方法的字符串。比如:
<?php
class Myclass {
function __toString() {
return "执行toString魔术方法";
}
}
$obj = new Myclass();
echo $obj;
可以看见输出对象当作字符串的时候,就会成功调用__toString魔术方法,并且返回它的字符串信息
__get()方法
该方法当访问一个对象的不可访问属性时被调用,会自动调用,如下
<?php
class Myclass{
public function __get($name){
echo $name;
}
}
$obj = new Myclass();
$obj->hello;
可以看见调用了一个不存在的hello,最后触发了__get()方法,输出了hello
__set()方法
__set($name, v a l u e ) 方法,当给对象的不可访问属性赋值时被调用。其中 value)方法,当给对象的不可访问属性赋值时被调用。其中 value)方法,当给对象的不可访问属性赋值时被调用。其中name指的是键名,$value指的是值。
<?php
class Myclass{
public function __set($name,$value){
echo $name."-----".$value;
}
}
$obj = new Myclass();
$obj->me = "hello";
最后可以看见成功调用__set()方法
__isset()方法
该方法,当用isset()或empty()判断该当前对象不可见属性的时候,就会调用
<?php
class Myclass{
//获取不可见属性时,被调到触发
public function __isset($a){
echo $a."<br>";
}
}
$obj = new Myclass();
isset($obj->a);
empty($obj->b);
可以看见当·访问量不可见的a和b属性的时候就成功输出了a和b。
__unset()方法
该魔术方法同__isset()方法差不多。当使用unset()判断一个该对象不可见属性的时候就会调用__isset()方法
<?php
class Myclass{
//获取不可见属性时,被调到触发
public function __unset($a){
echo $a."<br>";
}
}
$obj = new Myclass();
unset($obj->a);
可以看见成功调用了_unset()方法
__call()方法
__call()该方法在调用的方法不存在时会自动调用,这个方法接受两个参数。如下:
<?php
class MyClass
{
function __call($class_name, $args)
{
echo "__call方法调用成功<br>";
var_dump($class_name,$args);
}
}
$a=new MyClass();
$a->test("test1","test2");
test()方法是不存在的,访问php文件,可以看见成功调用__call()方法。
test是方法名,test1和test2是test方法的参数。
__callStatic()方法
- 当该方法的对象要调用的静态方法不存在或者权限不足时候会调用该方法
- 接受的参数为数组形式
如下例子:
<?php
class Myclass
{
private static function name()
{
echo 'hhhhhhhhh';
}
public function __callStatic($function_name, $arg)
{
echo "__callStatic方法调用成功";
var_dump($function_name,$arg);
die;
}
}
$a = new Myclass();
$a::test("参数1","参数2");
//或 Myclass::test("参数1","参数2");
?>
可以发现当该对象调用私有的静态name方法时,成功调用了__callStatic()方法,并且执行了不存在的方法