120dc877ad
* Prevent using post modules with the session It doesn't work reliably because of winpty and how the output is mangled. * Set the limit correctly * Fix Linux PTY downgrade issues * Remove filtering The filtering implementation is incomplete and unnecessary. Filtering is unnecessary because Linux sessions execute a stub on session start up that uses a combiantion of stty and a fifo to emulate a PTY-less session. Windows sessions do not need filtering because they have been explictly marked as being incompatible with the Post API which is confused by the extra characters. The filtering implementation is incomplete because it does not account for echo fragments that are split across lines. It also does not account for all of the ANSI escape codes. * Add module docs for enum_ssm
446 lines
14 KiB
Ruby
446 lines
14 KiB
Ruby
# -*- coding: binary -*-
|
|
|
|
require 'bindata'
|
|
require 'rex/post/channel'
|
|
|
|
module Rex::Proto::Http::WebSocket
|
|
class WebSocketError < StandardError
|
|
end
|
|
|
|
class ConnectionError < WebSocketError
|
|
def initialize(msg: 'The WebSocket connection failed', http_response: nil)
|
|
@message = msg
|
|
@http_response = http_response
|
|
end
|
|
|
|
attr_accessor :message, :http_response
|
|
alias to_s message
|
|
end
|
|
|
|
# This defines the interface that the standard socket is extended with to provide WebSocket functionality. It should be
|
|
# used on a socket when the server has already successfully handled a WebSocket upgrade request.
|
|
module Interface
|
|
#
|
|
# A channel object that allows reading and writing either text or binary data directly to the remote peer.
|
|
#
|
|
class Channel
|
|
include Rex::Post::Channel::StreamAbstraction
|
|
|
|
module SocketInterface
|
|
include Rex::Post::Channel::SocketAbstraction::SocketInterface
|
|
|
|
def type?
|
|
'tcp'
|
|
end
|
|
end
|
|
|
|
# The socket parameters describing the underlying connection.
|
|
# @!attribute [r] params
|
|
# @return [Rex::Socket::Parameters]
|
|
attr_reader :params
|
|
|
|
# @param [WebSocket::Interface] websocket the WebSocket that this channel is being opened on
|
|
# @param [nil, Symbol] read_type the data type(s) to read from the WebSocket, one of :binary, :text or nil (for both
|
|
# 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)
|
|
|
|
@websocket = websocket
|
|
@read_type = read_type
|
|
@write_type = write_type
|
|
@mutex = Mutex.new
|
|
|
|
# beware of: https://github.com/rapid7/rex-socket/issues/32
|
|
_, localhost, localport = websocket.getlocalname
|
|
_, peerhost, peerport = Rex::Socket.from_sockaddr(websocket.getpeername)
|
|
@params = Rex::Socket::Parameters.from_hash({
|
|
'LocalHost' => localhost,
|
|
'LocalPort' => localport,
|
|
'PeerHost' => peerhost,
|
|
'PeerPort' => peerport,
|
|
'SSL' => websocket.respond_to?(:sslctx) && !websocket.sslctx.nil?
|
|
})
|
|
|
|
@thread = Rex::ThreadFactory.spawn("WebSocketChannel(#{localhost}->#{peerhost})", false) do
|
|
websocket.wsloop do |data, data_type|
|
|
next unless @read_type.nil? || data_type == @read_type
|
|
|
|
data = on_data_read(data, data_type)
|
|
next if data.nil?
|
|
|
|
rsock.syswrite(data)
|
|
end
|
|
|
|
close
|
|
end
|
|
|
|
lsock.extend(SocketInterface)
|
|
lsock.channel = self
|
|
|
|
rsock.extend(SocketInterface)
|
|
rsock.channel = self
|
|
end
|
|
|
|
def closed?
|
|
@websocket.nil?
|
|
end
|
|
|
|
def close
|
|
@mutex.synchronize do
|
|
return if closed?
|
|
|
|
@websocket.wsclose
|
|
@websocket = nil
|
|
end
|
|
|
|
cleanup_abstraction
|
|
end
|
|
|
|
#
|
|
# Close the channel for write operations. This sends a CONNECTION_CLOSE request, after which (per RFC 6455 section
|
|
# 5.5.1) this side must not send any more data frames.
|
|
#
|
|
def close_write
|
|
if closed?
|
|
raise IOError, 'Channel has been closed.', caller
|
|
end
|
|
|
|
@websocket.put_wsframe(Frame.new(header: { opcode: Opcode::CONNECTION_CLOSE }))
|
|
end
|
|
|
|
#
|
|
# Write *buf* to the channel, optionally truncating it to *length* bytes.
|
|
#
|
|
# @param [String] buf The data to write to the channel.
|
|
# @param [Integer] length An optional length to truncate *data* to before
|
|
# sending it.
|
|
def write(buf, length = nil)
|
|
if closed?
|
|
raise IOError, 'Channel has been closed.', caller
|
|
end
|
|
|
|
if !length.nil? && buf.length >= length
|
|
buf = buf[0..length]
|
|
end
|
|
|
|
length = buf.length
|
|
buf = on_data_write(buf)
|
|
if @write_type == :binary
|
|
@websocket.put_wsbinary(buf)
|
|
elsif @write_type == :text
|
|
@websocket.put_wstext(buf)
|
|
end
|
|
|
|
length
|
|
end
|
|
|
|
#
|
|
# This provides a hook point that is called when data is read from the WebSocket peer. Subclasses can intercept and
|
|
# process the data. The default functionality does nothing.
|
|
#
|
|
# @param [String] data the data that was read
|
|
# @param [Symbol] data_type the type of data that was received, either :binary or :text
|
|
# @return [String, nil] if a string is returned, it's passed through the channel
|
|
def on_data_read(data, _data_type)
|
|
data
|
|
end
|
|
|
|
#
|
|
# This provides a hook point that is called when data is written to the WebSocket peer. Subclasses can intercept and
|
|
# process the data. The default functionality does nothing.
|
|
#
|
|
# @param [String] data the data that is being written
|
|
# @return [String, nil] if a string is returned, it's passed through the channel
|
|
def on_data_write(data)
|
|
data
|
|
end
|
|
end
|
|
|
|
#
|
|
# Send a WebSocket::Frame to the peer.
|
|
#
|
|
# @param [WebSocket::Frame] frame the frame to send to the peer.
|
|
def put_wsframe(frame, opts = {})
|
|
put(frame.to_binary_s, opts = opts)
|
|
end
|
|
|
|
#
|
|
# Build a WebSocket::Frame representing the binary data and send it to the peer.
|
|
#
|
|
# @param [String] value the binary value to use as the frame payload.
|
|
def put_wsbinary(value, opts = {})
|
|
put_wsframe(Frame.from_binary(value), opts = opts)
|
|
end
|
|
|
|
#
|
|
# Build a WebSocket::Frame representing the text data and send it to the peer.
|
|
#
|
|
# @param [String] value the binary value to use as the frame payload.
|
|
def put_wstext(value, opts = {})
|
|
put_wsframe(Frame.from_text(value), opts = opts)
|
|
end
|
|
|
|
#
|
|
# Read a WebSocket::Frame from the peer.
|
|
#
|
|
# @return [Nil, WebSocket::Frame] the frame that was received from the peer.
|
|
def get_wsframe(_opts = {})
|
|
frame = Frame.new
|
|
frame.header.read(self)
|
|
payload_data = ''
|
|
while payload_data.length < frame.payload_len
|
|
chunk = read(frame.payload_len - payload_data.length)
|
|
if chunk.empty? # no partial reads!
|
|
elog('WebSocket::Interface#get_wsframe: received an empty websocket payload data chunk')
|
|
return nil
|
|
end
|
|
|
|
payload_data << chunk
|
|
end
|
|
frame.payload_data.assign(payload_data)
|
|
frame
|
|
rescue ::IOError
|
|
wlog('WebSocket::Interface#get_wsframe: encountered an IOError while reading a websocket frame')
|
|
nil
|
|
end
|
|
|
|
#
|
|
# Build a channel to allow reading and writing from the WebSocket. This provides high level functionality so the
|
|
# caller needn't worry about individual frames.
|
|
#
|
|
# @return [WebSocket::Interface::Channel]
|
|
def to_wschannel(**kwargs)
|
|
Channel.new(self, **kwargs)
|
|
end
|
|
|
|
#
|
|
# Close the WebSocket. If the underlying TCP socket is still active a WebSocket CONNECTION_CLOSE request will be sent
|
|
# and then it will wait for a CONNECTION_CLOSE response. Once completed the underlying TCP socket will be closed.
|
|
#
|
|
def wsclose(opts = {})
|
|
return if closed? # there's nothing to do if the underlying TCP socket has already been closed
|
|
|
|
# this implementation doesn't handle the optional close reasons at all
|
|
frame = Frame.new(header: { opcode: Opcode::CONNECTION_CLOSE })
|
|
# close frames must be masked
|
|
# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1
|
|
frame.mask!
|
|
put_wsframe(frame, opts = opts)
|
|
while (frame = get_wsframe(opts))
|
|
break if frame.nil?
|
|
break if frame.header.opcode == Opcode::CONNECTION_CLOSE
|
|
# all other frames are dropped after our connection close request is sent
|
|
end
|
|
|
|
close # close the underlying TCP socket
|
|
end
|
|
|
|
#
|
|
# Run a loop to handle data from the remote end of the websocket. The loop will automatically handle fragmentation
|
|
# unmasking payload data and ping requests. When the remote connection is closed, the loop will exit. If specified the
|
|
# block will be passed data chunks and their data types.
|
|
#
|
|
def wsloop(opts = {}, &block)
|
|
buffer = ''
|
|
buffer_type = nil
|
|
|
|
# since web sockets have their own tear down exchange, use a synchronization lock to ensure we aren't closed until
|
|
# either the remote socket is closed or the teardown takes place
|
|
@wsstream_lock = Rex::ReadWriteLock.new
|
|
@wsstream_lock.synchronize_read do
|
|
while (frame = get_wsframe(opts))
|
|
frame.unmask! if frame.header.masked == 1
|
|
|
|
case frame.header.opcode
|
|
when Opcode::CONNECTION_CLOSE
|
|
put_wsframe(Frame.new(header: { opcode: Opcode::CONNECTION_CLOSE }).tap { |f| f.mask! }, opts = opts)
|
|
break
|
|
when Opcode::CONTINUATION
|
|
# a continuation frame can only be sent for a data frames
|
|
# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.4
|
|
raise WebSocketError, 'Received an unexpected continuation frame (uninitialized buffer)' if buffer_type.nil?
|
|
|
|
buffer << frame.payload_data
|
|
when Opcode::BINARY
|
|
raise WebSocketError, 'Received an unexpected binary frame (incomplete buffer)' unless buffer_type.nil?
|
|
|
|
buffer = frame.payload_data
|
|
buffer_type = :binary
|
|
when Opcode::TEXT
|
|
raise WebSocketError, 'Received an unexpected text frame (incomplete buffer)' unless buffer_type.nil?
|
|
|
|
buffer = frame.payload_data
|
|
buffer_type = :text
|
|
when Opcode::PING
|
|
# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2
|
|
put_wsframe(frame.dup.tap { |f| f.header.opcode = Opcode::PONG }, opts = opts)
|
|
end
|
|
|
|
next unless frame.header.fin == 1
|
|
|
|
if block_given?
|
|
# text data is UTF-8 encoded
|
|
# see: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
|
|
buffer.force_encoding('UTF-8') if buffer_type == :text
|
|
# release the stream lock before entering the callback, allowing it to close the socket if desired
|
|
@wsstream_lock.unlock_read
|
|
begin
|
|
block.call(buffer, buffer_type)
|
|
ensure
|
|
@wsstream_lock.lock_read
|
|
end
|
|
end
|
|
|
|
buffer = ''
|
|
buffer_type = nil
|
|
end
|
|
end
|
|
|
|
close
|
|
end
|
|
|
|
def close
|
|
# if #wsloop was ever called, a synchronization lock will have been initialized
|
|
@wsstream_lock.lock_write unless @wsstream_lock.nil?
|
|
begin
|
|
super
|
|
ensure
|
|
@wsstream_lock.unlock_write unless @wsstream_lock.nil?
|
|
end
|
|
end
|
|
end
|
|
|
|
class Opcode < BinData::Bit4
|
|
CONTINUATION = 0
|
|
TEXT = 1
|
|
BINARY = 2
|
|
CONNECTION_CLOSE = 8
|
|
PING = 9
|
|
PONG = 10
|
|
|
|
default_parameter assert: -> { !Opcode.name(value).nil? }
|
|
|
|
def self.name(value)
|
|
constants.select { |c| c.upcase == c }.find { |c| const_get(c) == value }
|
|
end
|
|
|
|
def to_sym
|
|
self.class.name(value)
|
|
end
|
|
end
|
|
|
|
class Frame < BinData::Record
|
|
endian :big
|
|
|
|
struct :header do
|
|
endian :big
|
|
hide :rsv1, :rsv2, :rsv3
|
|
|
|
bit1 :fin, initial_value: 1
|
|
bit1 :rsv1
|
|
bit1 :rsv2
|
|
bit1 :rsv3
|
|
opcode :opcode
|
|
bit1 :masked
|
|
bit7 :payload_len_sm
|
|
uint16 :payload_len_md, onlyif: -> { payload_len_sm == 126 }
|
|
uint64 :payload_len_lg, onlyif: -> { payload_len_sm == 127 }
|
|
uint32 :masking_key, onlyif: -> { masked == 1 }
|
|
end
|
|
string :payload_data, read_length: -> { payload_len }
|
|
|
|
class << self
|
|
private
|
|
|
|
def from_opcode(opcode, payload, last: true, mask: true)
|
|
frame = Frame.new(header: { fin: (last ? 1 : 0), opcode: opcode })
|
|
frame.payload_len = payload.length
|
|
frame.payload_data = payload
|
|
|
|
case mask
|
|
when TrueClass
|
|
frame.mask!
|
|
when Integer
|
|
frame.mask!(mask)
|
|
when FalseClass
|
|
else
|
|
raise ArgumentError, 'mask must be true, false or an integer (literal masking key)'
|
|
end
|
|
|
|
frame
|
|
end
|
|
end
|
|
|
|
def self.apply_masking_key(data, mask)
|
|
mask = [mask].pack('N').each_byte.to_a
|
|
xored = ''
|
|
data.each_byte.each_with_index do |byte, index|
|
|
xored << (byte ^ mask[index % 4]).chr
|
|
end
|
|
|
|
xored
|
|
end
|
|
|
|
def self.from_binary(value, last: true, mask: true)
|
|
from_opcode(Opcode::BINARY, value, last: last, mask: mask)
|
|
end
|
|
|
|
def self.from_text(value, last: true, mask: true)
|
|
from_opcode(Opcode::TEXT, value, last: last, mask: mask)
|
|
end
|
|
|
|
#
|
|
# Update the frame instance in place to apply a masking key to the payload data as defined in RFC 6455 section 5.3.
|
|
#
|
|
# @param [nil, Integer] key either an explicit 32-bit masking key or nil to generate a random one
|
|
# @return [String] the masked payload data is returned
|
|
def mask!(key = nil)
|
|
header.masked.assign(1)
|
|
key = rand(1..0xffffffff) if key.nil?
|
|
header.masking_key.assign(key)
|
|
payload_data.assign(self.class.apply_masking_key(payload_data, header.masking_key))
|
|
payload_data.value
|
|
end
|
|
|
|
#
|
|
# Update the frame instance in place to apply a masking key to the payload data as defined in RFC 6455 section 5.3.
|
|
#
|
|
# @return [String] the unmasked payload data is returned
|
|
def unmask!
|
|
payload_data.assign(self.class.apply_masking_key(payload_data, header.masking_key))
|
|
header.masked.assign(0)
|
|
payload_data.value
|
|
end
|
|
|
|
def payload_len
|
|
case header.payload_len_sm
|
|
when 127
|
|
header.payload_len_lg
|
|
when 126
|
|
header.payload_len_md
|
|
else
|
|
header.payload_len_sm
|
|
end
|
|
end
|
|
|
|
def payload_len=(value)
|
|
if value < 126
|
|
header.payload_len_sm.assign(value)
|
|
elsif value < 0xffff
|
|
header.payload_len_sm.assign(126)
|
|
header.payload_len_md.assign(value)
|
|
elsif value < 0x7fffffffffffffff
|
|
header.payload_len_sm.assign(127)
|
|
header.payload_len_lg.assign(value)
|
|
else
|
|
raise ArgumentError, 'payload length is outside the acceptable range'
|
|
end
|
|
end
|
|
end
|
|
end
|