diff --git a/README.md b/README.md index 08dd362..cedd49d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ SECFORCE Lorenzo Vogelsang (@ptrac3) - +You Gotta Hack That + + Felix Ryan (@gotta_hack) Description: ---- @@ -36,7 +38,9 @@ Options: [REQUIRED] `--input-file` Path of the captured .RAW file with a valid FIX login sequence -[REQUIRED] `--csv-log` Path for the output CSV log file +[REQUIRED] `--csv` Path for the output CSV log file + +`--seq-start` The sequence ID to start sending FIX messages with `--fuzz` Path of the file containing the payloads for fuzzing @@ -44,6 +48,10 @@ Options: `--auto-fuzz length step` It enables the auto-fuzz mode which generates UTF-8 payloads on the fly accordingly to the length and step values that were passed +`--sequential-fuzz` Effectively a brute forcer + +`--no-fuzz` Just send the original FIX messages to show that the tool has connectivity and everything is working correctly + Please also consider that --fuzz and --auto-fuzz are mutually exclusive parameters. diff --git a/fix.py b/fix.py index 1484c3d..3932f80 100644 --- a/fix.py +++ b/fix.py @@ -24,6 +24,7 @@ import itertools import fuzzer as fz from itertools import groupby +from socket import error as SocketError __author__ = "Thanos Polychronis and Lorenzo Vogelsang" __copyright__ = "Copyright 2017, SECFORCE LTD" @@ -49,9 +50,12 @@ host = parser.add_argument('--host', type=str, nargs='+', help='the IP of the FIX server', required=True) port = parser.add_argument('--port', type=int, help='the listening port', required=True) input_file = parser.add_argument('--input-file', type=str, nargs=1, help='PCAP file with FIX authentication and action(s) to fuzz', required=True) +seq_start = parser.add_argument('--seq-start', type=int, help='The start number for the sequence ID (inc initial logon), defaults to "2"', required=False, default=2) group = parser.add_mutually_exclusive_group(required=True) -fuzz = group.add_argument('--fuzz', default=0, type=str, metavar='', nargs='+', help='File containing payloads') +fuzz = group.add_argument('--fuzz', default=0, type=str, metavar='', nargs='+', help='File containing payloads') auto_fuzz = group.add_argument('--auto-fuzz', metavar=' ', help='Enable the auto-fuzz mode', nargs=2) +sequential_fuzz = group.add_argument('--sequential-fuzz', action='store_true', help='Enable the sequential-fuzz mode') +no_fuzz = group.add_argument('--no-fuzz', action='store_true', help='Just send the original unfettered version from file') csv = parser.add_argument('--csv', metavar='', type=str, nargs='+', help='Output Log file') param = parser.add_argument('--param', default=0, type=str, metavar='', nargs='+', help='Parameters to Fuzz') args = parser.parse_args() @@ -62,13 +66,12 @@ #Create header for CSV logging if args.csv: with open(args.csv[0], "w") as myfile: - myfile.write("TimeStamp,Message Sent,Message Received,Time Elapsed"+"\n") + myfile.write("TimeStamp,Message Sent,Send Seq,Message Received,Time Elapsed"+"\n") csv_file = str(args.csv[0]) def getFuzzList(file): with open(file, 'r') as fuzz: fuzzer = [line.rstrip() for line in fuzz] - print fuzzer return fuzzer def timestampGen(): @@ -84,21 +87,12 @@ def update_timestamp(message): #Whole timestamp tag+field timestamp_tag = "52="+timestamp ts = time.time() - newtimestamp = datetime.datetime.fromtimestamp(ts).strftime('%Y%m%d-%H:%M:%S.%f')[:-3] + newtimestamp = datetime.datetime.utcfromtimestamp(ts).strftime('%Y%m%d-%H:%M:%S.%f')[:-3] newtimestamp_tag = "52="+newtimestamp message = message.replace(timestamp_tag, newtimestamp_tag) return message,timestamp -def checksum(message): - - # The checksum field is removed from FIX message - message = str(message[:-7]) - # Checksum is computed - message_checksum = str(int(sum(bytearray(message)))%256).zfill(3) - return message_checksum - - def update_checksum(message): checksum_field = "10=" # The checksum field is removed from FIX message @@ -110,7 +104,7 @@ def update_checksum(message): return message_ok -def update_bodylength(message, checksum): +def update_bodylength(message): #9= extraction list_fix = [] soh = '\x01' @@ -123,43 +117,94 @@ def update_bodylength(message, checksum): message = message.strip(soh+"9="+str(body_value)) + soh - message = beginString+soh+"9="+str(len(message))+soh+message+"10="+checksum+soh + message = beginString+soh+"9="+str(len(message))+soh+message+"10=001"+soh list_fix.append(message) return message -def sendFuzzMessage(host, port, logonmsg, final): - - logonmsg,time_logon = update_timestamp(logonmsg) - logonmsg = update_checksum(logonmsg) - checksum_logonmsg = checksum(logonmsg) - s = socket.socket() - s.connect((host, port)) - s.send(logonmsg) - - for f in final: - fix_request,time_fuzz = update_timestamp(f) - checksum_msg = checksum(f) - fix_request = update_bodylength(fix_request, checksum_msg) - fix_request = update_checksum(fix_request) - start_time = time.time() - s.send(fix_request) - print "\n[-] Sent: " + fix_request +def sendFuzzMessage(host, port, logonmsg, final, current_seq_num): + requested_seq_num = None + logonmsg,time_logon,current_seq_num = update_fix_message(logonmsg, current_seq_num) + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((host, port)) + s.send(logonmsg) + print "\n[-] Logon Sent: " + logonmsg.replace('\x01', '^') fix_response = s.recv(1024) - print "[-] Received:" + fix_response - elapsed_time = (time.time() - start_time) - elapsed_time_ms = int(elapsed_time * 1000) - if args.csv: - csv(time_fuzz,fix_request,fix_response,elapsed_time_ms) - s.close - return final, fix_response, elapsed_time_ms, s - - -def update_fix_message(message): - message = update_timestamp(message) + print "[-] Logon Received:" + fix_response.replace('\x01', '^') + + # logon error detected + if re.findall("\x0135=5\x01", fix_response): + logout_error_message = re.findall("(?:\x0158=)(.*?)\x01", fix_response)[0] + print "\n[*] Server sent Logout message. Error text is: " + logout_error_message + if 'MsgSeqNum' in logout_error_message: + expected_seq_num = re.findall("(?:expecting )([0-9]+)(?: but received)", logout_error_message)[0] + print "\n[*] Sequence number expected is: " + expected_seq_num + " but we gave: " + str(current_seq_num-1) \ + + " will retry logon using expected sequence number" + # have to restart socket as server doesn't play otherwise + s.close + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((host, port)) + logonmsg, time_logon, current_seq_num = update_fix_message(logonmsg, int(expected_seq_num)) + s.send(logonmsg) + print "\n[-] Logon Sent: " + logonmsg.replace('\x01', '^') + fix_response = s.recv(1024) + print "[-] Logon Received:" + fix_response.replace('\x01', '^') + + for fix_message in final: + fix_request,time_fuzz,current_seq_num = update_fix_message(fix_message, current_seq_num) + start_time = time.time() + s.send(fix_request) + print "\n[-] Payload Sent: " + fix_request.replace('\x01', '^') + + fix_response = s.recv(1024) + print "[-] Payload Received:" + fix_response.replace('\x01', '^') + if re.findall("(?:\x0135=)2\x01", fix_response): + print "\n[*] Server sent ResendRequest message, extracting sequence ID for future messages" + requested_seq_num = int(re.findall("(?:\x017=)([0-9]+)\x01", fix_response)[0]) + print "\n[*] Sequence number requested is: " + str(requested_seq_num) + " but we are at: " + str(current_seq_num-1) \ + + " will use requested sequence number for future payloads. Also, this payload will not be re-fired" + if not fix_response: + print "\n[*] Server did not send a response - could indicate crash occurred" + fix_response = 'Blank response - investigate this and previous payload' + + s.close + except SocketError as e: + print ('Socket error: ' + str(e)) + fix_response = 'Socket error: ' + str(e) + + elapsed_time = (time.time() - start_time) + elapsed_time_ms = int(elapsed_time * 1000) + if args.csv: + apend_to_csv(time_fuzz, fix_request, str(int(current_seq_num)-1), fix_response, elapsed_time_ms) + if requested_seq_num: + return requested_seq_num + else: + return current_seq_num + + +def update_seqnum(message, current_seq_num): + # 34= extraction + seq_fix = dict(re.findall("(?:^|\x01)(34)=(.*?)\x01", message)) + # Extraction of the actual seq num + seq = seq_fix['34'] + # Whole seq tag+field + seq_tag = "34=" + seq + new_seq = str(current_seq_num) + new_check_sum_tag = "34=" + new_seq + message = message.replace(seq_tag, new_check_sum_tag) + current_seq_num += 1 + return message, current_seq_num + + +def update_fix_message(message, current_seq_num): + message,time_logon = update_timestamp(message) + message, current_seq_num = update_seqnum(message, current_seq_num) + message = update_bodylength(message) message = update_checksum(message) - return message + return message, time_logon, current_seq_num + def fix2log(message): message = message.replace(getSoh, "^") @@ -203,23 +248,43 @@ def fuzz_it(logonmsg, fix_requests): def test(fix_requests, dict_final, logonmsg): - - for i in fix_requests: - if i in dict_final.keys(): - params = ",".join(dict_final[i]).split(",") - - for param in params: - if args.auto_fuzz: - print("[AUTO-FUZZ-MODE] Now fuzzing %d field:" ) %int(param) - payloads = auto_fuzz(i, param) - else: - print("[Normal-FUZZ-MODE] Now fuzzing field %d: " ) %int(param) - payloads = normal_fuzz(i, param) + current_seq_num = args.seq_start + + # send an unfettered FIX message to test the thing works + print("Sending the first message from the input file as an unfettered FIX message\n") + initial = list() + initial.append(str(fix_requests[0])) + current_seq_num = sendFuzzMessage(args.host[0], args.port, logonmsg, initial, current_seq_num) + + print('-=-=-=-Fuzzing start-=-=-=-\n') + + try: + for message_to_fuzz in fix_requests: + if message_to_fuzz in dict_final.keys(): + params = ",".join(dict_final[message_to_fuzz]).split(",") + + for param in params: + if args.auto_fuzz: + print("[AUTO-FUZZ-MODE] Now fuzzing field: %d" ) %int(param) + payloads = auto_fuzz(message_to_fuzz, param) + elif args.sequential_fuzz: + print("[SEQUENTIAL-FUZZ-MODE] Now fuzzing field: %d") % int(param) + payloads = sequential_fuzz(message_to_fuzz, param) + elif args.no_fuzz: + print("No further action needed as not fuzzing, just sending unfettered") + else: + print("[NORMAL-FUZZ-MODE] Now fuzzing field: %d") %int(param) + payloads = normal_fuzz(message_to_fuzz, param) + + for payload in payloads: + final = list(fix_requests) + final[final.index(message_to_fuzz)] = payload + current_seq_num = sendFuzzMessage(args.host[0], args.port, logonmsg, final, current_seq_num) + except KeyboardInterrupt: + # if user "Ctrl-C", stop processing gracefully + print('Exiting...') + exit(1) - for payload in payloads: - final = list(fix_requests) - final[final.index(i)] = payload - sendFuzzMessage(args.host[0], args.port, logonmsg, final) def fuzz_replace(request, param, payload): newPart=re.findall('\d+', param) @@ -236,11 +301,26 @@ def fuzz_replace(request, param, payload): def normal_fuzz(request, param): normal_fuzz=[] for payload in getFuzzList(args.fuzz[0]): - fuzzed_message = fuzz_replace(request, param, payload) a = fuzz_replace(request,param,payload) normal_fuzz.append(a) return normal_fuzz + +def sequential_fuzz(request, param): + + sequential_fuzz=[] + + payloads_list = list() + for i in xrange(10000): + a = u"\\u%04x" % i + payloads_list.append(a.decode('unicode-escape')) + + for payload in payloads_list: + a = fuzz_replace(request,param,payload) + sequential_fuzz.append(a) + return sequential_fuzz + + def auto_fuzz(request, param): length = int(args.auto_fuzz[0]) step = int(args.auto_fuzz[1]) @@ -249,17 +329,28 @@ def auto_fuzz(request, param): utf8_encoded = "" utf_payloads_test_inc = fz.utf8_gen(i) utf8_fixed_plain_test_inc = utf_payloads_test_inc[0][0] - print utf_payloads_test_inc for field in utf8_fixed_plain_test_inc: utf8_encoded = utf8_encoded+field - #print utf8_encoded a = fuzz_replace(request,param,utf8_encoded) auto_fuzz.append(a) return auto_fuzz -def csv(time,request,response,elapsed): + +def message_cleaner(message): + SOH = '\x01' + newline = u"\u000A" + carriagereturn = u"\u000D" + comma = u"\u002C" + + clean_message = message.rstrip('\n').replace(SOH, '^').replace(newline, '[{nl}]').replace(carriagereturn, '[{cr}]').replace(comma, '[{com}]') + return clean_message + + +def apend_to_csv(time, request, req_seq, response, elapsed): + clean_request = message_cleaner(request) + clean_response = message_cleaner(response) with open(args.csv[0], "a") as myfile: - myfile.write(str(time).rstrip('\n')+","+str(request).rstrip('\n')+","+str(response).rstrip('\n')+","+str(elapsed).rstrip('\n')+"\n") + myfile.write(str(time).rstrip('\n')+","+str(clean_request)+","+str(req_seq)+","+str(clean_response)+","+str(elapsed).rstrip('\n')+"\n") def main(): FIX_id="" @@ -284,13 +375,13 @@ def main(): logonmsg = message_ok elif (logonmsg!="" and (re.search(r'30=0' ,message)) or (re.search(r'35=A'+getSoh ,message))): print "\n[INFO] Message is Not Logon: " + message_ok - elif (re.search(r'58=' ,message_ok)): - print "\n[INFO] This is a response, skipping...: " + message_ok - try: - FIX_id = re.search(r'49=(.*?)'+getSoh, message).group(1) - print "\n[+] Fix server ID is: " + FIX_id - except: - print "\n[-]FIX server ID was not found" +# elif (re.search(r'58=' ,message_ok)): +# print "\n[INFO] This is a response, skipping...: " + message_ok +# try: +# FIX_id = re.search(r'49=(.*?)'+getSoh, message).group(1) +# print "\n[+] Fix server ID is: " + FIX_id +# except: +# print "\n[-]FIX server ID was not found" else: #Creating the list of messages to fuzz fix_requests.append(message_ok)