Skip to content

Commit c860278

Browse files
moved socks5 and inhecttunnel forwarder from ssh-mitm
1 parent 646bdbb commit c860278

File tree

4 files changed

+370
-8
lines changed

4 files changed

+370
-8
lines changed

ssh_mitm_plugins/__entrypoints__.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,8 @@
55
'plugin-injectorshell = ssh_mitm_plugins.ssh.injectorshell:SSHInjectableForwarder',
66
'plugin-puttydos = ssh_mitm_plugins.ssh.putty_dos:SSHPuttyDoSForwarder'
77
],
8-
'SCPBaseForwarder': [
9-
8+
'LocalPortForwardingBaseForwarder': [
9+
'plugin-inject = ssh_mitm_plugins.tunnel.injectclienttunnel:InjectableClientTunnelForwarder',
10+
'plugin-socks5 = ssh_mitm_plugins.tunnel.socks5:SOCKS5TunnelForwarder'
1011
],
11-
'BaseSFTPServerInterface': [
12-
13-
],
14-
'SFTPHandlerBasePlugin': [
15-
16-
]
1712
}

ssh_mitm_plugins/tunnel/__init__.py

Whitespace-only changes.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import logging
2+
import re
3+
4+
import paramiko
5+
6+
from ssh_proxy_server.forwarders.tunnel import TunnelForwarder, LocalPortForwardingForwarder
7+
from ssh_proxy_server.plugins.session.tcpserver import TCPServerThread
8+
9+
10+
class ClientTunnelHandler:
11+
"""
12+
Similar to the ServerTunnelForwarder
13+
"""
14+
15+
def __init__(self, session, destination):
16+
self.session = session
17+
self.destination = destination
18+
19+
def handle_request(self, client, addr):
20+
try:
21+
logging.debug("Injecting direct-tcpip channel (%s -> %s) to client", addr, self.destination)
22+
remote_ch = self.session.ssh_client.transport.open_channel("direct-tcpip", self.destination, addr)
23+
TunnelForwarder(client, remote_ch)
24+
except paramiko.ssh_exception.ChannelException:
25+
client.close()
26+
logging.error("Could not setup forward from %s to %s.", addr, self.destination)
27+
28+
29+
class InjectableClientTunnelForwarder(LocalPortForwardingForwarder):
30+
"""Serve out direct-tcpip connections over a session on local ports
31+
"""
32+
33+
@classmethod
34+
def parser_arguments(cls):
35+
plugin_group = cls.parser().add_argument_group(cls.__name__)
36+
plugin_group.add_argument(
37+
'--tunnel-client-dest',
38+
dest='client_tunnel_dest',
39+
help='multiple direct-tcpip address/port combination to forward to (e.g. google.com:80, youtube.com:80)',
40+
required=True,
41+
nargs='+'
42+
)
43+
plugin_group.add_argument(
44+
'--tunnel-client-net',
45+
dest='client_tunnel_net',
46+
default='127.0.0.1',
47+
help='network on which to serve the client tunnel injector'
48+
)
49+
50+
session = None
51+
args = None
52+
tcpservers = []
53+
54+
# Setup should occur after master channel establishment
55+
56+
@classmethod
57+
def setup_injector(cls, session):
58+
parser_retval = cls.parser().parse_known_args(None, None)
59+
args, _ = parser_retval
60+
cls.session = session
61+
cls.args = args
62+
form = re.compile('.*:\d{1,5}')
63+
64+
for target in cls.args.client_tunnel_dest:
65+
if not form.match(target):
66+
logging.warning("--tunnel-client-dest %s does not match format host:port (e.g. google.com:80)", target)
67+
break
68+
destnet, destport = target.split(":")
69+
t = TCPServerThread(
70+
ClientTunnelHandler(session, (destnet, int(destport))).handle_request,
71+
run_status=cls.session.running,
72+
network=cls.args.client_tunnel_net
73+
)
74+
t.start()
75+
cls.tcpservers.append(t)
76+
logging.info(
77+
f"{session} created client tunnel injector for host {t.network} on port {t.port} to destination {target}"
78+
)

