本文约 6,098 字,预计阅读 16 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:一次随意的探测,就能拿到完整信息
黑客对一个 WordPress 站点的第一分钟通常做的事:
GET /wp-content/uploads/ → 看有没有目录列表
GET /readme.html → 看 WordPress 版本
GET /wp-config.php → 试能不能直接下载
GET /.git/config → 看有没有源码仓库
GET /wp-content/debug.log → 看有没有调试日志(含绝对路径)
GET /wp-content/plugins/ → 列出所有插件,找已知漏洞
这些请求全部依赖服务器的默认行为。默认配置的 Nginx/Apache 可能允许目录列表,而很多站点把 .git 目录整个传到了线上。
堵住这些口子不需要装任何插件,几行服务器配置就够。
原理分析:泄露是怎么发生的
目录列表:Web 服务器收到一个目录路径请求(如 /wp-content/uploads/),如果目录里没有 index.html / index.php,且开启了 autoindex,就会生成一个文件列表页面返回。
WordPress 在每个目录都放了空的 index.php 来防止这件事(这是 WordPress 的默认设计),但 uploads 目录没有——所以 /wp-content/uploads/ 是最常被攻破的点。
敏感文件直接下载:wp-config.php 是 PHP 文件,正常情况下 Web 服务器会交给 PHP 解析器执行,执行结果是空(因为它只定义常量)。但如果 PHP 解析失效(比如 PHP-FPM 挂了、配置错误),Nginx 会把 wp-config.php 当静态文件返回——数据库账号密码直接泄露。
.git 目录:用 Git 部署的站点,如果 .git 在 Web 目录下,攻击者可以用它还原完整源码,包括配置文件里的密钥。
debug.log:开了 WP_DEBUG_LOG 后,日志在 wp-content/debug.log,里面包含完整的服务器绝对路径、数据库错误、插件路径。任何人访问 /wp-content/debug.log 都能看到。
保姆级实操
第一步:先自查有没有泄露
curl -I https://example.com/wp-content/uploads/
curl -I https://example.com/wp-content/plugins/
curl -I https://example.com/readme.html
curl -I https://example.com/.git/config
curl -I https://example.com/wp-content/debug.log
curl -s https://example.com/wp-config.php | head -5
- 目录返回 200 且 body 是文件列表 → 目录列表开着;
.git/config返回 200 → 严重泄露;wp-config.php返回内容里有DB_PASSWORD→ 重大事故,立刻改密码。
用脚本批量检查:
#!/bin/bash
DOMAIN="https://example.com"
for p in /wp-content/uploads/ /wp-content/plugins/ /wp-content/themes/ /readme.html \
/license.txt /wp-config-sample.php /.git/config /.env /wp-content/debug.log \
/.htaccess /.user.ini /xmlrpc.php; do
code=$(curl -s -o /dev/null -w "%{http_code}" "$DOMAIN$p")
printf "%-32s %s\n" "$p" "$code"
done
第二步:Nginx 防护规则(完整版)
放 server 块里:
# 1. 关闭目录列表
autoindex off;
# 2. 敏感文件直接拒绝
location ~* /(wp-config\.php|wp-config-sample\.php|readme\.html|readme\.txt|license\.txt|install\.php) {
deny all;
access_log off;
log_not_found off;
}
# 3. 隐藏文件(.git/.env/.svn 等)
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# 4. 备份/日志文件
location ~* \.(bak|old|backup|swp|swo|sql|log|tar|tar\.gz|zip|rar)$ {
deny all;
access_log off;
log_not_found off;
}
# 5. uploads 目录禁止 PHP 执行
location ~* ^/wp-content/uploads/.*\.(php|php5|php7|phtml|phar)$ {
deny all;
access_log off;
}
# 6. wp-includes 目录
location ~* ^/wp-includes/[^/]+\.php$ {
deny all;
}
# 7. 只允许特定 PHP 文件被访问(最严格,适合不需要额外 PHP 入口的站点)
# location ~ \.php$ {
# include fastcgi_params;
# fastcgi_pass unix:/run/php/php8.2-fpm.sock;
# }
# location ~* ^/(?!index\.php|wp-admin|wp-login\.php|wp-cron\.php).*\.php$ {
# deny all;
# }
第三步:Apache 防护(.htaccess)
站点根目录 .htaccess 顶部:
# 关闭目录列表
Options -Indexes
# 保护 wp-config.php
<Files wp-config.php>
Require all denied
</Files>
# 保护 .htaccess 自身与其他隐藏文件
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# 保护 readme / license
<FilesMatch "^(readme\.html|readme\.txt|license\.txt|install\.php)$">
Require all denied
</FilesMatch>
# 保护备份和日志
<FilesMatch "\.(bak|old|sql|log|swp|tar\.gz)$">
Require all denied
</FilesMatch>
wp-content/uploads/.htaccess(禁止 PHP 执行):
<FilesMatch "\.(php|php5|phtml|phar)$">
Require all denied
</FilesMatch>
php_flag engine off
第四步:给每个目录补 index.php
WordPress 默认已在 wp-content、plugins、themes 里放了空 index.php。检查并补齐:
for d in wp-content wp-content/uploads wp-content/plugins wp-content/themes wp-content/upgrade; do
[ -f "/var/www/site/$d/index.php" ] || echo "<?php // silence" > "/var/www/site/$d/index.php"
done
更彻底的做法是放一个 index.html:
echo '<!DOCTYPE html><title></title>' > /var/www/site/wp-content/uploads/index.html
第五步:删除不需要的暴露文件
cd /var/www/site
rm -f readme.html license.txt wp-config-sample.php
rm -rf .git
rm -f wp-content/debug.log
rm -f *.sql *.bak *.old
不要把 .git 传到线上。如果用 Git 部署,正确做法是:
# 部署时用 rsync 排除 .git
rsync -avP --exclude='.git' --exclude='wp-config.php' ./ user@server:/var/www/site/
# 或者用 git archive 导出
git archive --format=tar HEAD | tar -x -C /tmp/deploy
第六步:debug.log 的长期处理
要么不开 WP_DEBUG_LOG(生产环境推荐),要么把日志写到 Web 目录外:
define( 'WP_DEBUG_LOG', '/var/log/wordpress/debug.log' );
确保目录存在且可写:
mkdir -p /var/log/wordpress && chown www-data:www-data /var/log/wordpress
第七步:phpinfo 与其他探测文件
find /var/www/site -name "phpinfo*.php" -o -name "info.php" -o -name "test.php" -o -name "*.sql"
调试完立刻删除这些文件。phpinfo() 会暴露完整的服务器配置、路径、扩展信息。
第八步:HTTP 响应头加固
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
nosniff 能防止浏览器把 .txt 当 HTML 解析(避免 XSS)。
第九步:验证
nginx -t && systemctl reload nginx
# 再跑一遍自查脚本,敏感路径应全部 403
常见踩坑与失败案例
- 案例一:某站用 Git 部署,
.git目录在线上暴露了半年,攻击者用它还原了完整源码,包括wp-config.php里的数据库凭证(虽然当前已改)。用curl -I /.git/config自查是必做项。 - 案例二:开了
WP_DEBUG_LOG忘了关,debug.log涨到 2GB 占满磁盘,站点 500。而且任何人都能下载它看到绝对路径。 location ~ /\.误伤:这条规则会挡掉所有以点开头的文件,包括/.well-known/acme-challenge/(Let’s Encrypt 验证路径),导致证书续期失败。要加例外:
nginx
location ^~ /.well-known/ {
allow all;
default_type "text/plain";
try_files $uri =404;
}
- 案例三:配了
location ~* /(wp-config\.php)但站点在子目录(/blog/wp-config.php),规则不匹配。正则要写成wp-config\.php$并考虑路径。 - Nginx 规则顺序:
location ~ /\.如果写在location ~ \.php$之前,/.php类请求会先被它匹配。正则 location 按配置文件中的出现顺序匹配,第一条命中即用。 Options -Indexes在 Nginx 上无效:这是 Apache 指令。Nginx 用autoindex off。- 对象存储上的目录列表:OSS/COS 默认也可能列出文件,要在云控制台关闭”静态网站的目录列表”或直接不开启子目录列举。
总结
这几项防护不需要任何插件,纯服务器配置就能完成,但覆盖的是最容易被利用的信息泄露口子:目录列表、wp-config.php、.git、备份文件、debug.log、phpinfo。操作顺序是:先用 curl 脚本自查 → 关 autoindex → 加敏感文件 deny 规则 → 删除 readme/license/.git/phpinfo → 把 debug 日志移到 Web 目录外 → 补 index.php/index.html → nginx -t 验证。两个易错点:.well-known 路径要放行(否则证书续期失败),正则 location 的顺序会影响匹配。另外部署时用 rsync --exclude='.git' 或 git archive,从源头避免源码泄露。
