CTF实战:文件协议利用与SSTI注入的完整攻击链剖析
1. 项目概述从解题到实战的思维跃迁最近在NewStarCTF 2025的Week3赛题里泡了几天发现这周的Web题目设计得相当有意思它没有停留在简单的“点按钮拿flag”层面而是把几个看似基础但实际渗透中经常遇到的技术点串了起来从最基础的文件协议利用一路升级到SSTI服务器端模板注入的实战对抗。很多刚接触CTF或者Web安全的朋友可能会觉得每个知识点单独看教程都能懂但一到比赛里面对一个完整的、没有提示的黑盒环境就不知道从哪里下手了。这篇内容我就以Week3的题目为蓝本拆解一下从信息搜集、漏洞发现到利用链构造的完整实战思路重点聊聊那些教程里不会写的“解题直觉”和“踩坑实录”。简单来说这次通关的核心路径围绕着两个关键技术展开文件协议File Protocol的非常规利用和SSTI的检测与利用。这不仅仅是两道题更代表了两类常见的攻击面前者关乎于如何绕过前端的路径限制直接与本地或服务器文件系统交互后者则是服务端逻辑漏洞的典型考验我们对后端模板引擎的理解和命令执行链的构造能力。无论你是想刷题提升排名还是希望将这些技巧应用到实际的渗透测试或代码审计中这套从浅入深的分析都能给你提供直接的参考。2. 核心思路拆解为什么是文件协议和SSTI在深入操作之前我们得先弄明白出题人把这两个点放在一起的意图是什么。这有助于我们建立一套应对类似场景的通用分析方法。2.1 文件协议题目的设计意图与常见陷阱文件协议file://在Web安全题目中通常不是考察你对协议本身有多了解而是考察你对浏览器同源策略SOP的绕过以及对应用程序路径解析逻辑的洞察。题目可能会在前端通过JavaScript对输入进行过滤限制你使用http://或https://但如果你尝试file://它可能就能绕过前端的检查被浏览器或后端某些组件以不同方式处理。这里的关键在于理解“上下文”。在浏览器中file://协议打开的页面其“源”Origin是本地文件系统。这可能导致一些有趣的行为绕过前端Ajax请求的域限制如果一个页面通过file://打开那么运行在该页面中的JavaScript向本地文件系统发起XMLHttpRequest或fetch请求时通常不会受到跨域限制尽管现代浏览器对此有更严格的策略。题目可能利用这一点让你通过file://协议去读取服务器上一个本应通过HTTP服务才能访问的敏感文件。路径遍历Path Traversal的另一种形式我们熟知的../遍历通常发生在HTTP请求参数中。而file://协议直接暴露了文件系统路径。题目可能给你一个输入框让你输入一个URL后端可能会使用类似curl、file_get_contents()等函数去获取内容。如果你输入file:///etc/passwd并且后端没有正确校验协议头就可能造成本地文件读取。Week3的题目很可能结合了前端的协议白名单过滤和后端的不安全文件操作函数。注意现代浏览器出于安全考虑对file://协议下页面发起的请求限制越来越多。例如默认情况下file://页面中的JavaScript不能向非file://的源发起请求。解题时往往需要结合题目描述、页面错误信息或特殊的浏览器配置如启动参数来模拟一个更宽松的环境。这就是为什么有时候题目会提示“请使用某某浏览器”或给出一个特定的“靶机”环境。2.2 SSTI漏洞的成因与在CTF中的常见形态SSTI服务器端模板注入本质上是将用户输入直接拼接到了后端模板引擎的渲染语句中。比如一个Python Flask应用正常的渲染是render_template(index.html, nameusername)这里的username是变量。但如果代码写成了render_template_string(username)而username用户可控那么用户输入{{7*7}}返回页面显示49就说明存在SSTI。在CTF中SSTI题目有几个特点黑盒探测你首先得判断存在哪个模板引擎Jinja2, Twig, Smarty, FreeMarker等。常用的探测payload如{{7*7}}、%7*7%、${7*7}等观察返回结果是否执行了计算。沙箱逃逸与命令执行单纯的计算证明漏洞存在但目标是执行系统命令如cat /flag或读取文件。这就需要我们掌握对应模板引擎的“内置函数”或“魔术方法”。例如在Jinja2中可以通过{{config}}、{{self}}或利用Python的类继承关系如.__class__.__mro__[2].__subclasses__()来一步步找到可以执行命令的类如os.popen、subprocess.Popen。过滤与绕过出题人不会让你直接使用os.system。他们会过滤关键词如os、import、eval、[、]、.等。这就需要我们掌握各种绕过技巧比如使用字符串拼接os、Unicode编码、十六进制编码、利用attr()过滤器、或通过request对象的内置属性来间接调用。Week3的SSTI题目很可能设置了一定的过滤规则需要我们构造一个不包含明显敏感字符的payload来完成攻击。这要求我们对模板引擎的语法和上下文环境有更深的理解。3. 通关实战详解从信息收集到Payload构造下面我将模拟Week3的解题流程把两个核心考点串联起来。请注意以下步骤是基于常见CTF题型和最佳实践的推演具体题目参数可能需要调整。3.1 第一阶段文件协议利用与初步信息泄露假设我们拿到的第一个题目是一个简单的“文件查看器”或“URL预览”功能。页面有一个输入框让你输入一个URL然后它会显示该URL的内容。步骤1基础功能测试首先输入一个正常的公网URL比如https://httpbin.org/get查看功能是否正常观察返回数据的格式是直接显示HTML还是只显示文本内容。同时查看网页源代码看是否有前端JavaScript对输入进行过滤。步骤2尝试文件协议输入file:///etc/passwd。这里可能出现几种情况直接成功页面显示了/etc/passwd的内容。这说明后端可能直接使用了file_get_contents($_POST[url])这类函数且没有做任何协议限制。这是最理想的情况。被前端拦截输入后页面没反应或者控制台出现JavaScript错误提示“不支持的协议”。这时需要打开浏览器开发者工具F12查看Network面板看请求是否真的发出了。也可能过滤是在前端我们可以尝试禁用JavaScript或者使用Burp Suite拦截修改请求包直接修改POST数据中的URL字段。返回特定错误比如显示“只能使用HTTP/HTTPS协议”。这说明后端有简单的协议检查。我们可以尝试协议混淆例如File:///etc/passwd(大小写)file:/etc/passwd(少一个斜杠)file://localhost/etc/passwd甚至利用URL解析特性如http://127.0.0.1file/etc/passwd这种通常不行但值得一试。步骤3利用信息泄露获取源码假设我们通过file://协议成功读取到了/etc/passwd确认了漏洞。下一步通常是寻找Web应用的源码。常见的路径有/var/www/html/index.php/app/app.py/usr/local/tomcat/webapps/ROOT/WEB-INF/web.xml通过读取/proc/self/cwd/app.py来获取当前进程的工作目录下的文件Linux特性。在CTF中flag可能直接放在Web根目录也可能需要通过源码分析才能找到下一步的线索。例如我们读取到了index.php发现其中包含了一段可疑的代码它接收一个参数template并直接使用render_template_string函数。这就引出了下一关。实操心得使用file://协议时路径的写法很重要。在Linux下绝对路径是file:///etc/passwd三个斜杠。在Windows下可能是file:///C:/Windows/win.ini。如果题目环境是Docker基础镜像不同常见配置文件路径也不同可以尝试读取/proc/version来确定系统信息。3.2 第二阶段SSTI漏洞的探测与利用链构造现在我们拿到了一个疑似存在SSTI的端点例如/render?templateuser_input。步骤1探测模板引擎发送一系列测试payload观察响应{{7*7}}- 返回49 (Jinja2/Twig)%7*7%- 返回49 (ERB, 旧版JSP)${7*7}- 返回49 (FreeMarker, Thymeleaf)${{7*7}}- 返回49 (Spring EL表达式){{7*7}}- 返回7777777 (Jinja2 字符串乘法)假设我们输入{{7*7}}返回了49基本确定是Jinja2引擎。步骤2探索内置对象与上下文尝试获取一些内置对象来了解环境{{config}} 查看Flask应用配置可能泄露SECRET_KEY等。{{self}} 在Jinja2中指向当前模板对象。{{request}} 获取请求对象在Flask中{{request.application.__globals__}}可能包含所有全局变量。如果这些关键词被过滤我们可以尝试更基础的方法{{.__class__}} 获取空字符串的类即class str。{{.__class__.__mro__}} 查看方法解析顺序会显示继承链通常是(class str, class object)。{{.__class__.__mro__[1].__subclasses__()}} 获取object类的所有子类。这是一个巨大的列表包含了当前Python环境中加载的所有类。步骤3构造命令执行Payload我们的目标是从子类列表中找到一个可以执行命令的类比如subprocess.Popen。我们需要知道它在列表中的索引。首先将子类列表输出。由于列表很长直接渲染可能被截断或导致错误。我们可以用循环或切片来查看。例如先输出长度{{.__class__.__mro__[1].__subclasses__().__len__()}}。然后写一个简单的Python脚本在本地模拟类似环境或者通过SSTI一点点遍历。一个更聪明的方法是搜索特定类名。但由于.和[可能被过滤我们需要使用Jinja2的过滤器。使用attr()过滤器来替代.{{.__class__.__mro__[1].__subclasses__() | selectattr(__name__, equalto, Popen) | list }}。但selectattr需要Jinja2版本支持且Popen可能不在直接子类中。更通用的方法是暴力遍历索引。我们可以构造一个循环但Jinja2的循环语法{% for %}可能在模板注入的上下文中无法使用。通常我们采用“猜索引”的方式。先找一些常见的危险类如os._wrap_close、subprocess.Popen、warnings.catch_warnings等。我们可以通过{{.__class__.__mro__[1].__subclasses__()[索引]}}来查看。假设我们通过本地测试或信息泄露得知subprocess.Popen在索引128的位置。构造执行命令的payload原始思路{{.__class__.__mro__[1].__subclasses__()[128](cat /flag, shellTrue, stdout-1).communicate()[0]}}但题目往往有过滤假设过滤了os、import、[、]、、等字符。步骤4绕过过滤技巧过滤中括号[] 可以使用.__getitem__()方法。例如[128]替换为.__getitem__(128)。过滤数字 数字可以用算术运算生成。128可以写成(10028)或(2**7)。过滤引号 字符串可以通过拼接、chr函数或利用request对象。拼接(ca t)。但需要引号。chr()函数(chr(99)chr(97)chr(116))得到cat。这需要我们能调用chr。利用request对象这是非常实用的技巧。在Flask中request对象是全局可用的。我们可以通过request.args或request.values从GET/POST参数中获取字符串从而绕过对引号的过滤。例如Payload:{{.__class__.__mro__[1].__subclasses__().__getitem__(128)(request.args.cmd, shellTrue, stdout-1).communicate()[0]}}同时在URL中传递参数cmdcat /flag过滤点号. 使用中括号[]和字符串形式或者使用attr()过滤器。例如a.b可以写成a[b]或a|attr(b)。综合绕过示例假设过滤了[、]、引号、os、import。最终Payload可能长这样使用attr和request.args{{()|attr(__class__)|attr(__mro__)|attr(__getitem__)(1)|attr(__subclasses__)()|attr(__getitem__)(128)(request.args.cmd, shellTrue, stdout-1)|attr(communicate)()|attr(__getitem__)(0)}}访问URL/render?template上述payloadcmdcat /flag步骤5获取Flag成功执行命令后回显的内容就会包含Flag。如果命令执行无回显可以尝试将输出重定向到Web目录下的一个文件然后通过Web访问该文件或者使用DNS外带、HTTP请求外带等方式。4. 常见问题与排查技巧实录在实际操作中尤其是比赛环境下几乎不会一帆风顺。下面记录几个我踩过的坑和解决方法。4.1 文件协议读取失败的可能原因问题现象可能原因排查与解决思路输入file:///etc/passwd后页面空白或报错1. 后端PHP配置中allow_url_fopen为Off。2. 后端使用了curl但未支持file协议。3. 前端AJAX请求因同源策略被浏览器阻止。1. 尝试读取其他已知存在的文件如/proc/self/environLinux或C:\Windows\win.iniWindows确认协议是否支持。2. 使用Burp Suite抓包确认请求是否成功发出查看后端返回的具体错误信息。3. 如果是前端限制尝试修改请求包或寻找其他未过滤的输入点如Header、Cookie。返回“禁止访问本地文件”后端代码明确检查了协议或主机名。1. 尝试使用file://localhost/etc/passwd。2. 尝试使用file://127.0.0.1/etc/passwd。3. 考虑是否可以利用php://、phar://等伪协议进行绕过。读取到的文件内容被截断或编码混乱后端或浏览器对内容进行了处理如HTML实体编码、截断非文本内容。1. 查看网页源代码CtrlU看原始内容是否完整。2. 尝试读取一个纯文本小文件测试。3. 如果目标是二进制文件如图片、压缩包考虑使用php://filter/readconvert.base64-encode/resource/etc/passwd这样的过滤器将文件内容以Base64编码形式读出然后解码。4.2 SSTI利用过程中的典型障碍问题现象可能原因排查与解决思路输入{{7*7}}等测试payload无反应原样输出。1. 不存在SSTI漏洞。2. 输入点不在模板渲染上下文中如可能在注释、静态文本里。3. 模板引擎不是常见类型。1. 尝试所有常见的模板语法进行探测。2. 仔细分析已获取的源码确认用户输入传递到了哪个模板函数。3. 尝试使用数学运算{{7*7}}看是否返回7777777这是Jinja2的特性。能执行{{7*7}}但执行{{config}}或访问类属性时报错如500错误。1. 沙箱环境限制了内置对象的访问。2. 某些属性或方法被从上下文中移除。1. 从最基础的{{.__class__}}开始尝试一步步向上探索。2. 尝试使用{{request}}它通常可用。3. 关注报错信息可能泄露环境信息如Python路径、Flask版本。构造的命令执行payload不生效无回显。1. 命令执行函数如Popen的索引找错。2. 命令语法错误或路径不对。3. 存在输出重定向或管道错误。4. 沙箱禁用了相关模块。1.验证索引先尝试调用一个无害的方法如{{.__class__.__mro__[1].__subclasses__()[假设索引].__name__}}看返回的是不是Popen。2.简化命令先执行whoami或id这样简单且必有回显的命令。3.检查回显使用.communicate()[0]获取标准输出.communicate()[1]获取标准错误。如果都没内容尝试stdout参数设为-1管道或一个文件描述符。4.尝试其他类如果Popen不行尝试寻找os._wrap_close、warnings.catch_warnings等类它们内部也可能引用os模块。关键词被过滤payload无法提交。WAF或代码层面对输入进行了正则匹配过滤。1.大小写绕过Os、PoPeN。2.双写绕过ooss、imimportport如果过滤是简单的替换为空。3.编码绕过\u006f\u0073(Unicode),%6f%73(URL编码)但要注意后端是否解码。4.利用字符串特性os、os4.3 关于“there was an error running the web service on the debug server”的联想在搜索相关热词时我注意到一个错误信息片段“there was an error running the web service on the debug server: error -67015”。这看起来像是某个特定IDE或框架如LabVIEW的调试服务器错误。虽然与本题核心SSTI无关但这种错误信息泄露本身就是一种安全隐患。在CTF或真实渗透中任何非标准的错误信息都可能暴露后端技术栈如Python/Flask, Node.js/Express, Java/Spring甚至版本号。这为我们后续的漏洞利用比如搜索该版本已知的公开漏洞提供了宝贵信息。因此养成习惯仔细阅读每一个错误回显哪怕是看起来无关紧要的。5. 工具与技巧提升解题效率手动构造复杂的SSTI payload非常耗时且容易出错。以下工具和技巧可以极大提升效率Tplmap/Ruby版一个自动化的SSTI检测与利用工具支持多种模板引擎。它可以自动检测引擎类型并尝试获取操作系统shell。在CTF中如果规则允许可以尝试使用。命令类似python tplmap.py -u http://target/render?templatetestBurp Suite Intruder当需要暴力猜测子类索引时用Intruder的Sniper模式将索引位置设为变量用数字序列进行爆破观察响应长度或内容的变化找到正确的索引。本地Python环境模拟在本地创建一个简单的Flask应用模拟题目环境用于安全地测试和调试payload。这是最可靠的方法。from flask import Flask, request, render_template_string app Flask(__name__) app.route(/test) def test(): template request.args.get(t, ) # 可以在这里模拟过滤规则 # filtered template.replace(os, ).replace(import, ) try: return render_template_string(template) except Exception as e: return str(e) if __name__ __main__: app.run(debugTrue)浏览器开发者工具控制台对于需要复杂字符串构造的payload可以现在浏览器的Console里用JavaScript写好拼接逻辑生成最终的payload字符串再复制到攻击请求中避免手动拼接出错。6. 防御视角从攻击中学习安全编码作为开发者如何避免自己的应用出现这类问题针对文件协议/任意文件读取白名单校验如果功能是获取远程URL内容应严格校验协议只允许http://和https://。使用安全的库避免直接使用file_get_contents($url)、curl_exec()等函数处理用户输入的URL。如果必须使用应使用parse_url()函数解析URL并检查scheme和host。设置PHP配置在生产环境中将allow_url_fopen和allow_url_include设置为Off。运行在最小权限环境Web服务进程应以低权限用户运行限制其可访问的文件系统范围。针对SSTI永远不要信任用户输入这是铁律。绝对不要将用户输入直接传递给模板渲染函数如render_template_string。使用安全的渲染方式始终使用render_template(template.html, **context)的方式将用户输入作为数据变量值传递给模板而不是作为模板结构的一部分。沙箱化模板环境如果业务确实需要动态模板如CMS的主题功能应使用严格的沙箱环境移除或重命名危险的全局函数和类。代码审计与自动化扫描在代码中搜索render_template_string、eval、exec等危险函数并检查其参数是否用户可控。通关NewStarCTF Week3的Web题目与其说是解开了几道题不如说是完成了一次小型攻防演练。从最外围的文件协议试探到深入核心的SSTI代码执行这条路径清晰地展示了攻击者如何利用应用逻辑的疏漏一步步扩大战果。真正掌握这些技巧不在于记住几个payload而在于理解其背后的原理协议处理、上下文渲染、对象继承链、过滤与绕过的博弈。把这些思路带入平时的代码审计和渗透测试中你会发现很多看似坚固的系统其实都存在着类似逻辑缺陷的“缝隙”。

相关新闻