搜索全站

输入至少 2 个字符,查找相关内容。

↑↓ 选择 Enter 打开Esc 关闭

MongoDB 与 Redis 取证

这两类存储放在同一篇,不是凑数——它们共享一个最关键的取证特征:大量有价值的数据可能不在磁盘文件里。

最后更新 2026-10-09版本 v2.0维护 DigiForensics查看历史 1

一、概述

这两类存储放在同一篇,不是凑数——它们共享一个最关键的取证特征:大量有价值的数据可能不在磁盘文件里。

存储 磁盘上的主要证据 不在磁盘上的部分 核心风险
MongoDB WiredTiger 数据文件、oplog(逻辑集合)、journal 未刷盘的内存缓冲、事务快照 按 data.db(旧引擎)去找,会完全找错文件
Redis RDB 快照、AOF 日志 ★ 未落盘的内存数据集——可能占全部数据的大部分 只查 RDB 会漏掉"最后快照之后"的关键操作

需要刻进认知的两条:

⭐ Redis 的主要证据很可能根本不在磁盘上

RDB 是周期快照,AOF 是写命令追加但可被重写裁剪。两次快照之间、以及尚未落盘的全部操作,只存在于 Redis 进程的内存里。 因此 Redis 取证必须把内存镜像作为一等检材,不能只拷 dump.rdb。

⭐ MongoDB 的 oplog 是逻辑集合,物理上是 WiredTiger 文件

oplog 存在 local 库的 oplog.rs 集合中,但它不是独立文件,而是通过 catalog 映射到 collection-*.wt 上的普通集合。"去找一个叫 oplog 的文件"是错误路径。

在取证流程中的位置:本篇属于分析环节,但它的第一个动作发生在采集规划时——服务器类案件的检材清单里必须提前写明 Redis 的内存镜像,只申请磁盘文件就来不及了。上游依赖是只读挂载与安全接入:为读 oplog 而启动一个隔离的 mongod 实例是本领域少见的写风险动作,必须在副本上做,原件目录挂 chmod -R a-w。时间线对齐按 数据库取证通用方法 的时区纪律走;Redis 的会话键要与内存里的连接态互相印证时,用 内存中的凭据与密钥 那条路径。

本篇处理的证据形态,MongoDB 侧落在 /var/lib/mongodb/ 下:

证据 文件或字段 取证价值
引擎配置 WiredTiger 决定用哪套工具、能否整体归档
数据块文件 collection-0--<oid>.wt、index-*.wt 实际数据与索引所在
catalog WiredTiger.turtle 集合名到文件名的映射表,解不出它就只能看到一堆无名块文件
journal WiredTigerLog/log-0000000001.wt 未 checkpoint 的写入、未提交事务
oplog local 库的 oplog.rs 集合 逻辑变更流水,op 字段区分 i/u/d,ts 是时间戳
旧引擎 data.db 见于 3.x 时代,按它去找会完全找错文件
启动参数 /etc/mongod.conf 的 security、keyFile 段 是否启用加密与认证

Redis 侧分配置与数据两处。配置里要看 dir 与 dbfilename(默认组合 /var/lib/redis/dump.rdb,别默认路径,实例可能改过)、save 策略行(它决定 RDB 快照周期,也就是两次快照之间的空窗有多大)、appendonly yes 时 appendonlydir/ 里的 AOF 分片、以及 appendfsync 的取值——everysec 意味着最多丢一秒的写入,丢的是一个时间窗口而不是一个文件。rename-command 被改过、masterauth 出现、bind 暴露到 0.0.0.0,都是现成的攻击痕迹,不必等分析阶段再去挖。

MongoDB 有哪些集合、每集合的物理文件在哪、oplog 里最近发生过哪些写删改(尤其是已删除文档的旧值)、journal 里有没有未提交或已回滚的事务;Redis 当前有哪些键、键的类型与 TTL、INFO 层面的运行时统计、AOF 或 RDB 覆盖到的最后一个时间点,以及配置层面有没有被削弱的安全项。

磁盘文件给不出"当前"状态——Redis 的写入若未落盘,只有内存镜像能补;MongoDB 的 checkpoint 间隔也会让磁盘落后于实际。两者都证明不了**"某个键或某个文档从未存在过":Redis 的 LRU 淘汰会主动删键,AOF 重写会把旧命令裁掉,MongoDB 的集合被 drop 后旧数据块可能还留在文件里但元数据已失联。oplog 里的记录只证明某条命令被执行过**,不证明它属于哪次业务请求;$natural 是存储序不是时间序。跨系统比对时间时,BSON Date 与 AOF 时间戳都要按 数据库取证通用方法 的规则处理时区。

