25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

277 lines
8.8 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 errno
  11. import argparse
  12. import atexit
  13. import signal
  14. import ConfigParser
  15. import time
  16. import binascii
  17. import hmac
  18. import hashlib
  19. import shlex
  20. import tncc
  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. def find_cookie(self, name):
  79. for cookie in self.cj:
  80. if cookie.name == name:
  81. return cookie
  82. return None
  83. def next_action(self):
  84. if self.find_cookie('DSID'):
  85. return 'connect'
  86. for form in self.br.forms():
  87. if form.name == 'frmLogin':
  88. return 'login'
  89. elif form.name == 'frmDefender':
  90. return 'key'
  91. elif form.name == 'frmConfirmation':
  92. return 'continue'
  93. else:
  94. raise Exception('Unknown form type:', form.name)
  95. return 'tncc'
  96. def run(self):
  97. # Open landing page
  98. self.r = self.br.open('https://' + self.args.host)
  99. while True:
  100. action = self.next_action()
  101. if action == 'tncc':
  102. self.action_tncc()
  103. elif action == 'login':
  104. self.action_login()
  105. elif action == 'key':
  106. self.action_key()
  107. elif action == 'continue':
  108. self.action_continue()
  109. elif action == 'connect':
  110. self.action_connect()
  111. self.last_action = action
  112. def action_tncc(self):
  113. # Run tncc host checker
  114. dspreauth_cookie = self.find_cookie('DSPREAUTH')
  115. if dspreauth_cookie is None:
  116. raise Exception('Could not find DSPREAUTH key for host checker')
  117. dssignin_cookie = self.find_cookie('DSSIGNIN')
  118. t = tncc.tncc(self.args.host);
  119. self.cj.set_cookie(t.get_cookie(dspreauth_cookie, dssignin_cookie))
  120. self.r = self.br.open(self.r.geturl())
  121. def action_login(self):
  122. # The token used for two-factor is selected when this form is submitted.
  123. # If we aren't getting a password, then get the key now, otherwise
  124. # we could be sitting on the two factor key prompt later on waiting
  125. # on the user.
  126. if self.args.password is None or self.last_action == 'login':
  127. if self.fixed_password:
  128. print 'Login failed (Invalid username or password?)'
  129. sys.exit(1)
  130. else:
  131. self.args.password = getpass.getpass('Password:')
  132. self.needs_2factor = False
  133. if self.needs_2factor:
  134. if self.args.oath:
  135. self.key = hotp(self.args.oath)
  136. else:
  137. self.key = getpass.getpass('Two-factor key:')
  138. else:
  139. self.key = None
  140. # Enter username/password
  141. self.br.select_form(nr=0)
  142. self.br.form['username'] = self.args.username
  143. self.br.form['password'] = self.args.password
  144. # Untested, a list of availables realms is provided when this
  145. # is necessary.
  146. # self.br.form['realm'] = [realm]
  147. self.r = self.br.submit()
  148. def action_key(self):
  149. # Enter key
  150. self.needs_2factor = True
  151. if self.args.oath:
  152. if self.last_action == 'key':
  153. print 'Login failed (Invalid OATH key)'
  154. sys.exit(1)
  155. self.key = hotp(self.args.oath)
  156. elif self.key is None:
  157. self.key = getpass.getpass('Two-factor key:')
  158. self.br.select_form(nr=0)
  159. self.br.form['password'] = self.key
  160. self.key = None
  161. self.r = self.br.submit()
  162. def action_continue(self):
  163. # Yes, I want to terminate the existing connection
  164. self.br.select_form(nr=0)
  165. self.r = self.br.submit()
  166. def action_connect(self):
  167. now = time.time()
  168. delay = 10.0 - (now - self.last_connect)
  169. if delay > 0:
  170. print 'Waiting %.0f...' % (delay)
  171. time.sleep(delay)
  172. self.last_connect = time.time();
  173. dsid = self.find_cookie('DSID').value
  174. action = []
  175. for arg in self.args.action:
  176. arg = arg.replace('%DSID%', dsid).replace('%HOST%', self.args.host)
  177. action.append(arg)
  178. p = subprocess.Popen(action, stdin=subprocess.PIPE)
  179. if args.stdin is not None:
  180. stdin = args.stdin.replace('%DSID%', dsid)
  181. stdin = stdin.replace('%HOST%', self.args.host)
  182. p.communicate(input = stdin)
  183. else:
  184. ret = p.wait()
  185. ret = p.returncode
  186. # Openconnect specific
  187. if ret == -errno.EPERM:
  188. self.cj.clear(self.args.host, '/', 'DSID')
  189. self.r = self.br.open(self.r.geturl())
  190. elif ret > 0:
  191. sys.exit(ret)
  192. def cleanup():
  193. os.killpg(0, signal.SIGTERM)
  194. if __name__ == "__main__":
  195. parser = argparse.ArgumentParser(conflict_handler='resolve')
  196. parser.add_argument('-h', '--host', type=str,
  197. help='VPN host name')
  198. parser.add_argument('-u', '--username', type=str,
  199. help='User name')
  200. parser.add_argument('-o', '--oath', type=str,
  201. help='OATH key for two factor authentication (hex)')
  202. parser.add_argument('-c', '--config', type=str,
  203. help='Config file')
  204. parser.add_argument('-s', '--stdin', type=str,
  205. help="String to pass to action's stdin")
  206. parser.add_argument('action', nargs=argparse.REMAINDER,
  207. metavar='<action> [<args...>]',
  208. help='External command')
  209. args = parser.parse_args()
  210. args.__dict__['password'] = None
  211. if len(args.action) and args.action[0] == '--':
  212. args.action = args.action[1:]
  213. if not len(args.action):
  214. args.action = None
  215. if args.config is not None:
  216. config = ConfigParser.RawConfigParser()
  217. config.read(args.config)
  218. for arg in ['username', 'host', 'password', 'oath', 'action', 'stdin']:
  219. if args.__dict__[arg] is None:
  220. try:
  221. args.__dict__[arg] = config.get('vpn', arg)
  222. except:
  223. pass
  224. if not isinstance(args.action, list):
  225. args.action = shlex.split(args.action)
  226. if args.username == None or args.host == None or args.action == []:
  227. print "--user, --host, and <action> are required parameters"
  228. sys.exit(1)
  229. atexit.register(cleanup)
  230. jvpn = juniper_vpn(args)
  231. jvpn.run()