You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

299 lines
9.9 KiB

  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. import subprocess
  4. import mechanize
  5. import cookielib
  6. import getpass
  7. import sys
  8. import os
  9. import ssl
  10. import argparse
  11. import atexit
  12. import signal
  13. import ConfigParser
  14. import time
  15. import binascii
  16. import hmac
  17. import hashlib
  18. import shlex
  19. import tncc
  20. if hasattr(ssl, '_create_unverified_context'):
  21. ssl._create_default_https_context = ssl._create_unverified_context
  22. """
  23. OATH code from https://github.com/bdauvergne/python-oath
  24. Copyright 2010, Benjamin Dauvergne
  25. * All rights reserved.
  26. * Redistribution and use in source and binary forms, with or without
  27. modification, are permitted provided that the following conditions are met:
  28. * Redistributions of source code must retain the above copyright
  29. notice, this list of conditions and the following disclaimer.
  30. * Redistributions in binary form must reproduce the above copyright
  31. notice, this list of conditions and the following disclaimer in the
  32. documentation and/or other materials provided with the distribution.'''
  33. """
  34. def truncated_value(h):
  35. bytes = map(ord, h)
  36. offset = bytes[-1] & 0xf
  37. v = (bytes[offset] & 0x7f) << 24 | (bytes[offset+1] & 0xff) << 16 | \
  38. (bytes[offset+2] & 0xff) << 8 | (bytes[offset+3] & 0xff)
  39. return v
  40. def dec(h,p):
  41. v = truncated_value(h)
  42. v = v % (10**p)
  43. return '%0*d' % (p, v)
  44. def int2beint64(i):
  45. hex_counter = hex(long(i))[2:-1]
  46. hex_counter = '0' * (16 - len(hex_counter)) + hex_counter
  47. bin_counter = binascii.unhexlify(hex_counter)
  48. return bin_counter
  49. def hotp(key):
  50. key = binascii.unhexlify(key)
  51. counter = int2beint64(int(time.time()) / 30)
  52. return dec(hmac.new(key, counter, hashlib.sha256).digest(), 6)
  53. class juniper_vpn(object):
  54. def __init__(self, args):
  55. self.args = args
  56. self.fixed_password = args.password is not None
  57. self.last_connect = 0
  58. self.br = mechanize.Browser()
  59. self.cj = cookielib.LWPCookieJar()
  60. self.br.set_cookiejar(self.cj)
  61. # Browser options
  62. self.br.set_handle_equiv(True)
  63. self.br.set_handle_redirect(True)
  64. self.br.set_handle_referer(True)
  65. self.br.set_handle_robots(False)
  66. # Follows refresh 0 but not hangs on refresh > 0
  67. self.br.set_handle_refresh(mechanize._http.HTTPRefreshProcessor(),
  68. max_time=1)
  69. # Want debugging messages?
  70. self.br.set_debug_http(True)
  71. self.br.set_debug_redirects(True)
  72. self.br.set_debug_responses(True)
  73. self.user_agent = 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.1) Gecko/2008071615 Fedora/3.0.1-1.fc9 Firefox/3.0.1'
  74. self.br.addheaders = [('User-agent', self.user_agent)]
  75. self.last_action = None
  76. self.needs_2factor = False
  77. self.key = None
  78. self.pass_postfix = None
  79. def find_cookie(self, name):
  80. for cookie in self.cj:
  81. if cookie.name == name:
  82. return cookie
  83. return None
  84. def next_action(self):
  85. if self.find_cookie('DSID'):
  86. print("OZAPTF: DSID found in cookie")
  87. return 'connect'
  88. for form in self.br.forms():
  89. if form.name == 'frmLogin':
  90. return 'login'
  91. elif form.name == 'frmDefender':
  92. return 'key'
  93. elif form.name == 'frmConfirmation':
  94. return 'continue'
  95. else:
  96. raise Exception('Unknown form type:', form.name)
  97. return 'tncc'
  98. def run(self):
  99. # Open landing page
  100. self.r = self.br.open('https://' + self.args.host)
  101. while True:
  102. action = self.next_action()
  103. if action == 'tncc':
  104. self.action_tncc()
  105. elif action == 'login':
  106. self.action_login()
  107. elif action == 'key':
  108. self.action_key()
  109. elif action == 'continue':
  110. self.action_continue()
  111. elif action == 'connect':
  112. self.action_connect()
  113. self.last_action = action
  114. def action_tncc(self):
  115. # Run tncc host checker
  116. dspreauth_cookie = self.find_cookie('DSPREAUTH')
  117. if dspreauth_cookie is None:
  118. raise Exception('Could not find DSPREAUTH key for host checker')
  119. dssignin_cookie = self.find_cookie('DSSIGNIN')
  120. t = tncc.tncc(self.args.host);
  121. self.cj.set_cookie(t.get_cookie(dspreauth_cookie, dssignin_cookie))
  122. self.r = self.br.open(self.r.geturl())
  123. def action_login(self):
  124. # The token used for two-factor is selected when this form is submitted.
  125. # If we aren't getting a password, then get the key now, otherwise
  126. # we could be sitting on the two factor key prompt later on waiting
  127. # on the user.
  128. if self.args.password is None or self.last_action == 'login':
  129. if self.fixed_password:
  130. print 'Login failed (Invalid username or password?)'
  131. sys.exit(1)
  132. else:
  133. self.args.password = getpass.getpass('Password:')
  134. self.needs_2factor = False
  135. if self.args.pass_prefix:
  136. self.pass_postfix = getpass.getpass("Secondary password postfix:")
  137. if self.needs_2factor:
  138. if self.args.oath:
  139. self.key = hotp(self.args.oath)
  140. else:
  141. self.key = getpass.getpass('Two-factor key:')
  142. else:
  143. self.key = None
  144. # Enter username/password
  145. self.br.select_form(nr=0)
  146. print("OZAPTF FORM: ")
  147. print("------")
  148. try:
  149. print(self.br.form)
  150. except:
  151. print("OZAPTF: Couldnt print form")
  152. print("------")
  153. self.br.form['username'] = self.args.username
  154. self.br.form['password'] = self.args.password
  155. if self.args.pass_prefix:
  156. if self.pass_postfix:
  157. secondary_password = "".join([ self.args.pass_prefix,
  158. self.pass_postfix])
  159. else:
  160. print 'Secondary password postfix not provided'
  161. sys.exit(1)
  162. self.br.form['password#2'] = secondary_password
  163. # Untested, a list of availables realms is provided when this
  164. # is necessary.
  165. # self.br.form['realm'] = [realm]
  166. self.r = self.br.submit()
  167. def action_key(self):
  168. # Enter key
  169. self.needs_2factor = True
  170. if self.args.oath:
  171. if self.last_action == 'key':
  172. print 'Login failed (Invalid OATH key)'
  173. sys.exit(1)
  174. self.key = hotp(self.args.oath)
  175. elif self.key is None:
  176. self.key = getpass.getpass('Two-factor key:')
  177. self.br.select_form(nr=0)
  178. self.br.form['password'] = self.key
  179. self.key = None
  180. self.r = self.br.submit()
  181. def action_continue(self):
  182. # Yes, I want to terminate the existing connection
  183. self.br.select_form(nr=0)
  184. self.r = self.br.submit()
  185. def action_connect(self):
  186. now = time.time()
  187. delay = 10.0 - (now - self.last_connect)
  188. if delay > 0:
  189. print 'Waiting %.0f...' % (delay)
  190. time.sleep(delay)
  191. self.last_connect = time.time();
  192. dsid = self.find_cookie('DSID').value
  193. print("OZAPTF DSID %s" % str(dsid))
  194. action = []
  195. for arg in self.args.action:
  196. print("OZAPTF OLD ARG: %s" % arg)
  197. arg = arg.replace('%DSID%', dsid).replace('%HOST%', self.args.host)
  198. print("OZAPTF NEW ARG: %s" % arg)
  199. action.append(arg)
  200. f=open("OUT", "w")
  201. p = subprocess.Popen(action, stdin=subprocess.PIPE, stdout=f, stderr=subprocess.STDOUT)
  202. if args.stdin is not None:
  203. stdin = args.stdin.replace('%DSID%', dsid)
  204. stdin = stdin.replace('%HOST%', self.args.host)
  205. p.communicate(input = stdin)
  206. else:
  207. ret = p.wait()
  208. ret = p.returncode
  209. f.close()
  210. # Openconnect specific
  211. if ret == 2:
  212. self.cj.clear(self.args.host, '/', 'DSID')
  213. self.r = self.br.open(self.r.geturl())
  214. def cleanup():
  215. os.killpg(0, signal.SIGTERM)
  216. if __name__ == "__main__":
  217. parser = argparse.ArgumentParser(conflict_handler='resolve')
  218. parser.add_argument('-h', '--host', type=str,
  219. help='VPN host name')
  220. parser.add_argument('-u', '--username', type=str,
  221. help='User name')
  222. parser.add_argument('-p', '--pass_prefix', type=str,
  223. help="Secondary password prefix")
  224. parser.add_argument('-o', '--oath', type=str,
  225. help='OATH key for two factor authentication (hex)')
  226. parser.add_argument('-c', '--config', type=str,
  227. help='Config file')
  228. parser.add_argument('-s', '--stdin', type=str,
  229. help="String to pass to action's stdin")
  230. parser.add_argument('action', nargs=argparse.REMAINDER,
  231. metavar='<action> [<args...>]',
  232. help='External command')
  233. args = parser.parse_args()
  234. args.__dict__['password'] = None
  235. if len(args.action) and args.action[0] == '--':
  236. args.action = args.action[1:]
  237. if not len(args.action):
  238. args.action = None
  239. if args.config is not None:
  240. config = ConfigParser.RawConfigParser()
  241. config.read(args.config)
  242. for arg in ['username', 'host', 'password', 'pass_prefix', 'oath', 'action', 'stdin']:
  243. if args.__dict__[arg] is None:
  244. try:
  245. args.__dict__[arg] = config.get('vpn', arg)
  246. except:
  247. pass
  248. if not isinstance(args.action, list):
  249. args.action = shlex.split(args.action)
  250. if args.username == None or args.host == None or args.action == []:
  251. print "--user, --host, and <action> are required parameters"
  252. sys.exit(1)
  253. atexit.register(cleanup)
  254. jvpn = juniper_vpn(args)
  255. jvpn.run()