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.
Files changed (23) hide show
  1. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/PKG-INFO +1 -1
  2. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/__init__.py +1 -1
  3. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/cmd/startxploit.py +4 -1
  4. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/xploit.py +87 -45
  5. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm.egg-info/PKG-INFO +1 -1
  6. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/setup.py +1 -1
  7. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/xfarm +9 -4
  8. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/MANIFEST.in +0 -0
  9. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/README.md +0 -0
  10. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/cmd/__init__.py +0 -0
  11. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/cmd/config.py +0 -0
  12. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/cmd/exploitinit.py +0 -0
  13. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/cmd/login.py +0 -0
  14. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/model.py +0 -0
  15. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/utils/__init__.py +0 -0
  16. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/utils/config.py +0 -0
  17. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm/utils/reqs.py +0 -0
  18. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm.egg-info/SOURCES.txt +0 -0
  19. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm.egg-info/dependency_links.txt +0 -0
  20. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm.egg-info/requires.txt +0 -0
  21. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/exploitfarm.egg-info/top_level.txt +0 -0
  22. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/requirements.txt +0 -0
  23. {exploitfarm-0.2.3 → exploitfarm-0.2.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: exploitfarm
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Exploit Farm client
5
5
  Home-page: https://github.com/pwnzer0tt1/exploitfarm
6
6
  Author: Pwnzer0tt1
@@ -1,5 +1,5 @@
1
1
 
2
- __version__ = "0.2.3"
2
+ __version__ = "0.2.4"
3
3
 
4
4
  from exploitfarm.utils import try_tcp_connection
5
5
  from exploitfarm.model import ServiceDTO, AttackMode, SetupStatus
@@ -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
- logger.write(log)
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
- flag_storage:"FlagStorage" = None
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 FlagStorage:
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 pick_flags(self):
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
- for arg in args:
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.flag_storage.add(ele["flags"], ele["target"], ele["start_time"], ele["end_time"], AttackExecutionStatus(ele["status"]), ele["error"])
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 post_flags(flags):
253
+ def post_attacks(attacks):
247
254
  try:
248
- g.config.reqs.submit_flags(flags, exploit=g.exploit_config.uuid)
249
- g.flag_storage.mark_as_sent(len(flags))
250
- qprint(f'{sum([len(ele["flags"]) for ele in flags])} flags posted to the server')
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
- flags_to_post = g.flag_storage.pick_flags()[:50]
260
- g.shared_memory["submitter_flags_in_queue"] = sum([len(ele["flags"]) for ele in flags_to_post])
261
- if flags_to_post: post_flags(flags_to_post)
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 = proc.stdout.read()
277
+ output = read_data()
271
278
 
272
279
  if isinstance(output, bytes):
273
280
  output = output.decode('utf-8', errors='replace')
274
281
 
275
- 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}")
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.flag_storage.add(flags, team["id"], start_time, end_time, AttackExecutionStatus.crashed, output)
295
+ g.attack_storage.add(flags, team["id"], start_time, end_time, AttackExecutionStatus.crashed, output)
284
296
  elif flags:
285
- g.flag_storage.add(flags, team["id"], start_time, end_time, AttackExecutionStatus.done, b'')
297
+ g.attack_storage.add(flags, team["id"], start_time, end_time, AttackExecutionStatus.done, b'')
286
298
  else:
287
- g.flag_storage.add(flags, team["id"], start_time, end_time, AttackExecutionStatus.noflags, output)
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 launch_sploit(team:dict, max_runtime:float):
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.flag_storage.pick_flags()}
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
- print(f"[bold yellow]Can't get config from the server: {repr(e)}")
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.flag_storage = FlagStorage(config.client_id)
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
- flags_block = [ele["flags"] for ele in g.flag_storage.pick_flags()]
491
- if len(flags_block) == 0:
534
+ attacks = g.attack_storage.pick_attacks()
535
+ if len(attacks) == 0:
492
536
  break
493
- for flags in flags_block:
494
- g.config.reqs.submit_flags({"flags":flags})
495
- g.flag_storage.mark_as_sent(len(flags))
496
- print(f"Submitted {len(flags)} flags as manual submissions.")
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
- print(f"[bold yellow]Can't submit flags due missing config")
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.flag_storage = FlagStorage(config.client_id)
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: exploitfarm
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Exploit Farm client
5
5
  Home-page: https://github.com/pwnzer0tt1/exploitfarm
6
6
  Author: Pwnzer0tt1
@@ -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.3"
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(10, help="The timeout for the test"),
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(interactive: bool = typer.Option(True, help="Interactive configuration mode", envvar="XFARM_INTERACTIVE")):
344
- g.interactive = 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