读者前提:需要能区分「事务日志」与「数据文件」这类概念——WiredTiger 的 journal 是预写日志,AOF 是命令追加,两者都不是备份,没开它们也不等于数据不安全,只是取证路径变了;需要知道集合与文件的映射关系不是靠文件名直读出来的,WiredTiger.turtle 是绕不开的一步;需要理解内存镜像取证的前提条件(见 内存证据的可采性与报告),否则很容易把「Redis 里没找到」写成「Redis 里没有」。没接触过这两种存储的读者,先看本章的目录构成判定引擎,再回来看后面的工具链。


二、核心原理

2.1 MongoDB 的实际文件布局

现代 MongoDB(3.2 起默认 WiredTiger 引擎)的数据目录长这样:

/var/lib/mongodb/
├── WiredTiger           # ★ 引擎主元数据文件(catalog 映射在这里)
├── WiredTiger.turtle     # 统计与元信息日志
├── WiredTigerLog.*       # 引擎 journal(= 引擎自己的 redo)
├── journal/              # 早期版本(mmapv1 时代)的 journal 目录
├── collection-0.wt       # 集合数据
├── index-1.wt            # 索引
└── diagnostic.data       # FTDC 诊断快照(可能含慢查询、连接信息)
事实 取证含义
集合数据与索引分文件存放 collection-N.wt 与 index-N.wt 必须配套取,单取 collection 得不到索引字段
文件名数字 N 不代表集合名 必须解析 WiredTiger catalog 文件才能建立"N → 集合名"映射
WiredTiger(无扩展名)是元数据主文件 它是打开数据库的入口,缺它整库不可映射
实际数据是 BSON 文档 一个文档可能跨多个 extent(4 KB 块),不是行式结构

关于 data.db:这是 **mmapv1 引擎时代(≤3.0)**的布局。不能把「读 data.db 前若干字节之后即为 BSON」当作所有版本的通则——WiredTiger 的块结构、压缩(Snappy/Zstd)与加密都与之完全不同。先判引擎,再谈解析。

2.2 oplog:逻辑集合,不是文件

oplog 记录每个写操作的 BSON 形式,是逻辑序列而非物理页。

① 恢复删除前值。 op: "d" 的条目含被删文档的完整原始 BSON,与 MySQL binlog 的价值完全对应。

② 重建时间线。 每条含 ts(秒 + 递增计数)、wall(客户端时间)、op、ns(库.集合)。

③ 证明时间基准。 可用于校准本机时间与其他来源的偏差。

关键提醒:oplog 位于 local 库,其保留策略由大小上限而非时间决定,是一个可被 truncate 的滚动窗口。因此「oplog 里没有」绝不能等同于「没发生过」。

未加密的 WiredTiger .wt 也可用专用工具直接解析,但版本匹配要求高,结论必须以工具实际输出为准。

2.3 Redis 的持久化三形态

形态 文件 内容 缺失时的影响
RDB dump.rdb 某一时刻的完整数据集快照(二进制紧凑格式,头部为 REDIS<版本号>) 回到最近一次快照,之后全部操作不可见
AOF appendonlydir/appendonly.aof.manifest + *.incr.aof + *.base.rdb(6.x 为单文件 appendonly.aof) 写命令追加日志 丢失 AOF → 无增量可补
无持久化 无 全部数据只在内存 重启即全部丢失,取证只能靠内存镜像
redis-check-rdb dump.rdb              # 校验与统计(不加载数据)
rdbtools --dump dump.rdb > rdbdump.json

AOF 的两种形态(版本不同,文件名与结构完全不同,必须先确定版本再选工具):

版本 形态 文件 取证要点
6.x 及更早 单文件 AOF appendonly.aof RESP 文本命令流,删改是原地重写,被后续命令覆盖的旧值无法保留
7.0 起默认 多部分 AOF appendonlydir/ 下的 appendonly.aof.manifest + *.base.rdb + *.incr.aof manifest 列出每个文件的 seq 与角色;*.base.rdb 是某时刻基线,*.incr.aof 是其后的增量命令

版本判据有三处可互证:目录中是否存在 appendonlydir/ 与 .manifest、内存中的 redis_version 字段、镜像内 redis.conf 文本。

看错版本会导致整个 AOF 段解析为空,且不会报错。

文件内 SELECT 指示数据库切换,MULTI/EXEC 指示事务块。

★ 一条必须记住的局限:DEL key 命令本身只含 key 名,不含旧值。Redis 的删除前值只能从 RDB 快照、更早的 AOF 或内存镜像中取得,这与 MySQL ROW 格式 binlog 直接给出 Delete_rows 旧值镜像有本质差别。这是 Redis 取证必须依赖内存镜像的根本原因之一。

2.4 内存取证:Redis 的核心战场

