#!/usr/bin/python # -*- coding: utf-8 -*- import sys import os import logging import StringIO import mechanize import cookielib import struct import socket import ssl import base64 import collections import zlib import HTMLParser import socket import netifaces import urlgrabber import urllib2 import platform import json import datetime import pyasn1_modules.pem import pyasn1_modules.rfc2459 import pyasn1.codec.der.decoder import xml.etree.ElementTree ssl._create_default_https_context = ssl._create_unverified_context debug = False logging.basicConfig(stream=sys.stderr, level=logging.DEBUG if debug else logging.INFO) MSG_POLICY = 0x58316 MSG_FUNK_PLATFORM = 0x58301 MSG_FUNK = 0xa4c01 # 0013 - Message def decode_0013(buf, indent): logging.debug('%scmd 0013 (Message) %d bytes', indent, len(buf)) ret = collections.defaultdict(list) while (len(buf) >= 12): length, cmd, out = decode_packet(buf, indent + " ") buf = buf[length:] ret[cmd].append(out) return ret # 0012 - u32 def decode_0012(buf, indent): logging.debug('%scmd 0012 (u32) %d bytes', indent, len(buf)) return struct.unpack(">I", buf) # 0016 - zlib compressed message def decode_0016(buf, indent): logging.debug('%scmd 0016 (compressed message) %d bytes', indent, len(buf)) _, compressed = struct.unpack(">I" + str(len(buf) - 4) + "s", buf) buf = zlib.decompress(compressed) ret = collections.defaultdict(list) while (len(buf) >= 12): length, cmd, out = decode_packet(buf, indent + " ") buf = buf[length:] ret[cmd].append(out) return ret # 0ce4 - encapsulation def decode_0ce4(buf, indent): logging.debug('%scmd 0ce4 (encapsulation) %d bytes', indent, len(buf)) ret = collections.defaultdict(list) while (len(buf) >= 12): length, cmd, out = decode_packet(buf, indent + " ") buf = buf[length:] ret[cmd].append(out) return ret # 0ce5 - string without hex prefixer def decode_0ce5(buf, indent): s = struct.unpack(str(len(buf)) + "s", buf)[0] logging.debug('%scmd 0ce5 (string) %d bytes', indent, len(buf)) s = s.rstrip('\0') logging.debug('%s', s) return s # 0ce7 - string with hex prefixer def decode_0ce7(buf, indent): id, s = struct.unpack(">I" + str(len(buf) - 4) + "s", buf) logging.debug('%scmd 0ce7 (id %08x string) %d bytes', indent, id, len(buf)) if s.startswith('COMPRESSED:'): typ, length, data = s.split(':', 2) s = zlib.decompress(data) s = s.rstrip('\0') logging.debug('%s', s) return (id, s) # 0cf0 - encapsulation def decode_0cf0(buf, indent): logging.debug('%scmd 0cf0 (encapsulation) %d bytes', indent, len(buf)) ret = dict() cmd, _, out = decode_packet(buf, indent + " ") ret[cmd] = out return ret # 0cf1 - string without hex prefixer def decode_0cf1(buf, indent): s = struct.unpack(str(len(buf)) + "s", buf)[0] logging.debug('%scmd 0cf1 (string) %d bytes', indent, len(buf)) s = s.rstrip('\0') logging.debug('%s', s) return s # 0cf3 - u32 def decode_0cf3(buf, indent): ret = struct.unpack(">I", buf) logging.debug('%scmd 0cf3 (u32) %d bytes - %d', indent, len(buf), ret[0]) return ret def decode_packet(buf, indent=""): cmd, _1, _2, length, _3 = struct.unpack(">IBBHI", buf[:12]) if length < 12: raise Exception("Invalid packet, cmd %04x, _1 %02x, _2 %02x, length %d" % (cmd, _1, _2, length)) data = buf[12:length] if length % 4: length += 4 - (length % 4) if cmd == 0x0013: data = decode_0013(data, indent) elif cmd == 0x0012: data = decode_0012(data, indent) elif cmd == 0x0016: data = decode_0016(data, indent) elif cmd == 0x0ce4: data = decode_0ce4(data, indent) elif cmd == 0x0ce5: data = decode_0ce5(data, indent) elif cmd == 0x0ce7: data = decode_0ce7(data, indent) elif cmd == 0x0cf0: data = decode_0cf0(data, indent) elif cmd == 0x0cf1: data = decode_0cf1(data, indent) elif cmd == 0x0cf3: data = decode_0cf3(data, indent) else: logging.debug('%scmd %04x(%02x:%02x) is unknown, length %d', indent, cmd, _1, _2, length) data = None return length, cmd, data def encode_packet(cmd, align, buf): align = 4 orig_len = len(buf) if align > 1 and (len(buf) + 12) % align: buf += struct.pack(str(align - len(buf) % align) + "x") return struct.pack(">IBBHI", cmd, 0xc0, 0x00, orig_len + 12, 0x0000583) + buf # 0013 - Message def encode_0013(buf): return encode_packet(0x0013, 4, buf) # 0012 - u32 def encode_0012(i): return encode_packet(0x0012, 1, struct.pack("I" + str(len(s)) + "sx", prefix, s)) # 0cf0 - encapsulation def encode_0cf0(buf): return encode_packet(0x0cf0, 4, buf) # 0cf1 - string without hex prefixer def encode_0cf1(s): s += '\0' return encode_packet(0x0ce5, 1, struct.pack(str(len(s)) + "s", s)) # 0cf3 - u32 def encode_0cf3(i): return encode_packet(0x0013, 1, struct.pack(" 0 self.br.set_handle_refresh(mechanize._http.HTTPRefreshProcessor(), max_time=1) # Want debugging messages? if debug: self.br.set_debug_http(True) self.br.set_debug_redirects(True) self.br.set_debug_responses(True) self.user_agent = 'Neoteris HC Http' self.br.addheaders = [('User-agent', self.user_agent)] def find_cookie(self, name): for cookie in self.cj: if cookie.name == name: return cookie return None def set_cookie(self, name, value): cookie = cookielib.Cookie(version=0, name=name, value=value, port=None, port_specified=False, domain=self.vpn_host, domain_specified=True, domain_initial_dot=False, path=self.path, path_specified=True, secure=True, expires=None, discard=True, comment=None, comment_url=None, rest=None, rfc2109=False) self.cj.set_cookie(cookie) def parse_response(self): # Read in key/token fields in HTTP response response = dict() last_key = '' for line in self.r.readlines(): line = line.strip() # Note that msg is too long and gets wrapped, handle it special if last_key == 'msg' and len(line): response['msg'] += line else: key = '' try: key, val = line.split('=', 1) response[key] = val except: pass last_key = key return response def parse_policy_response(self, msg_data): # The decompressed data is HTMLish, decode it. The value="" of each # tag is the data we want. objs = [] class ParamHTMLParser(HTMLParser.HTMLParser): def handle_starttag(self, tag, attrs): if tag.lower() == 'param': for key, value in attrs: if key.lower() == 'value': # It's made up of a bunch of key=value pairs separated # by semicolons d = dict() for field in value.split(';'): field = field.strip() try: key, value = field.split('=', 1) d[key] = value except: pass objs.append(d) p = ParamHTMLParser() p.feed(msg_data) p.close() return objs def parse_funk_response(self, msg_data): e = xml.etree.ElementTree.fromstring(msg_data) req_certs = dict() for cert in e.find('AttributeRequest').findall('CertData'): dns = dict() cert_id = cert.attrib['Id'] for attr in cert.findall('Attribute'): name = attr.attrib['Name'] value = attr.attrib['Value'] attr_type = attr.attrib['Type'] if attr_type == 'DN': dns[name] = dict(n.strip().split('=') for n in value.split(',')) else: # Unknown attribute type pass req_certs[cert_id] = dns return req_certs def gen_funk_platform(self): # We don't know if the xml parser on the other end is fully complaint, # just format a string like it expects. msg = " " % self.platform msg += " " def add_attr(key, val): return "" % (key, val) msg += add_attr('Platform', self.platform) if self.hostname: msg += add_attr(self.hostname, 'NETBIOSName') # Reversed for mac in self.mac_addrs: msg += add_attr(mac, 'MACAddress') # Reversed msg += " " return encode_0ce7(msg, MSG_FUNK_PLATFORM) def gen_funk_present(self): msg = " " % self.platform msg += " " return encode_0ce7(msg, MSG_FUNK) def gen_funk_response(self, certs): msg = " " % self.platform msg += " " msg += "" % self.platform for name, value in certs.iteritems(): msg += "" % (name, value.data.strip()) msg += "" % (name, value.data.strip()) msg += " " return encode_0ce7(msg, MSG_FUNK) def gen_policy_request(self): policy_blocks = collections.OrderedDict({ 'policy_request': { 'message_version': '3' }, 'esap': { 'esap_version': 'NOT_AVAILABLE', 'fileinfo': 'NOT_AVAILABLE', 'has_file_versions': 'YES', 'needs_exact_sdk': 'YES', 'opswat_sdk_version': '3' }, 'system_info': { 'os_version': '2.6.2', 'sp_version': '0', 'hc_mode': 'userMode' } }) msg = '' for policy_key, policy_val in policy_blocks.iteritems(): v = ''.join([ '%s=%s;' % (k, v) for k, v in policy_val.iteritems()]) msg += '' % (policy_key, v) return encode_0ce7(msg, 0xa4c18) def gen_policy_response(self, policy_objs): # Make a set of policies policies = set() for entry in policy_objs: if 'policy' in entry: policies.add(entry['policy']) # Try to determine on policy name whether the response should be OK # or NOTOK. Default to OK if we don't know, this may need updating. msg = '' for policy in policies: msg += '\npolicy:%s\nstatus:' % policy if 'Unsupported' in policy or 'Deny' in policy: msg += 'NOTOK\nerror:Unknown error' elif 'Required' in policy: msg += 'OK\n' else: # Default action msg += 'OK\n' return encode_0ce7(msg, MSG_POLICY) def get_cookie(self, dspreauth=None, dssignin=None): if dspreauth is None or dssignin is None: self.r = self.br.open('https://' + self.vpn_host) else: try: self.cj.set_cookie(dspreauth) except: self.set_cookie('DSPREAUTH', dspreauth) try: self.cj.set_cookie(dssignin) except: self.set_cookie('DSSIGNIN', dssignin) inner = self.gen_policy_request() inner += encode_0ce7('policy request\x00v4', MSG_POLICY) if self.funk: inner += self.gen_funk_platform() inner += self.gen_funk_present() msg_raw = encode_0013(encode_0ce4(inner) + encode_0ce5('Accept-Language: en') + encode_0cf3(1)) logging.debug('Sending packet -') decode_packet(msg_raw) post_attrs = { 'connID': '0', 'timestamp': '0', 'msg': base64.b64encode(msg_raw), 'firsttime': '1' } if self.deviceid: post_attrs['deviceid'] = self.deviceid post_data = ''.join([ '%s=%s;' % (k, v) for k, v in post_attrs.iteritems()]) self.r = self.br.open('https://' + self.vpn_host + self.path + 'hc/tnchcupdate.cgi', post_data) # Parse the data returned into a key/value dict response = self.parse_response() # msg has the stuff we want, it's base64 encoded logging.debug('Receiving packet -') msg_raw = base64.b64decode(response['msg']) _1, _2, msg_decoded = decode_packet(msg_raw) # Within msg, there is a field of data sub_strings = msg_decoded[0x0ce4][0][0x0ce7] # Pull the data out of the 'value' key in the htmlish stuff returned policy_objs = [] req_certs = dict() for str_id, sub_str in sub_strings: if str_id == MSG_POLICY: policy_objs += self.parse_policy_response(sub_str) elif str_id == MSG_FUNK: req_certs = self.parse_funk_response(sub_str) if debug: for obj in policy_objs: if 'policy' in obj: logging.debug('policy %s', obj['policy']) for key, val in obj.iteritems(): if key != 'policy': logging.debug('\t%s %s', key, val) # Try to locate the required certificates certs = dict() for cert_id, req_dns in req_certs.iteritems(): for cert in self.avail_certs: fail = False for dn_name, dn_vals in req_dns.iteritems(): for name, val in dn_vals.iteritems(): try: if dn_name == 'IssuerDN': assert val in cert.issuer[name] else: logging.warn('Unknown DN type %s', str(dn_name)) raise Exception() except: fail = True break if fail: break if not fail: certs[cert_id] = cert break if cert_id not in certs: logging.warn('Could not find certificate for %s', str(req_dns)) inner = '' if certs: inner += self.gen_funk_response(certs) inner += self.gen_policy_response(policy_objs) msg_raw = encode_0013(encode_0ce4(inner) + encode_0ce5('Accept-Language: en')) logging.debug('Sending packet -') decode_packet(msg_raw) post_attrs = { 'connID': '1', 'msg': base64.b64encode(msg_raw), 'firsttime': '1' } post_data = ''.join([ '%s=%s;' % (k, v) for k, v in post_attrs.iteritems()]) self.r = self.br.open('https://' + self.vpn_host + self.path + 'hc/tnchcupdate.cgi', post_data) # We have a new DSPREAUTH cookie return self.find_cookie('DSPREAUTH') class tncc_server(object): def __init__(self, s, t): self.sock = s self.tncc = t def process_cmd(self): buf = sock.recv(1024).decode('ascii') if not len(buf): sys.exit(0) cmd, buf = buf.split('\n', 1) cmd = cmd.strip() args = dict() for n in buf.split('\n'): n = n.strip() if len(n): key, val = n.strip().split('=', 1) args[key] = val if cmd == 'start': cookie = self.tncc.get_cookie(args['Cookie'], args['DSSIGNIN']) resp = '200\n3\n%s\n\n' % cookie.value sock.send(resp.encode('ascii')) elif cmd == 'setcookie': # FIXME: Support for periodic updates dsid_value = args['Cookie'] if __name__ == "__main__": vpn_host = sys.argv[1] funk = 'TNCC_FUNK' in os.environ and os.environ['TNCC_FUNK'] != '0' platform = os.environ.get('TNCC_PLATFORM', platform.system() + ' ' + platform.release()) if 'TNCC_HWADDR' in os.environ: mac_addrs = [n.strip() for n in os.environ['TNCC_HWADDR'].split(',')] else: mac_addrs = [] for iface in netifaces.interfaces(): try: mac = netifaces.ifaddresses(iface)[netifaces.AF_LINK][0]['addr'] assert mac != '00:00:00:00:00:00' mac_addrs.append(mac) except: pass hostname = os.environ.get('TNCC_HOSTNAME', socket.gethostname()) certs = [] if 'TNCC_CERTS' in os.environ: now = datetime.datetime.now() for f in os.environ['TNCC_CERTS'].split(','): cert = x509cert(f.strip()) if now < cert.not_before: logging.warn('WARNING: %s is not yet valid', f) if now > cert.not_after: logging.warn('WARNING: %s is expired', f) certs.append(cert) # \HKEY_CURRENT_USER\Software\Juniper Networks\Device Id device_id = os.environ.get('TNCC_DEVICE_ID') t = tncc(vpn_host, device_id, funk, platform, hostname, mac_addrs, certs) if len(sys.argv) == 4: dspreauth_value = sys.argv[2] dssignin_value = sys.argv[3] 'TNCC ', dspreauth_value, dssignin_value print t.get_cookie(dspreauth, dssignin).value else: sock = socket.fromfd(0, socket.AF_UNIX, socket.SOCK_SEQPACKET) server = tncc_server(sock, t) while True: server.process_cmd()