Elasticsearch集群取证

当我们成功仿真出 Elasticsearch 集群节点后,往往需要先把集群恢复到可用状态,才能通过 REST API 快速提取索引、用户、日志等关键信息。Elasticsearch 的几乎所有元数据都能通过 API 查询,因此恢复集群运行 > 直接翻数据目录。

Elasticsearch 基础知识

什么是 Elasticsearch

Elasticsearch 是基于 Apache Lucene 的分布式搜索引擎,由多个节点(Node)组成集群(Cluster),数据以索引(Index) 组织,索引被切分为分片(Shard),分片分散存储在各数据节点上,每个分片分为主分片(Primary)与副本分片(Replica)。节点按角色分工:

  • master —— 管理集群状态(节点加入/移除、索引创建、分片分配),master-eligible 节点间选举出 active master
  • data —— 存储分片数据、执行检索/写入(可细分 data_hot/data_warm/data_cold/data_frozen)
  • ingest —— 执行 Ingest Pipeline 数据预处理
  • coordinating —— 不配置任何角色即纯协调节点,转发/汇聚请求
  • voting_only —— 仅参与 master 选举投票,不做 master
  • remote_cluster_client —— 跨集群搜索/复制的默认客户端角色

集群状态(Cluster State)由 active master 维护并持久化到所有 master-eligible 节点的数据目录 _state/ 中;分片数据以 Lucene 段文件形式落盘。

取证要点速览

类别关键位置
REST APIhttp://<IP>:9200(默认),_cat/_cluster/_security 系列接口
节点间通信TCP 9300(transport 层)
主配置文件/etc/elasticsearch/elasticsearch.yml
配置目录/etc/elasticsearch/(jvm.options、log4j2.properties、certs、users 等)
程序目录/usr/share/elasticsearch/(bin、lib、plugins、modules)
数据目录/var/lib/elasticsearch/(默认,path.data 指定)
日志目录/var/log/elasticsearch/(<cluster_name>.log、slowlog、audit、deprecation、gc)
内置工具/usr/share/elasticsearch/bin/elasticsearch-*(users、keystore、certutil、reset-password…)
安全文件(native realm)/etc/elasticsearch/users、/etc/elasticsearch/users_roles、/etc/elasticsearch/role_mapping.yml
安全存储/etc/elasticsearch/elasticsearch.keystore(加密敏感配置)
集群状态持久化<path.data>/nodes/0/_state/(cluster state、索引元数据)
快照数据path.repo 指定的共享文件系统目录(fs 仓库)
systemd 单元systemctl status elasticsearch

离线取证技巧:无法启动集群怎么办

若集群起不来(证书损坏、quorum 不满足、数据目录缺失),数据与元数据仍可离线提取:

# 集群元数据(索引 mapping、settings、模板)
ls /var/lib/elasticsearch/nodes/0/_state/       # cluster state / meta state 文件(.st 二进制 + manifest)
# 索引数据按 UUID 组织
ls /var/lib/elasticsearch/nodes/0/indices/      # 每个子目录名即索引 UUID
cat /var/lib/elasticsearch/nodes/0/indices/<UUID>/_state/state-*.st   # 含索引名(strings 可见)

注意:Lucene 段文件是二进制格式,离线直接解析代价高,优先想办法把节点拉起来用 API 导出。单节点场景可用 discovery.type: single-node 绕过选举问题。

仿真环境网络配置

启动网卡

仿真出的节点默认网卡可能未启用:

ip link set ens33 up
dhclient ens33

修改网络绑定

Elasticsearch 默认只监听 127.0.0.1,集群仿真需改为对外可达地址。编辑各节点 /etc/elasticsearch/elasticsearch.yml:

# 对外监听地址(仿真机 IP 或 0.0.0.0)
network.host: 192.168.170.50
http.port: 9200
transport.port: 9300

# 集群名必须与现场一致,否则节点不会加入原集群
cluster.name: <原集群名>

# 节点名与角色(保持与原配置一致)
node.name: node-1
node.roles: [ master, data, ingest ]

集群发现配置

# master-eligible 节点互相发现(填写各 master 节点 IP)
discovery.seed_hosts: ["192.168.170.50", "192.168.170.51", "192.168.170.52"]

