Skip to content

Commit 32b2ec4

Browse files
added typehints and typechecking
1 parent e288dc4 commit 32b2ec4

File tree

15 files changed

+386
-220
lines changed

15 files changed

+386
-220
lines changed

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ def get_entry_points():
7171
'packaging',
7272
'colored',
7373
'rich',
74-
'requests'
74+
'requests',
75+
'typeguard'
7576
],
7677
extras_require={
7778
'plugins': [

ssh_proxy_server/audit/cli.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,32 @@
55
import sys
66

77
from paramiko.pkey import PublicBlob
8+
from typeguard import typechecked
89
from ssh_proxy_server.authentication import probe_host, Authenticator
910

1011

11-
def check_publickey(args: argparse.Namespace) -> None:
12+
@typechecked
13+
def check_publickey(args: argparse.Namespace) -> bool:
1214
key = open(args.public_key, 'rt').read()
15+
try:
16+
pubkey = PublicBlob.from_string(key)
17+
except:
18+
print("file is not a valid public key")
19+
return False
1320
if probe_host(
1421
hostname_or_ip=args.host,
1522
port=args.port,
1623
username=args.username,
17-
public_key=PublicBlob.from_string(key)
24+
public_key=pubkey
1825
):
1926
print("valid key")
27+
return True
2028
else:
2129
print("bad key")
30+
return False
2231

2332

33+
@typechecked
2434
def main() -> None:
2535
parser = argparse.ArgumentParser()
2636
subparsers = parser.add_subparsers(title='Available commands', dest="subparser_name", metavar='subcommand')
@@ -38,7 +48,8 @@ def main() -> None:
3848

3949
args = parser.parse_args(sys.argv[1:])
4050
if args.subparser_name == 'check-publickey':
41-
check_publickey(args)
51+
if not check_publickey(args):
52+
sys.exit(1)
4253
elif args.subparser_name == 'get-auth':
4354
auth_methods = Authenticator.get_auth_methods(args.host, args.port)
4455
if auth_methods:

ssh_proxy_server/authentication.py

Lines changed: 82 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,38 @@
33
import os
44
import sys
55
import socket
6-
import re
76

87
from typing import (
98
Optional,
10-
List
9+
List,
10+
Tuple,
11+
Text
1112
)
1213

13-
from colored.colored import stylize, attr, fg
14+
from colored.colored import stylize, attr, fg # type: ignore
15+
from paramiko import PKey
1416
from rich._emoji_codes import EMOJI
1517

1618
from enhancements.modules import BaseModule
1719
import paramiko
18-
from sshpubkeys import SSHKey
20+
from sshpubkeys import SSHKey # type: ignore
21+
from typeguard import typechecked
1922

2023
from ssh_proxy_server.clients.ssh import SSHClient, AuthenticationMethod
2124
from ssh_proxy_server.exceptions import MissingHostException
25+
from ssh_proxy_server.session import Session
2226

2327

24-
def probe_host(hostname_or_ip, port, username, public_key):
28+
@typechecked
29+
def probe_host(hostname_or_ip: Text, port: int, username: Text, public_key: paramiko.pkey.PublicBlob) -> bool:
2530

26-
def valid(self, msg):
31+
@typechecked
32+
def valid(self, msg: paramiko.message.Message) -> None:
2733
self.auth_event.set()
2834
self.authenticated = True
2935

30-
def parse_service_accept(self, m):
36+
@typechecked
37+
def parse_service_accept(self, m: paramiko.message.Message) -> None:
3138
# https://tools.ietf.org/html/rfc4252#section-7
3239
service = m.get_text()
3340
if not (service == "ssh-userauth" and self.auth_method == "publickey"):
@@ -44,7 +51,7 @@ def parse_service_accept(self, m):
4451

4552
valid_key = False
4653
try:
47-
client_handler_table = paramiko.auth_handler.AuthHandler._client_handler_table
54+
client_handler_table = paramiko.auth_handler.AuthHandler._client_handler_table # type: ignore
4855
client_handler_table[paramiko.common.MSG_USERAUTH_INFO_REQUEST] = valid
4956
client_handler_table[paramiko.common.MSG_SERVICE_ACCEPT] = parse_service_accept
5057

@@ -56,28 +63,29 @@ def parse_service_accept(self, m):
5663
# For compatibility with paramiko, we need to generate a random private key and replace
5764
# the public key with our data.
5865
key = paramiko.RSAKey.generate(2048)
59-
#key.public_blob =
60-
key.public_blob = public_key
66+
key.public_blob = public_key # type: ignore
6167
transport.auth_publickey(username, key)
6268
valid_key = True
6369
except paramiko.ssh_exception.AuthenticationException:
6470
pass
6571
finally:
66-
client_handler_table[paramiko.common.MSG_USERAUTH_INFO_REQUEST] = paramiko.auth_handler.AuthHandler._parse_userauth_info_request
67-
client_handler_table[paramiko.common.MSG_SERVICE_ACCEPT] = paramiko.auth_handler.AuthHandler._parse_service_accept
72+
client_handler_table[paramiko.common.MSG_USERAUTH_INFO_REQUEST] = paramiko.auth_handler.AuthHandler._parse_userauth_info_request # type: ignore
73+
client_handler_table[paramiko.common.MSG_SERVICE_ACCEPT] = paramiko.auth_handler.AuthHandler._parse_service_accept # type: ignore
6874
return valid_key
6975

7076

7177
class RemoteCredentials():
7278

79+
@typechecked
7380
def __init__(
7481
self, *,
75-
username: Optional[str] = None,
82+
username: str,
7683
password: Optional[str] = None,
7784
key=None,
7885
host: Optional[str] = None,
79-
port: Optional[int] = None) -> None:
80-
self.username: Optional[str] = username
86+
port: Optional[int] = None
87+
) -> None:
88+
self.username: str = username
8189
self.password: Optional[str] = password
8290
self.key = key
8391
self.host: Optional[str] = host
@@ -89,7 +97,8 @@ class Authenticator(BaseModule):
8997
REQUEST_AGENT_BREAKIN = False
9098

9199
@classmethod
92-
def parser_arguments(cls):
100+
@typechecked
101+
def parser_arguments(cls) -> None:
93102
plugin_group = cls.parser().add_argument_group(
94103
cls.__name__,
95104
"options for remote authentication"
@@ -156,15 +165,17 @@ def parser_arguments(cls):
156165
help='password for the honeypot'
157166
)
158167

159-
def __init__(self, session) -> None:
168+
@typechecked
169+
def __init__(self, session: Session) -> None:
160170
super().__init__()
161171
self.session = session
162172

173+
@typechecked
163174
def get_remote_host_credentials(
164175
self,
165176
username: str,
166177
password: Optional[str] = None,
167-
key=None
178+
key: Optional[PKey] = None
168179
) -> RemoteCredentials:
169180
if self.session.proxyserver.transparent:
170181
return RemoteCredentials(
@@ -183,6 +194,7 @@ def get_remote_host_credentials(
183194
)
184195

185196
@classmethod
197+
@typechecked
186198
def get_auth_methods(cls, host: str, port: int) -> Optional[List[str]]:
187199
auth_methods = None
188200
t = paramiko.Transport((host, port))
@@ -199,11 +211,12 @@ def get_auth_methods(cls, host: str, port: int) -> Optional[List[str]]:
199211
t.close()
200212
return auth_methods
201213

214+
@typechecked
202215
def authenticate(
203216
self,
204217
username: Optional[str] = None,
205218
password: Optional[str] = None,
206-
key=None,
219+
key: Optional[PKey] = None,
207220
store_credentials: bool = True
208221
) -> int:
209222
if store_credentials:
@@ -218,6 +231,10 @@ def authenticate(
218231
if key and not self.session.key:
219232
self.session.key = key
220233

234+
if self.session.remote_address[0] is None or self.session.remote_address[1] is None:
235+
logging.error("no remote host")
236+
return paramiko.common.AUTH_FAILED
237+
221238
try:
222239
if self.session.agent:
223240
return self.auth_agent(
@@ -245,52 +262,35 @@ def authenticate(
245262
logging.exception("internal error, abort authentication!")
246263
return paramiko.common.AUTH_FAILED
247264

248-
def auth_agent(self, username, host, port):
265+
@typechecked
266+
def auth_agent(self, username: Text, host: Text, port: int) -> int:
249267
raise NotImplementedError("authentication must be implemented")
250268

251-
def auth_password(self, username, host, port, password):
269+
@typechecked
270+
def auth_password(self, username: Text, host: Text, port: int, password: Text) -> int:
252271
raise NotImplementedError("authentication must be implemented")
253272

254-
def auth_publickey(self, username, host, port, key):
273+
@typechecked
274+
def auth_publickey(self, username: Text, host: Text, port: int, key: PKey) -> int:
255275
raise NotImplementedError("authentication must be implemented")
256276

257-
def auth_fallback(self, username):
258-
def parse_host(connectionurl):
259-
username = None
260-
password = None
261-
hostname = None
262-
port = 22
263-
if '@' in connectionurl:
264-
username = connectionurl[:connectionurl.rfind('@')]
265-
print(username)
266-
if ':' in username:
267-
password = username[username.rfind(':') + 1:]
268-
username = username[:username.rfind(':')]
269-
hostname = connectionurl[connectionurl.rfind('@') + 1:]
270-
if ':' in hostname:
271-
port = int(hostname[hostname.rfind(':') + 1:])
272-
hostname = hostname[:hostname.rfind(':')]
273-
return username, password, hostname, port
274-
277+
@typechecked
278+
def auth_fallback(self, username: Text) -> int:
275279
if not self.args.fallback_host:
276280
logging.error("\n".join([
277281
stylize(EMOJI['exclamation'] + " ssh agent not forwarded. Login to remote host not possible with publickey authentication.", fg('red') + attr('bold')),
278282
stylize(EMOJI['information'] + " To intercept clients without a forwarded agent, you can provide credentials for a honeypot.", fg('yellow') + attr('bold'))
279283
]))
280-
return paramiko.AUTH_FAILED
281-
try:
282-
fallback_username, fallback_password, fallback_host, fallback_port = parse_host(self.args.fallback_host)
283-
except Exception:
284-
logging.error(stylize(EMOJI['exclamation'] + " failed to parse connection string for honeypot - publickey authentication failed", fg('red') + attr('bold')))
285-
return paramiko.AUTH_FAILED
284+
return paramiko.common.AUTH_FAILED
285+
286286
auth_status = self.connect(
287-
user=fallback_username or username,
288-
password=fallback_password,
289-
host=fallback_host,
290-
port=int(fallback_port),
287+
user=self.args.fallback_username or username,
288+
password=self.args.fallback_password,
289+
host=self.args.fallback_host,
290+
port=self.args.fallback_port,
291291
method=AuthenticationMethod.password
292292
)
293-
if auth_status == paramiko.AUTH_SUCCESSFUL:
293+
if auth_status == paramiko.common.AUTH_SUCCESSFUL:
294294
logging.warning(
295295
stylize(EMOJI['warning'] + " publickey authentication failed - no agent forwarded - connecting to honeypot!", fg('yellow') + attr('bold')),
296296
)
@@ -300,11 +300,12 @@ def parse_host(connectionurl):
300300
)
301301
return auth_status
302302

303-
def connect(self, user, host, port, method, password=None, key=None):
303+
@typechecked
304+
def connect(self, user: Text, host: Text, port: int, method: AuthenticationMethod, password: Optional[Text] = None, key: Optional[PKey] = None) -> int:
304305
if not host:
305306
raise MissingHostException()
306307

307-
auth_status = paramiko.AUTH_FAILED
308+
auth_status = paramiko.common.AUTH_FAILED
308309
self.session.ssh_client = SSHClient(
309310
host,
310311
port,
@@ -316,32 +317,37 @@ def connect(self, user, host, port, method, password=None, key=None):
316317
)
317318
self.pre_auth_action()
318319
try:
319-
if self.session.ssh_client.connect():
320-
auth_status = paramiko.AUTH_SUCCESSFUL
320+
if self.session.ssh_client is not None and self.session.ssh_client.connect():
321+
auth_status = paramiko.common.AUTH_SUCCESSFUL
321322
except paramiko.SSHException:
322323
logging.error(stylize("Connection to remote server refused", fg('red') + attr('bold')))
323-
return paramiko.AUTH_FAILED
324-
self.post_auth_action(auth_status == paramiko.AUTH_SUCCESSFUL)
324+
return paramiko.common.AUTH_FAILED
325+
self.post_auth_action(auth_status == paramiko.common.AUTH_SUCCESSFUL)
325326
return auth_status
326327

327-
def pre_auth_action(self):
328+
@typechecked
329+
def pre_auth_action(self) -> None:
328330
pass
329331

330-
def post_auth_action(self, success):
332+
@typechecked
333+
def post_auth_action(self, success: bool) -> None:
331334
pass
332335

333336

334337
class AuthenticatorPassThrough(Authenticator):
335338
"""pass the authentication to the remote server (reuses the credentials)
336339
"""
337340

338-
def auth_agent(self, username, host, port):
341+
@typechecked
342+
def auth_agent(self, username: Text, host: Text, port: int) -> int:
339343
return self.connect(username, host, port, AuthenticationMethod.agent)
340344

341-
def auth_password(self, username, host, port, password):
345+
@typechecked
346+
def auth_password(self, username: Text, host: Text, port: int, password: Text) -> int:
342347
return self.connect(username, host, port, AuthenticationMethod.password, password=password)
343348

344-
def auth_publickey(self, username, host, port, key):
349+
@typechecked
350+
def auth_publickey(self, username: Text, host: Text, port: int, key: PKey) -> int:
345351
ssh_pub_key = SSHKey(f"{key.get_name()} {key.get_base64()}")
346352
ssh_pub_key.parse()
347353
if key.can_sign():
@@ -353,15 +359,20 @@ def auth_publickey(self, username, host, port, key):
353359
publickey = paramiko.pkey.PublicBlob(key.get_name(), key.asbytes())
354360
if probe_host(host, port, username, publickey):
355361
logging.debug(f"Found valid key for host {host}:{port} username={username}, key={key.get_name()} {ssh_pub_key.hash_sha256()} {ssh_pub_key.bits}bits")
356-
return paramiko.AUTH_SUCCESSFUL
357-
return paramiko.AUTH_FAILED
362+
return paramiko.common.AUTH_SUCCESSFUL
363+
return paramiko.common.AUTH_FAILED
358364

359-
def post_auth_action(self, success):
360-
def get_agent_pubkeys():
365+
@typechecked
366+
def post_auth_action(self, success: bool) -> None:
367+
@typechecked
368+
def get_agent_pubkeys() -> List[Tuple[Text, SSHKey, bool, Text]]:
361369
pubkeyfile_path = None
362370

371+
keys_parsed: List[Tuple[Text, SSHKey, bool, Text]] = []
372+
if self.session.agent is None:
373+
return keys_parsed
374+
363375
keys = self.session.agent.get_keys()
364-
keys_parsed = []
365376
for k in keys:
366377
ssh_pub_key = SSHKey(f"{k.get_name()} {k.get_base64()}")
367378
ssh_pub_key.parse()
@@ -385,8 +396,9 @@ def get_agent_pubkeys():
385396
else:
386397
logmessage.append(stylize("Remote authentication failed", fg('red')))
387398

388-
logmessage.append(f"\tRemote Address: {self.session.ssh_client.host}:{self.session.ssh_client.port}")
389-
logmessage.append(f"\tUsername: {self.session.username_provided}")
399+
if self.session.ssh_client is not None:
400+
logmessage.append(f"\tRemote Address: {self.session.ssh_client.host}:{self.session.ssh_client.port}")
401+
logmessage.append(f"\tUsername: {self.session.username_provided}")
390402

391403
if self.session.password_provided:
392404
display_password = None
@@ -403,7 +415,7 @@ def get_agent_pubkeys():
403415
if self.session.agent:
404416
ssh_keys = get_agent_pubkeys()
405417

406-
logmessage.append(f"\tAgent: {f'available keys: {len(ssh_keys)}' if ssh_keys else 'no agent'}")
418+
logmessage.append(f"\tAgent: {f'available keys: {len(ssh_keys or [])}' if ssh_keys else 'no agent'}")
407419
if ssh_keys is not None:
408420
logmessage.append("\n".join(
409421
[f"\t\tAgent-Key: {k[0]} {k[1].hash_sha256()} {k[1].bits}bits, can sign: {k[2]}" for k in ssh_keys]

0 commit comments

Comments
 (0)