forked from danwent/Perspectives-Server
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ssl_scan_sock.py
211 lines (176 loc) · 6.3 KB
/
ssl_scan_sock.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import socket
import struct
import time
import binascii
import hashlib
import traceback
import sys
import errno
import notary_common
# This is a lightweight version of SSL scanning that does not invoke openssl
# at all. Instead, it executes the initial steps of the SSL handshake directly
# using a TCP socket and parses the data itself
USE_SNI = False # Use server name indication: See section 3.1 of http://www.ietf.org/rfc/rfc4366.txt
class SSLScanTimeoutException(Exception):
pass
class SSLAlertException(Exception):
def __init__(self,value):
self.value = value
def read_data(s,data_len, timeout_sec):
buf_str = ""
start_time = time.time()
while(True):
try:
buf_str += s.recv(data_len - len(buf_str))
if len(buf_str) == data_len:
break
except socket.error, e:
if not is_nonblocking_exception(e):
raise e
if time.time() - start_time > timeout_sec:
raise SSLScanTimeoutException("timeout in read_data")
time.sleep(1)
return buf_str
def send_data(s, data, timeout_sec):
start_time = time.time()
while(True):
try:
s.send(data)
break
except socket.error, e:
if is_nonblocking_exception(e):
if time.time() - start_time > timeout_sec:
raise SSLScanTimeoutException("timeout in send_data")
time.sleep(1)
else:
raise e
def is_nonblocking_exception(e):
try:
return e.args[0] == errno.EAGAIN or \
e.args[0] == errno.EINPROGRESS or \
e.args[0] == errno.EALREADY
except:
return False
def do_connect(s, host, port, timeout_sec):
start_time = time.time()
while(True):
try:
s.connect((host, port))
break
except socket.error, e:
if is_nonblocking_exception(e):
if time.time() - start_time > timeout_sec:
raise SSLScanTimeoutException("timeout in do_connect")
time.sleep(1)
else:
raise e
def read_record(sock,timeout_sec):
rec_start = read_data(sock,5,timeout_sec)
if len(rec_start) != 5:
raise Exception("Error: unable to read start of record")
(rec_type, ssl_version, tls_version, rec_length) = struct.unpack('!BBBH',rec_start)
rest_of_rec = read_data(sock,rec_length,timeout_sec)
if len(rest_of_rec) != rec_length:
raise Exception("Error: unable to read full record")
return (rec_type, rest_of_rec)
def get_all_handshake_protocols(rec_data):
protos = []
while len(rec_data) > 0:
t, b1,b2,b3 = struct.unpack('!BBBB',rec_data[0:4])
l = (b1 << 16) | (b2 << 8) | b3
protos.append((t, rec_data[4: 4 + l]))
rec_data = rec_data[4 + l:]
return protos
# rfc 2246 says the server cert if the first one
# in the chain, so ignore everything else
def get_server_cert_from_protocol(proto_data):
proto_data = proto_data[3:] # get rid of 3-bytes describing length of all certs
(b1,b2,b3) = struct.unpack("!BBB",proto_data[0:3])
cert_len = (b1 << 16) | (b2 << 8) | b3
cert = proto_data[3: 3 + cert_len]
m = hashlib.md5()
m.update(cert)
fp = ""
digest_raw = m.digest()
for i in range(len(digest_raw)):
fp += binascii.b2a_hex(digest_raw[i]) + ":"
return fp[:-1]
def attempt_observation_for_service(service_id, timeout_sec):
dns, port = service_id.split(",")[0].split(":")
# if we want to try SNI, do such a scan but if that
# scan fails with an SSL alert, retry with a non SNI request
if USE_SNI and dns[-1:].isalpha():
try:
return run_scan(dns,port,timeout_sec,True)
except SSLAlertException:
pass
run_scan(dns,port,timeout_sec,False)
def run_scan(dns, port, timeout_sec, sni_query):
if sni_query:
# only do SNI query for DNS names, per RFC
client_hello_hex = get_sni_client_hello(dns)
else:
client_hello_hex = get_standard_client_hello()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(0)
do_connect(sock, dns, int(port),timeout_sec)
client_hello = binascii.a2b_hex(client_hello_hex)
send_data(sock, client_hello,timeout_sec)
fp = None
start_time = time.time()
while not fp:
t,rec_data = read_record(sock,timeout_sec)
if t == 22: # handshake message
all_hs_protos = get_all_handshake_protocols(rec_data)
for p in all_hs_protos:
if p[0] == 11:
# server certificate message
fp = get_server_cert_from_protocol(p[1])
break
elif t == 21: # alert message
raise SSLAlertException(rec_data)
if not fp:
time.sleep(1)
if time.time() - start_time > timeout_sec:
break
try:
sock.shutdown(socket.SHUT_RDWR)
except:
pass
sock.close()
if not fp:
raise SSLScanTimeoutException("timeout waiting for data")
return fp
def get_standard_client_hello():
return "8077010301004e0000002000003900003800003500001600001300000a0700c000003300003200002f0300800000050000040100800000150000120000090600400000140000110000080000060400800000030200800000ff9c82ce1e4bc89df2c726b7cebe211ef80a611945d140834eede5674b597be487"
def get_twobyte_hexstr(intval):
return "%0.2X" % (intval & 0xff00) + "%0.2X" % (intval & 0xff)
def get_threebyte_hexstr(intval):
return "%0.2X" % (intval & 0xff0000) + "%0.2X" % (intval & 0xff00) + "%0.2X" % (intval & 0xff)
def get_hostname_extension(hostname):
hex_hostname = binascii.b2a_hex(hostname)
hn_len = len(hostname)
return "0000" + get_twobyte_hexstr(hn_len + 5) + get_twobyte_hexstr(hn_len + 3) + \
"00" + get_twobyte_hexstr(hn_len) + hex_hostname
def get_sni_client_hello(hostname):
hn_extension = get_hostname_extension(hostname)
all_extensions = hn_extension
the_rest = "03014d786109055e4736b93b63c371507f824c2d0f05a25b2d54b6b52a1e43c2a52c00002800390038003500160013000a00330032002f000500040015001200090014001100080006000300ff020100" + get_twobyte_hexstr(len(all_extensions)/2) + all_extensions
proto_len = (len(the_rest) / 2)
rec_len = proto_len + 4
return "160301" + get_twobyte_hexstr(rec_len) + "01" + get_threebyte_hexstr(proto_len) + the_rest
if __name__ == "__main__":
if len(sys.argv) != 3 and len(sys.argv) != 2:
print >> sys.stderr, "ERROR: usage: <service-id> [notary-db-file>]"
exit(1)
service_id = sys.argv[1]
try:
fp = attempt_observation_for_service(service_id, 10)
if len(sys.argv) == 3:
notary_common.report_observation(sys.argv[2], service_id, fp)
else:
print "INFO: no database specified, not saving observation"
print "Successful scan complete: '%s' has key '%s' " % (service_id,fp)
except:
print "Error scanning for %s" % service_id
traceback.print_exc(file=sys.stdout)