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_id是abc123,那么对应的SESSION文件就是/tmp/sess_abc123(假设session.save_path为/tmp)。
这个文件的内容是经过序列化的字符串。默认的序列化处理器(session.serialize_handler)通常是php或php_binary。例如,当我们设置$_SESSION[‘user’] = ‘admin’;后,文件内容可能就是user|s:5:“admin”;这样的格式。这里的关键在于,$_SESSION数组中的键和值,都会以明文形式出现在这个文件里。
2.2 漏洞形成的核心链条
文件包含漏洞(如include($_GET[‘file’]))与SESSION文件的交叉,形成了如下的攻击链条:
- 存在文件包含点:应用程序存在未经过滤或过滤不严的文件包含漏洞,可以包含服务器上的任意文件,如
include(‘/tmp/’ . $_GET[‘file’])。 - SESSION存储路径已知或可猜:攻击者需要知道
session.save_path的值。这在很多环境下是默认的(如/tmp),或可以通过信息泄露(如phpinfo())获取。 - 能控制SESSION文件内容:攻击者需要有能力向
$_SESSION中写入可控的数据。这是整个链条中最关键、也最具技巧性的一环。控制的方式可能多样:- 直接赋值:极少数情况下,可能存在代码直接操作
$_SESSION[‘key’] = $_GET[‘input’];且未过滤。 - 反序列化入口:更常见的是,存在一个
session反序列化漏洞。例如,PHP在读取SESSION数据时,会对其进行反序列化。如果攻击者能控制session上传进度(session.upload_progress)或通过其他方式(如php_binary格式处理差异)注入序列化数据,就可能将恶意对象注入$_SESSION。但在文件包含的语境下,我们更关注的是直接写入文件的内容,而非反序列化触发,所以通常是将恶意代码作为字符串值写入。 - 利用SESSION初始化:在某些框架或自定义
SessionHandler中,可能存在从用户输入初始化SESSION的逻辑。
- 直接赋值:极少数情况下,可能存在代码直接操作
- 能获取或预测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时,会先对经过过滤器处理后的流进行解码。
更可靠的利用链如下:
- 写入一个经过
php://filter编码的Payload到SESSION。 - 包含时,使用对应的解码过滤器,使得最终被包含的内容是纯正的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_abc123php://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://filter的convert.base64-decode和write=特性,先解码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转义,那么<会变成<,无法形成PHP标签。这时就需要寻找其他不依赖<?php标签的执行方式,比如利用.htaccess的php_value指令(如果包含的是.htaccess文件且Apache支持),但这在SESSION包含中不常见。更可能的是,题目本意就是让你使用php://filter的编码来绕过转义。
陷阱5:SESSION文件内容包含序列化前缀导致语法错误
- 技巧:这是此类利用的标准解法,即前面提到的,使用
string.rot13或convert.iconv.*过滤器。string.rot13是最常用的,因为它是一种对称编码,且PHP支持在包含时解码。构造<?cuc ... ?>的Payload写入,包含时用string.rot13解码即可。有时也会使用convert.iconv.UTF-8.UTF-7等转换,原理类似。
一个典型的CTF解题流程可能是:
- 信息收集:找到文件包含点,尝试读取
/proc/self/environ、/etc/passwd或源码,确认session.save_path。 - 寻找SESSION写入点:检查是否有明显的
$_SESSION赋值,或尝试利用session.upload_progress。 - 构造Payload:将
<?php system(‘cat /flag’);?>进行rot13编码,得到<?cuc flfgrz(‘pngt /synt’);?>。 - 写入SESSION:通过找到的写入点(如上传表单的
PHP_SESSION_UPLOAD_PROGRESS字段),将编码后的Payload写入。 - 包含执行:使用
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改为redis或memcached,将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文件存储),在遇到另一个漏洞(文件包含)时,会产生意想不到的化学反应。作为防御者,我们的思维不能是孤立的,需要建立起“攻击面关联”的意识,通过安全的默认配置、最小权限原则和输入输出的严格校验,来构建纵深防御体系,让攻击者即便找到一个突破口,也难以串联形成完整的攻击链。