DigiForensics-mem.raw 中要拿的东西:

目标 取证价值
键空间与键清单 完整键列表,是「存在哪些数据」的直接证明
键类型标记 $(string) *(list) -(set) ~(hash),决定如何解释后续字节
键名中的业务标识 订单号、用户 ID、会话 ID、token
嵌入的 JSON / protobuf 可能直接包含业务内容
过期时间字段 区分「已过期未删除」与「仍有效」
AOF 缓冲区 aof_buf 尚未写入磁盘的写命令——关键增量

★ aof_buf 往往是最有价值、也最易被忽视的目标。 它是"已经执行但还没落盘"的命令序列。若关键行为发生在最后一次 AOF 重写之后、且进程尚未刷盘,那么这些行为在磁盘上完全不存在,只有内存里有。

先确认 Redis 是否在内存中,避免在错误的镜像上做无效工作

strings -n 8 DigiForensics-mem.raw | grep -E '^redis_version|^run_id' | head

2.5 主动导出的边界与风险

若服务端可达,redis-cli 提供了几条取证友好的路径:

命令 性质 取证适用性
INFO / CONFIG GET 只读 优先使用
--rdb <file> 客户端拉取当前数据集快照 可用;但它是快照,不是进程内存镜像
DUMP key 序列化单个键 需 RDB 格式;可与 RDB 交叉验证
BGSAVE 服务端写 RDB 会修改文件系统,取证中应先评估
DEBUG RELOAD 清空并从 RDB 重新加载 ⚠️ 高危:内存中未落盘的数据将被永久销毁

DEBUG RELOAD 在取证中应当被视为禁止操作。 它的作用是"丢掉当前内存、用磁盘快照重载"——如果内存里有未落盘的关键数据,执行它等于主动销毁证据。 该命令在部分版本中已被禁用或需特殊编译选项,行为以实际版本为准。 取证纪律:只读优先;任何可能改变状态的命令,必须先书面说明影响并取得委托方确认。

库跑在容器里时,RDB 文件与 dump 出来的副本都在卷内,需要先确定它挂在哪个卷上再动手,见第 03 篇。


三、操作步骤

3.1 MongoDB:确定引擎、归档与文件映射

1) 固定检材

sha256sum DigiForensics.dd DigiForensics-mem.raw | tee hashes.txt

2) 引擎判定

ls /mnt/case/var/lib/mongodb/WiredTiger 2>/dev/null && echo "→ WiredTiger" ls /mnt/case/var/lib/mongodb/data.db 2>/dev/null && echo "→ mmapv1(老版本)"

3) 整体归档(catalog + collection + index + journal 一个都不能少)

cd /mnt/case/var/lib/mongodb tar -cf mongo-db.tar WiredTiger WiredTiger.turtle WiredTigerLog.*
collection-.wt index-.wt diagnostic.data 2>/dev/null sha256sum mongo-db.tar

4) 建立 N → 集合名映射(复制后启动,禁止在原始路径上启动)

cp -a /mnt/case/var/lib/mongodb /work/mongo-copy chmod -R a-w /mnt/case/var/lib/mongodb # 原始目录立刻置只读 mongod --dbpath /work/mongo-copy --port 27018 --bind_ip 127.0.0.1 --fork mongosh --port 27018 --quiet --eval
'db.adminCommand({listDatabases:1}).databases.forEach(d=>print(d.name))'

最常见的错误:只拷 collection-*.wt 而漏掉 WiredTiger。没有 catalog 文件就无法知道哪个文件对应哪个集合,此时拿到的文件只是一堆无法解释的块。

3.2 MongoDB:oplog 时间线重建

mongosh --port 27018 --quiet --eval '
const s = db.getSiblingDB("local");
s.oplog.rs.find({op:{$in:["d","u","i"]}}).sort({$natural:-1}).limit(20)
 .forEach(e => printjson({ts: e.ts.toString(), op: e.op, ns: e.ns, o: e.o}));
// 顺带核算 oplog 窗口(必须写进报告)
const a = s.oplog.rs.find().sort({$natural:1}).limit(1).toArray()[0];
const b = s.oplog.rs.find().sort({$natural:-1}).limit(1).toArray()[0];
print("oplog 覆盖跨度(秒):" + (b.ts.t - a.ts.t));'
op 含义 旧值可读?
i insert —
u update ★ 含变更前后的字段
d delete ★ 含被删文档完整 BSON
c command(如 drop) 视命令而定

oplog 中的删除事件形态:

{ ts: Timestamp({ t: 1724371999, i: 1 }),
  wall: ISODate('2024-08-22T11:33:19.000+08:00'),
  op: 'd',
  ns: 'shopdb.orders',
  o: { _id: ObjectId('66a4c2f1e3b2a10012ab34cd'),
       orderNo: 'SO20240822000931',
       amount: Decimal128('8800.00'),
       userId: 'U100245',
       status: 'paid' } }

