首次公开!Golang打造的Githack及其深度解析

发表时间: 2024-02-15 10:56

前言

相信不少打过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 -F1HEADconfig*descriptionhooks/info/objects/refs/

需要重点关注的是HEAD 文件、(还未创建的)index 文件,和 objects 目录、refs 目录。这些文件或目录是 git 的核心组成部分,其中HEAD文件保存了当前所在分支,index文件保存了暂存区信息,objects目录存储了所有数据内容,refs目录则存储了指向数据的指针。

数据对象(blob object)

Git是一个内容寻址文件系统,这意味着 Git 的核心部分是一个简单的键值对数据库,我们可以简单地往数据库中插入任意类型的内容,数据库会返回一个键值,通过该键值可以在任意时刻再次检索该内容。

我们可以使用git的底层子命令:hash-object来理解上述这段话。首先,我们要确定objects目录为空:

Plaintext$ git init testInitialized empty Git repository in /tmp/test/.git/$ cd test$ find .git/objects

接下来,我们往数据库中存储一段内容:

Plaintext$ echo 'test content' | git hash-object -w --stdind670460b4b4aece5915caf5c68d12f560a9fe3e4

-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 d670460b4b4aece5915caf5c68d12f560a9fe3e4test 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-tree80865964295ae2f11d27383e5f9c0b58a8ef21da

为了验证其是一个树对象,可以使用cat-file:

Plaintext$ git cat-file -p 80865964295ae2f11d27383e5f9c0b58a8ef21da100644 blob d670460b4b4aece5915caf5c68d12f560a9fe3e4    test.txt

最后我们再使用status来查看一下当前git状态:

Plaintext$ git statusOn branch masterNo commits yetChanges to be committed:  (use "git rm --cached <file>..." to unstage)        new file:   test.txtChanges 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 80865964295ae2f11d27383e5f9c0b58a8ef21da97147cdc4915da7d51e98cd99cea5705e5e98045$ git cat-file -p 97147cdc4915da7d51e98cd99cea5705e5e98045tree 80865964295ae2f11d27383e5f9c0b58a8ef21daauthor WAY29 41830147+WAY29@users.noreply.github.com 1698912061 +0800committer WAY29 41830147+WAY29@users.noreply.github.com 1698912061 +0800first 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,执行以下代码:

Plaintexterr = 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 版本,对这个新功能进行试用与反馈。

本文作者:yaklang, 转载请注明来自FreeBuf.COM