koi-handler 0.11.3__tar.gz → 0.11.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 (65) hide show
  1. {koi_handler-0.11.3/src/koi_handler.egg-info → koi_handler-0.11.4}/PKG-INFO +1 -1
  2. {koi_handler-0.11.3 → koi_handler-0.11.4}/pyproject.toml +1 -1
  3. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/main.py +28 -3
  4. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/mcp/server.py +1 -1
  5. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/download.py +3 -3
  6. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/upload.py +1 -1
  7. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/obfuscate_ui.py +120 -54
  8. {koi_handler-0.11.3 → koi_handler-0.11.4/src/koi_handler.egg-info}/PKG-INFO +1 -1
  9. {koi_handler-0.11.3 → koi_handler-0.11.4}/LICENSE +0 -0
  10. {koi_handler-0.11.3 → koi_handler-0.11.4}/setup.cfg +0 -0
  11. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/listener.py +0 -0
  12. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/mcp/__init__.py +0 -0
  13. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/mcp/schema.py +0 -0
  14. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/blueprint.py +0 -0
  15. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/duplicate.py +0 -0
  16. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/env_dump.py +0 -0
  17. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/get_processes.py +0 -0
  18. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/get_users.py +0 -0
  19. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/ligolo.py +0 -0
  20. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/loader.py +0 -0
  21. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/network_enum.py +0 -0
  22. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/peas.py +0 -0
  23. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/populate_win.py +0 -0
  24. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/secrets_dump.py +0 -0
  25. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/sharphound.py +0 -0
  26. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/sysinfo.py +0 -0
  27. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/upload64.py +0 -0
  28. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/wifi_enum.py +0 -0
  29. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/modules/winscalate.py +0 -0
  30. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/session.py +0 -0
  31. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/tunnel/__init__.py +0 -0
  32. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/tunnel/agent_payload.py +0 -0
  33. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/tunnel/engine/__init__.py +0 -0
  34. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/tunnel/engine/__main__.py +0 -0
  35. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/tunnel/engine/agent.py +0 -0
  36. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/tunnel/engine/packet.py +0 -0
  37. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/tunnel/engine/proxy.py +0 -0
  38. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/tunnel/engine/stack.py +0 -0
  39. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/tunnel/engine/tcp.py +0 -0
  40. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/tunnel/engine/transport.py +0 -0
  41. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/tunnel/engine/tun.py +0 -0
  42. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/tunnel/manager.py +0 -0
  43. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/__init__.py +0 -0
  44. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/bash_obfuscate.py +0 -0
  45. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/cache.py +0 -0
  46. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/cli.py +0 -0
  47. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/config.py +0 -0
  48. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/connect/__init__.py +0 -0
  49. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/connect/base.py +0 -0
  50. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/connect/ssh.py +0 -0
  51. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/constants.py +0 -0
  52. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/detect.py +0 -0
  53. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/interact.py +0 -0
  54. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/logger.py +0 -0
  55. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/models.py +0 -0
  56. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/payloads.py +0 -0
  57. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/powerupgrade.py +0 -0
  58. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/ps_obfuscate.py +0 -0
  59. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/tcp.py +0 -0
  60. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi/utils/ui.py +0 -0
  61. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi_handler.egg-info/SOURCES.txt +0 -0
  62. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi_handler.egg-info/dependency_links.txt +0 -0
  63. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi_handler.egg-info/entry_points.txt +0 -0
  64. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi_handler.egg-info/requires.txt +0 -0
  65. {koi_handler-0.11.3 → koi_handler-0.11.4}/src/koi_handler.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: koi-handler
3
- Version: 0.11.3
3
+ Version: 0.11.4
4
4
  Summary: Reverse shell listener
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "koi-handler"
7
- version = "0.11.3"
7
+ version = "0.11.4"
8
8
  description = "Reverse shell listener"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -88,14 +88,35 @@ class _ArtHelpAction(argparse.Action):
88
88
  parser.exit()
89
89
 
90
90
 
91
+ def _print_version() -> None:
92
+ from koi.utils.ui import accent, bold, dim
93
+ print(f"koi {bold(__version__)}")
94
+ try:
95
+ from urllib.request import urlopen, Request
96
+ import json
97
+ req = Request(
98
+ "https://api.github.com/repos/b3rt1ng/Koi/releases/latest",
99
+ headers={"User-Agent": "koi/update-check", "Accept": "application/json"},
100
+ )
101
+ with urlopen(req, timeout=3) as resp:
102
+ latest = json.loads(resp.read())["tag_name"].lstrip("vV")
103
+ local = __version__.lstrip("vV")
104
+ if latest == local:
105
+ print(dim(" up to date"))
106
+ else:
107
+ print(f"\n{accent(f'v{latest}')} is available! {dim('pip install --upgrade koi-handler')}")
108
+ except Exception:
109
+ print(dim(" could not check for updates"))
110
+
111
+
91
112
  def main():
