From ae05a123bbcce65ed86254dd0e2bfb865a299d3a Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 24 Oct 2025 10:39:09 -0400 Subject: [PATCH 1/7] Don't open pass_file if it's blank --- lib/metasploit/framework/credential_collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/metasploit/framework/credential_collection.rb b/lib/metasploit/framework/credential_collection.rb index d0e82daf4ea74..592dc77c0397d 100644 --- a/lib/metasploit/framework/credential_collection.rb +++ b/lib/metasploit/framework/credential_collection.rb @@ -402,7 +402,7 @@ def each_password(user) yield ["", :password] end - if pass_file + if pass_file.present? File.open(pass_file, 'r:binary') do |pass_fd| pass_fd.each_line do |pass_from_file| pass_from_file.chomp! From 85c68463afb84413147687cfb4ecdef4682047be Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 24 Oct 2025 10:45:15 -0400 Subject: [PATCH 2/7] Use the new #starttls method --- lib/postgres/postgres-pr/connection.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/postgres/postgres-pr/connection.rb b/lib/postgres/postgres-pr/connection.rb index 49ca1a7b06cfd..8e230fe54d5b3 100644 --- a/lib/postgres/postgres-pr/connection.rb +++ b/lib/postgres/postgres-pr/connection.rb @@ -367,8 +367,7 @@ def establish_connection(uri, proxies, ssl = nil, ssl_opts = {}) @conn.write(ssl_request_message.dump) response = @conn.read(1) if response == 'S' - @conn.extend(Rex::Socket::SslTcp) - @conn.initsock_with_ssl_version(params, (params.ssl_version || Rex::Socket::Ssl::DEFAULT_SSL_VERSION)) + @conn.starttls(params) elsif response == 'N' # Server does not support SSL raise "SSL connection requested but server at #{u.host}:#{u.port} does not support SSL" From f3eeb598ac38d469f16d00a692fb3ac307051039 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 4 Nov 2025 17:51:21 -0500 Subject: [PATCH 3/7] Switch to the new fiber relay manager --- Gemfile | 3 +++ Gemfile.lock | 22 +++++++++++++++---- lib/rex/post/channel/socket_abstraction.rb | 6 ----- .../meterpreter/channels/pools/stream_pool.rb | 9 -------- .../channels/socket_abstraction.rb | 9 -------- .../socket_subsystem/tcp_client_channel.rb | 6 +++++ 6 files changed, 27 insertions(+), 28 deletions(-) diff --git a/Gemfile b/Gemfile index 1d7e16841b6a3..f169743f53bec 100644 --- a/Gemfile +++ b/Gemfile @@ -55,3 +55,6 @@ group :test do gem 'timecop' end +gem 'rex-core', git: 'https://github.com/zeroSteiner/rex-core', branch: 'feat/io/relay-manager' +gem 'rex-socket', git: 'https://github.com/zeroSteiner/rex-socket', branch: 'feat/tcp/starttls' + diff --git a/Gemfile.lock b/Gemfile.lock index 5534e1b77f483..535a271fb351e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,19 @@ +GIT + remote: https://github.com/zeroSteiner/rex-core + revision: 65d936d0d01f4f7fd41cbc676fc0af12aed536d6 + branch: feat/io/relay-manager + specs: + rex-core (0.1.35) + +GIT + remote: https://github.com/zeroSteiner/rex-socket + revision: 04b35d4085c7f8a621d41e50a5b1b6509b4f533e + branch: feat/tcp/starttls + specs: + rex-socket (0.1.64) + dnsruby + rex-core + PATH remote: . specs: @@ -484,7 +500,6 @@ GEM rex-core rex-struct2 rex-text - rex-core (0.1.34) rex-encoder (0.1.8) metasm rex-arch @@ -518,9 +533,6 @@ GEM metasm rex-core rex-text - rex-socket (0.1.62) - dnsruby - rex-core rex-sslscan (0.1.13) rex-core rex-socket @@ -679,6 +691,8 @@ DEPENDENCIES pry-byebug rake redcarpet + rex-core! + rex-socket! rspec-rails rspec-rerun rubocop (= 1.75.7) diff --git a/lib/rex/post/channel/socket_abstraction.rb b/lib/rex/post/channel/socket_abstraction.rb index d8067e744472a..3275717c4a79e 100644 --- a/lib/rex/post/channel/socket_abstraction.rb +++ b/lib/rex/post/channel/socket_abstraction.rb @@ -47,12 +47,6 @@ def getpeername end end - def close - super - channel.cleanup_abstraction - channel.close - end - attr_accessor :channel end end diff --git a/lib/rex/post/meterpreter/channels/pools/stream_pool.rb b/lib/rex/post/meterpreter/channels/pools/stream_pool.rb index 70f3db52ab181..fc04fdf268647 100644 --- a/lib/rex/post/meterpreter/channels/pools/stream_pool.rb +++ b/lib/rex/post/meterpreter/channels/pools/stream_pool.rb @@ -80,15 +80,6 @@ def dio_write_handler(packet, data) end end - # - # Closes the local half of the pool stream. - # - def dio_close_handler(packet) - rsock.close - - return super(packet) - end - # # Cleans up resources used by the channel. # diff --git a/lib/rex/post/meterpreter/channels/socket_abstraction.rb b/lib/rex/post/meterpreter/channels/socket_abstraction.rb index 22db0a1f73934..6a064d9fd5702 100644 --- a/lib/rex/post/meterpreter/channels/socket_abstraction.rb +++ b/lib/rex/post/meterpreter/channels/socket_abstraction.rb @@ -76,15 +76,6 @@ def dio_write_handler(packet, data) end end - # - # Performs a close operation on the right side of the local stream. - # - def dio_close_handler(packet) - rsock.close - - return super(packet) - end - # # Cleans up the stream abstraction. # diff --git a/lib/rex/post/meterpreter/extensions/stdapi/net/socket_subsystem/tcp_client_channel.rb b/lib/rex/post/meterpreter/extensions/stdapi/net/socket_subsystem/tcp_client_channel.rb index 38724721c15a8..ecd7f73c13e57 100644 --- a/lib/rex/post/meterpreter/extensions/stdapi/net/socket_subsystem/tcp_client_channel.rb +++ b/lib/rex/post/meterpreter/extensions/stdapi/net/socket_subsystem/tcp_client_channel.rb @@ -89,6 +89,12 @@ def initialize(client, cid, type, flags, packet, sock_params: nil) rsock.synchronize_access { rsock.initsock(@params) } end + def monitor_rsock(name = 'MonitorRemote') + # call #close on exit instead of #close_write because this is triggered in response to lsock.close + # lsock.close -> rsock EOF -> #close -> Meterpreter close + monitor_sock(rsock, sink: self, name: name, on_exit: method(:close)) + end + # # Closes the write half of the connection. # From 5c95aa6ff3db94b88b14754ab5c0305c3887244b Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 4 Nov 2025 17:51:47 -0500 Subject: [PATCH 4/7] Add the new encrypted MsTds channel --- lib/rex/proto/ms_tds/channel.rb | 62 +++++++++++++++++++++++++++++++++ lib/rex/proto/mssql/client.rb | 51 ++++++++++++++++++++------- 2 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 lib/rex/proto/ms_tds/channel.rb diff --git a/lib/rex/proto/ms_tds/channel.rb b/lib/rex/proto/ms_tds/channel.rb new file mode 100644 index 0000000000000..2a906c630854e --- /dev/null +++ b/lib/rex/proto/ms_tds/channel.rb @@ -0,0 +1,62 @@ +# -*- coding: binary -*- + +require 'bindata' + +module Rex::Proto::MsTds + class Channel + include Rex::IO::StreamAbstraction + + attr_reader :params + + # the socket that makes the outbound connection to the SQL server + attr_reader :sock + + def initialize(opts = {}) + @params = Rex::Socket::Parameters.from_hash(opts) + + # it doesn't work this way so throw an exception so that's clear + raise RuntimeError.new('SSL incompatible with MsTds::Socket') if @params.ssl + raise ArgumentError.new('MsTds::Socket must be TCP') if @params.proto != 'tcp' + + @sock = Rex::Socket.create_param(@params) + initialize_abstraction + + lsock.initinfo(@sock.peerinfo, @sock.localinfo) + + monitor_sock(@sock, sink: method(:_read_handler), name: 'MonitorLocal', on_exit: method(:_exit_handler)) + end + + def write(buf, opts = {}) + if negotiating_ssl? + Rex::IO::RelayManager.io_write_all(self.sock, [18, 0x01, buf.length + 8, 0x0000, 0x00, 0x00].pack('CCnnCC') + buf) - 8 + else + Rex::IO::RelayManager.io_write_all(self.sock, buf) + end + end + + def starttls + self.lsock.starttls(params) + end + + protected + + def negotiating_ssl? + return false unless self.lsock.is_a?(Rex::Socket::SslTcp) + return false if self.lsock.sslsock.state.start_with?('SSLOK') + + true + end + + def _exit_handler + self.rsock.close + end + + def _read_handler(buf) + if negotiating_ssl? + Rex::IO::RelayManager.io_write_all(self.rsock, buf[8..]) + 8 + else + Rex::IO::RelayManager.io_write_all(self.rsock, buf) + end + end + end +end diff --git a/lib/rex/proto/mssql/client.rb b/lib/rex/proto/mssql/client.rb index 42e26479890a1..796535fe6f3f7 100644 --- a/lib/rex/proto/mssql/client.rb +++ b/lib/rex/proto/mssql/client.rb @@ -75,6 +75,39 @@ def initialize(framework_module, framework, rhost, rport = 1433, proxies = nil, @current_database = '' end + def connect(global = true, opts={}) + dossl = false + if(opts.has_key?('SSL')) + dossl = opts['SSL'] + else + dossl = ssl + end + + @mstds_channel = Rex::Proto::MsTds::Channel.new( + 'PeerHost' => opts['RHOST'] || rhost, + 'PeerHostname' => opts['SSLServerNameIndication'] || opts['RHOSTNAME'], + 'PeerPort' => (opts['RPORT'] || rport).to_i, + 'LocalHost' => opts['CHOST'] || chost || "0.0.0.0", + 'LocalPort' => (opts['CPORT'] || cport || 0).to_i, + 'SSL' => dossl, + 'SSLVersion' => opts['SSLVersion'] || ssl_version, + 'SSLVerifyMode' => opts['SSLVerifyMode'] || ssl_verify_mode, + 'SSLKeyLogFile' => opts['SSLKeyLogFile'] || sslkeylogfile, + 'SSLCipher' => opts['SSLCipher'] || ssl_cipher, + 'Proxies' => proxies, + 'Timeout' => (opts['ConnectTimeout'] || connection_timeout || 10).to_i, + 'Context' => { 'Msf' => framework, 'MsfExploit' => framework_module } + ) + nsock = @mstds_channel.lsock + # enable evasions on this socket + set_tcp_evasions(nsock) + + # Set this socket to the global socket as necessary + self.sock = nsock if (global) + + return nsock + end + # MS SQL Server only supports Windows and Linux def map_compile_os_to_platform(server_info) return '' if server_info.blank? @@ -340,12 +373,12 @@ def mssql_login(user='sa', pass='', db='', domain_name='') # upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header # is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification if tdsencryption == true - proxy = TDSSSLProxy.new(sock, sslkeylogfile: sslkeylogfile) - proxy.setup_ssl - resp = proxy.send_recv(pkt) - else - resp = mssql_send_recv(pkt, 15, false) + #proxy = TDSSSLProxy.new(sock, sslkeylogfile: sslkeylogfile) + #proxy.setup_ssl + #resp = proxy.send_recv(pkt) + @mstds_channel.starttls end + resp = mssql_send_recv(pkt, 15, false) # Strip the TDS header resp = resp[3..-1] @@ -369,13 +402,7 @@ def mssql_login(user='sa', pass='', db='', domain_name='') pkt = pkt_hdr.pack("CCnnCC") + type3_blob - if self.tdsencryption == true - resp = mssql_ssl_send_recv(pkt, proxy) - proxy.cleanup - proxy = nil - else - resp = mssql_send_recv(pkt) - end + resp = mssql_send_recv(pkt) #SQL Server Authentication else From 9a605e54dce675529cf1d939507a800c03b571f0 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 4 Nov 2025 15:52:27 -0500 Subject: [PATCH 5/7] Initialize the info for web sockets --- lib/msf/core/handler/bind_aws_ssm.rb | 3 ++- lib/rex/proto/http/web_socket.rb | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/msf/core/handler/bind_aws_ssm.rb b/lib/msf/core/handler/bind_aws_ssm.rb index 668cb4ba1b3ea..6e19a9b1c821c 100644 --- a/lib/msf/core/handler/bind_aws_ssm.rb +++ b/lib/msf/core/handler/bind_aws_ssm.rb @@ -271,9 +271,10 @@ def start_handler # Configure Channel chan._start_ssm_keepalive if datastore['SSM_KEEP_ALIVE'] chan.params.comm = Rex::Socket::Comm::Local unless chan.params.comm + chan.params.peerhostname = peer_info['ComputerName'] chan.params.peerhost = peer_info['IpAddress'] chan.params.peerport = 0 - chan.params.peerhostname = peer_info['ComputerName'] + chan.lsock.initinfo(Rex::Socket.to_authority(peer_info['IpAddress'], 0), chan.lsock.localinfo) chan.update_term_size rescue => e print_error("AWS SSM handler failed: #{e.message}") diff --git a/lib/rex/proto/http/web_socket.rb b/lib/rex/proto/http/web_socket.rb index 3e8ed1397bbce..389d6451fa81d 100644 --- a/lib/rex/proto/http/web_socket.rb +++ b/lib/rex/proto/http/web_socket.rb @@ -44,8 +44,6 @@ def type? # binary and text) # @param [Symbol] write_type the data type to write to the WebSocket def initialize(websocket, read_type: nil, write_type: :binary) - initialize_abstraction - # a read type of nil will handle both binary and text frames that are received raise ArgumentError, 'read_type must be nil, :binary or :text' unless [nil, :binary, :text].include?(read_type) raise ArgumentError, 'write_type must be :binary or :text' unless %i[binary text].include?(write_type) @@ -66,6 +64,10 @@ def initialize(websocket, read_type: nil, write_type: :binary) 'SSL' => websocket.respond_to?(:sslctx) && !websocket.sslctx.nil? }) + initialize_abstraction + + lsock.initinfo(Rex::Socket.to_authority(peerhost, peerport), Rex::Socket.to_authority(localhost, localport)) + @thread = Rex::ThreadFactory.spawn("WebSocketChannel(#{localhost}->#{peerhost})", false) do websocket.wsloop do |data, data_type| next unless @read_type.nil? || data_type == @read_type From 9d00c0874bf281adbfd7e18bb75a425556a838fc Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 5 Nov 2025 14:18:20 -0500 Subject: [PATCH 6/7] Remove the old MS-TDS SSL Proxy code --- lib/metasploit/framework/mssql/tdssslproxy.rb | 145 ------------------ lib/rex/proto/mssql/client.rb | 23 +-- 2 files changed, 3 insertions(+), 165 deletions(-) delete mode 100644 lib/metasploit/framework/mssql/tdssslproxy.rb diff --git a/lib/metasploit/framework/mssql/tdssslproxy.rb b/lib/metasploit/framework/mssql/tdssslproxy.rb deleted file mode 100644 index 87628ec85f401..0000000000000 --- a/lib/metasploit/framework/mssql/tdssslproxy.rb +++ /dev/null @@ -1,145 +0,0 @@ -# -*- coding: binary -*- - -require 'openssl' - -# -# TDSSSLProxy: -# -# SQL Server uses the TDS protocol to transmit data between clients and -# servers. Of course this sits on top of TCP. -# -# By default, the TDS payload is not encrypted. However, if "force -# encryption" is set under the SQL Server protocol properties, it will -# use SSL/TLS to encrypt the TDS data. Oddly, the entire TCP stream is -# not encrypted (as is say for HTTPS), but instead a TDS header is -# put on the front of the TLS packet. As a result, the full TLS/SSL -# setup is done within a series of TDS payloads. -# -# This "proxy" basically creates a fake SSL endpoint (s2) from which it -# can add/remove the TDS header as required. This is implemented as a -# socket pair (think, a bidirectional pipe), where the other end is s1: -# -# sslsock <-> s1 <-> s2 <-> tdssock <-> target SQL Server. -# -# (tdssock is the reference to the "sock" from the scanner module) -# -# TO DO: -# -# This enables brute force of a SQL Server which requires encryption. -# However, future updates will permit any read/write using -# mssql_send_recv() to use crypto if required and transparently to -# other MSF developers. -# -# Cheers, JH - -class TDSSSLProxy - - TYPE_TDS7_LOGIN = 16 - TYPE_PRE_LOGIN_MESSAGE = 18 - STATUS_END_OF_MESSAGE = 0x01 - - def initialize(sock, sslkeylogfile: nil) - @tdssock = sock - @sslkeylogfile = sslkeylogfile - @s1, @s2 = Rex::Socket.tcp_socket_pair - end - - def cleanup - @running = false - @t1.join - end - - def write_to_keylog_file(ctx, sslkeylogfile) - # writing to the sslkeylogfile is required, it adds support for network capture decryption which is useful to - # decrypt TLS traffic in wireshark - if sslkeylogfile - unless ctx.respond_to?(:keylog_cb) - raise 'Unable to create sslkeylogfile - Ruby 3.2 or above required for this functionality' - end - - ctx.keylog_cb = proc do |_sock, line| - File.open(sslkeylogfile, 'ab') do |file| - file.write("#{line}\n") - end - end - end - end - - def setup_ssl - @running = true - @t1 = Thread.start { ssl_setup_thread } - ctx = OpenSSL::SSL::SSLContext.new(:SSLv23) - write_to_keylog_file(ctx, @sslkeylogfile) - ctx.ciphers = "ALL:!ADH:!EXPORT:!SSLv2:!SSLv3:+HIGH:+MEDIUM" - @ssl_socket = OpenSSL::SSL::SSLSocket.new(@s1, ctx) - @ssl_socket.connect - end - - def send_recv(pkt) - @ssl_socket.write(pkt) - done = false - resp = "" - - while (not done) - head = @ssl_socket.read(8) - if !(head and head.length == 8) - return false - end - - # Is this the last buffer? - if (head[1, 1] == "\x01" or not check_status) - done = true - end - - # Grab this block's length - rlen = head[2, 2].unpack('n')[0] - 8 - - while (rlen > 0) - buff = @ssl_socket.read(rlen) - return if not buff - resp << buff - rlen -= buff.length - end - - end - resp - end - - def ssl_setup_thread - while @running do - res = select([@tdssock, @s2], nil, nil, 0.1) - if res - res[0].each do |r| - # response from SQL Server for client - if r == @tdssock - resp = @tdssock.recv(4096) - if @ssl_socket.state[0, 5] == "SSLOK" - @s2.send(resp, 0) - else - @s2.send(resp[8..-1], 0) - end - end - - # request from client for SQL Server - if r == @s2 - resp = @s2.recv(4096) - # SSL negotiation completed - just send it on - if @ssl_socket.state[0, 5] == "SSLOK" - @tdssock.send(resp, 0) - # Still doing SSL - else - tds_pkt_len = 8 + resp.length - pkt_hdr = '' - pkt_hdr << [TYPE_PRE_LOGIN_MESSAGE, STATUS_END_OF_MESSAGE, tds_pkt_len, 0x0000, 0x00, 0x00].pack('CCnnCC') - pkt = pkt_hdr << resp - @tdssock.send(pkt, 0) - end - end - end - end - end - @s1.close - @s2.close - end -end - diff --git a/lib/rex/proto/mssql/client.rb b/lib/rex/proto/mssql/client.rb index 796535fe6f3f7..af7869ac9fa91 100644 --- a/lib/rex/proto/mssql/client.rb +++ b/lib/rex/proto/mssql/client.rb @@ -1,5 +1,4 @@ require 'metasploit/framework/tcp/client' -require 'metasploit/framework/mssql/tdssslproxy' require 'rex/proto/mssql/client_mixin' require 'rex/text' require 'msf/core/exploit' @@ -372,12 +371,7 @@ def mssql_login(user='sa', pass='', db='', domain_name='') # has a strange behavior that differs from the specifications # upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header # is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification - if tdsencryption == true - #proxy = TDSSSLProxy.new(sock, sslkeylogfile: sslkeylogfile) - #proxy.setup_ssl - #resp = proxy.send_recv(pkt) - @mstds_channel.starttls - end + @mstds_channel.starttls if tdsencryption resp = mssql_send_recv(pkt, 15, false) # Strip the TDS header @@ -484,15 +478,8 @@ def mssql_login(user='sa', pass='', db='', domain_name='') # Packet header and total length including header pkt = "\x10\x01" + [pkt.length + 8].pack('n') + [0].pack('n') + [1].pack('C') + "\x00" + pkt - if self.tdsencryption == true - proxy = TDSSSLProxy.new(sock, sslkeylogfile: sslkeylogfile) - proxy.setup_ssl - resp = mssql_ssl_send_recv(pkt, proxy) - proxy.cleanup - proxy = nil - else - resp = mssql_send_recv(pkt) - end + @mstds_channel.starttls if tdsencryption + resp = mssql_send_recv(pkt) end @@ -564,10 +551,6 @@ def mssql_prelogin(enc_error=false) data end - def mssql_ssl_send_recv(req, tdsproxy, timeout=15, check_status=true) - tdsproxy.send_recv(req) - end - def query(sqla, doprint=false, opts={}) info = { :sql => sqla } opts[:timeout] ||= 15 From 750e664a69c098ae3cd6e6825a526f42ce4f0ae0 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 5 Nov 2025 14:50:25 -0500 Subject: [PATCH 7/7] Fix the PostreSQL SSL tests for #starttls method --- spec/lib/rex/proto/postgresql/client_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/lib/rex/proto/postgresql/client_spec.rb b/spec/lib/rex/proto/postgresql/client_spec.rb index 018d85b1f898f..493eaec20f49f 100644 --- a/spec/lib/rex/proto/postgresql/client_spec.rb +++ b/spec/lib/rex/proto/postgresql/client_spec.rb @@ -41,8 +41,7 @@ allow(socket).to receive(:read).with(1).and_return('S') expect(socket).to receive(:write).with('ssl_request_data') - expect(socket).to receive(:extend).with(Rex::Socket::SslTcp) - expect(socket).to receive(:initsock_with_ssl_version) + expect(socket).to receive(:starttls).with(Rex::Socket::Parameters) client = described_class.new(db_name, 'username', 'password', "tcp://#{host}:#{port}", nil, true, ssl_opts) expect(client).to be_a(Msf::Db::PostgresPR::Connection)