Files
metasploit-gs/lib/rex/proto/http/web_socket/amazon_ssm.rb
T
Spencer McIntyre 120dc877ad Pr/collab/17430 (#41)
* 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
2023-05-22 17:11:16 -04:00

383 lines
12 KiB
Ruby

# -*- coding: binary -*-
require 'bindata'
module Rex::Proto::Http::WebSocket::AmazonSsm
module PayloadType
Output = 1
Error = 2
Size = 3
Parameter = 4
HandshakeRequest = 5
HandshakeResponse = 6
HandshakeComplete = 7
EncChallengeRequest = 8
EncChallengeResponse = 9
Flag = 10
def self.from_val(v)
self.constants.find {|c| self.const_get(c) == v }
end
end
module UUID
def self.unpack(bbuf)
sbuf = ""
[8...12].each do |idx|
sbuf << Rex::Text.to_hex(bbuf[idx])
end
sbuf << '-'
[12...14].each do |idx|
sbuf << Rex::Text.to_hex(bbuf[idx])
end
sbuf << '-'
[14...16].each do |idx|
sbuf << Rex::Text.to_hex(bbuf[idx])
end
sbuf << '-'
[0...2].each do |idx|
sbuf << Rex::Text.to_hex(bbuf[idx])
end
sbuf << '-'
[2...8].each do |idx|
sbuf << Rex::Text.to_hex(bbuf[idx])
end
sbuf.gsub("\\x",'')
end
def self.pack(sbuf)
parts = sbuf.split('-').map do |seg|
seg.chars.each_slice(2).map {|e| "\\x#{e.join}"}.join
end
[3, 4, 0, 1, 2].map do |part|
Rex::Text.hex_to_raw(parts[part])
end.join
end
def self.rand
self.unpack(Rex::Text.rand_text(16))
end
end
module Interface
module SsmChannelMethods
attr_accessor :rows
attr_accessor :cols
def _start_ssm_keepalive
@keepalive_thread = Rex::ThreadFactory.spawn('SsmChannel-Keepalive', false) do
while not closed? or @websocket.closed?
write ''
Rex::ThreadSafe.sleep(::Random.rand * 10 + 15)
end
@keepalive_thread = nil
end
end
def close
@keepalive_thread.kill if @keepalive_thread
@keepalive_thread = nil
super
end
def acknowledge_output(output_frame)
ack = output_frame.to_ack
# ack.header.sequence_number = @out_seq_num
@websocket.put_wsbinary(ack.to_binary_s)
# wlog("SsmChannel: acknowledge output #{output_frame.uuid}")
output_frame.uuid
end
def pause_publication
msg = SsmFrame.create_pause_pub
@publication = false
@websocket.put_wsbinary(msg.to_binary_s)
end
def start_publication
msg = SsmFrame.create_start_pub
@publication = true
@websocket.put_wsbinary(msg.to_binary_s)
end
def handle_output_data(output_frame)
return nil if @ack_message == output_frame.uuid
@ack_message = acknowledge_output(output_frame)
# TODO: handle Payload::* types
if ![PayloadType::Output, PayloadType::Error].any? { |e| e == output_frame.payload_type }
wlog("SsmChannel got unhandled output payload type: #{Payload.from_val(output_frame.payload_type)}")
return nil
end
output_frame.payload_data.value
end
def handle_acknowledge(ack_frame)
# wlog("SsmChannel: got acknowledge message #{ack_frame.uuid}")
begin
seq_num = JSON.parse(ack_frame.payload_data)['AcknowledgedMessageSequenceNumber'].to_i
@ack_seq_num = seq_num if seq_num > @ack_seq_num
rescue => e
elog("SsmChannel failed to parse ack JSON #{ack_frame.payload_data} due to #{e}!")
end
nil
end
def update_term_size
return unless ::IO.console
rows, cols = ::IO.console.winsize
unless rows == self.rows && cols == self.cols
set_term_size(cols, rows)
self.rows = rows
self.cols = cols
end
end
def set_term_size(cols, rows)
data = JSON.generate({cols: cols, rows: rows})
frame = SsmFrame.create(data)
frame.payload_type = PayloadType::Size
@websocket.put_wsbinary(frame.to_binary_s)
end
end
class SsmChannel < Rex::Proto::Http::WebSocket::Interface::Channel
include SsmChannelMethods
attr_reader :run_ssm_pub, :out_seq_num, :ack_seq_num, :ack_message
def initialize(websocket)
@ack_seq_num = 0
@out_seq_num = 0
@run_ssm_pub = true
@ack_message = nil
@publication = false
super(websocket, write_type: :binary)
end
def on_data_read(data, _data_type)
return data if data.blank?
ssm_frame = SsmFrame.read(data)
case ssm_frame.header.message_type.strip
when 'output_stream_data'
@publication = true # Linux sends stream data before sending start_publication message
return handle_output_data(ssm_frame)
when 'acknowledge'
# update ACK seqno
handle_acknowledge(ssm_frame)
when 'start_publication'
@out_seq_num = @ack_seq_num if @out_seq_num > 0
@publication = true
# handle session resumption - foregrounding or resumption of input
when 'pause_publication'
# @websocket.put_wsbinary(ssm_frame.to_ack.to_binary_s)
@publication = false
# handle session suspension - backgrounding or general idle
when 'input_stream_data'
# this is supposed to be a one way street
emsg = "SsmChannel received input_stream_data from SSM (!!)"
elog(emsg)
raise emsg
when 'channel_closed'
elog("SsmChannel got closed message #{ssm_frame.uuid}")
close
else
raise Rex::Proto::Http::WebSocket::ConnectionError.new(
msg: "Unknown AWS SSM message type: #{ssm_frame.header.message_type}"
)
end
nil
end
def on_data_write(data)
start_publication if not @publication
frame = SsmFrame.create(data)
frame.header.sequence_number = @out_seq_num
@out_seq_num += 1
frame.to_binary_s
end
def publishing?
@publication
end
end
def to_ssm_channel(publish_timeout: 10)
chan = SsmChannel.new(self)
if publish_timeout
# Waiting for the channel to start publishing
(publish_timeout * 2).times do
break if chan.publishing?
sleep 0.5
end
raise Rex::TimeoutError.new('Timed out while waiting for the channel to start publishing.') unless chan.publishing?
end
chan
end
end
class SsmFrame < BinData::Record
endian :big
struct :header do
endian :big
uint32 :header_length, initial_value: 116
string :message_type, length: 32, pad_byte: 0x20, initial_value: 'input_stream_data'
uint32 :schema_version, initial_value: 1
uint64 :created_date, default_value: lambda { (Time.now.to_f * 1000).to_i }
uint64 :sequence_number, initial_value: 0
uint64 :flags, value: 0 #lambda { sequence_number == 0 ? 1 : 0 }
string :message_id, length: 16, initial_value: UUID.pack(UUID.rand)
end
string :payload_digest, length: 32, default_value: -> { Digest::SHA256.digest(payload_data) }
uint32 :payload_type, default_value: PayloadType::Output
uint32 :payload_length, value: -> { payload_data.length }
string :payload_data, read_length: -> { payload_length }
class << self
def create(data = nil, mtype = 'input_stream_data')
return data if data.is_a?(SsmFrame)
frame = SsmFrame.new(header: {
message_type: mtype,
created_date: (Time.now.to_f * 1000).to_i,
message_id: UUID.pack(UUID.rand)
})
if !data.nil?
frame.payload_data = data
frame.payload_digest = Digest::SHA256.digest(data)
frame.payload_length = data.length
frame.payload_type = PayloadType::Output
end
frame
end
def create_pause_pub
uuid = UUID.rand
time = Time.now
data = JSON.generate({
MessageType: 'pause_publication',
SchemaVersion: 1,
MessageId: uuid,
CreateData: time.strftime("%Y-%m-%dT%T.%LZ")
})
frame = SsmFrame.new( header: {
message_type: 'pause_publication',
created_date: (time.to_f * 1000).to_i,
message_id: UUID.pack(uuid)
})
frame.payload_data = data
frame.payload_digest = Digest::SHA256.digest(data)
frame.payload_length = data.length
frame.payload_type = 0
frame
end
def create_start_pub
data = 'start_publication'
frame = SsmFrame.new( header: {
message_type: data,
created_date: (Time.now.to_f * 1000).to_i,
message_id: UUID.pack(UUID.rand)
})
frame.payload_data = data
frame.payload_digest = Digest::SHA256.digest(data)
frame.payload_length = data.length
frame.payload_type = 0
frame
end
def from_ws_frame(wsframe)
SsmFrame.read(wsframe.payload_data)
end
end
def uuid
UUID.unpack(header.message_id)
end
def to_ack
data = JSON.generate({
AcknowledgedMessageType: header.message_type.strip,
AcknowledgedMessageId: uuid,
AcknowledgedMessageSequenceNumber: header.sequence_number.to_i,
IsSequentialMessage: true
})
ack = SsmFrame.create(data, 'acknowledge')
ack.header.sequence_number = header.sequence_number
ack.header.flags = header.flags
ack
end
def length
to_binary_s.length
end
end
#
# Initiates a WebSocket session based on the params of SSM::Client#start_session
#
# @param [Aws::SSM::Types::StartSessionResponse] :session_init Parameters returned by #start_session
# @param [Integer] :timeout
#
# @return [Socket] Socket representing the authenticates SSM WebSocket connection
def connect_ssm_ws(session_init, timeout = 20)
# hack-up a "graceful fail-down" in the caller
# raise Rex::Proto::Http::WebSocket::ConnectionError.new(msg: 'WebSocket sessions still need structs/parsing')
ws_key = session_init.token_value
ssm_id = session_init.session_id
ws_url = URI.parse(session_init.stream_url)
opts = {}
opts['vhost'] = ws_url.host
opts['uri'] = ws_url.to_s.sub(/^.*#{ws_url.host}/, '')
opts['headers'] = {
'Connection' => 'Upgrade',
'Upgrade' => 'WebSocket',
'Sec-WebSocket-Version' => 13,
'Sec-WebSocket-Key' => ws_key
}
ctx = {
'Msf' => framework,
'MsfExploit' => self
}
http_client = Rex::Proto::Http::Client.new(ws_url.host, 443, ctx, true)
raise Rex::Proto::Http::WebSocket::ConnectionError.new if http_client.nil?
# Send upgrade request
req = http_client.request_raw(opts)
res = http_client.send_recv(req, timeout)
# Verify upgrade
unless res&.code == 101
http_client.close
raise Rex::Proto::Http::WebSocket::ConnectionError.new(http_response: res)
end
# see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-WebSocket-Accept
accept_ws_key = Rex::Text.encode_base64(OpenSSL::Digest::SHA1.digest(ws_key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))
unless res.headers['Sec-WebSocket-Accept'] == accept_ws_key
http_client.close
raise Rex::Proto::Http::WebSocket::ConnectionError.new(msg: 'Invalid Sec-WebSocket-Accept header', http_response: res)
end
# Extract and extend connection object
socket = http_client.conn
socket.extend(Rex::Proto::Http::WebSocket::Interface)
# Send initialization handshake
ssm_wsock_init = JSON.generate({
MessageSchemaVersion: '1.0',
RequestId: UUID.rand,
TokenValue: ws_key
})
socket.put_wstext(ssm_wsock_init)
# Extend with interface
socket.extend(Interface)
end
end