92
113
  parser = argparse.ArgumentParser(
93
114
  description="koi – multi-session reverse shell listener",
94
115
  add_help=False,
95
116
  )
96
117
  parser.add_argument("-h", "--help", action=_ArtHelpAction, help="show this help message and exit")
97
- parser.add_argument("-v", "--version", action="version", version=f"koi {__version__}",
98
- help="show the koi version and exit")
118
+ parser.add_argument("-v", "--version", action="store_true",
119
+ help="show the koi version, check for updates, and exit")
99
120
 
100
121
  listener_group = parser.add_argument_group("listener")
101
122
  listener_group.add_argument("--host", default=CONFIG["host"], help=f"Bind address (default: {CONFIG['host']})")
@@ -150,7 +171,11 @@ def main():
150
171
  help="Bearer token for the MCP server (default: saved in ~/.koi/config.json, or $KOI_MCP_TOKEN)")
151
172
 
152
173
  args = parser.parse_args()
153
-
174
+
175
+ if args.version:
176
+ _print_version()
177
+ sys.exit(0)
178
+
154
179
  if args.purge_cache:
155
180
  from koi.utils.cache import purge_cache
156
181
  ok = purge_cache()
@@ -554,7 +554,7 @@ class KoiMCPServer:
554
554
  if sess.os_type != "linux":
555
555
  raise ValueError(
556
556
  f"koi_exec runs POSIX shell and only supports Linux sessions; "
557
- f"#{sess.id} is {sess.os_type or 'unknown OS'} — use a module instead."
557
+ f"#{sess.id} is {sess.os_type or 'unknown OS'}, use a module instead."
558
558
  )
559
559
 
560
560
  command = arguments["command"]
@@ -141,7 +141,7 @@ class DownloadModule(KoiModule):
141
141
 
142
142
  if remote_size is not None and len(raw) != remote_size:
143
143
  self.err(
144
- f"Size mismatch: expected {remote_size} bytes, got {len(raw)} — file not saved."
144
+ f"Size mismatch: expected {remote_size} bytes, got {len(raw)} bytes. File not saved."
145
145
  )
146
146
  return
147
147
 
@@ -221,7 +221,7 @@ class DownloadModule(KoiModule):
221
221
  print()
222
222
 
223
223
  if not raw:
224
- self.err("Received empty archive — transfer may have timed out.")
224
+ self.err("Received empty archive, transfer may have timed out.")
225
225
  return
226
226
 
227
227
  with self.spinner(f"Extracting into {local_out}/..."):
@@ -252,7 +252,7 @@ class DownloadModule(KoiModule):
252
252
  zf.extract(info, path=local_out)
253
253
  file_count = sum(1 for i in safe if not i.filename.endswith("/"))
254
254
  except zipfile.BadZipFile as exc:
255
- self.err(f"Received incomplete archive — {exc}")
255
+ self.err(f"Received incomplete archive: {exc}")
256
256
  return
257
257
  except Exception as exc:
258
258
  self.err(f"Could not extract archive: {exc}")
@@ -112,7 +112,7 @@ class UploadModule(KoiModule):
112
112
  print()
113
113
 
114
114
  if not ok:
115
- self.err("Transfer failed — archive not delivered to target.")
115
+ self.err("Transfer failed! Archive not delivered to target.")
116
116
  return
117
117
 
118
118
  # 6. Extract on target
@@ -36,11 +36,13 @@ WIN_METHODS: list[tuple[str, str, Callable[[str], str]]] = [
36
36
  ("bullshit", "insert random no-op statements", ps_bullshit_obfuscate),
37
37
  ]
38
38
 
39
- _HIDE = "\033[?25l"
40
- _SHOW = "\033[?25h"
41
- _ALT_ON = "\033[?1049h"
42
- _ALT_OFF = "\033[?1049l"
43
- _CLEAR = "\033[2J\033[H"
39
+ _HIDE = "\033[?25l"
40
+ _SHOW = "\033[?25h"
41
+ _ALT_ON = "\033[?1049h"
42
+ _ALT_OFF = "\033[?1049l"
43
+ _CLEAR = "\033[2J\033[H"
44
+ _BPASTE_ON = "\033[?2004h"
45
+ _BPASTE_OFF = "\033[?2004l"
44
46
 
45
47
 
46
48
  def _getch() -> bytes:
@@ -52,7 +54,31 @@ def _getch() -> bytes:
52
54
  if ch == b'\x1b':
53
55
  r, _, _ = select.select([sys.stdin], [], [], 0.05)