# 全新集群首次引导(仅在所有 master 节点全新启动时使用一次,之后删除或注释)
cluster.initial_master_nodes: ["node-1", "node-2", "node-3"]

注意:cluster.initial_master_nodes 只在集群首次启动(数据目录为空)时有效。仿真恢复已有数据的节点时该配置会被忽略,切勿在已初始化的集群上反复修改,否则可能产生脑裂。只有单节点数据恢复可用 discovery.type: single-node 快速拉起。

启动服务并验证

systemctl start elasticsearch
systemctl status elasticsearch
journalctl -u elasticsearch -e --no-pager     # 启动报错全在这里

curl http://127.0.0.1:9200/_cluster/health?pretty            # 未开安全时
curl -k https://127.0.0.1:9200/_cluster/health?pretty -u elastic:<密码>   # 开了 xpack 安全时

时钟偏差

Elasticsearch 的 master 选举、快照时间戳、ILM 生命周期策略、审计日志都对时钟敏感,仿真节点时间不一致会导致集群反复选主或分片恢复异常,需先统一时间:

#1. 关闭自动同步(防止 chrony/NTP 与手动设置打架)
timedatectl set-ntp false
systemctl stop chrony && systemctl disable chrony

#2.手动统一时间(在每台节点上执行,时间保持一致)
timedatectl set-timezone Asia/Shanghai
timedatectl set-time "2024-01-01 12:00:00"
hwclock --systohc        # 同步写入硬件时钟 RTC

#3.确认时间一致(逐台执行比对)
date
timedatectl

#4.重启服务(时间一致后重启恢复集群)
systemctl restart elasticsearch

系统取证

基本系统信息

版本号

curl -s http://127.0.0.1:9200/ | grep version          # 在线:ES 版本、Lucene 版本、构建哈希
cat /usr/share/elasticsearch/lib/elasticsearch-*.jar   # 离线:jar 文件名即版本
dpkg -l | grep elasticsearch     # deb 包版本(deb 安装时)
rpm -qa | grep elasticsearch     # rpm 包版本(rpm 安装时)

_nodes 接口还能看到每个节点精确的版本与 JVM 信息:

curl -s http://127.0.0.1:9200/_nodes?pretty | grep -E '"name"|"version"'

主机内核版本

uname -a

主机名与节点名

hostnamectl
cat /etc/hostname
cat /etc/hosts                          # 集群各节点的 IP/主机名映射常记录在此
grep node.name /etc/elasticsearch/elasticsearch.yml

时间服务器地址

cat /etc/chrony/chrony.conf    # server/pool 行即时间源(7 及以后多为 chrony)
cat /etc/ntp.conf              # 老系统可能是 ntpd
chronyc sources                # 在线状态查看
timedatectl                    # 时区与时间同步状态

集群信息

集群健康与状态

curl -s http://127.0.0.1:9200/_cluster/health?pretty

关注字段:

  • cluster_name —— 集群名
  • status —— green(全部分片就位)/ yellow(副本缺失,数据可读)/ red(主分片缺失,数据已丢)
  • number_of_nodes / number_of_data_nodes —— 节点数
  • active_primary_shards / active_shards —— 在线分片数
  • unassigned_shards —— 未分配分片数(>0 说明有分片恢复失败)

节点列表与角色

curl -s http://127.0.0.1:9200/_cat/nodes?v&h=name,ip,node.role,master,heap.percent,ram.percent,disk.used_percent
curl -s http://127.0.0.1:9200/_nodes?pretty          # 每个节点的完整信息:角色、JVM、插件、处理器、绑定 IP
curl -s http://127.0.0.1:9200/_cat/master?v          # 当前 active master

node.role 列含义:m=master-eligible,d=data,i=ingest,l=machine learning,r=remote_cluster_client,v=voting_only,-=协调节点。

集群设置与元数据

curl -s http://127.0.0.1:9200/_cluster/settings?include_defaults=true&pretty   # 生效的全部集群级配置
curl -s http://127.0.0.1:9200/_cluster/state/metadata?pretty                   # 索引/模板/ILM 元数据总览
curl -s http://127.0.0.1:9200/_cat/plugins?v                                   # 各节点安装的插件(含第三方插件)
curl -s http://127.0.0.1:9200/_cat/tasks?v                                     # 当前正在执行的集群任务

