☰
PHP本地文件包含漏洞深度解析:从原理到WordPress插件审计实战
2026/10/11 4:26:53 网站建设 项目流程

1. 项目概述:从一次真实的插件漏洞审计说起

最近在复盘一个老项目时,重新审视了一个经典的WordPress插件本地文件包含漏洞。这个漏洞本身并不复杂,但它像一面镜子,清晰地映照出在PHP代码审计中,开发者常犯的几个关键性错误,以及安全研究者如何系统性地定位和利用这类问题。这个实验的核心,不仅仅是复现一个CVE编号,而是深入理解漏洞背后的代码逻辑、触发条件以及防御思路。无论你是刚入门代码审计的新手,还是想巩固PHP安全知识的中级开发者,通过这个深度解析,你都能获得一套可复用的审计方法论和实战技巧。

LFI,即本地文件包含,是PHP Web应用中的一个高危漏洞。它允许攻击者通过包含服务器本地的文件(如配置文件、日志文件、甚至上传的恶意文件)来读取敏感信息或执行任意代码。在WordPress庞大的插件生态中,由于开发质量参差不齐,LFI漏洞时有发生。本次我们聚焦的漏洞案例,将带你一步步拆解漏洞成因,从参数传递追踪到危险函数调用,再到如何构造利用链。我会分享我在审计过程中使用的工具链、思考路径以及那些文档里不会写的“踩坑”经验。

2. 漏洞原理与PHP包含机制深度剖析

2.1 LFI漏洞的核心:失控的文件路径

要理解LFI,必须先吃透PHP中几个关键的文件包含函数:include、require、include_once、require_once。它们的本质是将指定路径的文件内容读取并作为PHP代码执行。漏洞产生的根本原因,是开发者将用户可控的输入(如$_GET、$_POST、$_COOKIE中的参数)直接或经过不安全的处理后,传递给了这些包含函数的路径参数。

举个例子,一个危险的代码片段可能长这样:

$page = $_GET['page']; include('/pages/' . $page . '.php');

开发者本意是让用户通过?page=about来加载/pages/about.php。但如果攻击者传入?page=../../../../etc/passwd,经过拼接后,代码试图包含/pages/../../../../etc/passwd,这实际上指向了系统的/etc/passwd文件,导致敏感信息泄露。这就是最经典的目录遍历(Path Traversal)结合文件包含。

注意:很多初级开发者会认为用htmlspecialchars或addslashes处理用户输入就能防住文件包含,这是完全错误的。这些函数用于防御XSS或SQL注入,它们不改变文件路径的语义。防御LFI的关键在于对路径进行“白名单”校验或严格的“规范化”处理。

2.2 PHP伪协议:将LFI升级为RCE的利器

如果LFI只能读文件,那危害还算有限。但PHP内置的众多“伪协议”让LFI的危害性呈指数级增长,极易转化为远程代码执行。这是审计时必须重点关注的升级点。

  1. php://filter协议:用于读取文件源码。当服务器配置不允许直接读取.php文件内容时(因为会被执行),可以利用该协议进行编码转换后读取。例如:?file=php://filter/convert.base64-encode/resource=index.php这会将index.php的内容进行base64编码后输出,解码即可获得源代码。这在代码审计和信息收集中极其常用。

  2. php://input协议:这是将LFI变为RCE的“王牌”。它允许访问请求的原始数据流。当allow_url_include配置为On时(虽然默认是Off,但某些环境会开启),攻击者可以这样利用:

    GET /vuln.php?file=php://input HTTP/1.1 ... POST数据:<?php system('id'); ?>

    此时,include函数会执行POST数据流中的PHP代码。我在实际测试中,发现很多内部系统或老旧服务器仍存在此危险配置。

  3. data://协议:同样可用于代码执行。例如:?file=data://text/plain,<?php phpinfo();?>或?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+(base64编码的php代码)。