54
56
  if r:
55
- ch += os.read(fd, 2)
57
+ nxt = os.read(fd, 1)
58
+ if nxt == b'[':
59
+ param = bytearray()
60
+ while True:
61
+ b = os.read(fd, 1)
62
+ if not b:
63
+ break
64
+ param.extend(b)
65
+ if 0x40 <= b[0] <= 0x7E:
66
+ break
67
+ if len(param) > 16:
68
+ break
69
+ seq = b'[' + bytes(param)
70
+ if seq == b'[200~':
71
+ paste = bytearray()
72
+ while len(paste) < 500_000:
73
+ b = os.read(fd, 1)
74
+ if not b:
75
+ break
76
+ paste.extend(b)
77
+ if paste.endswith(b'\x1b[201~'):
78
+ return b'\x1b[200~' + bytes(paste[:-6])
79
+ return b'\x1b[200~' + bytes(paste)
80
+ return b'\x1b' + seq
81
+ return b'\x1b' + nxt
56
82
  return ch
57
83
  finally:
58
84
  termios.tcsetattr(fd, termios.TCSADRAIN, old)
@@ -67,7 +93,7 @@ def _render_menu(title: str, options: list[str], cursor: int) -> None:
67
93
  sys.stdout.write(f" {accent('►')} {bold(accent(opt))}\n")
68
94
  else:
69
95
  sys.stdout.write(f" {dim(opt)}\n")
70
- sys.stdout.write(f"\n {dim('↑/↓')} navigate {dim('Enter')} select {dim('q')} cancel\n")
96
+ sys.stdout.write(f"\n {dim('↑/↓')} navigate {dim('Enter')} select {dim('⌫')} back {dim('q')} cancel\n")
71
97
  sys.stdout.flush()
72
98
 
73
99
 
@@ -115,6 +141,9 @@ def _render_obfuscator(
115
141
  f"\n{indent}{dim('↑/↓')} navigate "
116
142
  f"{dim('Enter')} apply "
117
143
  f"{dim('r')} reset "
144
+ f"{dim('^R')} original "
145
+ f"{dim('paste')} replace "
146
+ f"{dim('⌫')} back "
118
147
  f"{dim('q')} quit & print\n"
119
148
  )
120
149
  sys.stdout.flush()
@@ -124,8 +153,8 @@ def _run_obfuscator_loop(
124
153
  title: str,
125
154
  base: str,
126
155
  methods: list[tuple[str, str, Callable]],
127
- ) -> tuple[str, list[str]]:
128
- """Run the interactive obfuscator. Returns (final_payload, chain)."""
156
+ ) -> tuple[str, list[str]] | None:
157
+ original = base
129
158
  current = base
130
159
  chain: list[str] = []
131
160
  cursor = 0
