exploitfarm 0.2.3__tar.gz → 0.2.4__tar.gz
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.
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/PKG-INFO +1 -1
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/__init__.py +1 -1
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/cmd/startxploit.py +4 -1
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/xploit.py +87 -45
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm.egg-info/PKG-INFO +1 -1
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/setup.py +1 -1
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/xfarm +9 -4
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/MANIFEST.in +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/README.md +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/cmd/__init__.py +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/cmd/config.py +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/cmd/exploitinit.py +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/cmd/login.py +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/model.py +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/utils/__init__.py +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/utils/config.py +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/utils/reqs.py +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm.egg-info/SOURCES.txt +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm.egg-info/dependency_links.txt +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm.egg-info/requires.txt +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm.egg-info/top_level.txt +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/requirements.txt +0 -0
- {exploitfarm-0.2.3 → exploitfarm-0.2.4}/setup.cfg +0 -0
|
@@ -284,7 +284,10 @@ class XploitRun(App):
|
|
|
284
284
|
while True:
|
|
285
285
|
if g.exit_event.is_set(): return
|
|
286
286
|
log = g.print_queue.get()
|
|
287
|
-
|
|
287
|
+
if isinstance(log, str):
|
|
288
|
+
logger.write(escape(log))
|
|
289
|
+
else:
|
|
290
|
+
logger.write(log)
|
|
288
291
|
|
|
289
292
|
def flag_graph_update(self):
|
|
290
293
|
while True:
|
|
@@ -13,11 +13,12 @@ from math import ceil
|
|
|
13
13
|
from exploitfarm.utils.config import ClientConfig, ExploitConfig
|
|
14
14
|
from multiprocessing import Queue
|
|
15
15
|
from datetime import datetime as dt, timedelta
|
|
16
|
-
import datetime
|
|
16
|
+
import datetime, io
|
|
17
17
|
from exploitfarm.model import AttackExecutionStatus, AttackMode
|
|
18
18
|
from rich import print
|
|
19
19
|
from uuid import UUID
|
|
20
20
|
from copy import deepcopy
|
|
21
|
+
from rich.markup import escape
|
|
21
22
|
|
|
22
23
|
os_windows = (os.name == 'nt')
|
|
23
24
|
|
|
@@ -31,7 +32,7 @@ class g:
|
|
|
31
32
|
print_queue: Queue = None
|
|
32
33
|
last_attack_time: dt = None
|
|
33
34
|
config_update_event:Queue = Queue(100)
|
|
34
|
-
|
|
35
|
+
attack_storage:"AttackStorage" = None
|
|
35
36
|
instance_storage:"InstanceStorage" = None
|
|
36
37
|
exit_event = None
|
|
37
38
|
server_id = None
|
|
@@ -71,7 +72,7 @@ class InstanceStorage:
|
|
|
71
72
|
self.n_completed += 1
|
|
72
73
|
self.n_killed += was_killed
|
|
73
74
|
|
|
74
|
-
class
|
|
75
|
+
class AttackStorage:
|
|
75
76
|
"""
|
|
76
77
|
Thread-safe storage comprised of a set and a post queue.
|
|
77
78
|
|
|
@@ -114,7 +115,7 @@ class FlagStorage:
|
|
|
114
115
|
|
|
115
116
|
self._queue.append(execution_submit)
|
|
116
117
|
|
|
117
|
-
def
|
|
118
|
+
def pick_attacks(self):
|
|
118
119
|
with self._lock:
|
|
119
120
|
return self._queue[:]
|
|
120
121
|
|
|
@@ -130,9 +131,15 @@ class FlagStorage:
|
|
|
130
131
|
WARNING_RUNTIME = 5
|
|
131
132
|
|
|
132
133
|
def qprint(*args):
|
|
133
|
-
|
|
134
|
+
try:
|
|
135
|
+
for arg in args:
|
|
136
|
+
if isinstance(arg, str):
|
|
137
|
+
g.print_queue.put(escape(arg), block=False)
|
|
138
|
+
else:
|
|
139
|
+
g.print_queue.put(arg, block=False)
|
|
140
|
+
except (KeyboardInterrupt, ValueError):
|
|
141
|
+
traceback.print_exc()
|
|
134
142
|
pass
|
|
135
|
-
g.print_queue.put(arg, block=False)
|
|
136
143
|
|
|
137
144
|
class InvalidSploitError(Exception):
|
|
138
145
|
pass
|
|
@@ -198,7 +205,7 @@ def repush_flags():
|
|
|
198
205
|
if old_queue_data["server_id"] != g.server_id:
|
|
199
206
|
raise Exception("Server ID mismatch")
|
|
200
207
|
for ele in old_queue_data["queue"]:
|
|
201
|
-
g.
|
|
208
|
+
g.attack_storage.add(ele["flags"], ele["target"], ele["start_time"], ele["end_time"], AttackExecutionStatus(ele["status"]), ele["error"])
|
|
202
209
|
except Exception:
|
|
203
210
|
if os.path.exists(".flag_queue.json"):
|
|
204
211
|
os.remove(".flag_queue.json")
|
|
@@ -243,11 +250,11 @@ def update_server_config():
|
|
|
243
250
|
qprint(f'Config update loop died: {repr(e)}')
|
|
244
251
|
shutdown()
|
|
245
252
|
|
|
246
|
-
def
|
|
253
|
+
def post_attacks(attacks):
|
|
247
254
|
try:
|
|
248
|
-
g.config.reqs.submit_flags(
|
|
249
|
-
g.
|
|
250
|
-
qprint(f'{sum([len(ele["flags"]) for ele in
|
|
255
|
+
g.config.reqs.submit_flags(attacks, exploit=g.exploit_config.uuid)
|
|
256
|
+
g.attack_storage.mark_as_sent(len(attacks))
|
|
257
|
+
qprint(f'{sum([len(ele["flags"]) for ele in attacks])} flags posted to the server')
|
|
251
258
|
g.shared_memory["submitter_status"] = True
|
|
252
259
|
except Exception as e:
|
|
253
260
|
g.shared_memory["submitter_status"] = False
|
|
@@ -256,39 +263,54 @@ def post_flags(flags):
|
|
|
256
263
|
def run_post_loop():
|
|
257
264
|
try:
|
|
258
265
|
for _ in once_in_a_period(g.submit_pool_timeout):
|
|
259
|
-
|
|
260
|
-
g.shared_memory["submitter_flags_in_queue"] = sum([len(ele["flags"]) for ele in
|
|
261
|
-
if
|
|
266
|
+
attack_to_post = g.attack_storage.pick_attacks()[:50] #50 is to avoid the backend goes in timeout
|
|
267
|
+
g.shared_memory["submitter_flags_in_queue"] = sum([len(ele["flags"]) for ele in attack_to_post])
|
|
268
|
+
if attack_to_post: post_attacks(attack_to_post)
|
|
262
269
|
except Exception as e:
|
|
263
270
|
g.shared_memory["submitter_status"] = False
|
|
264
271
|
qprint(traceback.format_exc())
|
|
265
272
|
qprint(f'Posting loop died: {repr(e)}')
|
|
266
273
|
|
|
267
|
-
def process_sploit_filter(proc:subprocess.Popen, team: dict, start_time: dt, killed: bool):
|
|
274
|
+
def process_sploit_filter(proc:subprocess.Popen, team: dict, start_time: dt, killed: bool, read_data:callable, final_output: bool = True):
|
|
268
275
|
try:
|
|
269
276
|
end_time = dt.now(datetime.timezone.utc)
|
|
270
|
-
output =
|
|
277
|
+
output = read_data()
|
|
271
278
|
|
|
272
279
|
if isinstance(output, bytes):
|
|
273
280
|
output = output.decode('utf-8', errors='replace')
|
|
274
281
|
|
|
275
|
-
|
|
282
|
+
if final_output:
|
|
283
|
+
qprint(f"Output of the sploit of {team['host']} killed[{killed}] status[{proc.returncode}]: '{team.get('short_name') or team.get('name') or team.get('id')}' started: {start_time} ended: {end_time}\n{output}")
|
|
284
|
+
|
|
285
|
+
if not final_output and killed:
|
|
286
|
+
qprint("☠️ xFarm killed the sploit due to timeout!")
|
|
287
|
+
|
|
276
288
|
config = deepcopy(g.shared_memory["config"])
|
|
277
289
|
flag_format = re.compile(config["config"]["FLAG_REGEX"])
|
|
278
290
|
flags = list(map(str, set(flag_format.findall(output))))
|
|
279
|
-
|
|
291
|
+
|
|
280
292
|
if killed or proc.returncode != 0:
|
|
281
293
|
if killed:
|
|
282
294
|
output = "THIS PROCESS HAS BEEN KILLED BY EXPLOITFARM DUE TO TIMEOUT\n-------- OUTPUT -------\n\n" + output
|
|
283
|
-
g.
|
|
295
|
+
g.attack_storage.add(flags, team["id"], start_time, end_time, AttackExecutionStatus.crashed, output)
|
|
284
296
|
elif flags:
|
|
285
|
-
g.
|
|
297
|
+
g.attack_storage.add(flags, team["id"], start_time, end_time, AttackExecutionStatus.done, b'')
|
|
286
298
|
else:
|
|
287
|
-
g.
|
|
299
|
+
g.attack_storage.add(flags, team["id"], start_time, end_time, AttackExecutionStatus.noflags, output)
|
|
288
300
|
except Exception as e:
|
|
301
|
+
traceback.print_exc()
|
|
289
302
|
qprint(f'Failed to process sploit output: {repr(e)}')
|
|
290
303
|
|
|
291
|
-
def
|
|
304
|
+
def read_and_print(stdout:io.BytesIO, buffer:io.BytesIO):
|
|
305
|
+
# Read stdout line by line in a separate thread
|
|
306
|
+
for line in iter(stdout.readline, b''):
|
|
307
|
+
qprint(line.decode('utf-8', errors='replace')) # Print to main stdout in real-time
|
|
308
|
+
buffer.write(line)
|
|
309
|
+
buffer.flush()
|
|
310
|
+
buffer.write(stdout.read())
|
|
311
|
+
stdout.close()
|
|
312
|
+
|
|
313
|
+
def launch_sploit(team:dict, max_runtime:float, stream_output:bool=False) -> tuple[subprocess.Popen, int, callable]:
|
|
292
314
|
# For sploits written in Python, this env variable forces the interpreter to flush
|
|
293
315
|
# stdout and stderr after each newline. Note that this is not default behavior
|
|
294
316
|
# if the sploit's output is redirected to a pipe.
|
|
@@ -317,18 +339,33 @@ def launch_sploit(team:dict, max_runtime:float):
|
|
|
317
339
|
kernel32.SetConsoleCtrlHandler(win_ignore_ctrl_c, True)
|
|
318
340
|
|
|
319
341
|
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=need_close_fds, env=env, shell=True)
|
|
342
|
+
read_function = None
|
|
343
|
+
if stream_output:
|
|
344
|
+
pipe = proc.stdout
|
|
345
|
+
buffer = io.BytesIO()
|
|
346
|
+
thr = threading.Thread(target=read_and_print, args=(pipe, buffer))
|
|
347
|
+
thr.start()
|
|
348
|
+
def func():
|
|
349
|
+
thr.join()
|
|
350
|
+
return buffer.getvalue()
|
|
351
|
+
read_function = func
|
|
320
352
|
if os_windows:
|
|
321
353
|
kernel32.SetConsoleCtrlHandler(win_ignore_ctrl_c, False)
|
|
354
|
+
|
|
355
|
+
if not read_function:
|
|
356
|
+
def func():
|
|
357
|
+
return proc.stdout.read()
|
|
358
|
+
read_function = func
|
|
322
359
|
|
|
323
|
-
return proc, g.instance_storage.register_start(proc)
|
|
360
|
+
return proc, g.instance_storage.register_start(proc), read_function
|
|
324
361
|
|
|
325
362
|
|
|
326
|
-
def run_sploit(team:dict, max_runtime:float):
|
|
363
|
+
def run_sploit(team:dict, max_runtime:float, stream_output:bool=False):
|
|
327
364
|
start_time = dt.now(datetime.timezone.utc)
|
|
328
365
|
if g.exit_event.is_set(): return
|
|
329
366
|
try:
|
|
330
367
|
g.shared_memory["team-"+str(team["id"])+"-executing"] = True
|
|
331
|
-
proc, instance_id = launch_sploit(team, max_runtime)
|
|
368
|
+
proc, instance_id, read_data = launch_sploit(team, max_runtime, stream_output)
|
|
332
369
|
except Exception as e:
|
|
333
370
|
if isinstance(e, FileNotFoundError):
|
|
334
371
|
qprint(f'Sploit file or the interpreter for it not found: {repr(e)}')
|
|
@@ -352,8 +389,9 @@ def run_sploit(team:dict, max_runtime:float):
|
|
|
352
389
|
need_kill = True
|
|
353
390
|
with g.instance_storage.lock:
|
|
354
391
|
proc.kill()
|
|
392
|
+
|
|
355
393
|
g.shared_memory["team-"+str(team["id"])+"-executing"] = False
|
|
356
|
-
process_sploit_filter(proc, team, start_time, need_kill)
|
|
394
|
+
process_sploit_filter(proc, team, start_time, need_kill, read_data, not stream_output)
|
|
357
395
|
g.instance_storage.register_stop(instance_id, need_kill)
|
|
358
396
|
except Exception as e:
|
|
359
397
|
qprint(traceback.format_exc())
|
|
@@ -417,6 +455,7 @@ def xploit(path: str):
|
|
|
417
455
|
float(g.instance_storage.n_killed) / g.instance_storage.n_completed * 100))
|
|
418
456
|
|
|
419
457
|
max_runtime = calc_runtime_timeout()
|
|
458
|
+
|
|
420
459
|
if max_runtime == WARNING_RUNTIME:
|
|
421
460
|
qprint(f"⚠️ WARNING: The runtime of the attack is too low: consider increasing the number thread pool size")
|
|
422
461
|
g.shared_memory["runtime_timeout"] = max_runtime
|
|
@@ -455,7 +494,7 @@ def shutdown(restart:bool=False):
|
|
|
455
494
|
g.restart_event.set()
|
|
456
495
|
g.exit_event.set()
|
|
457
496
|
try:
|
|
458
|
-
data = {"server_id": g.server_id, "queue":g.
|
|
497
|
+
data = {"server_id": g.server_id, "queue":g.attack_storage.pick_attacks()}
|
|
459
498
|
if data:
|
|
460
499
|
with open(".flag_queue.json", "wb") as f:
|
|
461
500
|
f.write(orjson.dumps(data))
|
|
@@ -472,41 +511,44 @@ def xploit_one(config: ClientConfig, team: str, path: str, timeout: float = 30):
|
|
|
472
511
|
os.chdir(path)
|
|
473
512
|
try:
|
|
474
513
|
g.config = config
|
|
514
|
+
try:
|
|
515
|
+
g.config.fetch_status()
|
|
516
|
+
except Exception as e:
|
|
517
|
+
qprint(f"[bold yellow]Can't get config from the server: {repr(e)}")
|
|
518
|
+
return
|
|
475
519
|
try:
|
|
476
520
|
g.shared_memory = {"config": g.config.status}
|
|
477
521
|
except Exception as e:
|
|
478
|
-
|
|
522
|
+
qprint(f"[bold yellow]Can't get config from the server: {repr(e)}")
|
|
479
523
|
g.shared_memory = {}
|
|
480
524
|
g.exploit_config = ExploitConfig.read(path)
|
|
481
525
|
g.print_queue = Queue(100)
|
|
482
|
-
g.
|
|
526
|
+
g.attack_storage = AttackStorage(config.client_id)
|
|
483
527
|
g.instance_storage = InstanceStorage()
|
|
484
528
|
g.exit_event = threading.Event()
|
|
485
529
|
g.restart_event = threading.Event()
|
|
486
530
|
threading.Thread(target=run_printer_queue).start()
|
|
487
|
-
run_sploit({"id":0, "host":team}, timeout)
|
|
531
|
+
run_sploit({"id":0, "host":team}, timeout, stream_output=True)
|
|
488
532
|
try:
|
|
489
533
|
while True:
|
|
490
|
-
|
|
491
|
-
if len(
|
|
534
|
+
attacks = g.attack_storage.pick_attacks()
|
|
535
|
+
if len(attacks) == 0:
|
|
492
536
|
break
|
|
493
|
-
for
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
537
|
+
for attack in attacks:
|
|
538
|
+
if len(attack["flags"]) > 0:
|
|
539
|
+
g.config.reqs.submit_flags({"flags":attack["flags"]})
|
|
540
|
+
g.attack_storage.mark_as_sent(1)
|
|
541
|
+
if len(attack["flags"]) > 0:
|
|
542
|
+
qprint(f"Submitted {len(attack["flags"])} flags as manual submissions.")
|
|
497
543
|
except Exception as e:
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
try:
|
|
501
|
-
while not g.print_queue.empty():
|
|
502
|
-
print(g.print_queue.get(block=False))
|
|
503
|
-
except Exception:
|
|
504
|
-
pass
|
|
544
|
+
qprint(f"[bold yellow]Can't submit flags due missing config")
|
|
505
545
|
except KeyboardInterrupt:
|
|
506
546
|
pass
|
|
507
547
|
finally:
|
|
508
|
-
g.print_queue.close()
|
|
509
548
|
shutdown()
|
|
549
|
+
for _ in once_in_a_period(0.1):
|
|
550
|
+
if g.print_queue.empty():
|
|
551
|
+
g.print_queue.close()
|
|
510
552
|
|
|
511
553
|
|
|
512
554
|
|
|
@@ -518,7 +560,7 @@ def start_xploit(config: ClientConfig, shared_dict:dict, print_queue: Queue, poo
|
|
|
518
560
|
g.server_status_refresh_period = server_status_refresh_period
|
|
519
561
|
g.exploit_config = ExploitConfig.read(path)
|
|
520
562
|
g.print_queue = print_queue
|
|
521
|
-
g.
|
|
563
|
+
g.attack_storage = AttackStorage(config.client_id)
|
|
522
564
|
g.instance_storage = InstanceStorage()
|
|
523
565
|
g.exit_event = exit_event if exit_event else threading.Event()
|
|
524
566
|
g.restart_event = restart_event if restart_event else threading.Event()
|
|
@@ -6,7 +6,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
6
6
|
with open('requirements.txt', 'r', encoding='utf-8') as f:
|
|
7
7
|
required = [ele.strip() for ele in f.read().splitlines() if not ele.strip().startswith("#") and ele.strip() != ""]
|
|
8
8
|
|
|
9
|
-
VERSION = "0.2.
|
|
9
|
+
VERSION = "0.2.4"
|
|
10
10
|
|
|
11
11
|
setuptools.setup(
|
|
12
12
|
name="exploitfarm",
|
|
@@ -5,6 +5,7 @@ from rich import print
|
|
|
5
5
|
from rich.markup import escape
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
|
|
8
|
+
from typer import Abort
|
|
8
9
|
from enum import Enum
|
|
9
10
|
from exploitfarm.utils.reqs import get_url
|
|
10
11
|
from exploitfarm.cmd.config import InitialConfiguration, inital_config_setup, ClientConfig
|
|
@@ -82,11 +83,11 @@ def reset():
|
|
|
82
83
|
@app.command(help="Start the exploit")
|
|
83
84
|
def start(
|
|
84
85
|
path: str = typer.Argument(".", help="The path of the exploit"),
|
|
85
|
-
pool_size: PositiveInt = typer.Option(50, help="The number of workers to start"),
|
|
86
|
+
pool_size: PositiveInt = typer.Option(50, "-p", help="The number of workers to start"),
|
|
86
87
|
submit_pool_timeout: PositiveInt = typer.Option(3, help="The timeout for the submit pool to wait for new attack results and send flags"),
|
|
87
88
|
server_status_refresh_period: PositiveInt = typer.Option(5, help="The period to refresh the server status"),
|
|
88
89
|
test: Optional[str] = typer.Option(None, help="Test the exploit"),
|
|
89
|
-
test_timeout: PositiveInt = typer.Option(
|
|
90
|
+
test_timeout: PositiveInt = typer.Option(30, "-t", help="The timeout for the test"),
|
|
90
91
|
):
|
|
91
92
|
path = os.path.abspath(path)
|
|
92
93
|
from exploitfarm.xploit import start_xploit, shutdown, xploit_one
|
|
@@ -340,12 +341,16 @@ def init(
|
|
|
340
341
|
|
|
341
342
|
|
|
342
343
|
@app.callback()
|
|
343
|
-
def main(
|
|
344
|
-
g.interactive =
|
|
344
|
+
def main(no_interactive: bool = typer.Option(False, "--no-interactive", "-I", help="Interactive configuration mode", envvar="XFARM_INTERACTIVE")):
|
|
345
|
+
g.interactive = not no_interactive
|
|
345
346
|
|
|
346
347
|
if __name__ == "__main__":
|
|
347
348
|
try:
|
|
348
349
|
app()
|
|
350
|
+
except KeyboardInterrupt:
|
|
351
|
+
print("[bold yellow]Operation cancelled[/]")
|
|
352
|
+
except Abort:
|
|
353
|
+
print("[bold yellow]Operation cancelled[/]")
|
|
349
354
|
except ReqsError as e:
|
|
350
355
|
print("[bold red]The server returned an error: {e}[/]")
|
|
351
356
|
except RequestsTimeout as e:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|