从SESSION文件包含漏洞看PHP安全攻防:原理、利用与防御
2026/9/7 15:02:17 网站建设 项目流程

1. 从一道CTF题看SESSION的“另一面”

最近在复盘一些经典的Web安全CTF题目时,遇到了一道关于文件包含漏洞的题,它的切入点不是常规的include($_GET[‘file’])包含日志或者上传文件,而是巧妙地利用了PHP的SESSION机制。这道题让我重新审视了SESSION——这个我们日常开发中用于维持用户状态、再熟悉不过的组件,在攻击者眼中可能是一个隐藏的“文件上传点”和“代码执行跳板”。很多开发者和初学安全的同学,对SESSION的理解可能停留在“服务器存储的键值对”,却忽略了它在服务器上本质也是以文件形式存在的。这就为一种特殊的文件包含漏洞利用方式打开了大门:包含SESSION文件本身。

传统的文件包含漏洞利用,往往需要攻击者想方设法在服务器上留下一个包含恶意代码的文件,比如通过文件上传功能、写入日志、利用php://input等伪协议。但如果服务器开启了SESSION,并且session.save_path目录已知或可猜测,同时存在文件包含漏洞,那么攻击者可能完全不需要“上传”这个动作。他只需要让服务器“主动”为他生成一个包含恶意代码的SESSION文件即可。这听起来有点绕,但原理其实很直接:SESSION文件的内容部分来源于用户可控的$_SESSION超全局变量。如果我们能通过某种方式(比如反序列化漏洞、参数污染等)向$_SESSION中注入恶意代码,再通过文件包含漏洞去包含这个SESSION文件,就能实现远程代码执行。

这道CTF题正是考察了这个知识点。它模拟了一个看似只有文件包含功能,却没有直接文件上传入口的场景。解题的关键就在于意识到SESSION文件可以被包含,并找到控制SESSION文件内容的方法。接下来,我将详细拆解这类漏洞的原理、利用条件、具体利用步骤,并分享在实战和CTF中挖掘此类漏洞的心得与技巧。

2. SESSION机制与文件包含漏洞的交叉点

要理解这个漏洞,我们必须先抛开“键值对”的抽象概念,深入到PHP中SESSION的底层实现。当session_start()被调用时,PHP会根据session_id(通常通过Cookie中的PHPSESSID传递)来唯一标识一个用户会话。会话数据需要持久化存储,默认的存储方式就是文件。

2.1 SESSION文件的存储与命名

PHP的session.save_handler配置决定了存储方式,默认为files。此时,会话数据会被序列化后保存到session.save_path指定的目录中。文件的命名规则通常是sess_加上session_id。例如,如果session_idabc123,那么对应的SESSION文件就是/tmp/sess_abc123(假设session.save_path/tmp)。

这个文件的内容是经过序列化的字符串。默认的序列化处理器(session.serialize_handler)通常是phpphp_binary。例如,当我们设置$_SESSION[‘user’] = ‘admin’;后,文件内容可能就是user|s:5:“admin”;这样的格式。这里的关键在于,$_SESSION数组中的键和值,都会以明文形式出现在这个文件里。

2.2 漏洞形成的核心链条

文件包含漏洞(如include($_GET[‘file’]))与SESSION文件的交叉,形成了如下的攻击链条:

  1. 存在文件包含点:应用程序存在未经过滤或过滤不严的文件包含漏洞,可以包含服务器上的任意文件,如include(‘/tmp/’ . $_GET[‘file’])
  2. SESSION存储路径已知或可猜:攻击者需要知道session.save_path的值。这在很多环境下是默认的(如/tmp),或可以通过信息泄露(如phpinfo())获取。
  3. 能控制SESSION文件内容:攻击者需要有能力向$_SESSION中写入可控的数据。这是整个链条中最关键、也最具技巧性的一环。控制的方式可能多样:
    • 直接赋值:极少数情况下,可能存在代码直接操作$_SESSION[‘key’] = $_GET[‘input’];且未过滤。
    • 反序列化入口:更常见的是,存在一个session反序列化漏洞。例如,PHP在读取SESSION数据时,会对其进行反序列化。如果攻击者能控制session上传进度(session.upload_progress)或通过其他方式(如php_binary格式处理差异)注入序列化数据,就可能将恶意对象注入$_SESSION。但在文件包含的语境下,我们更关注的是直接写入文件的内容,而非反序列化触发,所以通常是将恶意代码作为字符串值写入。
    • 利用SESSION初始化:在某些框架或自定义SessionHandler中,可能存在从用户输入初始化SESSION的逻辑。
  4. 能获取或预测SESSION_ID:攻击者需要知道自己的SESSION文件名,即sess_后面的部分。这通常就是当前会话的PHPSESSID,浏览器会自动携带。在攻击中,攻击者就是利用自己的会话。

