Files
metasploit-gs/lib/rex/proto/http/web_socket.rb
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

446 lines
14 KiB
Ruby
Raw Normal View History

2020-07-31 10:08:26 -04:00
# -*- coding: binary -*-
2020-07-31 10:08:26 -04:00
require 'bindata'
require 'rex/post/channel'
2020-07-31 10:08:26 -04:00
2021-11-05 10:41:07 -04:00
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'
2021-09-24 14:18:55 -04:00
end
2021-11-05 10:41:07 -04:00
end
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
# 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
2023-01-21 08:29:28 -05:00
def initialize(websocket, read_type: nil, write_type: :binary)
2021-11-05 10:41:07 -04:00
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)
2021-11-05 10:41:07 -04:00
@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
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
close
end
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
lsock.extend(SocketInterface)
lsock.channel = self
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
rsock.extend(SocketInterface)
rsock.channel = self
end
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
def closed?
@websocket.nil?
end
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
def close
@mutex.synchronize do
return if closed?
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
@websocket.wsclose
@websocket = nil
end
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
cleanup_abstraction
end
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
#
# 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
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
@websocket.put_wsframe(Frame.new(header: { opcode: Opcode::CONNECTION_CLOSE }))
end
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
#
# 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
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
if !length.nil? && buf.length >= length
buf = buf[0..length]
end
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
length = buf.length
buf = on_data_write(buf)
if @write_type == :binary
2023-01-21 08:29:28 -05:00
@websocket.put_wsbinary(buf)
2021-11-05 10:41:07 -04:00
elsif @write_type == :text
2023-01-21 08:29:28 -05:00
@websocket.put_wstext(buf)
2021-11-05 10:41:07 -04:00
end
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
length
end
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
#
# 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
2020-07-31 10:08:26 -04:00
2021-11-05 10:41:07 -04:00
#
# 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
2020-07-31 10:08:26 -04:00
2021-11-05 10:41:07 -04:00
#
# 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
2020-07-31 10:08:26 -04:00
2021-11-05 10:41:07 -04:00
#
# 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
2021-11-05 10:41:07 -04:00
#
# 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
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
#
# Read a WebSocket::Frame from the peer.
#
2023-02-01 16:11:56 -05:00
# @return [Nil, WebSocket::Frame] the frame that was received from the peer.
2021-11-05 10:41:07 -04:00
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
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
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
2021-09-24 14:18:55 -04:00
2021-11-05 10:41:07 -04:00
#
# 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))
2023-02-01 16:11:56 -05:00
break if frame.nil?
break if frame.header.opcode == Opcode::CONNECTION_CLOSE
2021-11-05 10:41:07 -04:00
# 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
2021-11-05 10:41:07 -04:00
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
2021-09-30 16:53:24 -04:00
begin
2021-11-05 10:41:07 -04:00
block.call(buffer, buffer_type)
2021-09-30 16:53:24 -04:00
ensure
2021-11-05 10:41:07 -04:00
@wsstream_lock.lock_read
2021-09-30 16:53:24 -04:00
end
end
2021-11-05 10:41:07 -04:00
buffer = ''
buffer_type = nil
2021-09-30 16:53:24 -04:00
end
2021-11-05 10:41:07 -04:00
end
2020-07-31 10:08:26 -04:00
2021-11-05 10:41:07 -04:00
close
end
2021-11-05 10:41:07 -04:00
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
2021-11-05 10:41:07 -04:00
class Opcode < BinData::Bit4
CONTINUATION = 0
TEXT = 1
BINARY = 2
CONNECTION_CLOSE = 8
PING = 9
PONG = 10
2020-07-31 10:08:26 -04:00
2021-11-05 10:41:07 -04:00
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
2021-11-05 10:41:07 -04:00
frame
end
end
2021-11-05 10:41:07 -04:00
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
2021-11-05 10:41:07 -04:00
xored
end
2021-11-05 10:41:07 -04:00
def self.from_binary(value, last: true, mask: true)
from_opcode(Opcode::BINARY, value, last: last, mask: mask)
end
2021-11-05 10:41:07 -04:00
def self.from_text(value, last: true, mask: true)
from_opcode(Opcode::TEXT, value, last: last, mask: mask)
end
2021-11-05 10:41:07 -04:00
#
# 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
2020-07-31 10:08:26 -04:00
2021-11-05 10:41:07 -04:00
#
# 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
2020-07-31 10:08:26 -04:00
2021-11-05 10:41:07 -04:00
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
2020-07-31 10:08:26 -04:00
2021-11-05 10:41:07 -04:00
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
2020-07-31 10:08:26 -04:00
end
end
end