从暴露的 .git/ 目录逐步恢复 Git 仓库文件的技术分析

对于网站上暴露的 /.git/ 目录,我们无法通过 git 命令克隆下整个仓库,并且也无法查找到所引信息。但 /.git/ 目录下,有些文件名称是固定的,如 index 文件和 config 文件,如果我们能够下载这些固定文件,就能够通过这些文件,一点一点地恢复大部分 /.git/ 目录的对象信息。

config 文件是文本文件,能够记录 git 仓库的配置,包括本地的分支名,index 是二进制文件,记录当前暂存区的文件信息,从这里我门可以获得一些文件的 sha1,而通过 sha1 我们就能够找到文件对应的 object 文件的存放位置,从而下载文件。

当然,这里的信息并不一定能下载到整个 .git 目录中的所有 object 文件,所以我们还需要其他信息,前面提到,在 config 文件中能获取本地的分支名,而一般分支的信息会在 ./refs/heads/<分支名> 中保存,这里保存的是 <分支名> 最后一个提交的 sha1。

一个 git 仓库的 config 信息大概如下

[core]
    repositoryformatversion = 0
    filemode = false
    bare = false
    logallrefupdates = true
    symlinks = false
    ignorecase = true
[remote "github"]
    url = 
    fetch = 
[branch "main"]
    remote = github
    merge = refs/heads/main

如果我们获取到的 config 文件时正确的,那么在 branch 节中,就能够获取到这个仓库下的所有分支名

以 main 分支为例,在 .git 文件夹中,输入命令 cat ./refs/heads/main,会输出类似 c90555b479cc957993003410ca5e49da1aaf299f 这样的 sha1 也就是对象文件的 id。输入 git cat-file -t c90555b479cc957993003410ca5e49da1aaf299f 我们得到输出 commit 说明这个文件的确是 commit 对象的文件,那么这个文件存储了什么信息呢?

运行 git cat-file commit c90555b479cc957993003410ca5e49da1aaf299f 命令,我们会得到类似下面的输出:

tree 258f9034136a2e3e960e4022dc912b9bd460c158
parent c1b6f4bc5a453b25af63298391d02197ed9ddbfd
author 秃头灯笼鱼 <ttdlyu@163.com> 1693967346 +0800
committer 秃头灯笼鱼 <ttdlyu@163.com> 1693967346 +0800

release:v1.2.3

我们不难发现这里又出现了两个对象 id,并且一个是 tree,一个是 parent,那么不用查看其类型,一个就是 tree 类型,而另一个仍是 commit 类型,parent 就像指向上一个提交的指针一样,顺着这个指针,我们能够获得所有提交的对象 id,我们再来看看 tree 中记录的消息是什么吧:运行 git cat-file tree 258f9034136a2e3e960e4022dc912b9bd460c158,得到如下输出:

100644 .eslintignore?????{_Ec???c?Q*p100644 .eslintrc.jsonT?{??
                                                               ??x@G?
                                                                     .?B$>
                                                                          100644 .gitattributes.oЌ????׹??W~100644 .gitignore~?M?|
           D?Zw????lV100644 .prettierignore??]??^?MN4x?˯??h?100644 .prettierrc.yml?"cq?)36m?????;{p??   100644 LICENSE?^V?E6?'??mj?&?100644 README.md????s??u|????+????100644 action.yml? 
        f鄊40000 dist?V?@7?ڜ?'
                              s??0??Y100644 logo.svg^?
                                                      ?f?:}?dD?????100644 package-lock.json)"?Id?q??1????vs100644 package.json???:?~
              ?x?{z$??)w100644 pnpm-lock.yaml?^?BW!9١x߭?vTvK??40000 src2?ڨ}?(?<1?oqA?aZn?

我们能够从这一堆看似乱码的文本中,找到几个文件名,但是这个要怎么看?

别急,git cat-file 命令的文档中有提到一个选项:-p pretty-print <object> content 意思是按照对象的类型输出其内容,我们尝试一下 git -p cat-file 258f9034136a2e3e960e4022dc912b9bd460c158 命令,这个命令的输出如下:

100644 blob 849ddff3b7ec917b5f4563e9a6d3ea63ea512a70    .eslintignore
100644 blob 54d57bc0df0ca981784047f50c2e1fbe42243e0b    .eslintrc.json
100644 blob 2e051e1f6fd08cfab71fef7fc7d7b9f4e157137e    .gitattributes
100644 blob 7e874de4a67c0c1744cc5a0e1177bbb887e26c56    .gitignore
100644 blob 93a15d9e17db5ea34d4e3478decbaff507f368c9    .prettierignore
100644 blob 95226371c22933366dc0b68ffad43b7b70999609    .prettierrc.yml
100644 blob e1ef12c82baa1d066656dbe2a9f36d016aa92685    LICENSE
100644 blob a48297f9739e08c1e3757ce7f987dd2b9f8bc3db    README.md
100644 blob 1497200d011ae15e1456a14536a3270b66e9848a    action.yml
040000 tree 9456e84037f1b8da9c9e270c73d8cd0f3088e359    dist
100644 blob 5ecb1e0bbd66b53a1c007df91f6444869290d5d2    logo.svg
100644 blob 2922d64964c2719ece310fe4e8aa0694b0767318    package-lock.json
100644 blob a7c3e33acb7e1e0cc9061c78a67b7a24ced42977    package.json
100644 blob cf5edc425721391fd9a178dfadb77654764b9def    pnpm-lock.yaml
040000 tree 32c0daa87db828a83c31996f7141ea615a6ea714    src

这一次,我们得到的是一个目录信息,并且有各个条目的对象 id,其中 tree 还是目录,blob 就是文件。那么我们可以通过这里提供的信息,结合前面的 parent 对象 id 一点一点的往下排查,就能够获得 .git 目录下大部分的对象文件名称,从而恢复仓库中的文件。

值得注意的是,在 .git 目录中,还有 logs 目录,通过分支名就能够获得这个分支的一些日志消息,分支对应的日志在 ./logs/refs/heads/<分支名>,访问这个文件,我们就能够得到如下的文本信息:

0000000000000000000000000000000000000000 c90555b479cc957993003410ca5e49da1aaf299f ttdly <ttldyu@163.com> 1750479202 +0800   clone: from github.com:ttdly/poke.git

但这里的信息是否准确就不知道了,比如我这里从 github 中克隆的仓库,它只记录了我克隆的操作,远程仓库中记录的提交信息并没有在这里被记录。

cd ..