当这四个条件满足时,攻击者可以:先访问一个能向$_SESSION写入数据的页面(或利用相关漏洞),将PHP代码(如``)作为值写入。然后,再访问文件包含点,尝试包含自己的SESSION文件(如/tmp/sess_abc123。如果包含成功,写入的PHP代码就会被服务器解析执行。

注意:这里有一个重要的细节。直接包含原始的SESSION文件可能会因为文件开头包含序列化格式字符(如user|s:18:“”)而导致PHP解析错误。因此,攻击时往往需要结合php://filter伪协议进行编码转换,先读取文件内容,然后解码执行。这是此类利用的一个标准技巧。

3. 实战利用:从理论到Getshell

我们通过一个高度简化的模拟场景来还原整个利用过程。假设目标环境如下:

  • PHP应用,session.save_path = “/tmp”
  • 存在一个页面write.php,不安全地将用户输入存入SESSION
  • 存在一个页面include.php,存在文件包含漏洞。

3.1 漏洞代码模拟

write.php (存在可控SESSION写入点)

<?php session_start(); // 危险操作:未经过滤直接将GET参数存入SESSION if(isset($_GET[‘data’])){ $_SESSION[‘payload’] = $_GET[‘data’]; echo “Data written to session.”; } ?>

include.php (存在文件包含漏洞)

<?php $file = $_GET[‘file’]; include($file); // 危险!未做任何过滤 ?>

3.2 分步攻击利用

第一步:注入恶意代码到SESSION文件

攻击者访问write.php,并通过参数将PHP代码写入SESSION。为了防止引号等字符破坏序列化结构,通常需要对Payload进行编码,或者确保它作为整个字符串值的一部分。一个简单的方法是使用Base64编码后再写入,包含时再解码。但更直接的方式是利用PHP的短标签和避免破坏序列化格式。

例如,访问:

http://target.com/write.php?data=<?php system(‘id’);?>

此时,攻击者浏览器中的PHPSESSID假设为abc123。那么,在服务器的/tmp目录下,就会生成一个文件sess_abc123,其内容大致为:

payload|s:23:“<?php system(‘id’);?>“;

注意,这里的;是序列化格式的一部分,不是Payload的内容。Payload字符串<?php system(‘id’);?>被完整地存储了。

第二步:利用文件包含漏洞执行SESSION中的代码

直接包含/tmp/sess_abc123会失败,因为PHP解释器会试图解析整个文件内容,开头的payload|s:23:“会导致语法错误。这时就需要php://filter伪协议出场。

攻击者构造如下请求:

http://target.com/include.php?file=php://filter/convert.base64-decode/resource=/tmp/sess_abc123

这个Payload的意图是:先读取/tmp/sess_abc123文件的内容,然后对其进行Base64解码,最后将解码后的内容传递给include。但是,我们写入的内容并不是Base64编码的,所以解码会乱,执行不会成功。这是新手常犯的错误。

正确的思路是:我们需要让SESSION文件中的某一部分在经过php://filter链式处理后,变成可执行的PHP代码。一个经典的方法是使用convert.iconv.*过滤器进行字符集转换,或者利用string.rot13过滤器。string.rot13是一个简单的编码,PHP在执行include时,会先对经过过滤器处理后的流进行解码。

更可靠的利用链如下:

  1. 写入一个经过php://filter编码的Payload到SESSION
  2. 包含时,使用对应的解码过滤器,使得最终被包含的内容是纯正的PHP代码。

例如,我们可以写入:

http://target.com/write.php?data=<?cuc flfgrz(‘vq’);?>

这是<?php system(‘id’);?>经过rot13编码后的结果。然后,攻击者访问:

http://target.com/include.php?file=php://filter/read=string.rot13/resource=/tmp/sess_abc123

php://filter会读取sess_abc123的内容,并对整个内容进行rot13解码。解码后,文件内容变成:

payload|s:23:“<?php system(‘id’);?>“;

虽然序列化格式部分payload|s:23:“和结尾的“;也被解码了(但解码后可能变成无意义的字符),但重要的是,<?php system(‘id’);?>这段代码被正确还原了。当PHP引擎解释这个文件时,它会寻找<?php ... ?>标签并执行其中的代码。序列化格式的乱码部分位于PHP标签之外,会被当作普通文本忽略(或导致一个警告,但不会阻止执行)。这样,system(‘id’)命令就被成功执行了。

在实际的CTF题目中,条件可能更苛刻。例如,write.php可能不存在,需要寻找其他控制SESSION的途径,比如:

  • session.upload_progress:这是一个PHP特性,在上传文件时,可以在$_SESSION中创建一个包含上传进度的数组。攻击者可以通过构造特殊的上传表单和POST数据,将恶意代码写入这个数组。这是此类题目非常常见的考点。
  • 反序列化漏洞触发__wakeup__destruct:如果存在反序列化点,并且可以触发魔术方法向$_SESSION写数据。

3.3 利用php://filter的链式操作

在更复杂的情况下,可能需要组合多个过滤器。例如,如果目标服务器对包含的文件后缀有检查(比如要求包含.php文件),我们可以利用php://filterconvert.base64-decodewrite=特性,先解码Payload,再将其“写入”一个虚拟的.php文件中被包含。

一个高级的Payload构造示例(假设需要绕过.php后缀限制):

include.php?file=php://filter/write=convert.base64-decode/resource=php://temp

然后,通过POST数据向这个请求体发送Base64编码后的PHP代码。write=过滤器会将解码后的内容写入resource指定的流(这里是php://temp内存流),然后include会包含这个流。由于php://temp没有后缀限制,且内容已经是解码后的纯PHP代码,从而绕过检查。但这需要能控制POST体数据,在文件包含场景中通常与php://input结合,属于另一个技巧。

4. CTF解题中的常见陷阱与绕过技巧

在CTF比赛中,这类题目不会直接给出所有条件,需要选手主动挖掘和组合。以下是一些常见的陷阱和对应的技巧:

陷阱1:SESSION路径未知

  • 技巧:尝试常见的默认路径,如/tmp,/var/lib/php/sessions,/var/tmp。利用phpinfo()信息泄露是首选。如果没有,可以尝试目录遍历漏洞配合包含,或者利用报错信息回显路径。

陷阱2:无法直接控制$_SESSION变量

  • 技巧:重点检查session.upload_progress。这是PHP的一个功能,当文件上传时,可以在$_SESSION[‘upload_progress_xxx’]中跟踪进度。通过构造一个文件上传表单,并在POST数据中插入恶意字段名,有可能将数据写入SESSION。例如,一个名为PHP_SESSION_UPLOAD_PROGRESS的字段,其值可能会被处理。这是此类题目的高频考点。
  • 技巧:寻找反序列化漏洞。全局搜索unserialize,session_start()之前的session相关操作。有时,题目会提供一个反序列化入口,反序列化后的对象会在其魔术方法(如__wakeup,__destruct)中执行$_SESSION[‘key’] = $this->data;这样的操作。

陷阱3:文件包含点有后缀限制

  • 技巧:使用php://filter时,resource=部分可以指向SESSION文件,但最终包含的是经过过滤器处理的,而不是原文件。因此,后缀限制通常对php://filter无效。如果限制是黑名单过滤了php:等字符串,可以尝试大小写、双写、添加多余字符(php://filter)等方式绕过。

陷阱4:写入SESSION的代码对特殊字符进行了过滤或转义

  • 技巧:如果过滤了<>,可以尝试使用PHP短标签<?=(需要开启short_open_tag),或者利用php://filter的编码特性,写入编码后的Payload。例如,如果代码对输入进行了htmlspecialchars转义,那么<会变成&lt;,无法形成PHP标签。这时就需要寻找其他不依赖<?php标签的执行方式,比如利用.htaccessphp_value指令(如果包含的是.htaccess文件且Apache支持),但这在SESSION包含中不常见。更可能的是,题目本意就是让你使用php://filter的编码来绕过转义。

陷阱5:SESSION文件内容包含序列化前缀导致语法错误

  • 技巧:这是此类利用的标准解法,即前面提到的,使用string.rot13convert.iconv.*过滤器。string.rot13是最常用的,因为它是一种对称编码,且PHP支持在包含时解码。构造<?cuc ... ?>的Payload写入,包含时用string.rot13解码即可。有时也会使用convert.iconv.UTF-8.UTF-7等转换,原理类似。

一个典型的CTF解题流程可能是:

  1. 信息收集:找到文件包含点,尝试读取/proc/self/environ/etc/passwd或源码,确认session.save_path
  2. 寻找SESSION写入点:检查是否有明显的$_SESSION赋值,或尝试利用session.upload_progress
  3. 构造Payload:将<?php system(‘cat /flag’);?>进行rot13编码,得到<?cuc flfgrz(‘pngt /synt’);?>
  4. 写入SESSION:通过找到的写入点(如上传表单的PHP_SESSION_UPLOAD_PROGRESS字段),将编码后的Payload写入。
  5. 包含执行:使用php://filter/read=string.rot13/resource=/tmp/sess_yoursessionid去包含,获取命令执行结果。

5. 防御之道:如何避免SESSION被包含

从开发和安全加固的角度,我们需要多层面布防,切断这个攻击链条:

1. 杜绝文件包含漏洞这是根本。永远不要将用户输入直接传递给文件包含函数(include,require,include_once,require_once)。如果必须动态包含,请使用白名单机制,只允许包含预设的几个安全文件。

2. 安全配置SESSION

  • 修改默认存储路径:不要使用/tmp这类全局可读的目录作为session.save_path。将其设置为一个仅Web服务器用户有读写权限的专用目录。
  • 使用安全的存储方式:将session.save_handler改为redismemcached,将SESSION数据存储在内存数据库中,彻底避免文件落地。这是最推荐的方案。
  • 严格设置目录权限:确保session.save_path目录的权限为700(所有者读写执行),所有者是Web服务用户(如www-data, nginx),其他用户无任何权限。

3. 对SESSION数据进行严格过滤和校验

  • 不要将任何用户可控的、未经验证和过滤的数据直接存入$_SESSION。对待$_SESSION要和对待数据库输入一样,进行严格的类型检查、长度限制和内容过滤。
  • 特别是对于从反序列化、session.upload_progress等潜在入口进入$_SESSION的数据,要有清晰的校验逻辑。

4. 关闭危险特性

  • 如果应用不需要文件上传进度跟踪功能,可以在php.ini中关闭session.upload_progress.enabled,从根本上杜绝通过此途径污染SESSION
  • 确保allow_url_include设置为Off(默认值),防止包含远程URL。

5. 使用Web应用防火墙(WAF)部署WAF规则,检测异常的文件包含请求路径,特别是包含/tmp/sess_php://filter等特征的请求。

6. 代码审计与安全意识在代码审计中,将文件包含漏洞和SESSION操作点(尤其是用户输入直接操作$_SESSION的点)作为重点检查对象。让开发团队理解,SESSION不是“安全区”,它同样需要防范注入。

这道关于包含SESSION的CTF题目,从一个精巧的角度揭示了安全风险的关联性。它告诉我们,一个普通的特性(SESSION文件存储),在遇到另一个漏洞(文件包含)时,会产生意想不到的化学反应。作为防御者,我们的思维不能是孤立的,需要建立起“攻击面关联”的意识,通过安全的默认配置、最小权限原则和输入输出的严格校验,来构建纵深防御体系,让攻击者即便找到一个突破口,也难以串联形成完整的攻击链。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询