@@ -134,7 +163,13 @@ def _run_obfuscator_loop(
134
163
  _render_obfuscator(title, current, chain, cursor, methods)
135
164
  ch = _getch()
136
165
 
137
- if ch == b'\x1b[A':
166
+ if ch.startswith(b'\x1b[200~'):
167
+ pasted = ch[6:].decode("utf-8", errors="replace").strip()
168
+ if pasted:
169
+ base = pasted
170
+ current = pasted
171
+ chain = []
172
+ elif ch == b'\x1b[A':
138
173
  cursor = (cursor - 1) % len(methods)
139
174
  elif ch == b'\x1b[B':
140
175
  cursor = (cursor + 1) % len(methods)
@@ -145,6 +180,12 @@ def _run_obfuscator_loop(
145
180
  elif ch in (b'r', b'R'):
146
181
  current = base
147
182
  chain = []
183
+ elif ch == b'\x12':
184
+ base = original
185
+ current = original
186
+ chain = []
187
+ elif ch in (b'\x7f', b'\x08'):
188
+ return None
148
189
  elif ch in (b'q', b'Q', b'\x03', b'\x04'):
149
190
  break
150
191
 
@@ -172,58 +213,83 @@ def run_obfuscate_ui(iface: str | None, port: int) -> None:
172
213
  final_chain: list[str] = []
173
214
  final_label = ""
174
215
 
175
- sys.stdout.write(_ALT_ON + _HIDE)
216
+ iface_names = list(interfaces.keys())
217
+ has_iface_menu = selected_iface is None
218
+ gen = PayloadGenerator(port)
219
+ iface_cursor = 0
220
+ os_cursor = 0
221
+
222
+ sys.stdout.write(_ALT_ON + _HIDE + _BPASTE_ON)
176
223
  sys.stdout.flush()
177
224
  try:
178
- if selected_iface is None:
179
- iface_names = list(interfaces.keys())
180
- iface_cursor = 0
181
- while True:
182
- _render_menu("Obfuscate Select Interface", iface_names, iface_cursor)
183
- ch = _getch()
184
- if ch == b'\x1b[A':
185
- iface_cursor = (iface_cursor - 1) % len(iface_names)
186
- elif ch == b'\x1b[B':
187
- iface_cursor = (iface_cursor + 1) % len(iface_names)
188
- elif ch in (b'\r', b'\n'):
189
- selected_iface = iface_names[iface_cursor]
190
- break
191
- elif ch in (b'q', b'Q', b'\x03'):
192
- raise _Cancelled
193
-
194
- gen = PayloadGenerator(port)
195
-
196
- os_cursor = 0
225
+ step = "interface" if has_iface_menu else "platform"
197
226
  while True:
198
- _render_menu("Obfuscate Select Platform", ["Windows", "Linux"], os_cursor)
199
- ch = _getch()
200
- if ch == b'\x1b[A':
201
- os_cursor = (os_cursor - 1) % 2
202
- elif ch == b'\x1b[B':
203
- os_cursor = (os_cursor + 1) % 2
204
- elif ch in (b'\r', b'\n'):
227
+ if step == "interface":
228
+ while True:
229
+ _render_menu("Obfuscate Select Interface", iface_names, iface_cursor)
230
+ ch = _getch()
231
+ if ch == b'\x1b[A':
232
+ iface_cursor = (iface_cursor - 1) % len(iface_names)
233
+ elif ch == b'\x1b[B':
234
+ iface_cursor = (iface_cursor + 1) % len(iface_names)
235
+ elif ch in (b'\r', b'\n'):
236
+ selected_iface = iface_names[iface_cursor]
237
+ step = "platform"
238
+ break
239
+ elif ch in (b'\x7f', b'\x08', b'q', b'Q', b'\x03'):
240
+ raise _Cancelled
241
+ if step != "platform":
242
+ continue
243
+
244
+ if step == "platform":
245
+ back = False
246
+ while True:
247
+ _render_menu("Obfuscate Select Platform", ["Windows", "Linux"], os_cursor)
248
+ ch = _getch()
249
+ if ch == b'\x1b[A':
250
+ os_cursor = (os_cursor - 1) % 2
251
+ elif ch == b'\x1b[B':
252
+ os_cursor = (os_cursor + 1) % 2
253
+ elif ch in (b'\r', b'\n'):
254
+ step = "obfuscate"
255
+ break
256
+ elif ch in (b'\x7f', b'\x08'):
257
+ if has_iface_menu:
258
+ step = "interface"
259
+ else:
260
+ raise _Cancelled
261
+ back = True
262
+ break
263
+ elif ch in (b'q', b'Q', b'\x03'):
264
+ raise _Cancelled
265
+ if back:
266
+ continue
267
+
268
+ if step == "obfuscate":
269
+ payloads = gen.for_interface(selected_iface)
270
+ if payloads is None:
271
+ raise _Cancelled
272
+ if os_cursor == 0:
273
+ result = _run_obfuscator_loop(
274
+ "Windows Payload Obfuscator", payloads["powershell"], WIN_METHODS
275
+ )
276
+ final_label = "powershell"
277
+ else:
278
+ result = _run_obfuscator_loop(
279
+ "Linux Payload Obfuscator", payloads["bash (alt)"], LINUX_METHODS
280
+ )
281
+ final_label = "bash"
282
+
283
+ if result is None:
284
+ step = "platform"
285
+ continue
286
+ final_payload, final_chain = result
205
287
  break
206
- elif ch in (b'q', b'Q', b'\x03'):
207
- raise _Cancelled
208
-
209
- payloads = gen.for_interface(selected_iface)
210
- if payloads is None:
211
- raise _Cancelled
212
- if os_cursor == 0:
213
- final_payload, final_chain = _run_obfuscator_loop(
214
- "Windows Payload Obfuscator", payloads["powershell"], WIN_METHODS
215
- )
216
- final_label = "powershell"
217
- else:
218
- final_payload, final_chain = _run_obfuscator_loop(
219
- "Linux Payload Obfuscator", payloads["bash (alt)"], LINUX_METHODS
220
- )
221
- final_label = "bash"
222
288
 
223
289
  except (_Cancelled, KeyboardInterrupt):
224
290
  pass
225
291
  finally:
226
- sys.stdout.write(_SHOW + _ALT_OFF + "\n")
292
+ sys.stdout.write(_BPASTE_OFF + _SHOW + _ALT_OFF + "\n")
227
293
  sys.stdout.flush()
228
294
 
229
295
  if not final_payload:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: koi-handler
3
- Version: 0.11.3
3
+ Version: 0.11.4
4
4
  Summary: Reverse shell listener
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
File without changes
File without changes