在审计WordPress插件时,如果发现存在未经验证的文件包含点,要立即检查服务器PHP环境是否支持这些危险协议,这是评估漏洞实际危害等级的关键一步。

2.3 WordPress插件环境下的特殊风险点

WordPress插件运行在WP框架内,这带来了一些独特的风险场景:

  • 绝对路径泄露:包含错误可能返回WP的绝对路径,为后续攻击提供信息。
  • 日志文件包含:通过包含WP的访问日志、错误日志,可能注入PHP代码(如果日志中记录了请求参数)。
  • 临时文件/上传文件包含:如果插件有上传功能,且上传路径可预测或可被包含,可能实现“上传图片马+包含执行”的组合攻击。
  • $_SERVER变量利用:如$_SERVER[‘DOCUMENT_ROOT’]在某些配置下可能不可靠,基于它进行路径拼接可能导致包含逃逸。

3. 靶场环境搭建与审计工具链准备

3.1 实验环境快速搭建

为了安全地复现和深入研究漏洞,我强烈建议在隔离的虚拟环境中进行。我个人的首选是Docker,它轻量、可重复、且一键清理。

  1. 使用Docker Compose部署脆弱环境: 我准备了一个docker-compose.yml文件,它包含了存在漏洞的WordPress版本和特定插件。

    version: '3.8' services: wordpress: image: wordpress:5.8-php7.4-apache # 选择一个与漏洞时间线匹配的版本 container_name: wp-lfi-lab ports: - "8080:80" environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: wpuser WORDPRESS_DB_PASSWORD: wppass WORDPRESS_DB_NAME: wpdb volumes: - ./wp-data:/var/www/html - ./vulnerable-plugin:/var/www/html/wp-content/plugins/vulnerable-plugin # 挂载存在漏洞的插件代码 depends_on: - db db: image: mysql:5.7 container_name: wp-lfi-db environment: MYSQL_DATABASE: wpdb MYSQL_USER: wpuser MYSQL_PASSWORD: wppass MYSQL_ROOT_PASSWORD: rootpass volumes: - ./db-data:/var/lib/mysql

    执行docker-compose up -d,几分钟后一个完整的WordPress就运行在http://localhost:8080了。将存在LFI漏洞的插件代码放到./vulnerable-plugin目录下,然后在WP后台启用它即可。

  2. 关键PHP配置调整:为了充分演示漏洞影响,有时需要修改Docker容器内的PHP配置(php.ini)。

    docker exec -it wp-lfi-lab bash apt-get update && apt-get install -y vim vim /usr/local/etc/php/php.ini

    找到并修改以下两项(仅用于实验,生产环境严禁!):

    allow_url_fopen = On allow_url_include = On

    修改后重启Apache服务:service apache2 restart。这为我们测试php://input和data://协议提供了条件。

3.2 代码审计工具与核心思路

工欲善其事,必先利其器。我的审计工具链分为静态分析和动态测试两部分。

静态分析(找入口、理逻辑):

  1. IDE + 全局搜索:PHPStorm或VSCode是首选。使用它们的全局搜索功能,直接搜索危险函数名:

    • include,require,include_once,require_once
    • file_get_contents,readfile,highlight_file(有时用于读取文件)
    • fopen,fread(结合用户输入也可能有问题) 搜索时,注意查看函数调用处的上下文,追踪参数来源。
  2. 专用代码审计工具:对于大型插件,可以使用RIPS(旧版开源)、Fortify或SonarQube等工具进行初步的自动化扫描,它们能快速定位可疑的代码片段。但切记,工具只是辅助,最终必须人工确认。

动态测试(验证漏洞、构造利用):

  1. 浏览器开发者工具:用于观察请求和响应,修改参数。
  2. Burp Suite / OWASP ZAP:代理工具,用于拦截、重放、篡改HTTP请求,是测试漏洞是否存在的核心。我常用Burp的Repeater模块反复调试Payload。
  3. 命令行工具Curl:用于快速测试和自动化Payload发送,特别是在测试php://input协议时非常方便。
    curl -X POST "http://localhost:8080/wp-content/plugins/vuln-plugin/page.php?file=php://input" --data "<?php echo system('whoami'); ?>"

