@Longlone师傅
前言
相信不少打过ctf的师傅对githack这个词并不陌生,它实际上是利用git源码泄露漏洞的工具名称。Git 源码泄露漏指的的是由于配置不当,客户端可以通过 http/https 协议直接访问服务器本地 .git 文件夹中的内容。而我们知道,.git文件夹是 Git(版本控制工具)存储代码信息的文件夹,这意味着我们的源码可能会通过该文件夹泄露出去。
小实验:通过.git文件夹恢复文件
在此之前,师傅们可以先做一个小实验,即将一个代码仓库的 .git 文件夹单独复制出来到另外的一个文件夹中,看看会发生什么?
可以看到,执行git status后依然能看到被删除的文件,这时候只需要简单地执行git checkout -- .恢复最后一次commit的提交时的状态,被删除的文件也就恢复了,这实际上证明,假如能下载 .git 文件夹,就相当于下载了源码。
Githack原理
知道了上述这点之后,我们关注的重点变成了如何下载 .git 文件夹。一个最简单的情况是该目录由于中间件( apache / nginx) 配置不当导致其能直接目录遍历,这时候只需要通过 wget -r或者编写脚本递归遍历下载文件夹和文件即可。
假如不能目录遍历,那么我们就需要先了解Git内部原理,才能够进行接下来的工作。(Git内部原理 章节的内容参考 Pro Git第二版)
Git内部原理
首先要明白的是, Git 本质上是一个现代化的版本控制系统,而我们常用的 git 命令则是操纵这个系统的命令行工具。
当我们通过git init命令创建一个新的存储库时,其 .git 目录如下所示:
Plaintext
$ ls -F1
HEAD
config*
description
hooks/
info/
objects/
refs/
需要重点关注的是HEAD 文件、(还未创建的)index 文件,和 objects 目录、refs 目录。这些文件或目录是 git 的核心组成部分,其中HEAD文件保存了当前所在分支,index文件保存了暂存区信息,objects目录存储了所有数据内容,refs目录则存储了指向数据的指针。
数据对象(blob object)
Git是一个内容寻址文件系统,这意味着 Git 的核心部分是一个简单的键值对数据库,我们可以简单地往数据库中插入任意类型的内容,数据库会返回一个键值,通过该键值可以在任意时刻再次检索该内容。
我们可以使用git的底层子命令:hash-object来理解上述这段话。首先,我们要确定objects目录为空:
Plaintext
$ git init test
Initialized empty Git repository in /tmp/test/.git/
$ cd test
$ find .git/objects
接下来,我们往数据库中存储一段内容:
Plaintext
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
-w 选项指示 hash-object 命令存储数据对象;若不指定此选项,则该命令仅返回对应的键值。--stdin 选项则指示该命令从标准输入读取内容;若不指定此选项,则须在命令尾部给出待存储文件的路径。该命令会返回一段 SHA-1 哈希值作为键值,现在我们再来看看 .git/objects 文件夹:
Plaintext
$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
现在可以在 objects 目录下看到一个文件。这就是 Git 存储内容的方式—— 一个文件对应一条内容,以计算出来的 SHA-1 哈希值为文件命名。校验和的前两个字符用于命名子目录,余下的 38 个字符则用作文件名。
使用git的底层子命令:cat-file可以读取刚刚存储的内容,为 cat-file 指定 -p 选项可指示该命令自动判断内容的类型,并为我们显示格式友好的内容:
Plaintext
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
树对象(tree object)
接下来要讨论的对象类型是树对象,它能解决文件名保存的问题,也允许我们将多个文件组织到一起。Git 以一种类似于 UNIX 文件系统的方式存储内容,所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容。一个树对象包含了一条或多条树对象记录,每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息,一个示例图如下:
为了创建一个树对象,我们首先需要通过暂存一些文件来创建暂存区,可以使用 git 的底层子命令:update-index来完成:
Plaintext
$ git update-index --add --cacheinfo 100644,d670460b4b4aece5915caf5c68d12f560a9fe3e4,test.txt
--add选项是必须的,因为此前该数据并不存在于暂存区中,该选项会将该数据加入暂存区。--cacheinfo也是必须的,这是因为我们将一段数据而非普通文件加入暂存区。其后跟着以逗号分隔的三个参数,分别为文件模式(这里为100644,即普通文件),SHA-1 键值与想要保存为的文件名。
执行完该命令后不会有任何输出,接下来我们再使用 git 的底层子命令:write-tree来创建一个树对象。
Plaintext
$ git write-tree
80865964295ae2f11d27383e5f9c0b58a8ef21da
为了验证其是一个树对象,可以使用cat-file:
Plaintext
$ git cat-file -p 80865964295ae2f11d27383e5f9c0b58a8ef21da
100644 blob d670460b4b4aece5915caf5c68d12f560a9fe3e4 test.txt
最后我们再使用status来查看一下当前git状态:
Plaintext
$ git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: test.txt
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
deleted: test.txt
可以看到我们已经将数据放入了暂存区中,由于当前文件夹中并没有 test.txt 这个文件,所以其显示为 deleted 。
提交对象(commit object)
提交对象解决了以下问题:假如我们存在多个树对象,而每个树对象则有一个独立的键值,我们不可能去记忆所有的键值。
所以我们将树对象提交为提交对象,在其上附加上作者信息/作者邮箱以及一段注释。
使用 git 的底层子命令:write-tree来从一个树对象中创建提交对象。
Plaintext
$ echo 'first commit' | git commit-tree 80865964295ae2f11d27383e5f9c0b58a8ef21da
97147cdc4915da7d51e98cd99cea5705e5e98045
$ git cat-file -p 97147cdc4915da7d51e98cd99cea5705e5e98045
tree 80865964295ae2f11d27383e5f9c0b58a8ef21da
author WAY29 41830147+WAY29@users.noreply.github.com 1698912061 +0800
committer WAY29 41830147+WAY29@users.noreply.github.com 1698912061 +0800
first commit
实际上,在正常使用 git 时,当前提交对象还会指向前一个提交对象(第一个提交对象则不会指向任何提交对象),以此形成一个链表,一个提交对象的示例图如下:
除了上述提交的几个对象之外,实际上git还存在tag和repack对象,在此不再做介绍。
具体实现
在了解了 Git 内部原理之后,我们就可以开始来编写 Githack 了,在Yaklang中实现的 Githack 使用了 go-git 这个依赖库。其执行流程如下:
使用案例
我们以 ctfhub-技能树-Web-信息泄露-Git泄露-Log 题目为例,以此展示如何在 Yaklang 中使用 Githack ,启动题目后,我们获取靶场地址,这里的靶场地址是:
http://challenge-9615288119a7b693.sandbox.ctfhub.com:10800/
此时我们打开Yakit,执行以下代码:
Plaintext
err = git.GitHack("http://challenge-9615288119a7b693.sandbox.ctfhub.com:10800/", "C:\\Users\\xxx\\Desktop\\git\\gittest")
// 第一个参数是存在 Git 源码泄露的 URL 地址,第二个参数是要存储的目标文件夹
// 后续可以接收多个选项参数,例如 git.threads(10) 指定线程数,git.useLocalGitBinary(true) 指定使用本地环境变量中的git命令进行git fsck进行兜底
// git.httpOpts(poc.redirectTimes(10), poc.https(true)) 指定http请求时请求选项参数
die(err)
执行完毕后,若代码没有出现报错,则证明代码已经恢复成功,我们来查看一下 Git 仓库情况:
可以看到,我们已经成功恢复出 Git 仓库的日志,根据日志我们知道,最新的一次commit已经把 flag 删除了,所以我们需要切换到的第二个 commit ,即add flag这个 commit ,之后我们直接 ls 即可看到 flag ,读取即可。
结合上面的案例,我们可以知道在 Yaklang 中调用 Githack 是一件非常简单的事情,也十分欢迎广大师傅们更新最新 Yaklang 版本,对这个新功能进行试用与反馈。