主配置文件

cat /etc/elasticsearch/elasticsearch.yml

取证关注点:

  • cluster.name、node.name、node.roles —— 集群拓扑与角色
  • network.host、discovery.seed_hosts —— 集群成员 IP
  • path.data、path.logs —— 数据与日志实际位置(可能被改到非默认路径)
  • path.repo —— 快照仓库允许路径
  • xpack.security.* —— 安全功能开关、TLS 证书路径、审计开关
  • 异常的配置项(如指向陌生 IP 的 network.host、被关闭的安全功能)往往是攻击者改动的痕迹

索引与数据取证

索引清单

curl -s http://127.0.0.1:9200/_cat/indices?v&h=index,health,status,pri,rep,docs.count,store.size,creation.date.string
  • docs.count —— 文档数(yellow/red 索引该值可能不完整)
  • creation.date.string —— 索引创建时间,可用于还原数据写入时间线
  • . 开头的是系统索引(.security 用户数据、.kibana*、.tasks、.ds-* 数据流后端索引)
curl -s http://127.0.0.1:9200/<索引名>/_mapping?pretty    # 索引字段结构
curl -s http://127.0.0.1:9200/<索引名>/_settings?pretty   # 分片数、副本数、slowlog 阈值、ILM 策略
curl -s http://127.0.0.1:9200/<索引名>/_count             # 文档计数
curl -s http://127.0.0.1:9200/_cat/aliases?v              # 别名(数据流/读写别名)
curl -s http://127.0.0.1:9200/_index_template?pretty      # 索引模板

分片分配状态

curl -s http://127.0.0.1:9200/_cat/shards?v&h=index,shard,prirep,state,docs,store,node

关注 UNASSIGNED 与 RELOCATING 的分片:

curl -s http://127.0.0.1:9200/_cat/allocation?v                      # 各节点磁盘水位
curl -s http://127.0.0.1:9200/_cat/recovery?v                        # 正在进行的分片恢复
curl -s -XPOST 'http://127.0.0.1:9200/_cluster/allocation/explain?pretty' -H 'Content-Type: application/json' -d '{
  "index": "<索引名>", "shard": 0, "primary": true
}'                                                                   # 分片未分配的具体原因(磁盘水位/副本不足/数据损坏)

磁盘水位(默认 cluster.routing.allocation.disk.watermark.*)触发时分片会被锁定,elasticsearch.yml 或 _cluster/settings 中可查到。

导出索引数据

方法一:elasticdump(推荐,直观)

需要 Node.js 环境,导出为 JSON 便于离线分析:

npm install elasticdump -g

elasticdump --input=http://127.0.0.1:9200/<索引名> --output=/导出目录/<索引名>.mapping.json --type=mapping   # 先导 mapping
elasticdump --input=http://127.0.0.1:9200/<索引名> --output=/导出目录/<索引名>.data.json --type=data --limit=5000 # 再导数据
# 开了安全认证:
elasticdump --input=http://elastic:<密码>@127.0.0.1:9200/<索引名> --output=/导出目录/<索引名>.data.json --type=data

大索引用 --limit 分片导出避免 OOM;导出文件为 NDJSON,可直接 grep/jq 分析。

方法二:scroll 原生导出

不装任何工具,纯 curl 完成:

# 第一批(scroll=1m 表示快照游标存活 1 分钟)
curl -s 'http://127.0.0.1:9200/<索引名>/_search?scroll=1m&pretty' -H 'Content-Type: application/json' -d '{
  "size": 5000,
  "query": { "match_all": {} }
}' > /导出目录/batch_1.json

# 后续批次(用上一批返回的 _scroll_id)
curl -s 'http://127.0.0.1:9200/_search/scroll' -H 'Content-Type: application/json' -d '{
  "scroll": "1m",
  "scroll_id": "<上一批返回的_scroll_id>"
}' > /导出目录/batch_2.json

scroll 是对时间点的快照式读取,适合取证(导出期间新写入的数据不会混进来)。

方法三:快照导出(最完整,保真度最高)

快照保留 Lucene 原始文件,可跨集群完整还原:

#1. 注册 fs 类型仓库(路径必须在 elasticsearch.yml 的 path.repo 中声明,改完需重启节点)
#    elasticsearch.yml 增加: path.repo: ["/var/lib/elasticsearch/snapshots"]