这条 oplog 条目独立证明:2024-08-22 该订单存在,金额 8800.00,状态 paid,随后被删除。 即使集合中已无此文档,oplog 只要还在窗口内,事实就可复原。

3.3 MongoDB:加密实例的处理

若启用了 WiredTiger 加密,.wt 内容不可解析。

处理顺序:① 找配置中 encryption.keyFile 指向的密钥文件、配置中心凭据、运维交接材料;② 在内存镜像中搜索 master key(配置加载时会将其放入进程内存);③ 两者皆无 → 明确记录"无法解密",说明影响范围,不得推测内容。

grep -Ei 'encryption|keyFile|enableEncryption' /etc/mongod.conf
xxd /etc/mongo-keyfile | head -6      # 密钥文件本身是 96 字节原始密钥

3.4 Redis:定位、归档与离线解析

1) 定位配置与数据目录

ps aux | grep '[r]edis-server' grep -E '^\s*(dir|dbfilename|appendonly|appendfsync|save|rename-command)' /etc/redis/redis.conf

2) 归档(AOF 目录整体取走,manifest 不能缺)—— 两种版本二选一

cd /mnt/case/var/lib/redis tar -cf redis-data.tar dump.rdb appendonlydir/ 2>/dev/null # 7.0+ 多部分 AOF tar -cf redis-data.tar dump.rdb appendonly.aof 2>/dev/null # 6.x 单文件 AOF sha256sum redis-data.tar

3) 离线解析(manifest 列出 base/incr 两部分)

redis-check-rdb dump.rdb rdbtools --dump dump.rdb > rdbdump.json 2>/dev/null cat appendonlydir/appendonly.aof.manifest

配置项 取证意义
dir + dbfilename 数据文件实际路径——常被改到非默认目录,容易漏掉
appendfsync always(每条落盘)/ everysec(最多丢 1 秒)/ no(由 OS 决定,可能丢很多)
save RDB 快照策略;save 900 1 = 900 秒内 1 次修改就存
rename-command 是否禁用了危险命令;被禁用的 DEBUG 印证 2.5 的风险提示

AOF 命令解码器(RESP 协议,可直接运行):

cat > resp.py <<'PY' import sys d = open(sys.argv[1], 'rb').read()

第三个参数为输出上限,仅用于抽样预览;不传则完整解码全文件

cap = int(sys.argv[2]) if len(sys.argv) > 2 else None i = n = 0 while i < len(d) and (cap is None or n < cap): if d[i:i+1] != b'*': # 跳过非数组头(SELECT、
、注释等) i += 1; continue j = d.find(b'\r
', i) if j < 0: break argc, args, i = int(d[i+1:j]), [], j + 2 for _ in range(argc): if d[i:i+1] != b'$': args = []; break j = d.find(b'\r
', i); ln = int(d[i+1:j]); i = j + 2 args.append(d[i:i+ln].decode('utf-8', 'replace')); i += ln + 2 if args: n += 1; print(f"[{n}] {' '.join(args)}") print(f"# 共解码 {n} 条命令", file=sys.stderr) PY

python3 resp.py appendonlydir/appendonly.aof.1.incr.aof > aof-cmds.txt # 完整解码 python3 resp.py appendonlydir/appendonly.aof.1.incr.aof 20 # 仅抽样看格式 grep -an 'DEL|UNLINK|FLUSHALL|FLUSHDB|CONFIG SET' aof-cmds.txt | head -40

统计口径:aof-cmds.txt 的行数是命令条数(一条 AOF 命令一行,MULTI/EXEC 块内命令同样逐条计数)。取证报告中引用"解码出 N 条命令"时必须说明这个口径,不要把它与"数据条数"或"键数量"混用。

3.5 Redis:内存镜像分析(重点)

1) 确认 Redis 驻留痕迹与配置

strings -n 6 DigiForensics-mem.raw | grep -E 'redis_version|redis_mode|run_id' | sort -u

2) 键空间清单(★ 最重要的一步)

典型形态:order:SO20240822000931 / session:9f3a2b7c41e5 / cart:8d2f1a

strings -n 4 DigiForensics-mem.raw | grep -E '^[a-zA-Z][a-zA-Z0-9_]*:[A-Za-z0-9_.:-]+$'
| sort -u > keys.txt wc -l keys.txt && head -20 keys.txt

3) 业务内容:嵌入 JSON 的值

strings -n 12 DigiForensics-mem.raw | grep -E '^{"(orderId|userId|amount|token)' | head -2

安全验证 当前请求需要先完成一次滑块验证。