/
evalsC2client.py
executable file
·374 lines (293 loc) · 16.4 KB
/
evalsC2client.py
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
#!/usr/bin/python3
# ---------------------------------------------------------------------------
# evalsC2client.py - Interact with Evals C2 server.
# Copyright 2023 MITRE Engenuity. Approved for public release. Document number CT0005.
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
# This project makes use of ATT&CK®
# ATT&CK Terms of Use - https://attack.mitre.org/resources/terms-of-use/
# Usage:
# ./evalsC2client.py --set-task [GUID] "task string"
# Revision History:
# ---------------------------------------------------------------------------
import argparse
import json
import pprint
import requests
import sys
import time
import traceback
from datetime import datetime, timedelta
API_RESP_TYPE_KEY = 'type'
API_RESP_STATUS_KEY = 'status'
API_RESP_DATA_KEY = 'data'
API_RESP_STATUS_SUCCESS = 0
RESP_TYPE_CTRL = 0 # API control messages (for error messages, generic success messages). Will contain string data
RESP_TYPE_VERSION = 1 #version message (for GetVersion). Will contain string data
RESP_TYPE_CONFIG = 2 # config message (for GetConfig). Will contain json data
RESP_TYPE_SESSIONS = 3 # C2 sessions message (for GetSessionByGuid and GetSessions). Will contain json data
RESP_TYPE_TASK_CMD = 4 # task command message (for GetTaskCommandBySessionId and GetBootstrapTask). Will contain string data
RESP_TYPE_TASK_OUTPUT = 5 # task output message (for GetTaskOutputBySessionId and GetTaskOutput). Will contain string data
RESP_TYPE_TASK_INFO = 6 # task data message (for GetTask). Will contain json data
TASK_STATUS_KEY = 'taskStatus'
TASK_GUID_KEY = 'guid'
TASK_COMMAND_KEY = 'command'
TASK_OUTPUT_KEY = 'taskOutput'
TASK_STATUS_NEW = 0
TASK_STATUS_PENDING = 1
TASK_STATUS_FINISHED = 2
TASK_STATUS_DISCARDED = 3
VERBOSE_OUTPUT = False
# Custom exception for handling API responses
class ApiResponseException(Exception):
pass
def print_stderr(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def verbose_print(*args, **kwargs):
if VERBOSE_OUTPUT:
print(*args, **kwargs)
# ToDo - need to add unit tests for this script
def validate_api_resp_format(resp_dict):
"""Throws ApiResponseException when detecting an invalid API response format (missing JSON field, etc)"""
if API_RESP_TYPE_KEY not in resp_dict:
raise ApiResponseException('Malformed API response: missing API response type key "' + API_RESP_TYPE_KEY + '"')
elif not isinstance(resp_dict[API_RESP_TYPE_KEY], int):
raise ApiResponseException('Malformed API response: response type is not an int.')
if API_RESP_STATUS_KEY not in resp_dict:
raise ApiResponseException('Malformed API response: missing API response status key "' + API_RESP_STATUS_KEY + '"')
elif not isinstance(resp_dict[API_RESP_STATUS_KEY], int):
raise ApiResponseException('Malformed API response: response status is not an int.')
if API_RESP_DATA_KEY not in resp_dict:
raise ApiResponseException('Malformed API response: missing API response data key "' + API_RESP_DATA_KEY + '"')
# verify response type and data type
resp_type = resp_dict[API_RESP_TYPE_KEY]
resp_data = resp_dict[API_RESP_DATA_KEY]
if resp_type in [RESP_TYPE_CTRL, RESP_TYPE_VERSION, RESP_TYPE_TASK_CMD, RESP_TYPE_TASK_OUTPUT]:
if not isinstance(resp_data, str):
raise ApiResponseException('Malformed API response: expected str data type for response type {}'.format(resp_type))
elif resp_type in [RESP_TYPE_CONFIG, RESP_TYPE_TASK_INFO] and not isinstance(resp_data, dict):
raise ApiResponseException('Malformed API response: expected dictionary data type for response type {}'.format(resp_type))
elif resp_type == RESP_TYPE_TASK_INFO:
if TASK_STATUS_KEY not in resp_data:
raise ApiResponseException('Malformed API response: missing task status key "' + API_RESP_TYPE_KEY + '"')
if TASK_GUID_KEY not in resp_data:
raise ApiResponseException('Malformed API response: missing task GUID key "' + TASK_GUID_KEY + '"')
if TASK_COMMAND_KEY not in resp_data:
raise ApiResponseException('Malformed API response: missing task command key "' + TASK_COMMAND_KEY + '"')
elif resp_type == RESP_TYPE_SESSIONS:
if resp_data is not None and not isinstance(resp_data, list):
raise ApiResponseException('Malformed API response: expected None or list data type for response type {}'.format(resp_type))
def verify_api_resp_success(resp_dict):
"""Throws ApiResponseException if API response indicates failure."""
if resp_dict[API_RESP_STATUS_KEY] != API_RESP_STATUS_SUCCESS:
raise ApiResponseException('Received unsuccessful API response: ' + resp_dict[API_RESP_DATA_KEY])
def extract_response(api_resp_str, expected_type):
resp_dict = json.loads(api_resp_str)
validate_api_resp_format(resp_dict)
verify_api_resp_success(resp_dict)
resp_type = resp_dict[API_RESP_TYPE_KEY]
if resp_type != expected_type:
raise ApiResponseException('Expected response type {0}, got {1}'.format(expected_type, resp_type))
return resp_dict[API_RESP_DATA_KEY]
def extract_and_print_response(response, resp_type):
try:
resp_data = extract_response(response.text, resp_type)
if resp_type in [RESP_TYPE_CTRL, RESP_TYPE_VERSION, RESP_TYPE_TASK_CMD, RESP_TYPE_TASK_OUTPUT]:
# string response data
print(resp_data)
elif resp_type in [RESP_TYPE_CONFIG, RESP_TYPE_TASK_INFO]:
# dictionary data
print(json.dumps(resp_data, sort_keys=True, indent=4))
elif resp_type == RESP_TYPE_SESSIONS:
# None or list data
if resp_data is None:
resp_data = []
print(json.dumps(resp_data, sort_keys=True, indent=4))
except ApiResponseException as e:
print_stderr("ApiResponseException: {0}\nAPI response text:\n{1}\n".format(str(e), response.text))
except Exception as e:
print_stderr("Unhandled exception: {0}\nAPI response text:\n{1}\n".format(str(e), response.text))
traceback.print_exc()
def extract_and_print_single_session_response(response):
try:
sessions = extract_response(response.text, RESP_TYPE_SESSIONS)
print(json.dumps(sessions[0], sort_keys=True, indent=4))
except ApiResponseException as e:
print_stderr("ApiResponseException: {0}\nAPI response text:\n{1}\n".format(str(e), response.text))
except Exception as e:
print_stderr("Unhandled exception: {0}\nAPI response text:\n{1}\n".format(str(e), response.text))
traceback.print_exc()
"""API Wrappers"""
def get_server_version(port: str):
url = "http://localhost:{0}/api/v1.0/version".format(port)
r = requests.get(url)
extract_and_print_response(r, RESP_TYPE_VERSION)
def get_server_config(port: str):
url = "http://localhost:{0}/api/v1.0/config".format(port)
r = requests.get(url)
extract_and_print_response(r, RESP_TYPE_CONFIG)
def get_implant_sessions(port: str):
url = "http://localhost:{0}/api/v1.0/sessions".format(port)
r = requests.get(url)
extract_and_print_response(r, RESP_TYPE_SESSIONS)
def get_session_by_guid(guid: str, port: str):
url = "http://localhost:{0}/api/v1.0/session/".format(port) + guid
r = requests.get(url)
extract_and_print_single_session_response(r)
def delete_session(guid: str, port: str):
url = "http://localhost:{0}/api/v1.0/session/delete/".format(port) + guid
r = requests.delete(url)
extract_and_print_response(r, RESP_TYPE_CTRL)
def get_task_by_session_id(guid: str, port: str):
url = "http://localhost:{0}/api/v1.0/session/{1}/task".format(port, guid)
r = requests.get(url)
print(r.text)
def set_task_by_session_id(guid, task: str, port: str):
url = "http://localhost:{0}/api/v1.0/session/{1}/task".format(port, guid)
r = requests.post(url, task)
extract_and_print_response(r, RESP_TYPE_TASK_INFO)
def delete_task_by_session_id(guid: str, port: str):
url = "http://localhost:{0}/api/v1.0/session/{1}/task".format(port, guid)
r = requests.delete(url)
extract_and_print_response(r, RESP_TYPE_CTRL)
def get_task_output_by_session_id(guid: str, port: str):
url = "http://localhost:{0}/api/v1.0/session/{1}/task/output".format(port, guid)
r = requests.get(url)
extract_and_print_response(r, RESP_TYPE_TASK_OUTPUT)
def delete_task_output_by_session_id(guid: str, port: str):
url = "http://localhost:{0}/api/v1.0/session/{1}/task/output".format(port, guid)
r = requests.delete(url)
extract_and_print_response(r, RESP_TYPE_CTRL)
def get_bootstrap_task(handler: str, port: str):
url = "http://localhost:{0}/api/v1.0/bootstraptask/".format(port) + handler
r = requests.get(url)
print(r.text)
def set_bootstrap_task(handler, task: str, port: str):
url = "http://localhost:{0}/api/v1.0/bootstraptask/".format(port) + handler
r = requests.post(url, task)
extract_and_print_response(r, RESP_TYPE_CTRL)
def delete_bootstrap_task(handler: str, port: str):
url = "http://localhost:{0}/api/v1.0/bootstraptask/".format(port) + handler
r = requests.delete(url)
extract_and_print_response(r, RESP_TYPE_CTRL)
""" FOR OPERATOR USABILITY """
def get_task_status(task_guid: str, port):
url = 'http://localhost:{0}/api/v1.0/task/{1}'.format(port, task_guid)
r = requests.get(url)
task_data = extract_response(r.text, RESP_TYPE_TASK_INFO)
return task_data[TASK_STATUS_KEY]
def get_task_output(task_guid: str, port):
url = 'http://localhost:{0}/api/v1.0/task/{1}'.format(port, task_guid)
r = requests.get(url)
task_data = extract_response(r.text, RESP_TYPE_TASK_INFO)
if TASK_OUTPUT_KEY in task_data.keys():
return task_data[TASK_OUTPUT_KEY]
else:
return ""
def set_and_complete_task(session_guid, task: str, port: str, timeout: int):
url = "http://localhost:{0}/api/v1.0/session/{1}/task".format(port, session_guid)
response = requests.post(url, task)
try:
task_data = extract_response(response.text, RESP_TYPE_TASK_INFO)
task_guid = task_data[TASK_GUID_KEY]
task_command = task_data[TASK_COMMAND_KEY]
verbose_print('Set task with ID {0} for session {1} with command {2}'.format(task_guid, session_guid, task_command))
verbose_print('Waiting up to {0} seconds for task output'.format(timeout))
now = datetime.now()
timeout_deadline = now + timedelta(seconds=timeout)
finished = False
while (datetime.now() < timeout_deadline) and not finished:
task_status = get_task_status(task_guid, port)
if task_status in [TASK_STATUS_FINISHED, TASK_STATUS_DISCARDED]:
finished = True
break
time.sleep(5)
if not finished:
# We timed out
if task_status == TASK_STATUS_NEW:
print_stderr('Timed out while waiting for implant with session ID {0} to pick up task {1}.'.format(session_guid, task_guid))
elif task_status == TASK_STATUS_PENDING:
print_stderr('Timed out while waiting for implant with session ID {0} to send output for task {1}.'.format(session_guid, task_guid))
elif task_status == TASK_STATUS_DISCARDED:
print_stderr('Task {1} was discarded for session ID {0}. Could not obtain output'.format(session_guid, task_guid))
elif task_status == TASK_STATUS_FINISHED:
task_output = get_task_output(task_guid, port)
verbose_print('Received output for task {1} from session ID {0}:'.format(session_guid, task_guid))
print(task_output)
except ApiResponseException as e:
print_stderr("ApiResponseException: {0}\nAPI response text:\n{1}\n".format(str(e), response.text))
except Exception as e:
print_stderr("Unhandled exception: {0}\nAPI response text:\n{1}\n".format(str(e), response.text))
traceback.print_exc()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--get-version", action="store_true", help="get control server version")
parser.add_argument("--get-config", action="store_true", help="get server config")
parser.add_argument("--get-sessions", action="store_true", help="list current C2 sessions")
parser.add_argument("--set-port", default="9999", metavar=('PORT'), help="set the c2 server API port (default 9999)")
parser.add_argument("--get-session", metavar=('SESSIONID'), help="get detailed info for a session specified by SESSIONID")
parser.add_argument("--del-session", metavar=('SESSIONID'), help="delete session from C2 server as specified by SESSIONID")
parser.add_argument("--get-task", metavar=('SESSIONID'), help="get current task from session as specified by SESSIONID")
parser.add_argument("--set-task", nargs=2, metavar=('SESSIONID', 'COMMAND'), help="set a task for a session as specified by SESSIONID")
parser.add_argument("--del-task", metavar=('SESSIONID'), help="delete a task from a session as specified by SESSIONID")
parser.add_argument("--get-output", metavar=('SESSIONID'), help="get task output from a session as specified by SESSIONID")
parser.add_argument("--del-output", metavar=('SESSIONID'), help="delete task output from a session as specified by SESSIONID")
parser.add_argument("--get-bootstrap-task", metavar=('HANDLER'), help="get current bootstrap task for new sessions for the specified handler")
parser.add_argument("--set-bootstrap-task", nargs=2, metavar=('HANDLER', 'COMMAND'), help="set a bootstrap task for new sessions for the specified handler")
parser.add_argument("--del-bootstrap-task", metavar=('HANDLER'), help="delete a bootstrap task for new sessions for the specified handler")
parser.add_argument("--set-and-complete-task", nargs=2, metavar=('SESSIONID', 'COMMAND'),
help="set a task for a session as specified by SESSIONID, wait for the command to finish, and then return the output.")
parser.add_argument("--task-wait-timeout", default=120, metavar=('TIMEOUT'),
help="number of seconds to wait for the command to finish (default 120 seconds). Only used with --set-and-complete-task.")
parser.add_argument("-v", "--verbose", action="store_true", help="Toggle verbose standard output")
args = parser.parse_args()
if args.verbose:
global VERBOSE_OUTPUT
VERBOSE_OUTPUT = True
if args.get_version:
get_server_version(args.set_port)
elif args.get_config:
get_server_config(args.set_port)
elif args.get_sessions:
get_implant_sessions(args.set_port)
elif args.get_session:
guid = args.get_session
get_session_by_guid(guid, args.set_port)
elif args.del_session:
guid = args.del_session
delete_session(guid, args.set_port)
elif args.get_task:
guid = args.get_task
get_task_by_session_id(guid, args.set_port)
elif args.set_task:
guid, task = args.set_task
set_task_by_session_id(guid, task, args.set_port)
elif args.del_task:
guid = args.del_task
delete_task_by_session_id(guid, args.set_port)
elif args.get_output:
guid = args.get_output
get_task_output_by_session_id(guid, args.set_port)
elif args.del_output:
guid = args.del_output
delete_task_output_by_session_id(guid, args.set_port)
elif args.get_bootstrap_task:
handler = args.get_bootstrap_task
get_bootstrap_task(handler, args.set_port)
elif args.set_bootstrap_task:
handler, task = args.set_bootstrap_task
set_bootstrap_task(handler, task, args.set_port)
elif args.del_bootstrap_task:
handler = args.del_bootstrap_task
delete_bootstrap_task(handler, args.set_port)
elif args.set_and_complete_task:
session_guid, command = args.set_and_complete_task
timeout = 120
if args.task_wait_timeout:
timeout = int(args.task_wait_timeout)
set_and_complete_task(session_guid, command, args.set_port, timeout)
else:
parser.print_help()
if __name__ == "__main__":
main()