Skip to content

Commit

Permalink
fix(*): #21 fix RCE via core.fsmonitor in .git/config and `.git/h…
Browse files Browse the repository at this point in the history
…ooks/*`
  • Loading branch information
WangYihang committed Mar 1, 2022
1 parent 1ac385e commit f691b18
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 78 deletions.
211 changes: 137 additions & 74 deletions GitHacker/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import requests
import os
import threading
import queue
import argparse
import bs4
import coloredlogs
import git
import logging
import os
import queue
import re
import git
import requests
import shutil
import subprocess
import argparse
import bs4
import tempfile
import shutil
import threading


__version__ = "1.1.0"
Expand All @@ -24,7 +24,7 @@ def md5(data):


class GitHacker():
def __init__(self, url, dst, threads=0x08, brute=True) -> None:
def __init__(self, url, dst, threads=0x08, brute=True, disable_manually_check=True) -> None:
self.q = queue.Queue()
self.url = url
self.dst = tempfile.mkdtemp()
Expand All @@ -34,6 +34,57 @@ def __init__(self, url, dst, threads=0x08, brute=True) -> None:
self.max_semanic_version = 10
self.brute = brute
self.verify = False
self.disable_manually_check = disable_manually_check
self.default_git_files_maybe_dangerous = [
[".git", "config"],
[".git", "hooks", "applypatch-msg.sample"],
[".git", "hooks", "applypatch-msg"],
[".git", "hooks", "commit-msg.sample"],
[".git", "hooks", "commit-msg"],
[".git", "hooks", "fsmonitor-watchman.sample"],
[".git", "hooks", "fsmonitor-watchman"],
[".git", "hooks", "post-update.sample"],
[".git", "hooks", "post-update"],
[".git", "hooks", "pre-applypatch.sample"],
[".git", "hooks", "pre-applypatch"],
[".git", "hooks", "pre-commit.sample"],
[".git", "hooks", "pre-commit"],
[".git", "hooks", "pre-merge-commit.sample"],
[".git", "hooks", "pre-merge-commit"],
[".git", "hooks", "pre-push.sample"],
[".git", "hooks", "pre-push"],
[".git", "hooks", "pre-rebase.sample"],
[".git", "hooks", "pre-rebase"],
[".git", "hooks", "pre-receive.sample"],
[".git", "hooks", "pre-receive"],
[".git", "hooks", "prepare-commit-msg.sample"],
[".git", "hooks", "prepare-commit-msg"],
[".git", "hooks", "update.sample"],
[".git", "hooks", "update"],
]

self.default_git_files = [
[".git", "COMMIT_EDITMSG"],
[".git", "description"],
[".git", "FETCH_HEAD"],
[".git", "HEAD"],
[".git", "index"],
[".git", "info", "exclude"],
[".git", "logs", "HEAD"],
[".git", "logs", "refs", "remotes", "origin", "HEAD"],
[".git", "logs", "refs", "stash"],
[".git", "ORIG_HEAD"],
[".git", "packed-refs"],
[".git", "refs", "remotes", "origin", "HEAD"],
# git stash
[".git", "refs", "stash"],
# pack
[".git", "objects", "info", "alternates"],
[".git", "objects", "info", "http-alternates"],
[".git", "objects", "info", "packs"],
]

self.complete_basic_files_list()

def start(self):
# Ensure the target is a git folder via `.git/HEAD`
Expand Down Expand Up @@ -66,6 +117,28 @@ def sighted(self):
self.add_folder(self.url, ".git/")
self.q.join()
return self.git_clone()

def is_dangerous_git_file(self, filepath):
normalized_path = os.path.normpath(filepath)
# We consider all files not in self.default_git_files_maybe_dangerous
# are safe.But that could be dangerous when git add another config file
# someday which may lead to another RCE, so this function should be more
# conservative to return False. Maybe a white list is safer. (TODO)
for dangerous_git_file in self.default_git_files_maybe_dangerous:
dangerous_git_filepath = os.path.sep.join(dangerous_git_file)
if normalized_path.endswith(dangerous_git_filepath):
return True

# The following operation will mark any files under `.git/hooks` to be
# dangerous. Consider all git hooks could be dangerous, this operation
# is not redundant with the previous for loop, because the git may add
# more default hook files someday. I don't want to continuously maintain
# the self.default_git_files_maybe_dangerous blacklist.
normalized_folder = os.path.split(normalized_path)[0]
if normalized_folder.endswith(os.path.sep.join([".git", "hooks"])):
return True

return False

def add_folder(self, base_url, folder):
url = f"{base_url}{folder}"
Expand All @@ -80,8 +153,10 @@ def add_folder(self, base_url, folder):
self.add_folder(url, href)
else:
file_url = f"{url}{href}"
path = file_url.replace(self.url, "").split("/")
self.q.put(path)
# The following if statment prevent from access other domain which may lead to CSRF attack.
if file_url.startswith(self.url):
filepath = file_url[len(self.url):].strip().replace("..", "").split("/")
self.q.put(filepath)

def blind(self):
logging.info('Downloading basic files...')
Expand Down Expand Up @@ -158,70 +233,27 @@ def add_head_file_tasks(self):
n += self.add_hashes_parsed(data)
return n

def add_basic_file_tasks(self):
files = [
[".git", "COMMIT_EDITMSG"],
[".git", "config"],
[".git", "description"],
[".git", "FETCH_HEAD"],
[".git", "HEAD"],
[".git", "hooks", "applypatch-msg.sample"],
[".git", "hooks", "commit-msg.sample"],
[".git", "hooks", "fsmonitor-watchman.sample"],
[".git", "hooks", "post-update.sample"],
[".git", "hooks", "pre-applypatch.sample"],
[".git", "hooks", "pre-commit.sample"],
[".git", "hooks", "pre-merge-commit.sample"],
[".git", "hooks", "pre-push.sample"],
[".git", "hooks", "pre-rebase.sample"],
[".git", "hooks", "pre-receive.sample"],
[".git", "hooks", "prepare-commit-msg.sample"],
[".git", "hooks", "update.sample"],
[".git", "hooks", "applypatch-msg"],
[".git", "hooks", "commit-msg"],
[".git", "hooks", "fsmonitor-watchman"],
[".git", "hooks", "post-update"],
[".git", "hooks", "pre-applypatch"],
[".git", "hooks", "pre-commit"],
[".git", "hooks", "pre-merge-commit"],
[".git", "hooks", "pre-push"],
[".git", "hooks", "pre-rebase"],
[".git", "hooks", "pre-receive"],
[".git", "hooks", "prepare-commit-msg"],
[".git", "hooks", "update"],
[".git", "index"],
[".git", "info", "exclude"],
[".git", "logs", "HEAD"],
[".git", "logs", "refs", "remotes", "origin", "HEAD"],
[".git", "logs", "refs", "stash"],
[".git", "ORIG_HEAD"],
[".git", "packed-refs"],
[".git", "refs", "remotes", "origin", "HEAD"],
# git stash
[".git", "refs", "stash"],
# pack
[".git", "objects", "info", "alternates"],
[".git", "objects", "info", "http-alternates"],
[".git", "objects", "info", "packs"],
]

def complete_basic_files_list(self):
# git tags
if self.brute:
for major in range(self.max_semanic_version):
for minor in range(self.max_semanic_version):
for patch in range(self.max_semanic_version):
files.append(
self.default_git_files.append(
[".git", "refs", "tags", f"v{major}.{minor}.{patch}"])
files.append(
self.default_git_files.append(
[".git", "refs", "tags", f"{major}.{minor}.{patch}"])
else:
files.append([".git", "refs", "tags", "v0.0.1"])
files.append([".git", "refs", "tags", "0.0.1"])
files.append([".git", "refs", "tags", "v1.0.0"])
files.append([".git", "refs", "tags", "1.0.0"])

branch_names = ["master", "main", "dev", "release", "test",
"testing", "feature", "ng", "fix", "hotfix", "quickfix"]
self.default_git_files.append([".git", "refs", "tags", "v0.0.1"])
self.default_git_files.append([".git", "refs", "tags", "0.0.1"])
self.default_git_files.append([".git", "refs", "tags", "v1.0.0"])
self.default_git_files.append([".git", "refs", "tags", "1.0.0"])

branch_names = [
"master", "main", "dev", "release",
"test", "testing", "feature", "ng",
"fix", "hotfix", "quickfix",
]

# git remote branches
expand_branch_name_folder = [
Expand All @@ -235,9 +267,14 @@ def add_basic_file_tasks(self):
for branch_name in branch_names:
folder_copy = folder.copy()
folder_copy[-1] = branch_name
files.append(folder_copy)
self.default_git_files.append(folder_copy)

def add_basic_file_tasks(self):
n = 0
for item in files:
for item in self.default_git_files:
self.q.put(item)
n += 1
for item in self.default_git_files_maybe_dangerous:
self.q.put(item)
n += 1
return n
Expand Down Expand Up @@ -275,22 +312,46 @@ def check_file_content(self, content):

def wget(self, url, path):
response = requests.get(url, verify=self.verify)
# path from Apache/Nginx could be dangerous
if ".." in path:
logging.error(f"Malicious repo detected: {url}")
sanitized_path = path.replace("..", "")
logging.warning(f"Replacing {path} with {sanitized_path}")
path = sanitized_path

# if manually check is disabled, we will definitely not downloading any dangerous git files
if self.disable_manually_check and self.is_dangerous_git_file(path):
logging.error(f"{path} is potential dangerous, skip downloading this file")
return (-1, -1, False)

folder = os.path.dirname(path)
try: os.makedirs(folder)
except: pass
status_code = response.status_code
content = response.content
result = False
if status_code == 200 and self.check_file_content(content):
with open(path, "wb") as f:
n = f.write(content)
if n == len(content):
result = True
# if manually check is enabled, we will ask user to confirm the security of the potentially dangerous file
if not self.disable_manually_check and self.is_dangerous_git_file(path):
logging.error(f"{path} is potential dangerous, you need to confirm the content is safe.")
seperator = f"{'-' * 0x10} {path} {'-' * 0x10}"
logging.warning(seperator)
print(content)
safe = input(f"Are you sure that the content of {path} is safe? (y/N)").strip().lower() == 'y'
if safe:
with open(path, "wb") as f:
n = f.write(content)
if n == len(content):
result = True
else:
logging.warning(f"{path} is marked as dangerous, it will not be downloaded.")
result = False
else:
# the file is not dangerous, just save it
with open(path, "wb") as f:
n = f.write(content)
if n == len(content):
result = True
return (status_code, len(content), result)


Expand All @@ -314,7 +375,8 @@ def main():
group.add_argument('--url', help='url of the target website which expose `.git` folder')
group.add_argument('--url-file', help='url file that contains a list of urls of the target website which expose `.git` folder')
parser.add_argument('--output-folder', required=True, help='the local folder which will be the parent folder of all exploited repositories, every repo will be stored in folder named md5(url).')
parser.add_argument('--brute', required=False, help='enable brute forcing branch/tag names')
parser.add_argument('--brute', required=False, default=False, help='enable brute forcing branch/tag names', action='store_true')
parser.add_argument('--disable-manually-check-dangerous-git-files', required=False, default=True, help='disable manually check dangerous git files which may lead to *RCE* (eg: .git/config, .git/hook/fsmonitor-watchman) when downloading malicious .git folders. If this argument is given, GitHacker will not download the files which may be dangerous at all.', action='store_true')
parser.add_argument('--threads', required=False, default=0x04, type=int, help='threads number to download from internet')
parser.add_argument('--version', action='version', version=__version__)
args = parser.parse_args()
Expand All @@ -338,6 +400,7 @@ def main():
dst=folder,
threads=args.threads,
brute=args.brute,
disable_manually_check=args.disable_manually_check_dangerous_git_files,
).start()
if result:
succeed_urls.append(url)
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ audition.

## Security Issues

#### 2021-08-01 [Fixed](https://github.com/WangYihang/GitHacker/commit/e105b5c04329e9c4b8080029976bc73d12b1f23f): Malicious .git folder maybe harmful to the user of this tool
#### 2021-08-01 [Fixed](https://github.com/WangYihang/GitHacker/commit/e105b5c04329e9c4b8080029976bc73d12b1f23f): Malicious .git folder maybe harmful to the user of this tool (Reported by [Driver Tom](https://drivertom.blogspot.com))

* [别想偷我源码:通用的针对源码泄露利用程序的反制(常见工具集体沦陷)](https://drivertom.blogspot.com/2021/08/git.html)

#### 2022-03-01 [Fixed](): Arbitrary file write via recursive file downloader (Reported by [Justin Steven](https://twitter.com/justinsteven))

#### 2022-03-01 [Fixed](): RCE via `.git/config` and `.git/hooks/*` files (Reported by [Justin Steven](https://twitter.com/justinsteven))

## Comparison of other tools

Expand Down Expand Up @@ -61,6 +63,8 @@ githacker --url http://127.0.0.1/.git/ --folder result

## TODO

- [ ] Fix stash files missing due to the fix of #21 (`git clone` can't download stash files)
- [ ] Fix infinit downloading 404 files
- [ ] ~~Download packed files firstly~~ (Unsolvable via [StackOverflow](https://stackoverflow.com/questions/27789484/how-does-git-know-the-sha1-name-of-the-pack-files))
- [x] Download tags and branches when Index enabled
- [x] Try common tags and branches when Index disabled
Expand All @@ -84,6 +88,7 @@ githacker --url http://127.0.0.1/.git/ --folder result

## Acknowledgement

- [Justin Steven](https://twitter.com/justinsteven)
- [Driver Tom](https://drivertom.blogspot.com)
- [lesion1999](https://github.com/lesion1999)

Expand Down
6 changes: 3 additions & 3 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def diffall():
for folder in glob.glob("./test/*"):
basename = os.path.basename(folder)
origin_path = os.path.join('test', basename, "www")
current_path = os.path.join('playground', basename)
current_path = glob.glob(f"{os.path.join('playground', basename)}/*")[0]
same, total, difference, right_absence = diff(
origin_path, current_path)
ratio = (same / total) * 100
Expand Down Expand Up @@ -184,10 +184,10 @@ def main():
with open(os.path.join(html_folder, "index.php"), "w") as f:
f.write("<?php @readfile($_GET['file']);?>")
os.system(
"python3 GitHacker/__init__.py --brute --url 'http://127.0.0.1/?file=../.git/' --folder playground/{}".format(os.path.basename(folder)))
"python3 GitHacker/__init__.py --disable-manually-check-dangerous-git-files --brute --url 'http://127.0.0.1/?file=../.git/' --output-folder playground/{}".format(os.path.basename(folder)))
else:
os.system(
"python3 GitHacker/__init__.py --brute --url 'http://127.0.0.1/' --folder playground/{}".format(os.path.basename(folder)))
"python3 GitHacker/__init__.py --disable-manually-check-dangerous-git-files --brute --url 'http://127.0.0.1/' --output-folder playground/{}".format(os.path.basename(folder)))

# Stop docker
os.chdir(os.path.join(cwd, folder))
Expand Down

0 comments on commit f691b18

Please sign in to comment.