ssh_mitm_plugins/tunnel/socks5.py

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import logging
2+
from enum import Enum
3+
import socket
4+
from typing import (
5+
TYPE_CHECKING,
6+
List,
7+
Optional,
8+
Tuple,
9+
Union,
10+
Text
11+
)
12+
13+
import paramiko
14+
from typeguard import typechecked
15+
from rich._emoji_codes import EMOJI
16+
from colored.colored import stylize, fg, attr # type: ignore
17+
18+
import ssh_proxy_server
19+
from ssh_proxy_server.forwarders.tunnel import TunnelForwarder, LocalPortForwardingForwarder
20+
from ssh_proxy_server.plugins.session.tcpserver import TCPServerThread
21+
if TYPE_CHECKING:
22+
from ssh_proxy_server.session import Session
23+
24+
25+
class Socks5Error(Exception):
26+
pass
27+
28+
29+
class Socks5Types(Enum):
30+
"""Basisklasse für Socks5 Daten"""
31+
32+
def __str__(self):
33+
return self.value
34+
35+
def __add__(self, other):
36+
return self.value + other
37+
38+
def __radd__(self, other):
39+
return other + self.value
40+
41+
42+
class Socks5AuthenticationType(Socks5Types):
43+
"""Authentifizierungstypen für den Socks Proxy"""
44+
NONE = b"\x00"
45+
PASSWORD = b"\x02"
46+
47+
48+
class Socks5Command(Socks5Types):
49+
"""Kommandos für den Socks Proxy"""
50+
CONNECT = b"\x01"
51+
BIND = b"\x02"
52+
UDP = b"\x03"
53+
54+
55+
class Socks5AddressType(Socks5Types):
56+
"""Addresstypen für den Socks Proxy"""
57+
IPv4 = b"\x01"
58+
DOMAIN = b"\x03"
59+
IPv6 = b"\x04"
60+
61+
62+
class Socks5CommandReply(Socks5Types):
63+
"""Bestättigungen für den Socks Proxy"""
64+
SUCCESS = b"\x00"
65+
GENERAL_FAILURE = b"\x01"
66+
CONNECTION_NOT_ALLOWED = b"\x02"
67+
NETWORK_UNREACHABLE = b"\x03"
68+
HOST_UNREACHABLE = b"\x04"
69+
CONNECTION_REFUSED = b"\x05"
70+
TTL_EXPIRED = b"\x06"
71+
COMMAND_NOT_SUPPORTED = b"\x07"
72+
ADDR_TYPE_NOT_SUPPORTED = b"\x00"
73+
74+
75+
class Socks5Server():
76+
"""Socks5 kompatibler Forwarder
77+
Dieser Socks5 Forwarder unterstützt Authentifizierung.
78+
"""
79+
SOCKSVERSION = b"\x05"
80+
AUTH_PASSWORD_VERSION = b"\x01"
81+
82+
def __init__(self, listenaddress, username=None, password=None):
83+
self.listenaddress = listenaddress
84+
self.username = username
85+
self.password = password
86+
self.auth_required = self.username and self.password
87+
88+
@property
89+
def server_ip(self):
90+
"""Liefert die IP Adresse des Socks Proxy zurück"""
91+
return b"".join([bytes([int(i)]) for i in self.listenaddress[0].split(".")])
92+
93+
@property
94+
def server_port(self):
95+
"""Liefert den Port den Socks Proxy zurück"""
96+
server_port = self.listenaddress[1]
97+
return bytes([int(server_port / 256)]) + bytes([int(server_port % 256)])
98+
99+
100+
def _get_auth_methods(self, clientsock):
101+
"""Ermittelt die angebotenen Authentifizierungsmechanismen"""
102+
if clientsock.recv(1) != Socks5Server.SOCKSVERSION:
103+
raise Socks5Error("Invalid Socks5 Version")
104+
methods_count = int.from_bytes(clientsock.recv(1), byteorder='big')
105+
try:
106+
methods = [Socks5AuthenticationType(bytes([m])) for m in clientsock.recv(methods_count)]
107+
except ValueError:
108+
raise Socks5Error("Invalid methods")
109+
if len(methods) != methods_count:
110+
raise Socks5Error("Invalid number of methods")
111+
return methods
112+
113+
def _authenticate(self, clientsock):
114+
"""Authentifiziert den Benutzer"""
115+
authmethods = self._get_auth_methods(clientsock)
116+
117+
if not self.auth_required and Socks5AuthenticationType.NONE in authmethods:
118+
clientsock.sendall(Socks5Server.SOCKSVERSION + Socks5AuthenticationType.NONE)
119+
return True
120+
elif self.auth_required and Socks5AuthenticationType.PASSWORD in authmethods:
121+
clientsock.sendall(Socks5Server.SOCKSVERSION + Socks5AuthenticationType.PASSWORD)
122+
else:
123+
clientsock.sendall(Socks5Server.SOCKSVERSION + b"\xFF")
124+
logging.warning("client does not offer supported authentication types")
125+
return False
126+
127+
if Socks5Server.AUTH_PASSWORD_VERSION != clientsock.recv(1):
128+
raise Socks5Error('Wrong Authentication Version')
129+
130+
username_len = int.from_bytes(clientsock.recv(1), byteorder='big')
131+
username = clientsock.recv(username_len).decode("utf8")
132+
if len(username) != username_len:
133+
raise Socks5Error("Invalid username length")
134+
135+
password_len = int.from_bytes(clientsock.recv(1), byteorder='big')
136+
password = clientsock.recv(password_len).decode("utf8")
137+
if len(password) != password_len:
138+
raise Socks5Error("Invalid password length")
139+
140+
if self.check_credentials(username, password):
141+
clientsock.sendall(Socks5Server.AUTH_PASSWORD_VERSION + b"\x00")
142+
return True
143+
144+
logging.warning("Authentication failed")
145+
clientsock.sendall(Socks5Server.AUTH_PASSWORD_VERSION + b"\x01")
146+
return False
147+
148+
def _get_address(self, clientsock):
149+
"""Ermittelt das Ziel aus der Socks Anfrage"""
150+
# check socks version
151+
if clientsock.recv(1) != Socks5Server.SOCKSVERSION:
152+
raise Socks5Error("Invalid Socks5 Version")
153+
# get socks command
154+
try:
155+
command = Socks5Command(clientsock.recv(1))
156+
except ValueError:
157+
raise Socks5Error("Invalid Socks5 command")
158+
159+
if clientsock.recv(1) != b"\x00":
160+
raise Socks5Error("Reserved byte must be 0x00")
161+
162+
try:
163+
address_type = Socks5AddressType(clientsock.recv(1))
164+
except ValueError:
165+
raise Socks5Error("Invalid Socks5 address type")
166+
167+
if address_type is Socks5AddressType.IPv4:
168+
dst_addr, dst_port = clientsock.recv(4), clientsock.recv(2)
169+
if len(dst_addr) != 4 and dst_port != 2:
170+
raise Socks5Error("Invalid IPv4 Address")
171+
dst_addr = ".".join([str(i) for i in dst_addr])
172+
elif address_type is Socks5AddressType.DOMAIN:
173+
addr_len = int.from_bytes(clientsock.recv(1), byteorder='big')
174+
dst_addr, dst_port = clientsock.recv(addr_len), clientsock.recv(2)
175+
if len(dst_addr) != addr_len and dst_port != 2:
176+
raise Socks5Error("Invalid domain")
177+
dst_addr = "".join([chr(i) for i in dst_addr])
178+
elif address_type is Socks5AddressType.IPv6:
179+
dst_addr, dst_port = clientsock.recv(16), clientsock.recv(2)
180+
if len(dst_addr) != 16 and dst_port != 2:
181+
raise Socks5Error("Invalid IPv6 Address")
182+
tmp_addr = []
183+
for i in range(len(dst_addr) / 2):
184+
tmp_addr.append(chr(dst_addr[2 * i] * 256 + dst_addr[2 * i + 1]))
185+
dst_addr = ":".join(tmp_addr)
186+
else:
187+
raise Socks5Error("Unhandled address type")
188+
189+
dst_port = dst_port[0] * 256 + dst_port[1]
190+
191+
address = None
192+
reply = Socks5CommandReply.COMMAND_NOT_SUPPORTED
193+
if command is Socks5Command.CONNECT:
194+
address = (dst_addr, dst_port)
195+
reply = Socks5CommandReply.SUCCESS
196+
197+
clientsock.sendall(
198+
Socks5Server.SOCKSVERSION +
199+
reply +
200+
b"\x00" +
201+
Socks5AddressType.IPv4 +
202+
self.server_ip +
203+
self.server_port
204+
)
205+
206+
return address
207+
208+
def check_credentials(self, username, password):
209+
"""Prüft Benutzername und Passwort"""
210+
return username == self.username and password == self.password
211+
212+
def get_address(self, clientsock):
213+
try:
214+
if self._authenticate(clientsock):
215+
return self._get_address(clientsock)
216+
except Socks5Error as sockserror:
217+
logging.error("Socks5 Error: %s", str(sockserror))
218+
return None
219+
220+
221+
class ClientTunnelHandler:
222+
"""
223+
Similar to the RemotePortForwardingForwarder
224+
"""
225+
226+
@typechecked
227+
def __init__(
228+
self,
229+
session: 'ssh_proxy_server.session.Session'
230+
) -> None:
231+
self.session = session
232+
233+
@typechecked
234+
def handle_request(self, listenaddr: Tuple[Text, int], client: Union[socket.socket, paramiko.Channel], addr: Optional[Tuple[str, int]]) -> None:
235+
if self.session.ssh_client is None or self.session.ssh_client.transport is None:
236+
return
237+
destination: Optional[Tuple[Text, int]] = None
238+
socks5connection = Socks5Server(listenaddr)
239+
destination = socks5connection.get_address(client)
240+
if destination is None:
241+
client.close()
242+
logging.error("unable to parse socks5 request")
243+
return
244+
try:
245+
logging.debug("Injecting direct-tcpip channel (%s -> %s) to client", addr, destination)
246+
remote_ch = self.session.ssh_client.transport.open_channel("direct-tcpip", destination, addr)
247+
TunnelForwarder(client, remote_ch)
248+
except paramiko.ssh_exception.ChannelException:
249+
client.close()
250+
logging.error("Could not setup forward from %s to %s.", addr, destination)
251+
252+
253+
class SOCKS5TunnelForwarder(LocalPortForwardingForwarder):
254+
"""Serve out direct-tcpip connections over a session on local ports
255+
"""
256+
257+
@classmethod
258+
@typechecked
259+
def parser_arguments(cls) -> None:
260+
plugin_group = cls.parser().add_argument_group(cls.__name__)
261+
plugin_group.add_argument(
262+
'--tunnel-client-net',
263+
dest='client_tunnel_net',
264+
default='127.0.0.1',
265+
help='network on which to serve the client tunnel injector'
266+
)
267+
268+
tcpservers: List[TCPServerThread] = []
269+
270+
# Setup should occur after master channel establishment
271+
272+
@classmethod
273+
@typechecked
274+
def setup(cls, session: 'ssh_proxy_server.session.Session') -> None:
275+
parser_retval = cls.parser().parse_known_args(None, None)
276+
args, _ = parser_retval
277+
278+
t = TCPServerThread(
279+
ClientTunnelHandler(session).handle_request,
280+
run_status=session.running,
281+
network=args.client_tunnel_net
282+
)
283+
t.start()
284+
cls.tcpservers.append(t)
285+
logging.info((
286+
f"{EMOJI['information']} {stylize(session.sessionid, fg('light_blue') + attr('bold'))}"
287+
" - "
288+
f"created SOCKS5 proxy server on port {t.port}. connect with: {stylize(f'nc -X 5 -x localhost:{t.port} address port', fg('light_blue') + attr('bold'))}"
289+
))

0 commit comments

Comments
 (0)