feat: add host-sec-check

This commit is contained in:
2026-09-04 11:54:07 +08:00
parent ef22c6829e
commit b28acc66d8
9 changed files with 539 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
# host-sec-check
对一台 Linux/VPS 做入侵取向的安全检查:先取证,再给你一份结论和处置清单。默认不改机器。
## 什么时候用
- 「这台机做一次安全检查」
- 「好像被黑了 / 有矿机 / 服务无故 OOM」
- 「看看哪些端口对公网开着」
- 检查另一台机器时,直接点名主机:`对 vvps4 跑 /host-sec-check`
适用于单台 Linux(systemd + 常见 Docker/k3s)。不是应用代码审计,也不是云账号合规审计。
## 使用前准备
- SSH 能上(别名或 `user@host`)
- 尽量有 sudo 或 docker 组(没有也能做浅查)
- 告诉 Agent 哪张 IP 是公网,方便从外面探测
- 若要清理、关端口、重启:明确说「同意处置」
## 使用示例
```text
对 ace@vvps2 做 host-sec-check,只出报告先不要改。
```
```text
用 host-sec-check 查 vora3,公网 IP 是 x.x.x.x,发现未授权的就列处置项问我。
```
## Agent 会做什么
1. 采集账号、监听端口、cron/systemd、Docker/k3s、`ld.so.preload`、内存/hugepage。
2. 怀疑进程隐藏时,用容器读宿主机,避免被矿机库骗过。
3. 从另一台机器探测公网端口(含 IPv6,如果有)。
4. 给出结论:是否沦陷、入口、对外端口、建议处置。
5. 你点头后才停容器、占位矿机文件、收端口或重启。
## 如何判断完成
报告里有结论表;若做了处置,公网已关端口连不上,业务域名仍可用。密钥、token 不应出现在完整原文里。
+130
View File
@@ -0,0 +1,130 @@
---
name: host-sec-check
description: >-
Linux/VPS 主机安全检查与入侵取证:先取证再处置。覆盖矿机、ld.so.preload 进程隐藏、带宽挂机容器、对外端口、SSH/Docker/k8s 入口、cron/systemd 持久化、hugepage/OOM。
Use when: 安全检查, 被黑, 入侵, 矿机, 排查端口, 对公网放开, rpcbind, 挂机赚钱, Portainer, 安全审计, host security, compromise, cryptominer, exposure, /host-sec-check.
---
# host-sec-check
对一台 Linux 主机做安全检查。默认 **只读取证 + 出报告**。清理、关端口、重启、轮换密钥必须用户明确同意后再做。
需要:SSH 目标(`user@host` 或 ssh config 别名)、哪张网卡/IP 算公网。没有公网 IP 就只报「监听面」,并标明未做外部探测。
## 不适用
- Web 应用代码审计、依赖 CVE 扫描(可在报告里列为后续)
- 云厂商安全组控制台(能查就查,查不到就写「未验证」)
- 未获准的破坏性操作(`rm -rf`、整盘 wipe、强制重启)
## 铁律
1. **先取证,后动手。** 重启、删文件、停服务会毁掉证据。
2. **不要把密钥打进对话。** token、密码、authorized_keys、kubeconfig 只写「存在/已轮换/路径」,不贴原文。
3. **sudo 失败不要停。** 用户在 `docker` 组时,用特权容器 `nsenter` 看宿主机(见 [references/access.md](references/access.md))。
4. **怀疑进程隐藏时,不要只信宿主机 `ps`。** 从**不加载宿主机 `ld.so.preload` 的容器**读 `/proc` 和文件。
5. 宿主机 `chattr` 可能被隐藏库挡住;改 immutable 文件用 **Ubuntu/Alpine 自带 chattr** 操作 bind-mount 的 `/host/...`。
收集脚本(可选,先跑再解读):
```bash
bash <skill-dir>/scripts/collect.sh USER@HOST
```
`<skill-dir>` 是本 skill 目录。脚本只采集,不修改系统。
## 流程
按顺序做。每步记下「正常 / 可疑 / 未查到」。完整项见 [references/checklist.md](references/checklist.md)。
### 1. 快照
- 主机名、发行版、uptime、负载、`free -h`、`df -h`、公网/内网/Tailscale IP
- `ss -lntup`(最好 root)
- `docker ps -a`、compose 项目
- 若有 k3s/k8s:agent 是否在跑、能否 `kubectl get pods -A`
### 2. 入口
- sshd:`Port`、`PermitRootLogin`、`PasswordAuthentication`、`authorized_keys`(root 与所有 UID≥1000)
- `last` / `lastb` / `journalctl -u ssh`:异常登录;**无交互登录却出现 docker run 也是入口信号**
- sudoers、`docker` 组(= root)、NOPASSWD
- 对公网的管理面:Portainer / Cockpit / 面板 / k8s apiserver / Docker 端口,尤其 `-p 0.0.0.0:…` 且挂了 `docker.sock` 或 `/:/host`
- 单元文件或 compose 里的明文 token(k3s `--token`、agent secret)
### 3. 持久化
读 [references/persistence.md](references/persistence.md)。至少查:
- crontab(root 与所有用户)、`/etc/cron.*`
- `/etc/systemd/system/*.service`、timers
- `/etc/ld.so.preload`(用**外部** cat/hexdump,宿主机 cat 可能被藏空)
- `chattr +i` 的异常二进制(`lsattr`)
- 隐藏 Docker 数据根(非 `/var/lib/docker` 的 dockerd `--data-root`)
### 4. 进程与隐藏库
- 从特权容器列 `/proc/*/cmdline`(绕过 preload)
- 对照宿主机 `ps`:只在容器视角出现的进程 = 隐藏
- `/proc/*/maps` 是否映射陌生 `.so`(`libgcwrap`、`libprocesshider`、随机名)
- 矿机常见名:`perfcc`、`xmrig`、`kdevtmpfsi`、`javavm64`;同体积多副本(例如都是 11MB)
- hugepage:`grep Huge /proc/meminfo`。`nr_hugepages>0` 且进程列表对不上,多半是矿机预留
### 5. 容器与编排
- `docker ps -a`:镜像名含 earnfm / packetstream / repocket / traffmonetizer / honeygain / proxyrack / bitping / antgain 等挂机池
- inspect:`Privileged`、Binds(`docker.sock`、`/:/host`)、Ports 是否 `0.0.0.0`
- 第二套 dockerd / 异常 `--bridge`
- k8s:节点上是否有与 Git 仓库无关的 workload;控制面不健康时从 kubelet `pods/*/etc-hosts` 读 Pod 名
### 6. 对外暴露
- 本机 `0.0.0.0` / `[::]` 监听
- **从另一台机器探测公网 IP**(含 IPv6)。只看 `ss` 会漏安全组,只看安全组会漏「监听但没防火墙」
- 高危:`rpcbind :111`、明文 HTTP 应用端口、Docker/Portainer、k8s 6443、数据库
- 管理 SSH 若可走 Tailscale,公网 SSH 算多余攻击面
### 7. 资源与日志
- 无 swap + 高 hugepage → 应用易 OOM
- `journalctl` / `auth.log` 在处置时间点附近;日志空也要记
- 出站连接:`ss -tp` 看陌生 IP(不要把结果里的密钥打出来)
### 8. 补漏(本次 vvps2 没做或做得浅的)
见 checklist 后半:补丁、SUID、世界可写、意外用户、IPv6、定时器、内核模块、出站 C2、云安全组、备份是否可用。
## 报告格式
先给结论(是否沦陷、最可能入口、对外还开着什么),再给证据表,最后给 **需用户点头的处置清单**。不要默默清理。
```
结论: 沦陷 / 可疑 / 未见异常
最可能入口: …
对外端口: …
持久化: …
隐藏/矿机: …
容器/编排: …
未查项: …
建议处置: (等确认)
```
## 处置(仅在用户同意后)
原则见 [references/cleanup.md](references/cleanup.md)。
- 停并删除未授权容器;镜像也删
- 矿机二进制:删掉后用 **空文件 + `chattr +i` 占路径**
- 清空并锁定 `/etc/ld.so.preload`
- 关公网绑定:改成 `127.0.0.1` 或 Tailscale IP(若回源走 Tailscale 主机名,必须绑 TS IP,不能只绑 loopback)
- 停无用服务(如 `rpcbind.socket` + `rpcbind.service`,必要时 mask)
- 重启才能卸掉仍映射已删 `.so` 的老进程(sshd/dockerd)。先取证再重启
- 轮换:暴露过的 token、面板、k3s bootstrap。控制面 API 不通就不要假轮换,写阻塞原因
## 验证
- 公网探测:已关端口失败;该留的业务域名仍 200
- `ss`:无 `0.0.0.0:已关端口`
- `grep -l libgcwrap /proc/*/maps`(或同类库)重启后为 0
- 占位文件仍在且 immutable
- 业务容器 `restart: always` 仍在
@@ -0,0 +1,48 @@
# 取证入口(无 sudo 时)
目标用户常见情况:`sudo` 要密码,但在 `docker` 组。Docker 组等价于 root。
## 读宿主机(绕过 preload)
容器用自己的 libc,**不会**加载宿主机 `/etc/ld.so.preload`。
```bash
docker run --rm -i --privileged --pid=host --net=host -v /:/host alpine:latest \
cat /host/etc/ld.so.preload
hexdump -C /host/etc/ld.so.preload # 在容器内
```
列隐藏进程:
```bash
docker run --rm --privileged --pid=host alpine:latest sh -c \
'for d in /proc/[0-9]*; do
pid=${d#/proc/}
cmd=$(tr "\0" " " < "$d/cmdline" 2>/dev/null) || continue
[ -n "$cmd" ] && echo "$pid $cmd"
done'
```
## 改宿主机(systemctl / chattr)
`chroot /host /usr/bin/chattr` 会加载宿主机 preload,可能 **stat 不到** 被藏文件。
用发行版自带 chattr 操作 `/host/...`:
```bash
docker run --rm -i --privileged --pid=host --net=host -v /:/host ubuntu:latest bash -s <<'SH'
chattr -i /host/etc/ld.so.preload
: > /host/etc/ld.so.preload
chattr +i /host/etc/ld.so.preload
lsattr /host/etc/ld.so.preload
SH
```
systemctl / reboot:
```bash
docker run --rm --privileged --pid=host --net=host alpine:latest \
nsenter -t 1 -m -u -i -n -- systemctl disable --now UNIT
```
不要用业务容器当跳板;用 `alpine`/`ubuntu` 临时 `--rm`。查到一半时 `docker ps` 里出现的随机名容器就是这个跳板。
@@ -0,0 +1,75 @@
# 检查项
每项:做了 / 跳过(原因) / 可疑。
## A. 快照
- [ ] 主机名、OS、内核、uptime
- [ ] CPU/负载、内存(含 hugepage、swap)、磁盘
- [ ] 网卡:公网 v4/v6、内网、Tailscale/VPN
- [ ] 监听端口 + 进程(root `ss -lntup`)
- [ ] 本机防火墙:nft/iptables/ufw;INPUT 策略
- [ ] 云安全组(做不到就写未查)
## B. 身份与入口
- [ ] sshd 配置:端口、root、密码、Pubkey
- [ ] 所有 `authorized_keys`(含 `/root`、`/home/*`、git 用户)
- [ ] `last` / `lastb` / ssh journal
- [ ] `/etc/passwd` 里 UID 0、系统外用户、登录 shell
- [ ] sudoers、NOPASSWD、docker/lxd/libvirt 组
- [ ] 对公网的面板:Portainer/Cockpit/宝塔/1Panel/k8s/Docker TCP
- [ ] 单元或 compose 里的明文 bootstrap token
## C. 持久化
- [ ] 用户与系统 crontab、`/etc/cron.*`
- [ ] systemd 服务与 timer(非发行版默认)
- [ ] `ld.so.preload` + 对应 `.so`(容器视角 hexdump)
- [ ] `lsattr` 发现的 immutable 异常文件
- [ ] rc.local、profile.d、bashrc 后门
- [ ] 第二套 container runtime
## D. 进程
- [ ] 容器视角 vs 宿主机 `ps` 差集
- [ ] maps 里的陌生 so
- [ ] 矿机/挖矿/proxy 客户端特征名与同体积副本
- [ ] hugepage 预留但无对应进程
- [ ] 高 CPU 且不在业务名单里的进程
## E. 容器 / k8s
- [ ] `docker ps -a` 与 inspect(privileged、sock、`/:/host`、`0.0.0.0`)
- [ ] 挂机赚钱镜像名单
- [ ] k8s/k3s:节点 Ready、该节点 Pod 列表
- [ ] API 不通时:kubelet `pods/*/etc-hosts`、crictl
- [ ] 工作负载是否能在 Git/编排仓库里对上
## F. 暴露
- [ ] `0.0.0.0` 与 `[::]` 监听清单
- [ ] **从外部**扫公网 IPv4(和 IPv6)
- [ ] rpcbind :111、DB、Redis、应用明文端口
- [ ] 管理 SSH 是否可收到 VPN
- [ ] 证书/域名入口是否可被 IP:端口绕过
## G. 补漏(容易漏)
- [ ] IPv6 公网地址同样在听
- [ ] 世界可写目录、异常 SUID
- [ ] 未装补丁 / `apt` 过期很久
- [ ] 出站长连接(C2)
- [ ] 内核模块 `lsmod` 异常
- [ ] 时间偏移(影响日志)
- [ ] 备份是否还能用(清理前不要删数据盘)
- [ ] 日志被清空或 journal vacuum
- [ ] Web 根目录 webshell(若有 nginx/caddy/apache)
- [ ] `.git` 或备份文件可下载
- [ ] 云 metadata 是否对容器开放
## H. 报告与处置确认
- [ ] 结论表
- [ ] 用户确认后再改
- [ ] 改完公网+域名双侧验证
@@ -0,0 +1,59 @@
# 处置(用户同意之后)
## 顺序
1. 证据已落盘(collect 输出、关键 ls/hexdump/docker inspect)
2. 停进程 / 停容器
3. 删或占位文件
4. 关公网端口
5. 最后才重启(卸已加载的隐藏库)
## 占位
对矿机路径(`/usr/bin/perfcc`、`/tmp/javavm64`、隐藏 docker 根名等):
```bash
chattr -i "$path" 2>/dev/null
rm -f "$path"
: > "$path"
chmod 000 "$path"
chattr +i "$path"
```
`/etc/ld.so.preload` 占空文件,权限 `644` + immutable,让动态链接器能读到「没有库」。
目录型隐藏根:停进程并卸挂载后删除,再在同名路径放 immutable 空文件,阻止再 `mkdir`。
改 immutable 时用容器自带 `chattr` 操作 `/host/...`,不要 `chroot` 到宿主机再跑 chattr。见 access.md。
## 容器
```bash
docker stop NAME && docker rm NAME
docker rmi IMAGE
```
不要 `docker system prune -a`。
## 端口
- 入口在本机 loopback → `127.0.0.1:PORT:CONTAINER`
- 入口走 Tailscale 主机名 → 必须绑 **当前 Tailscale IPv4**,不能只绑 127.0.0.1
- 可同时绑 loopback + Tailscale,禁止 `0.0.0.0` / `[::]`
改 compose 后 force-recreate,再从公网 IP 和业务域名两边探测。
## rpcbind
```bash
systemctl disable --now rpcbind.socket rpcbind.service
systemctl mask rpcbind.socket rpcbind.service
```
无对外 NFS 再做。确认 `:111` 消失。
## 重启
重启前确认业务是 `restart: always`,并告知会短暂中断。
重启后:`grep -l <so名> /proc/*/maps` 为 0;若已清 hugepage 则 `HugePages_Total` 为 0;域名 200。
@@ -0,0 +1,40 @@
# 持久化位置
按这个名单扫。出现「刚被排查时时间戳又刷新」= 仍有 watchdog。
## 定时
- `crontab -u root -l` 以及 `/var/spool/cron/crontabs/*`
- `/etc/cron.d/` `/etc/cron.hourly/` `/etc/cron.daily/` `/etc/cron.weekly/`
- systemd timers:`systemctl list-timers --all`
## 服务
- `/etc/systemd/system/*.service`(尤其非发行版默认的)
- `ExecStart` 指向 `/tmp`、`/dev/shm`、`/root/.config`、随机短名
- `Restart=always` 的陌生服务
## 动态链接器
- `/etc/ld.so.preload`:内容常是 `/lib/libXXX.so`(约十几字节)。宿主机 `cat` 可能显示空,用容器 hexdump
- 对应 `.so` 可能 `chattr +i`、setgid
- 常见名:`libgcwrap.so`、`libpprocps.so`、`libfsnldev.so`、`libprocesshider.so`
## 二进制与分身
- `/usr/bin` `/bin` `/usr/local/bin` `/tmp` 下刚改过的可执行文件
- 同字节大小的多份拷贝(矿机改名)
- 占位对抗:空文件 `chmod 000` + `chattr +i`(见 cleanup.md)
## 隐藏运行时
- 第二套 dockerd:`--data-root` 不在 `/var/lib/docker`,`--host unix://` 异常路径
- 混淆文件名(把 docker 子命令拼进无关单词)
- k3s 的 `containerd-shim` 被借去跑 **moby** namespace + 异常 `--address`
## 登录与启动
- `~/.bashrc` / `/etc/profile.d` 里 curl|bash
- `/etc/rc.local`
- SSH `authorized_keys` 多出来的 key
- 意外用户、UID 0 非 root
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# 只采集,不修改。用法: collect.sh [user@]host
# 输出写到 stdout;完整日志建议重定向到文件。
set -euo pipefail
HOST="${1:?usage: collect.sh user@host}"
SSH=(ssh -o BatchMode=yes -o ConnectTimeout=15 "$HOST")
echo "===== host-sec-check collect $(date -u +%Y-%m-%dT%H:%M:%SZ) host=$HOST ====="
remote() {
"${SSH[@]}" bash -s <<'EOS'
set +e
echo "----- id / os / uptime -----"
id; hostname; uname -a
. /etc/os-release 2>/dev/null; echo "$PRETTY_NAME"
uptime
echo "----- mem / hugepage / disk -----"
free -h
grep -E "MemAvailable|HugePages_|Hugetlb|SwapTotal" /proc/meminfo
df -h
echo "----- addresses -----"
ip -4 addr; ip -6 addr show scope global
echo "----- listen (may miss pid without root) -----"
ss -lntup 2>/dev/null || netstat -lntup 2>/dev/null
echo "----- docker -----"
docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || echo "no docker"
echo "----- preload (host cat; may be hidden) -----"
ls -la /etc/ld.so.preload 2>/dev/null
echo "preload_host=[$(cat /etc/ld.so.preload 2>/dev/null)]"
echo "----- cron files -----"
ls -la /etc/cron.d /etc/cron.hourly /etc/cron.daily 2>/dev/null
echo "----- systemd extras -----"
ls /etc/systemd/system/*.service 2>/dev/null
echo "----- users uid0 / human -----"
awk -F: '$3==0 || ($3>=1000 && $3<65534) {print}' /etc/passwd
echo "----- sshd -----"
grep -E "^(Port|PermitRootLogin|PasswordAuthentication|PubkeyAuthentication)" /etc/ssh/sshd_config /etc/ssh/sshd_config.d/* 2>/dev/null
echo "----- last -----"
last -a | head -15
EOS
}
remote
# docker 组:从容器读 preload 真值
if "${SSH[@]}" "groups | grep -qw docker" 2>/dev/null; then
echo "----- preload via alpine (bypass host libc) -----"
"${SSH[@]}" "docker run --rm --privileged -v /:/host alpine:latest hexdump -C /host/etc/ld.so.preload" 2>/dev/null \
|| echo "alpine hexdump failed"
fi
echo "===== collect done ====="
echo "下一步: 按 SKILL.md 解读;不要把本输出里的密钥贴进聊天。"
+89
View File
@@ -0,0 +1,89 @@
---
name: note-style
description: 将现有材料整理、压缩或改写为可长期查阅的知识笔记,采用教科书、技术手册或标准说明文风格,并保留事实、公式、适用条件、安全警告和参考资料。适用于用户要求整理 Obsidian 笔记、消除聊天或 AI 写作痕迹、减少重复和空话、统一术语与技术表达,或将技术原理、操作方案改写为结构化知识文档的任务。
---
# 整理知识笔记
## 工作流程
1. 识别材料的核心问题、目标读者和材料类型。
2. 提取事实、定义、公式、条件、因果关系、步骤、例证、边界、安全警告和参考资料。
3. 删除与核心问题无关的铺垫,并合并重复内容。
4. 按材料类型选择结构并重写全文。
5. 检查技术准确性、术语、格式、重复和表达密度。
6. 输出完整笔记;存在事实或技术疑点时,追加“待核实项”。
## 保留与删减
- 保留原文中的事实、公式、适用条件、例外、安全限制和参考资料。
- 不自行补充没有依据的结论,也不为缩短篇幅而删除必要条件或公式推导。
- 围绕核心问题组织内容,删除无关铺垫。
- 使每句话至少承担一种功能:定义概念、说明条件、解释因果、给出步骤、提供例证或指出边界。
- 使同一知识点只完整解释一次。避免在摘要、正文、表格、总结和检查表之间复述。
- 在不损失必要信息的前提下,默认将篇幅压缩约 30%;若用户另有篇幅要求,以用户要求为准。
## 语言规范
- 使用中性、准确、简洁的书面语。
- 直接陈述内容,不添加聊天式过渡、开场铺垫、段末复述或写作过程说明。
- 删除情绪化、评价性、劝导性、宣传性和表演式表达,如“非常美妙”“加分项”“更稳妥”“请仔细看”。
- 不使用反问、口号、拟人化表达或用感叹号制造重点。
- 减少“通常、可能、建议、优先、具体来看”等模糊限定词。确有不确定性时,明确不确定性的来源和适用条件。
- 每段只表达一个主题;能用一句话说明的内容不扩写成多句。
- 将评价句改写为“条件、机制、结果”或“对象、操作、限制”。
## 技术表达
- 首次出现专业术语时给出简短定义,后文统一名称。
- 明确符号、变量、单位、对象类型、参考系、输入输出和变换方向。
- 区分严格定义、直观解释和示例,不用比喻替代定义。
- 每个概念最多保留一个必要的例子或比喻;使用比喻后,以正式术语重述结论。
- 检查概括的适用范围,将无条件的“X 代表 Y”改为带条件的准确表述。
- 不擅自修正不确定或可能有误的技术内容,将其列入“待核实项”。
## 结构选择
根据材料类型采用下列骨架,并按实际内容删去不需要的章节。
### 技术原理类
1. 问题或目标
2. 符号与定义
3. 原理或推导
4. 示例
5. 适用条件与限制
6. 简短结论
### 操作与方案类
1. 目标与适用范围
2. 基本原则
3. 操作步骤
4. 分类处理
5. 安全边界
6. 检查表
不要同时保留内容重复的“摘要”“重新解释”“具体调整”和“总结”。摘要已经给出结论时,正文只提供依据和展开。
## 格式规范
- 使用描述性标题,避免“到底是什么”“究竟该怎么做”等口语化标题。
- 保持标题编号连续、层级一致。
- 仅对首次定义的术语、关键条件和禁止事项使用粗体。
- 减少引号和括号,统一数学符号、变量格式和单位格式。
- 保留适合 Obsidian 的 Markdown、YAML frontmatter、Wiki-links、表格、公式和代码块;除非用户要求,不改变其语义。
## 完成检查
- 检查同一结论是否出现在两个以上位置,只保留信息最完整且位置最合适的一处。
- 检查相邻段落能否分别用一句话概括;含义相同时合并。
- 检查所有评价句是否已改为可验证的条件、机制、结果、操作或限制。
- 检查技术概括、符号定义、事实和适用条件是否成立。
- 检查内部链接、表格、公式、代码块、安全警告和参考资料是否完整保留。
## 输出
1. 先输出整理后的完整笔记。
2. 仅在存在技术或事实疑点时追加“待核实项”,逐条列出疑点。
3. 不解释改写过程,不输出修改心得,不复述本技能的要求。
+4
View File
@@ -0,0 +1,4 @@
interface:
display_name: "教科书式笔记整理"
short_description: "将材料压缩并改写为准确、简洁的结构化知识笔记"
default_prompt: "使用 $note-style 将这份材料整理为可长期查阅的知识笔记。"