前言
随着学习向前,遇到的新知识点,也就是对以前反序列化学习的内容补充
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文件标识
1 |
|
可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。也就是说如果我们留下这个标志位,构造一个图片或者其他文件,那么可以绕过上传限制,并且被 phar 这函数识别利用。比如
1 | setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub |
a manifest describing the contents 压缩文件信息,phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

the file contents
被压缩文件的内容。
[optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾,格式如下:

签证尾部的01代表md5加密,02代表sha1加密,04代表sha256加密,08代表sha512加密
当我们修改文件的内容时,签名就会变得无效,这个时候需要更换一个新的签名
更换签名的脚本(别的师傅的)
1 | from hashlib import sha1 |
Phar反序列化
Phar之所以能反序列化,是因为Phar文件会以序列化的形式存储用户自定义的meta-data,PHP使用phar_parse_metadata在解析meta数据时,会调用php_var_unserialize进行反序列化操作。
利用条件
1 | 1、phar文件能够上传至服务器 |
如果题目限制了,
phar://不能出现在头几个字符。可以用Bzip / Gzip协议绕过。
1
2
3
4 $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反序列化
1
2
3 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来上传最后访问也可以识别
1 |
|
在生成的phar文件里我们也都可以找到我们构造的内容
赛题复现
MyPicDisk(DASCTF 2023 & 0X401七月暑期挑战赛)

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

下载看源码(重要的地方我标注出来了)
1 |
|
思路就很明显了
方法一:
我们首先通过xpath盲注得到admin密码,然后再构造phar文件,通过md5_file()触发pahr反序列化
密码:15035371139
生成phar文件
1 |
|

后缀改为gif,上传

使用phar协议以及触发file_md5()函数
payload:?file=phar://phar.gif&todo=md5

Getflag

方法二:

直接拼接不细讲了
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()

这个函数的返回值是布尔型,既不会成功也不会报错,它的作用是打开Session,并且随机生成一个32位的session_id,session的全部机制也是基于这个session_id,服务器就是通过这个唯一的session_id来区分出这是哪个用户访问的:
可以看到我标注出来的两个部分我认为是重点
一步一步来
首先看看session的生成

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

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_probability和session.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


有但是内容是空的
我们直接改session值看看会发生什么



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


发现成功写进去了,它的内容就是将键值对序列化之后的结果
总结一下大致过程就是:
HTTP请求一个页面后,如果用到开启session,会去读COOKIE中的PHPSESSID是否有,如果没有,则会新生成一个session_id,先存入COOKIE中的PHPSESSID中,再生成一个sess_前缀文件。当有写入$_SESSION的时候,就会往sess_文件里序列化写入数据。当读取到session变量的时候,先会读取COOKIE中的PHPSESSID,获得session_id,然后再去找这个sess_session_id文件,来获取对应的数据。由于默认的PHPSESSID是临时的会话,在浏览器关闭后就会消失,所以,当我们打开浏览器重新访问的时候,就会新生成session_id和sess_session_id这个文件。
同时也发现了这个特殊的序列化格式
在这之前就要看看session.serialize_handler中定义的三种序列化/反序列化的处理器
| 处理器名称 | 存储格式 |
|---|---|
| php | 键名 + 竖线 + 经过serialize()函数序列化处理的值(默认使用) |
| php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值 |
| php_serialize | 经过serialize()函数序列化处理的数组(php>5.5.4) |
php
1 |
|

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


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

这个的格式是直接进行序列化,把session中的键和值都会被进行序列化操作,然后把它当成一个数组返回回来
php处理器和php_serialize处理器这两个处理器生成的序列化格式本身是没有问题的,但是如果这两个处理器混合起来用,就会造成危害。原理就在php处理器写入时的格式为键名+竖线|+经过serialize()序列化处理后的值那它读取时,肯定就会以竖线|作为一个分隔符,前面的为键名,后面的为键值,然后将键值进行反序列化操作;而php_serialize处理器是直接进行序列化,然后返回序列化后的数组,那我们在传入的序列化内容前加一个分隔符|,从而正好序列化我们传入的内容
简单Demo
定义一个session.php文件,用于传入 session值,文件内容如下:
1 |
|
存在另一个class.php 文件,内容如下:
1 |
|
这两个文件的作用很清晰,session.php文件的处理器是php_serialize,class.php文件的处理器是php,session.php文件的作用是传入可控的 session值,class.php文件的作用是在反序列化开始前输出Who are you?,反序列化结束的时候输出name值。
这两个文件如果想要利用,我们要在session.php文件传入|+序列化格式的值,然后再次访问class.php文件的时候,就会在调用session值的时候,触发漏洞。
payload
1 |
|


再次访问class.php

后记
越学越能意识到php体系的结束才算是入门Web的学习之路。前段时间听了已经工作了的学长们的分享,已经对学习的建议,让我少了许多困惑。
首先,学习的深度一定要有,不能只浮在于表面,要跟随知识点横向竖向拓展,不能学习的时候就按照别的师傅的博客思路走,那么学习收到的成果并不一定很多。想起来我之前跟别人说的话大概是“每天都坐在同一个环境下相同的时间学习,排除个体效率的差异,凭什么比别人强?那么你就要有别人所不具备的,不论是方法还是心态等,但是在这之前,你要做到别人都做到了的”即使是我自己说的,但是果然自己也不一定就悟透了,最基本的我连原话都记的不是那么清,就像读同一本书每次读都有不同的感受一样。
还有就是,专精,要有自己的长处,不能只局限于什么都会一点,那么等于什么都不会,而是要有自己所非常擅长的方向,领域,别人达不到的。
最后就是,少想一点,踏实下来学好自己的方向内容,平院长说的对,想的太多就是选择太多了导致的,至少现在我没必要想这么多,毕竟才算是刚刚入门Web。