我的核心审计思路:

  1. 入口点收集:梳理插件所有对外开放的PHP文件(通常是通过admin_menu添加的后台页面,或通过add_shortcode注册的前端短代码处理文件)。
  2. 数据流追踪:从$_GET、$_POST、$_REQUEST等超全局变量出发,看它们流向了哪里。重点关注未经过滤或过滤不严就直接用于文件操作、数据库查询、命令执行的参数。
  3. 敏感函数定位:如上所述,全局搜索文件包含、文件读取、命令执行(system,exec,shell_exec)、数据库操作($wpdb->query中直接拼接变量)等函数。
  4. 逻辑漏洞审视:检查权限验证(current_user_can)、Nonce校验(wp_verify_nonce)是否缺失或可绕过。很多插件漏洞出在“以为只有管理员能访问,实则未做校验”的地方。

4. 漏洞案例深度拆解与代码逐行分析

假设我们审计一个名为“Simple File Viewer”的插件,它提供了一个短代码[sfv_view],用于在前端显示指定文件的内容。插件核心代码文件simple-file-viewer.php如下:

<?php /** * Plugin Name: Simple File Viewer */ function sfv_shortcode_handler($atts) { // 短代码属性解析,默认file参数为'readme.txt' $atts = shortcode_atts(array( 'file' => 'readme.txt', ), $atts, 'sfv_view'); // 获取要查看的文件路径 $file_path = sanitize_text_field($atts['file']); // 错误的安全感来源! // 构建绝对路径:插件目录 + 文件路径 $base_dir = plugin_dir_path(__FILE__) . 'uploads/'; $full_path = $base_dir . $file_path; // 检查文件是否存在 if (file_exists($full_path)) { // 读取并高亮显示文件内容 highlight_file($full_path); } else { echo '<p>File not found.</p>'; } } add_shortcode('sfv_view', 'sfv_shortcode_handler'); ?>

4.1 漏洞点定位与错误过滤分析

一眼看去,开发者似乎有安全意识,他使用了WordPress的sanitize_text_field()函数来处理$atts[‘file’]输入。这正是第一个致命误区。让我们看看这个函数到底做了什么:

sanitize_text_field()函数的主要作用是:

  • 去除无效的UTF-8字符。
  • 将HTML标签<、>、&等转换为HTML实体(如<变为&lt;)。
  • 去除换行符、制表符等空白字符。
  • 去除非法的控制字符。

关键问题:它不会对文件路径遍历字符../或..\进行任何处理!../在HTML上下文中不是特殊字符,sanitize_text_field()会原样放过它。因此,$file_path变量仍然可能包含../../../etc/passwd这样的恶意路径。

4.2 路径拼接与漏洞触发

漏洞触发的关键在于第13行:$full_path = $base_dir . $file_path;。

  • $base_dir是固定的:/var/www/html/wp-content/plugins/simple-file-viewer/uploads/
  • $file_path是用户通过短代码属性控制的:[sfv_view file="../../../etc/passwd"]

拼接后的$full_path为:/var/www/html/wp-content/plugins/simple-file-viewer/uploads/../../../etc/passwd

路径中的uploads/../会相互抵消,最终file_exists()和highlight_file()函数尝试访问的是:/var/www/html/wp-content/plugins/simple-file-viewer/etc/passwd不,等等,这里还有一个../。让我们仔细计算:

  1. 原始基础路径:.../uploads/
  2. 拼接用户输入:../../../etc/passwd
  3. 规范化后:.../uploads/../->.../(抵消一级).../../->wp-content/plugins/(再抵消一级)wp-content/plugins/../->wp-content/(再抵消一级)
  4. 最终路径:/var/www/html/wp-content/etc/passwd

实际上,由于uploads目录在插件内,使用../../../可以一路回溯到网站根目录甚至系统根目录。file_exists()和highlight_file()都会接受这个路径。如果/etc/passwd文件存在且Web服务器用户有权读取,其内容就会被highlight_file()函数输出到页面上。

实操心得:在测试路径遍历时,我经常使用../../../../../../../../etc/passwd这种“超量”的../。因为不同环境的目录深度不同,多写一些可以确保能回溯到根目录。同时,Burp Suite的Intruder模块非常适合用来模糊测试../的层数。

4.3 漏洞利用实战演示

假设这个短代码被用在了一篇ID为1的文章里。攻击者访问这篇文章的URL可能是:http://target-site.com/?p=1。

利用步骤1:信息收集(读取WP配置文件)WordPress的配置文件wp-config.php位于网站根目录,包含数据库用户名、密码、密钥等核心信息。我们可以构造短代码属性来读取它。 由于短代码属性需要嵌入文章,对于攻击者,更现实的场景是找到一个已存在该短代码的页面,然后通过参数污染或直接修改请求来利用。但为了演示,我们假设攻击者能控制短代码属性。

原短代码用法:[sfv_view file="report.pdf"]恶意构造:[sfv_view file="../../../../wp-config.php"](假设插件在wp-content/plugins/下,需要向上回溯4层到根目录)

访问该页面后,wp-config.php的源码(包含数据库密码)就会以语法高亮的形式显示在网页上。

利用步骤2:尝试升级为RCE(利用PHP伪协议)如果服务器配置了allow_url_include=On,我们可以尝试利用php://input执行命令。 但注意,我们的漏洞点是highlight_file(),不是include()。highlight_file()用于显示源码,它不会执行PHP代码。所以,直接包含php://input是无效的。

然而,我们可以利用php://filter来读取服务器上其他.php文件的源码,例如插件自身的其他文件、主题的functions.php,寻找其他漏洞(如代码注入、不安全的反序列化)。或者,如果服务器开启了目录列表,或存在文件上传功能,我们可以结合LFI读取上传的恶意文件(例如一个图片Webshell)。

利用步骤3:日志文件注入(经典攻击链)这是一种更高级的利用方式,将LFI转化为RCE。前提是Web服务器错误日志或访问日志的路径已知且可读。

  1. 向网站发起一个包含PHP代码的非法请求,例如访问一个不存在的页面,并将PHP代码放在User-Agent头中:
    GET /non-existent-page.php HTTP/1.1 Host: target.com User-Agent: <?php system($_GET['cmd']); ?>
    这个请求会导致404错误,但User-Agent中的PHP代码会被记录到服务器的错误日志(如/var/log/apache2/error.log)或访问日志中。
  2. 利用LFI漏洞包含这个日志文件:[sfv_view file="../../../../var/log/apache2/error.log"]此时,日志文件被highlight_file()读取并输出。由于日志文件是纯文本,PHP代码不会被服务器执行,只是显示出来。
  3. 关键转折:如果漏洞点不是highlight_file(),而是include()或require(),那么当包含这个日志文件时,其中的<?php system($_GET[‘cmd’]); ?>就会被当作PHP代码执行,攻击者就可以通过?cmd=id来执行系统命令。

我们这个案例中漏洞函数是highlight_file(),所以无法直接执行日志中的代码。但这个思路在遇到include型LFI时极其有效,务必掌握。

5. 漏洞修复方案与安全编码实践

5.1 针对本案例的修复

修复这个漏洞,需要从输入验证和路径解析两方面入手。

方案一:白名单验证(最安全)如果插件只允许查看uploads/目录下有限的几种文件(如.pdf,.txt,.jpg),那么白名单是最佳选择。

$allowed_files = array('report.pdf', 'notice.txt', 'default.jpg'); $requested_file = sanitize_text_field($atts['file']); if (!in_array($requested_file, $allowed_files)) { echo '<p>Invalid file requested.</p>'; return; } $full_path = $base_dir . $requested_file;

方案二:严格路径规范化与目录锁定如果需求更动态,需要允许查看uploads/下的任意文件,但必须防止目录穿越。

$requested_file = sanitize_text_field($atts['file']); // 使用 basename() 函数直接去除路径信息,只保留文件名部分 $safe_file_name = basename($requested_file); // 或者,更严格地,只允许字母数字、点、下划线、连字符 if (!preg_match('/^[a-zA-Z0-9._-]+$/', $requested_file)) { wp_die('Invalid file name.'); } $full_path = $base_dir . $safe_file_name; // 额外检查:最终路径是否以 $base_dir 开头 $real_base = realpath($base_dir); $real_full = realpath($full_path); if ($real_base === false || $real_full === false || strpos($real_full, $real_base) !== 0) { wp_die('Access denied.'); }

这里realpath()函数会解析掉所有的../和符号链接,返回绝对路径。然后检查最终路径是否以允许的基目录开头,这是防御目录遍历的黄金标准。

方案三:使用WordPress内置函数如果文件是媒体库的一部分,应使用WordPress的附件ID机制和wp_get_attachment_url()、wp_get_attachment_file等函数,完全避免手动处理路径。

5.2 通用安全编码准则

  1. 永远不要相信用户输入:对所有来自客户端($_GET,$_POST,$_COOKIE,$_REQUEST,$_SERVER中部分变量)的数据进行严格的验证和过滤。验证=检查是否符合预期格式(如是否在预定义列表中),过滤=移除或转义危险字符。
  2. 使用“白名单”而非“黑名单”:定义什么是允许的,比定义什么是不允许的要安全得多。黑名单总会有遗漏。
  3. 最小权限原则:运行Web服务的用户(如www-data)应仅拥有必要文件的最小读取权限。数据库用户也应仅被授予应用所需的最小权限。
  4. 及时更新:保持WordPress核心、主题和插件更新到最新版本。已知漏洞的利用代码(Exploit)通常在新版本发布后很快就会出现。
  5. 使用安全函数:
    • 对于数据库操作,永远使用$wpdb->prepare()进行参数化查询,杜绝SQL注入。
    • 输出到HTML时,使用esc_html(),esc_attr()等函数防御XSS。
    • 执行系统命令是最后的选择,如果必须,使用escapeshellarg()或escapeshellcmd()对参数进行转义。
  6. 进行安全审计:在插件发布前,进行代码安全审计,或使用自动化工具进行扫描。对于关键业务插件,考虑聘请专业的安全人员进行渗透测试。

6. 审计实战进阶:挖掘更深层的漏洞链

一个简单的LFI可能只是冰山一角。在真实的审计中,我们需要思考如何将它与其它漏洞串联,形成更具破坏力的攻击链。

场景:LFI + 文件上传 = 远程代码执行假设另一个插件“Easy Avatar”存在任意文件上传漏洞,允许用户上传.php文件到wp-content/uploads/目录下,但上传后的文件名是随机的(如avatar_5f8a3b2c.php)。

  1. 攻击者利用文件上传漏洞,上传一个Webshell(shell.php)。
  2. 由于文件名随机,攻击者不知道完整的访问路径。
  3. 但是,如果“Simple File Viewer”插件存在LFI,并且uploads/目录是可索引的(或者存在其他信息泄露),攻击者可能通过LFI遍历uploads/目录,或者结合时间戳猜测文件名,最终找到并包含上传的Webshell,获得代码执行权限。

场景:LFI + 配置文件读取 = 数据库接管如前所述,通过LFI读取wp-config.php,直接获取数据库凭证。攻击者可以远程连接数据库,修改管理员密码、插入恶意管理员用户、甚至植入持久化后门。

场景:LFI作为信息收集的跳板通过LFI,攻击者可以读取:

  • .htaccess文件:了解服务器配置和访问规则。
  • php.ini文件:了解PHP安全配置(如allow_url_include是否开启)。
  • 其他插件的源码:寻找更多漏洞。
  • proc/self/environ文件(Linux系统):在某些特定配置下,可能包含环境变量,甚至Web进程的敏感信息。

7. 常见问题与排查技巧实录

在漏洞复现和代码审计过程中,我遇到了不少“坑”,这里分享一些典型的排查思路和解决方法。

问题1:Payload明明正确,为什么文件包含不成功?

  • 检查文件权限:Web服务器用户(如www-data)可能没有目标文件的读取权限。使用docker exec进入容器,ls -la /path/to/file查看权限。
  • 检查路径计算:../的层数可能算错了。使用绝对路径进行测试。在漏洞代码中临时添加echo $full_path; exit;来输出最终路径,确认其是否符合预期。
  • 检查PHP配置:allow_url_fopen和allow_url_include可能为Off,导致伪协议无法使用。通过创建一个phpinfo.php文件来查看配置。
  • 检查函数限制:目标可能使用的是file_get_contents()而不是include()。前者读取文件内容但不会执行PHP代码,对于包含php://input执行代码是无效的。

问题2:如何快速判断一个文件包含点是否可利用?我通常会用一个简单的测试流程:

  1. 基础遍历测试:../../../../etc/passwd。如果成功,证明存在目录遍历。
  2. PHP Filter协议测试:php://filter/convert.base64-encode/resource=index.php。如果返回base64编码,证明协议可用,且可以读取源码。
  3. PHP Input协议测试:使用Burp或Curl发送POST请求,Body中包含<?php echo ‘test’;?>。如果页面上输出test,则存在RCE风险。注意:此测试可能产生实际影响,务必在授权环境下进行。
  4. 日志文件包含测试:尝试包含已知的日志路径,如/var/log/apache2/access.log。

问题3:在代码审计中,除了include/require,还有哪些函数需要警惕?一个更广泛的“危险函数”列表:

  • 文件系统类:file_get_contents(),readfile(),fopen()/fread()/fwrite(),file(),copy(),rename(),unlink()(删除文件)。
  • 命令执行类:system(),exec(),shell_exec(),passthru(),popen(), 反引号` `。
  • 代码执行类:eval(),assert(),create_function()(已弃用),preg_replace()的/e修饰符(已弃用)。
  • 反序列化:unserialize(),如果参数可控,可能导致对象注入漏洞。
  • 动态函数/变量:$func()(变量函数),$$var(可变变量),如果用户可控制函数名或变量名,可能导致意外行为。

问题4:WordPress插件审计有哪些特别的关注点?

  • Admin AJAX:插件通过wp_ajax_和wp_ajax_nopriv_钩子暴露的端点。这些端点可能缺乏权限校验(capability和nonce)。
  • Admin Post:类似Admin AJAX,通过admin_post_钩子。
  • REST API:插件注册的自定义REST端点,检查权限回调(permission_callback)是否设置正确。
  • 短代码:短代码处理函数是否对属性进行了充分验证。
  • 设置页面:插件在后台保存设置时,是否对所有选项进行了安全校验。
  • 数据库操作:是否使用$wpdb->prepare()。
  • 文件操作:是否使用wp_handle_upload()等安全函数处理上传,是否对文件路径进行了校验。

问题5:遇到混淆或加密的代码怎么办?有些恶意插件或后门会使用base64_encode、gzinflate、eval或自定义加密来隐藏代码。对于这种情况:

  1. 搜索eval(、base64_decode(、gzinflate(等关键字。
  2. 将可疑的加密字符串提取出来,在隔离环境中手动解码执行,观察输出。可以使用在线的PHP代码沙箱,但务必注意安全。
  3. 使用专业的代码审计工具或IDE的调试功能,动态跟踪变量值。

最后,保持耐心和细心是代码审计最重要的品质。每一个漏洞的发现,都源于对异常数据流的执着追踪和对“用户输入”的绝对怀疑。这份对LFI漏洞的深度解析,希望能为你打开PHP代码安全世界的一扇门。真正的安全,始于每一行严谨的代码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询