3
3
import os
4
4
import sys
5
5
import socket
6
- import re
7
6
8
7
from typing import (
9
8
Optional ,
10
- List
9
+ List ,
10
+ Tuple ,
11
+ Text
11
12
)
12
13
13
- from colored .colored import stylize , attr , fg
14
+ from colored .colored import stylize , attr , fg # type: ignore
15
+ from paramiko import PKey
14
16
from rich ._emoji_codes import EMOJI
15
17
16
18
from enhancements .modules import BaseModule
17
19
import paramiko
18
- from sshpubkeys import SSHKey
20
+ from sshpubkeys import SSHKey # type: ignore
21
+ from typeguard import typechecked
19
22
20
23
from ssh_proxy_server .clients .ssh import SSHClient , AuthenticationMethod
21
24
from ssh_proxy_server .exceptions import MissingHostException
25
+ from ssh_proxy_server .session import Session
22
26
23
27
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 :
25
30
26
- def valid (self , msg ):
31
+ @typechecked
32
+ def valid (self , msg : paramiko .message .Message ) -> None :
27
33
self .auth_event .set ()
28
34
self .authenticated = True
29
35
30
- def parse_service_accept (self , m ):
36
+ @typechecked
37
+ def parse_service_accept (self , m : paramiko .message .Message ) -> None :
31
38
# https://tools.ietf.org/html/rfc4252#section-7
32
39
service = m .get_text ()
33
40
if not (service == "ssh-userauth" and self .auth_method == "publickey" ):
@@ -44,7 +51,7 @@ def parse_service_accept(self, m):
44
51
45
52
valid_key = False
46
53
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
48
55
client_handler_table [paramiko .common .MSG_USERAUTH_INFO_REQUEST ] = valid
49
56
client_handler_table [paramiko .common .MSG_SERVICE_ACCEPT ] = parse_service_accept
50
57
@@ -56,28 +63,29 @@ def parse_service_accept(self, m):
56
63
# For compatibility with paramiko, we need to generate a random private key and replace
57
64
# the public key with our data.
58
65
key = paramiko .RSAKey .generate (2048 )
59
- #key.public_blob =
60
- key .public_blob = public_key
66
+ key .public_blob = public_key # type: ignore
61
67
transport .auth_publickey (username , key )
62
68
valid_key = True
63
69
except paramiko .ssh_exception .AuthenticationException :
64
70
pass
65
71
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
68
74
return valid_key
69
75
70
76
71
77
class RemoteCredentials ():
72
78
79
+ @typechecked
73
80
def __init__ (
74
81
self , * ,
75
- username : Optional [ str ] = None ,
82
+ username : str ,
76
83
password : Optional [str ] = None ,
77
84
key = None ,
78
85
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
81
89
self .password : Optional [str ] = password
82
90
self .key = key
83
91
self .host : Optional [str ] = host
@@ -89,7 +97,8 @@ class Authenticator(BaseModule):
89
97
REQUEST_AGENT_BREAKIN = False
90
98
91
99
@classmethod
92
- def parser_arguments (cls ):
100
+ @typechecked
101
+ def parser_arguments (cls ) -> None :
93
102
plugin_group = cls .parser ().add_argument_group (
94
103
cls .__name__ ,
95
104
"options for remote authentication"
@@ -156,15 +165,17 @@ def parser_arguments(cls):
156
165
help = 'password for the honeypot'
157
166
)
158
167
159
- def __init__ (self , session ) -> None :
168
+ @typechecked
169
+ def __init__ (self , session : Session ) -> None :
160
170
super ().__init__ ()
161
171
self .session = session
162
172
173
+ @typechecked
163
174
def get_remote_host_credentials (
164
175
self ,
165
176
username : str ,
166
177
password : Optional [str ] = None ,
167
- key = None
178
+ key : Optional [ PKey ] = None
168
179
) -> RemoteCredentials :
169
180
if self .session .proxyserver .transparent :
170
181
return RemoteCredentials (
@@ -183,6 +194,7 @@ def get_remote_host_credentials(
183
194
)
184
195
185
196
@classmethod
197
+ @typechecked
186
198
def get_auth_methods (cls , host : str , port : int ) -> Optional [List [str ]]:
187
199
auth_methods = None
188
200
t = paramiko .Transport ((host , port ))
@@ -199,11 +211,12 @@ def get_auth_methods(cls, host: str, port: int) -> Optional[List[str]]:
199
211
t .close ()
200
212
return auth_methods
201
213
214
+ @typechecked
202
215
def authenticate (
203
216
self ,
204
217
username : Optional [str ] = None ,
205
218
password : Optional [str ] = None ,
206
- key = None ,
219
+ key : Optional [ PKey ] = None ,
207
220
store_credentials : bool = True
208
221
) -> int :
209
222
if store_credentials :
@@ -218,6 +231,10 @@ def authenticate(
218
231
if key and not self .session .key :
219
232
self .session .key = key
220
233
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
+
221
238
try :
222
239
if self .session .agent :
223
240
return self .auth_agent (
@@ -245,52 +262,35 @@ def authenticate(
245
262
logging .exception ("internal error, abort authentication!" )
246
263
return paramiko .common .AUTH_FAILED
247
264
248
- def auth_agent (self , username , host , port ):
265
+ @typechecked
266
+ def auth_agent (self , username : Text , host : Text , port : int ) -> int :
249
267
raise NotImplementedError ("authentication must be implemented" )
250
268
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 :
252
271
raise NotImplementedError ("authentication must be implemented" )
253
272
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 :
255
275
raise NotImplementedError ("authentication must be implemented" )
256
276
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 :
275
279
if not self .args .fallback_host :
276
280
logging .error ("\n " .join ([
277
281
stylize (EMOJI ['exclamation' ] + " ssh agent not forwarded. Login to remote host not possible with publickey authentication." , fg ('red' ) + attr ('bold' )),
278
282
stylize (EMOJI ['information' ] + " To intercept clients without a forwarded agent, you can provide credentials for a honeypot." , fg ('yellow' ) + attr ('bold' ))
279
283
]))
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
+
286
286
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 ,
291
291
method = AuthenticationMethod .password
292
292
)
293
- if auth_status == paramiko .AUTH_SUCCESSFUL :
293
+ if auth_status == paramiko .common . AUTH_SUCCESSFUL :
294
294
logging .warning (
295
295
stylize (EMOJI ['warning' ] + " publickey authentication failed - no agent forwarded - connecting to honeypot!" , fg ('yellow' ) + attr ('bold' )),
296
296
)
@@ -300,11 +300,12 @@ def parse_host(connectionurl):
300
300
)
301
301
return auth_status
302
302
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 :
304
305
if not host :
305
306
raise MissingHostException ()
306
307
307
- auth_status = paramiko .AUTH_FAILED
308
+ auth_status = paramiko .common . AUTH_FAILED
308
309
self .session .ssh_client = SSHClient (
309
310
host ,
310
311
port ,
@@ -316,32 +317,37 @@ def connect(self, user, host, port, method, password=None, key=None):
316
317
)
317
318
self .pre_auth_action ()
318
319
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
321
322
except paramiko .SSHException :
322
323
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 )
325
326
return auth_status
326
327
327
- def pre_auth_action (self ):
328
+ @typechecked
329
+ def pre_auth_action (self ) -> None :
328
330
pass
329
331
330
- def post_auth_action (self , success ):
332
+ @typechecked
333
+ def post_auth_action (self , success : bool ) -> None :
331
334
pass
332
335
333
336
334
337
class AuthenticatorPassThrough (Authenticator ):
335
338
"""pass the authentication to the remote server (reuses the credentials)
336
339
"""
337
340
338
- def auth_agent (self , username , host , port ):
341
+ @typechecked
342
+ def auth_agent (self , username : Text , host : Text , port : int ) -> int :
339
343
return self .connect (username , host , port , AuthenticationMethod .agent )
340
344
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 :
342
347
return self .connect (username , host , port , AuthenticationMethod .password , password = password )
343
348
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 :
345
351
ssh_pub_key = SSHKey (f"{ key .get_name ()} { key .get_base64 ()} " )
346
352
ssh_pub_key .parse ()
347
353
if key .can_sign ():
@@ -353,15 +359,20 @@ def auth_publickey(self, username, host, port, key):
353
359
publickey = paramiko .pkey .PublicBlob (key .get_name (), key .asbytes ())
354
360
if probe_host (host , port , username , publickey ):
355
361
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
358
364
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 ]]:
361
369
pubkeyfile_path = None
362
370
371
+ keys_parsed : List [Tuple [Text , SSHKey , bool , Text ]] = []
372
+ if self .session .agent is None :
373
+ return keys_parsed
374
+
363
375
keys = self .session .agent .get_keys ()
364
- keys_parsed = []
365
376
for k in keys :
366
377
ssh_pub_key = SSHKey (f"{ k .get_name ()} { k .get_base64 ()} " )
367
378
ssh_pub_key .parse ()
@@ -385,8 +396,9 @@ def get_agent_pubkeys():
385
396
else :
386
397
logmessage .append (stylize ("Remote authentication failed" , fg ('red' )))
387
398
388
- logmessage .append (f"\t Remote Address: { self .session .ssh_client .host } :{ self .session .ssh_client .port } " )
389
- logmessage .append (f"\t Username: { self .session .username_provided } " )
399
+ if self .session .ssh_client is not None :
400
+ logmessage .append (f"\t Remote Address: { self .session .ssh_client .host } :{ self .session .ssh_client .port } " )
401
+ logmessage .append (f"\t Username: { self .session .username_provided } " )
390
402
391
403
if self .session .password_provided :
392
404
display_password = None
@@ -403,7 +415,7 @@ def get_agent_pubkeys():
403
415
if self .session .agent :
404
416
ssh_keys = get_agent_pubkeys ()
405
417
406
- logmessage .append (f"\t Agent: { f'available keys: { len (ssh_keys )} ' if ssh_keys else 'no agent' } " )
418
+ logmessage .append (f"\t Agent: { f'available keys: { len (ssh_keys or [] )} ' if ssh_keys else 'no agent' } " )
407
419
if ssh_keys is not None :
408
420
logmessage .append ("\n " .join (
409
421
[f"\t \t Agent-Key: { k [0 ]} { k [1 ].hash_sha256 ()} { k [1 ].bits } bits, can sign: { k [2 ]} " for k in ssh_keys ]
0 commit comments