diff --git a/setup.py b/setup.py index 46d572f..80aef8c 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ , platforms = 'Linux' , url = "https://github.com/schlatterbeck/snxvpn" , scripts = ['snxconnect'] - , install_requires = [ 'bs4', 'pycrypto', 'lxml' ] + , install_requires = [ 'bs4', 'pycrypto', 'lxml', 'rsa' ] , classifiers = \ [ 'Development Status :: 3 - Alpha' , 'License :: OSI Approved :: ' + license diff --git a/snxconnect.py b/snxconnect.py index 66e99b3..0999640 100644 --- a/snxconnect.py +++ b/snxconnect.py @@ -5,16 +5,17 @@ import os.path import sys import socket +import rsa +import ssl +import time try : - from urllib2 import build_opener, HTTPCookieProcessor, Request + from urllib2 import build_opener, HTTPCookieProcessor, Request, HTTPSHandler from urllib import urlencode from httplib import IncompleteRead - rsatype = long except ImportError : - from urllib.request import build_opener, HTTPCookieProcessor, Request + from urllib.request import build_opener, HTTPCookieProcessor, Request, HTTPSHandler from urllib.parse import urlencode from http.client import IncompleteRead - rsatype = int try : from cookielib import LWPCookieJar except ImportError : @@ -23,7 +24,6 @@ from getpass import getpass from argparse import ArgumentParser from netrc import netrc, NetrcParseError -from Crypto.PublicKey import RSA from struct import pack, unpack from subprocess import Popen, PIPE from snxvpnversion import VERSION @@ -76,13 +76,19 @@ def __init__ (self, args) : self.args = args self.jar = j = LWPCookieJar () self.has_cookies = False + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE if self.args.cookiefile : self.has_cookies = True try : j.load (self.args.cookiefile, ignore_discard = True) except IOError : self.has_cookies = False - self.opener = build_opener (HTTPCookieProcessor (j)) + handlers = [HTTPCookieProcessor (j)] + if self.args.ssl_noverify: + handlers.append(HTTPSHandler(context=context)) + self.opener = build_opener (*handlers) self.nextfile = args.file # end def __init__ @@ -114,7 +120,19 @@ def call_snx (self) : f.write (answer) f.close () print ("SNX connected, to leave VPN open, leave this running!") - answer = sock.recv (4096) # should block until snx dies + try: + while True: + time.sleep(4000000) + # answer = sock.recv (4096) # should block until snx dies + except KeyboardInterrupt: + sys.stdout.write ('\b\b\r') + sys.stdout.flush() + sys.stdout.write ("Shutting down ...\n") + sys.stdout.flush() + try: + sys.exit(0) + except SystemExit: + os._exit(0) # end def call_snx def debug (self, s) : @@ -159,7 +177,7 @@ def login (self) : self.open () self.debug (self.purl) if self.purl.endswith ('Portal/Main') : - self.open ('sslvpn/SNX/extender') + self.open (self.args.extender) self.parse_extender () self.generate_snx_info () return True @@ -168,6 +186,7 @@ def login (self) : self.jar.clear () self.next_file (self.purl) self.debug (self.nextfile) + print ('Visiting login page ...') self.open () self.debug (self.purl) # Get the RSA parameters from the javascript in the received html @@ -179,6 +198,7 @@ def login (self) : else : print ('No RSA javascript file found, cannot login') return + print ('Fetching RSA javascript file ...') self.open (do_soup = False) self.parse_rsa_params () if not self.modulus : @@ -191,22 +211,34 @@ def login (self) : break self.debug (self.nextfile) - enc = PW_Encode (modulus = self.modulus, exponent = self.exponent) + self.debug(self.purl) + password = rsa.pkcs1.encrypt(self.args.password.encode('UTF-8'), rsa.PublicKey(self.modulus, self.exponent)) + password = ''.join ('%02x' % b_ord (c) for c in reversed (password)) d = dict \ - ( password = enc.encrypt (self.args.password) + ( password = password , userName = self.args.username , selectedRealm = self.args.realm , loginType = self.args.login_type , vpid_prefix = self.args.vpid_prefix , HeightData = self.args.height_data ) + self.debug (urlencode(d)) + print ("Sending login information ...") self.open (data = urlencode (d)) self.debug (self.purl) self.debug (self.info) + + errorMessage = self.soup.select_one(".errorMessage") + if errorMessage : + print ("Error: %s" % errorMessage.string) + return + while 'MultiChallenge' in self.purl : d = self.parse_pw_response () otp = getpass ('One-time Password: ') - d ['password'] = enc.encrypt (otp) + otp = rsa.pkcs1.encrypt(self.args.password, rsa.PublicKey(self.modulus, self.exponent)) + otp = ''.join ('%02x' % b_ord (c) for c in reversed (otp)) + d ['password'] = otp self.debug ("nextfile: %s" % self.nextfile) self.debug ("purl: %s" % self.purl) self.open (data = urlencode (d)) @@ -215,7 +247,8 @@ def login (self) : if self.args.save_cookies : self.jar.save (self.args.cookiefile, ignore_discard = True) self.debug ("purl: %s" % self.purl) - self.open ('sslvpn/SNX/extender') + print ("Fetching extender information ...") + self.open (self.args.extender) self.debug (self.purl) self.debug (self.info) self.parse_extender () @@ -224,6 +257,9 @@ def login (self) : else : print ("Unexpected response, looking for MultiChallenge or Portal") self.debug ("purl: %s" % self.purl) + self.debug (getattr(self.soup.find('span', attrs={'class': 'errorMessage'}), 'string', '')) + if not self.soup.find('span', attrs={'class': 'errorMessage'}): + self.debug(self.soup) return # end def login @@ -245,7 +281,7 @@ def open (self, filepart = None, data = None, do_soup = True) : url = '/'.join (('%s:/' % self.args.protocol, self.args.host, filepart)) if data : data = data.encode ('ascii') - rq = Request (url, data) + rq = Request (url, data, headers={'User-Agent': self.args.useragent}) self.f = f = self.opener.open (rq, timeout = 10) if do_soup : # Sometimes we get incomplete read. So we read everything @@ -267,12 +303,13 @@ def parse_extender (self) : program via a socket. """ for script in self.soup.find_all ('script') : - if '/* Extender.user_name' in script.text : + text = script.text or script.string or "" + if '/* Extender.user_name' in text : break else : print ("Error retrieving extender variables") return - for line in script.text.split ('\n') : + for line in text.split ('\n') : if '/* Extender.user_name' in line : break stmts = line.split (';') @@ -331,70 +368,13 @@ def parse_rsa_params (self) : print ('No RSA parameters found, cannot login') return self.debug (repr (vars)) - self.modulus = rsatype (vars ['modulus'], 16) - self.exponent = rsatype (vars ['exponent'], 16) + self.modulus = int (vars ['modulus'], 16) + self.exponent = int (vars ['exponent'], 16) + # end def parse_rsa_params # end class HTML_Requester -class PW_Encode (object) : - """ RSA encryption module with special padding and reversing to be - compatible with checkpoints implementation. - Test with non-random padding to get known value: - >>> p = PW_Encode (testing = True) - >>> print (p.encrypt ('xyzzy')) - 451c2d5b491ee22d6f7cdc5a20f320914668f8e01337625dfb7e0917b16750cfbafe38bfcb68824b30d5cc558fa1c6d542ff12ac8e1085b7a9040f624ab39f625cabd77d1d024c111e42fede782e089400d2c9b1d6987c0005698178222e8500243f12762bebba841eae331d17b290f80bca6c3f8a49522fb926646c24db3627 - >>> print (p.encrypt ('XYZZYxyzzyXYZZYxyzzy')) - a529e86cf80dd131e3bdae1f6dbab76f67f674e42041dde801ebdb790ab0637d56cc82f52587f2d4d34d26c490eee3a1ebfd80df18ec41c4440370b1ecb2dec3f811e09d2248635dd8aab60a97293ec0315a70bf024b33e8a8a02582fbabc98dd72d913530151e78b47119924f45b711b9a1189d5eec5a20e6f9bc1d44bfd554 - """ - - def __init__ (self, modulus = None, exponent = None, testing = False) : - m = rsatype \ - ( b'c87e9e96ffde3ec47c3f116ea5ac0e15' - b'34490b3da6dbbedae1af50dc32bf1012' - b'bdb7e1ff67237e0302b48c8731f343ff' - b'644662de2bb21d2b033127660e525d58' - b'889f8f6f05744906dddc8f4b85e0916b' - b'5d9cf5b87093ed260238674f143801b7' - b'e58a18795adc9acefaf0f378326fea19' - b'9ac6e5a88be83a52d4a77b3bba5f1aed' - , 16 - ) - e = rsatype (b'010001', 16) - m = modulus or m - e = exponent or e - self.pubkey = RSA.construct ((m, e)) - self.testing = testing - # end def __init__ - - def pad (self, txt) : - l = (self.pubkey.size () + 7) >> 3 - r = [] - r.append (b'\0') - # Note that first reversing and then encoding to utf-8 would - # *not* be correct! - for x in iterbytes (reversed (txt.encode ('utf-8'))) : - r.append (x) - r.append (b'\0') - n = l - len (r) - 2 - if self.testing : - r.append (b'\1' * n) - else : - r.append (os.urandom (n)) - r.append (b'\x02') - r.append (b'\x00') - return b''.join (reversed (r)) - # end def pad - - def encrypt (self, password) : - x = self.pad (password) - e = self.pubkey.encrypt (x, '')[0] - e = ''.join ('%02x' % b_ord (c) for c in reversed (e)) - return e - # end def encrypt - -# end class PW_Encode - def main () : # First try to parse config-file ~/.snxvpnrc: home = os.environ.get ('HOME') @@ -439,12 +419,23 @@ def main () : , help = 'File part of URL default="%(default)s"' , default = cfg.get ('file', 'sslvpn/Login/Login') ) + cmd.add_argument \ + ( '-E', '--extender' + , help = 'File part of URL default="%(default)s"' + , default = cfg.get ('extender', 'sslvpn/SNX/extender') + ) cmd.add_argument \ ( '-H', '--host' , help = 'Host part of URL default="%(default)s"' , default = host , required = not host ) + cmd.add_argument \ + ( '--ssl-noverify' + , help = 'Skip SSL verification default="%(default)s"' + , default = False + , required = False + ) cmd.add_argument \ ( '--height-data' , help = 'Height data in form, default "%(default)s"' @@ -483,6 +474,11 @@ def main () : ' want a full path here' , default = cfg.get ('snxpath', 'snx') ) + cmd.add_argument \ + ( '-u', '--useragent' + , help = 'User-Agent to be passed to Checkpoint Portal, default="%(default)s"' + , default = cfg.get ('useragent', 'Mozilla/5.0 (X11; Linux x86_64; rv:100.0) Gecko/20100101 Firefox/100.0') + ) cmd.add_argument \ ( '-U', '--username' , help = 'Login username, default="%(default)s"' diff --git a/snxvpnversion.py b/snxvpnversion.py new file mode 100644 index 0000000..f3ea782 --- /dev/null +++ b/snxvpnversion.py @@ -0,0 +1 @@ +VERSION="1.3-0-be631df"