gitlabrat 1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitlabrat-1.0.dist-info/METADATA +72 -0
- gitlabrat-1.0.dist-info/RECORD +22 -0
- gitlabrat-1.0.dist-info/WHEEL +4 -0
- gitlabrat-1.0.dist-info/entry_points.txt +2 -0
- gitlabrat-1.0.dist-info/licenses/LICENSE +674 -0
- labrat/__init__.py +0 -0
- labrat/cli/__init__.py +0 -0
- labrat/cli/agents.py +60 -0
- labrat/cli/auth.py +60 -0
- labrat/cli/common.py +76 -0
- labrat/cli/projects.py +95 -0
- labrat/cli/users.py +40 -0
- labrat/controllers/__init__.py +0 -0
- labrat/controllers/agents.py +47 -0
- labrat/controllers/auth.py +55 -0
- labrat/controllers/projects.py +164 -0
- labrat/controllers/users.py +68 -0
- labrat/core/__init__.py +0 -0
- labrat/core/agent.py +120 -0
- labrat/core/config.py +63 -0
- labrat/core/utils.py +106 -0
- labrat/main.py +19 -0
labrat/core/agent.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from urllib.parse import urlparse
|
|
2
|
+
import requests
|
|
3
|
+
from bs4 import BeautifulSoup
|
|
4
|
+
import gitlab
|
|
5
|
+
|
|
6
|
+
class Agent:
|
|
7
|
+
def __init__(self, url, use_ldap=False, username=None, password=None, private_token=None, api_version=4, section=None):
|
|
8
|
+
self.url = url
|
|
9
|
+
self.host = urlparse(url).hostname
|
|
10
|
+
self.use_ldap = use_ldap
|
|
11
|
+
self.username = username
|
|
12
|
+
self.password = password
|
|
13
|
+
self.private_token = private_token
|
|
14
|
+
self.api_version = api_version
|
|
15
|
+
self.section = section
|
|
16
|
+
|
|
17
|
+
self.session = requests.Session()
|
|
18
|
+
self.gitlab = None
|
|
19
|
+
self.id = None
|
|
20
|
+
self.is_admin = False
|
|
21
|
+
self.is_bot = False
|
|
22
|
+
self.is_authenticated = False
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def label(self):
|
|
26
|
+
return f"{self.username}@{self.host}"
|
|
27
|
+
|
|
28
|
+
def auth(self, private_token=None):
|
|
29
|
+
if private_token is not None:
|
|
30
|
+
self.private_token = private_token
|
|
31
|
+
|
|
32
|
+
self.gitlab = gitlab.Gitlab(self.url, private_token=self.private_token, keep_base_url=True, api_version=self.api_version)
|
|
33
|
+
self.gitlab.auth()
|
|
34
|
+
self.is_authenticated = True
|
|
35
|
+
|
|
36
|
+
user = self.gitlab.user
|
|
37
|
+
self.id = user.id
|
|
38
|
+
self.username = user.username
|
|
39
|
+
self.section = self.section if self.section else f"{self.id}@{self.host}"
|
|
40
|
+
self.is_admin = getattr(self.gitlab.user, 'is_admin', False)
|
|
41
|
+
self.is_bot = getattr(self.gitlab.user, 'bot', False)
|
|
42
|
+
|
|
43
|
+
def get_csrf_token(self):
|
|
44
|
+
r = self.session.get(f"{self.url}/")
|
|
45
|
+
soup = BeautifulSoup(r.text, "html.parser")
|
|
46
|
+
return soup.find("meta", {"name": "csrf-token"})["content"]
|
|
47
|
+
|
|
48
|
+
def login(self):
|
|
49
|
+
csrf_token = self.get_csrf_token()
|
|
50
|
+
if self.use_ldap:
|
|
51
|
+
payload = {
|
|
52
|
+
"authenticity_token": csrf_token,
|
|
53
|
+
"username": self.username,
|
|
54
|
+
"password": self.password,
|
|
55
|
+
"user[remember_me]": 1,
|
|
56
|
+
}
|
|
57
|
+
self.session.post(f"{self.url}/users/auth/ldapmain/callback", data=payload)
|
|
58
|
+
else:
|
|
59
|
+
payload = {
|
|
60
|
+
"authenticity_token": csrf_token,
|
|
61
|
+
"user[login]": self.username,
|
|
62
|
+
"user[password]": self.password,
|
|
63
|
+
"user[remember_me]": 1,
|
|
64
|
+
}
|
|
65
|
+
self.session.post(f"{self.url}/users/sign_in", data=payload)
|
|
66
|
+
|
|
67
|
+
def create_pat(self, token_name="private token", token_scopes=["api", "read_repository", "write_repository"]):
|
|
68
|
+
csrf_token = self.get_csrf_token()
|
|
69
|
+
payload = {
|
|
70
|
+
"personal_access_token[name]": token_name,
|
|
71
|
+
"personal_access_token[scopes][]": token_scopes,
|
|
72
|
+
}
|
|
73
|
+
headers = {"X-CSRF-Token": csrf_token}
|
|
74
|
+
r = self.session.post(
|
|
75
|
+
f"{self.url}/-/user_settings/personal_access_tokens",
|
|
76
|
+
data=payload,
|
|
77
|
+
headers=headers
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
response_json = r.json()
|
|
82
|
+
if "new_token" in response_json:
|
|
83
|
+
return response_json.get("new_token")
|
|
84
|
+
else:
|
|
85
|
+
return response_json.get("token")
|
|
86
|
+
except ValueError:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def add_ssh_key(self, title, key):
|
|
90
|
+
self.gitlab.user.keys.create({'title': title, 'key': key})
|
|
91
|
+
|
|
92
|
+
def to_dict(self):
|
|
93
|
+
return {
|
|
94
|
+
key: value
|
|
95
|
+
for key, value in {
|
|
96
|
+
"url": self.url,
|
|
97
|
+
"username": self.username,
|
|
98
|
+
"password": self.password,
|
|
99
|
+
"private_token": self.private_token,
|
|
100
|
+
"is_admin": self.is_admin,
|
|
101
|
+
"use_ldap": self.use_ldap,
|
|
102
|
+
"api_version": self.api_version,
|
|
103
|
+
}.items()
|
|
104
|
+
if value is not None
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def from_dict(cls, data):
|
|
109
|
+
return cls(
|
|
110
|
+
url=data.get("url"),
|
|
111
|
+
username=data.get("username"),
|
|
112
|
+
password=data.get("password"),
|
|
113
|
+
private_token=data.get("private_token"),
|
|
114
|
+
use_ldap=(True if data.get("use_ldap") == "True" else False),
|
|
115
|
+
api_version=int(data.get("api_version", 4)),
|
|
116
|
+
section=data.get("_name", None)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def __repr__(self):
|
|
120
|
+
return self.to_dict().__repr__()
|
labrat/core/config.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from labrat.core.agent import Agent
|
|
5
|
+
from labrat.core.utils import obj_filter
|
|
6
|
+
|
|
7
|
+
class Config:
|
|
8
|
+
def __init__(self, config_file="~/.python-gitlab.cfg", preauth=False, authed_only=False):
|
|
9
|
+
self.config_file = str(Path(config_file).expanduser())
|
|
10
|
+
self._config = configparser.ConfigParser()
|
|
11
|
+
self._config.read(self.config_file)
|
|
12
|
+
self.preauth = preauth
|
|
13
|
+
self.authed_only = authed_only
|
|
14
|
+
|
|
15
|
+
def __getitem__(self, section):
|
|
16
|
+
try:
|
|
17
|
+
return self._config[section]
|
|
18
|
+
except KeyError:
|
|
19
|
+
return {}
|
|
20
|
+
|
|
21
|
+
def __setitem__(self, section, value):
|
|
22
|
+
self._config[section] = value
|
|
23
|
+
self._save()
|
|
24
|
+
|
|
25
|
+
def _save(self):
|
|
26
|
+
with open(self.config_file, "w") as file:
|
|
27
|
+
self._config.write(file)
|
|
28
|
+
|
|
29
|
+
def __iter__(self, filter=None):
|
|
30
|
+
for section in self.sections():
|
|
31
|
+
if section not in ["DEFAULT", "global"]: # Skip special sections
|
|
32
|
+
|
|
33
|
+
data = self[section]
|
|
34
|
+
agent = Agent.from_dict(data) # Create an Agent instance from the section data
|
|
35
|
+
|
|
36
|
+
if self.preauth or self.authed_only:
|
|
37
|
+
try:
|
|
38
|
+
agent.auth()
|
|
39
|
+
except Exception as e:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
if filter and not obj_filter(agent, filter):
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
if agent.is_authenticated or not self.authed_only:
|
|
46
|
+
yield section, agent
|
|
47
|
+
|
|
48
|
+
def filter(self, filter):
|
|
49
|
+
return self.__iter__(filter)
|
|
50
|
+
|
|
51
|
+
def sections(self):
|
|
52
|
+
return self._config.sections()
|
|
53
|
+
|
|
54
|
+
def has_section(self, section):
|
|
55
|
+
return self._config.has_section(section)
|
|
56
|
+
|
|
57
|
+
def remove_section(self, section):
|
|
58
|
+
if section in self._config:
|
|
59
|
+
self._config.remove_section(section)
|
|
60
|
+
self._save()
|
|
61
|
+
return True
|
|
62
|
+
else:
|
|
63
|
+
return False
|
labrat/core/utils.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
def parse_host_range(host_pattern):
|
|
4
|
+
"""Parse a host pattern and generate a list of individual host addresses.
|
|
5
|
+
|
|
6
|
+
Keyword arguments:
|
|
7
|
+
- host_pattern: The host pattern to parse (e.g., "team{01..15}.ccdc.local")
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
hosts = []
|
|
11
|
+
|
|
12
|
+
# Match brace expansion pattern (e.g., "team{01..15}.ccdc.local")
|
|
13
|
+
brace_expansion_match = re.match(r"(.*)\{(\d+)\.\.(\d+)\}(.*)", host_pattern)
|
|
14
|
+
if brace_expansion_match:
|
|
15
|
+
prefix = brace_expansion_match.group(1)
|
|
16
|
+
start = brace_expansion_match.group(2)
|
|
17
|
+
end = brace_expansion_match.group(3)
|
|
18
|
+
suffix = brace_expansion_match.group(4)
|
|
19
|
+
|
|
20
|
+
# Padding for leading zeros
|
|
21
|
+
padding = max(len(start), len(end)) if start[0] == '0' or end[0] == '0' else 0
|
|
22
|
+
|
|
23
|
+
for i in range(int(start), int(end) + 1):
|
|
24
|
+
hosts.append(f"{prefix}{i:0{padding}d}{suffix}")
|
|
25
|
+
return hosts
|
|
26
|
+
|
|
27
|
+
# If no range is detected, return the host as-is
|
|
28
|
+
return [host_pattern]
|
|
29
|
+
|
|
30
|
+
def obj_filter(obj, queries):
|
|
31
|
+
"""Filter an object based on a list of filters. Filters can include simple substrings or field selection and regex patterns.
|
|
32
|
+
|
|
33
|
+
Filter criteria:
|
|
34
|
+
- Searching on all fields (e.g., `j\\.doe`)
|
|
35
|
+
- Field selection (e.g., `username=j.doe`)
|
|
36
|
+
|
|
37
|
+
Operators:
|
|
38
|
+
- `=`: Equal
|
|
39
|
+
- `!=`: Not equal
|
|
40
|
+
- `=~`: Regex match
|
|
41
|
+
- `!~`: Regex not match
|
|
42
|
+
|
|
43
|
+
Keyword arguments:
|
|
44
|
+
- obj: The object to filter.
|
|
45
|
+
- queries: A list of filter strings to apply.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
result = False
|
|
49
|
+
for query in queries:
|
|
50
|
+
equals_op = True
|
|
51
|
+
regex_op = True
|
|
52
|
+
|
|
53
|
+
# Operator parsing
|
|
54
|
+
parsed_query = re.match(r"([a-zA-Z_]+)(!=|=~|!~|=)(.*)", query)
|
|
55
|
+
if parsed_query:
|
|
56
|
+
field = parsed_query.group(1)
|
|
57
|
+
operator = parsed_query.group(2)
|
|
58
|
+
pattern = parsed_query.group(3)
|
|
59
|
+
|
|
60
|
+
equals_op = not operator[0] == "!"
|
|
61
|
+
regex_op = operator[-1] == "~"
|
|
62
|
+
|
|
63
|
+
value = getattr(obj, field, None)
|
|
64
|
+
else:
|
|
65
|
+
pattern = query
|
|
66
|
+
value = get_attrs(obj)
|
|
67
|
+
|
|
68
|
+
# Ensure value is searchable
|
|
69
|
+
if value is not None:
|
|
70
|
+
# Exclude keys from search
|
|
71
|
+
if isinstance(value, dict):
|
|
72
|
+
value = str(value.values())
|
|
73
|
+
else:
|
|
74
|
+
value = str(value)
|
|
75
|
+
|
|
76
|
+
# Regex or literal search
|
|
77
|
+
if regex_op:
|
|
78
|
+
search = re.search(pattern, value, flags=re.IGNORECASE) is not None
|
|
79
|
+
else:
|
|
80
|
+
search = pattern.casefold() == value.casefold()
|
|
81
|
+
|
|
82
|
+
# Negate search if using not-operator
|
|
83
|
+
if search == equals_op:
|
|
84
|
+
result = True
|
|
85
|
+
else:
|
|
86
|
+
return False
|
|
87
|
+
elif equals_op:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
def get_attrs(obj):
|
|
93
|
+
"""Get attributes of an object as a dictionary.
|
|
94
|
+
|
|
95
|
+
Keyword arguments:
|
|
96
|
+
- obj: The object to get attributes from.
|
|
97
|
+
"""
|
|
98
|
+
attrs = {}
|
|
99
|
+
if hasattr(obj, "_attrs"):
|
|
100
|
+
attrs.update(obj._attrs) # Include extended GitLab attributes
|
|
101
|
+
if hasattr(obj, "_updated_attrs"):
|
|
102
|
+
attrs.update(obj._updated_attrs) # Include updated attributes
|
|
103
|
+
if not attrs:
|
|
104
|
+
attrs = {k: v for k, v in vars(obj).items() if not hasattr(v, '__dict__')}
|
|
105
|
+
|
|
106
|
+
return attrs if attrs else None
|
labrat/main.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# labrat/main.py
|
|
2
|
+
import argparse
|
|
3
|
+
from labrat.cli import agents, auth, projects, users
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
parser = argparse.ArgumentParser(
|
|
7
|
+
prog="labrat",
|
|
8
|
+
description="LabRat: GitLab exploitation orchestrator"
|
|
9
|
+
)
|
|
10
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
11
|
+
|
|
12
|
+
for command in [agents, auth, projects, users]:
|
|
13
|
+
command.build_parser(subparsers)
|
|
14
|
+
|
|
15
|
+
args = parser.parse_args()
|
|
16
|
+
args.func(args)
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
main()
|