curl -s -XPUT 'http://127.0.0.1:9200/_snapshot/my_repo?pretty' -H 'Content-Type: application/json' -d '{
  "type": "fs",
  "settings": { "location": "/var/lib/elasticsearch/snapshots", "compress": true }
}'

#2. 创建快照(全部索引)
curl -s -XPUT 'http://127.0.0.1:9200/_snapshot/my_repo/snapshot_1?wait_for_completion=true&pretty'
# 只快照指定索引: body 加 "indices": "index1,index2"

#3. 查看快照
curl -s 'http://127.0.0.1:9200/_snapshot/_all?pretty'                    # 全部仓库
curl -s 'http://127.0.0.1:9200/_snapshot/my_repo/_all?pretty'            # 仓库内快照及起止时间
curl -s 'http://127.0.0.1:9200/_snapshot/my_repo/snapshot_1?pretty'      # 快照详情(索引列表、分片、耗时)

#4. 打包带走
tar -czvf /导出目录/es_snapshot.tar.gz -C /var/lib/elasticsearch/snapshots .
sha256sum /导出目录/es_snapshot.tar.gz | tee es_snapshot.sha256           # 完整性校验

还原到分析环境:在新集群同样注册该目录为仓库后 POST /_snapshot/my_repo/snapshot_1/_restore。

注意:若现场 elasticsearch.yml 没有 path.repo,需要先添加并滚动重启节点才能注册 fs 仓库;也可用 S3/HDFS 等仓库类型(需插件)。

存储取证

数据目录结构

<path.data>(默认 /var/lib/elasticsearch/)布局:

/var/lib/elasticsearch/
├── node.lock                          # 节点运行锁(运行时存在)
├── nodes/0/
│   ├── _state/                        # 集群级元数据:cluster state、索引元数据(.st 二进制 + manifest)
│   ├── node.lock
│   └── indices/
│       └── <索引UUID>/
│           ├── _state/state-*.st      # 索引级元数据(索引名可读)
│           └── 0/                     # 分片号
│               ├── index/             # Lucene 段文件(.cfs/.cfe/.si/_*.liv 等,真实数据)
│               ├── translog/          # 未刷盘的事务日志(最近的写入)
│               └── _state/            # 分片元数据(分配信息、是否主分片)

索引名 → UUID 的映射可通过 _cat/indices 的 uuid 列获取,也可直接 strings state-*.st 查看:

curl -s http://127.0.0.1:9200/_cat/indices?v&h=index,uuid,docs.count
strings /var/lib/elasticsearch/nodes/0/indices/<UUID>/_state/state-*.st | less

磁盘占用

