从SESSION文件包含漏洞看PHP安全攻防:原理、利用与防御
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?filephp://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?filephp://filter/readstring.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?filephp://filter/writeconvert.base64-decode/resourcephp://temp然后通过POST数据向这个请求体发送Base64编码后的PHP代码。write过滤器会将解码后的内容写入resource指定的流这里是php://temp内存流然后include会包含这个流。由于php://temp没有后缀限制且内容已经是解码后的纯PHP代码从而绕过检查。但这需要能控制POST体数据在文件包含场景中通常与php://input结合属于另一个技巧。4. CTF解题中的常见陷阱与绕过技巧在CTF比赛中这类题目不会直接给出所有条件需要选手主动挖掘和组合。以下是一些常见的陷阱和对应的技巧陷阱1SESSION路径未知技巧尝试常见的默认路径如/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标签的执行方式比如利用.htaccess的php_value指令如果包含的是.htaccess文件且Apache支持但这在SESSION包含中不常见。更可能的是题目本意就是让你使用php://filter的编码来绕过转义。陷阱5SESSION文件内容包含序列化前缀导致语法错误技巧这是此类利用的标准解法即前面提到的使用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/readstring.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文件存储在遇到另一个漏洞文件包含时会产生意想不到的化学反应。作为防御者我们的思维不能是孤立的需要建立起“攻击面关联”的意识通过安全的默认配置、最小权限原则和输入输出的严格校验来构建纵深防御体系让攻击者即便找到一个突破口也难以串联形成完整的攻击链。

相关新闻