curl -s http://127.0.0.1:9200/_cat/indices?v&h=index,store.size,pri.store.size&s=store.size:desc   # 各索引占用
curl -s http://127.0.0.1:9200/_cat/allocation?v                                                    # 各节点分片数与磁盘
du -sh /var/lib/elasticsearch/nodes/0/indices/*                                                    # 离线核对

快照仓库

curl -s http://127.0.0.1:9200/_snapshot/_all?pretty     # 注册的仓库、类型、位置
grep path.repo /etc/elasticsearch/elasticsearch.yml     # 本地允许的仓库路径
ls /var/lib/elasticsearch/snapshots/                    # fs 仓库内容(index-* 元数据 + 分片数据目录)

fs 仓库目录中的 index-0、index.latest 是快照清单,indices/<索引名>/<分片>/ 下是增量块文件,可直接打包带走异地还原。

用户与权限取证

安全功能状态

grep xpack.security /etc/elasticsearch/elasticsearch.yml
# xpack.security.enabled: false  → 无认证,谁都能读写(常见于被入侵或配置失误现场)
# 该配置缺失且版本为 8.x      → 默认开启安全

用户列表

curl -s -u elastic:<密码> http://127.0.0.1:9200/_security/user?pretty          # 全部 native 用户、角色、邮箱、启用状态
curl -s -u elastic:<密码> http://127.0.0.1:9200/_security/role?pretty          # 自定义角色与权限(cluster/indices 权限点)
curl -s -u elastic:<密码> http://127.0.0.1:9200/_security/role_mapping?pretty  # 外部用户/组的映射规则
curl -s -u elastic:<密码> http://127.0.0.1:9200/_security/api_key?pretty       # 现存 API Key
curl -s -u elastic:<密码> http://127.0.0.1:9200/_security/service/_all/credential?pretty   # Service Account Token

重点排查:可疑的新建用户、superuser 角色分配、来源不明的 API Key(常用于持久化)。

离线:native realm 密码文件

8.x 默认把 native 用户存在 .security-7 系统索引里,但 file realm 仍是文件形式:

cat /etc/elasticsearch/users          # 格式 user:bcrypt哈希,可交给 hashcat(-m 3200)破解
cat /etc/elasticsearch/users_roles    # 用户-角色映射
cat /etc/elasticsearch/role_mapping.yml

命令行用户工具

/usr/share/elasticsearch/bin/elasticsearch-users list          # file realm 用户列表
/usr/share/elasticsearch/bin/elasticsearch-users useradd admin2 -p xxx -r superuser   # 应急添加管理员
/usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic        # 重置 elastic 内置用户密码
/usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic -a     # 自动生成强密码
/usr/share/elasticsearch/bin/elasticsearch-keystore list       # keystore 中的敏感配置键名

证书

ls /etc/elasticsearch/certs/     # 常见 elastic-certificates.p12、http.p12、transport.p12、ca.crt
openssl pkcs12 -info -in /etc/elasticsearch/certs/http.p12 -nokeys   # 查看证书主题/有效期
/usr/share/elasticsearch/bin/elasticsearch-certutil cert --ca ...    # 需重签时使用

TLS 证书泄露(p12 + 密码)意味着 transport 层可被伪造节点接入,需一并取证带走。

日志取证

日志目录默认 /var/log/elasticsearch/,实际位置以 elasticsearch.yml 的 path.logs 为准。

主日志(重点)

ls /var/log/elasticsearch/
# <集群名>.log            —— 主日志:启动、选举、分片分配、异常堆栈
# <集群名>.json           —— JSON 格式主日志(8.x 默认)
# <集群名>_deprecation.log/json —— 弃用功能告警
# gc.log / gc.log.*       —— JVM GC 日志
cat /var/log/elasticsearch/<集群名>.log
journalctl -u elasticsearch --since "2024-01-01" --no-pager

主日志可还原:节点加入/退出集群时间、master 选举变更(elected-as-master)、分片恢复失败原因、OOM/磁盘满事件、非法请求报错。

慢查询日志

按索引生成,记录执行超过阈值的搜索/写入,是发现数据窃取(大批量导出)的重要线索:

ls /var/log/elasticsearch/*slowlog*
# <集群名>_index_search_slowlog.log/json     —— 慢查询(含具体查询语句)
# <集群名>_index_indexing_slowlog.log/json   —— 慢写入

阈值由索引设置控制(可在 _settings 中看到):

curl -s http://127.0.0.1:9200/<索引名>/_settings?pretty | grep -A20 slowlog
# index.search.slowlog.threshold.query.warn / index.indexing.slowlog.threshold.index.warn …
# "-1" 表示关闭

安全审计日志

需 xpack.security.audit.enabled: true(配置在 elasticsearch.yml),默认输出到 <集群名>_audit.json:

cat /var/log/elasticsearch/<集群名>_audit.json | head

关键事件(event.action 字段):

  • authentication_success / authentication_failed —— 登录成功/失败(含来源 IP、用户名,可查爆破)
  • access_denied —— 越权访问尝试
  • run_as_granted / run_as_denied —— 代理身份切换
  • security_config_change —— 用户/角色被增删改(攻击者留后门的直接证据)
  • tampered_request —— 请求被篡改

审计事件类型由 xpack.security.audit.logfile.events.include/exclude 控制,取证时确认该配置是否被攻击者关闭过。

HTTP 访问层

Elasticsearch 本身没有 access log,HTTP 请求日志需前置代理(Nginx)或 audit log 的 access_granted 事件。若现场部署了 Kibana,其日志(/var/log/kibana/ 或 journalctl)与 Nginx access.log 可补充访问来源。

系统日志

journalctl -u elasticsearch -e --no-pager
cat /var/log/auth.log      # SSH 登录(谁登上了节点)
cat /var/log/syslog