Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/lib/rex/proto/http/web_socket/amazon_ssm.rb
Views: 11766
# -*- coding: binary -*-12require 'bindata'34module Rex::Proto::Http::WebSocket::AmazonSsm5module PayloadType6Output = 17Error = 28Size = 39Parameter = 410HandshakeRequest = 511HandshakeResponse = 612HandshakeComplete = 713EncChallengeRequest = 814EncChallengeResponse = 915Flag = 101617def self.from_val(v)18self.constants.find {|c| self.const_get(c) == v }19end20end2122module UUID23def self.unpack(bbuf)24sbuf = ""25[8...12].each do |idx|26sbuf << Rex::Text.to_hex(bbuf[idx])27end28sbuf << '-'29[12...14].each do |idx|30sbuf << Rex::Text.to_hex(bbuf[idx])31end32sbuf << '-'33[14...16].each do |idx|34sbuf << Rex::Text.to_hex(bbuf[idx])35end36sbuf << '-'37[0...2].each do |idx|38sbuf << Rex::Text.to_hex(bbuf[idx])39end40sbuf << '-'41[2...8].each do |idx|42sbuf << Rex::Text.to_hex(bbuf[idx])43end44sbuf.gsub("\\x",'')45end4647def self.pack(sbuf)48parts = sbuf.split('-').map do |seg|49seg.chars.each_slice(2).map {|e| "\\x#{e.join}"}.join50end51[3, 4, 0, 1, 2].map do |part|52Rex::Text.hex_to_raw(parts[part])53end.join54end5556def self.rand57self.unpack(Rex::Text.rand_text(16))58end59end6061module Interface62module SsmChannelMethods63attr_accessor :rows64attr_accessor :cols6566def _start_ssm_keepalive67@keepalive_thread = Rex::ThreadFactory.spawn('SsmChannel-Keepalive', false) do68while not closed? or @websocket.closed?69write ''70Rex::ThreadSafe.sleep(::Random.rand * 10 + 15)71end72@keepalive_thread = nil73end74end7576def close77@keepalive_thread.kill if @keepalive_thread78@keepalive_thread = nil79super80end8182def acknowledge_output(output_frame)83ack = output_frame.to_ack84# ack.header.sequence_number = @out_seq_num85@websocket.put_wsbinary(ack.to_binary_s)86# wlog("SsmChannel: acknowledge output #{output_frame.uuid}")87output_frame.uuid88end8990def pause_publication91msg = SsmFrame.create_pause_pub92@publication = false93@websocket.put_wsbinary(msg.to_binary_s)94end9596def start_publication97msg = SsmFrame.create_start_pub98@publication = true99@websocket.put_wsbinary(msg.to_binary_s)100end101102def handle_output_data(output_frame)103return nil if @ack_message == output_frame.uuid104105@ack_message = acknowledge_output(output_frame)106# TODO: handle Payload::* types107if ![PayloadType::Output, PayloadType::Error].any? { |e| e == output_frame.payload_type }108wlog("SsmChannel got unhandled output payload type: #{Payload.from_val(output_frame.payload_type)}")109return nil110end111112output_frame.payload_data.value113end114115def handle_acknowledge(ack_frame)116# wlog("SsmChannel: got acknowledge message #{ack_frame.uuid}")117begin118seq_num = JSON.parse(ack_frame.payload_data)['AcknowledgedMessageSequenceNumber'].to_i119@ack_seq_num = seq_num if seq_num > @ack_seq_num120rescue => e121elog("SsmChannel failed to parse ack JSON #{ack_frame.payload_data} due to #{e}!")122end123nil124end125126def update_term_size127return unless ::IO.console128129rows, cols = ::IO.console.winsize130unless rows == self.rows && cols == self.cols131set_term_size(cols, rows)132self.rows = rows133self.cols = cols134end135end136137def set_term_size(cols, rows)138data = JSON.generate({cols: cols, rows: rows})139frame = SsmFrame.create(data)140frame.payload_type = PayloadType::Size141@websocket.put_wsbinary(frame.to_binary_s)142end143end144145class SsmChannel < Rex::Proto::Http::WebSocket::Interface::Channel146include SsmChannelMethods147attr_reader :run_ssm_pub, :out_seq_num, :ack_seq_num, :ack_message148149def initialize(websocket)150@ack_seq_num = 0151@out_seq_num = 0152@run_ssm_pub = true153@ack_message = nil154@publication = false155156super(websocket, write_type: :binary)157end158159def on_data_read(data, _data_type)160return data if data.blank?161162ssm_frame = SsmFrame.read(data)163case ssm_frame.header.message_type.strip164when 'output_stream_data'165@publication = true # Linux sends stream data before sending start_publication message166return handle_output_data(ssm_frame)167when 'acknowledge'168# update ACK seqno169handle_acknowledge(ssm_frame)170when 'start_publication'171@out_seq_num = @ack_seq_num if @out_seq_num > 0172@publication = true173# handle session resumption - foregrounding or resumption of input174when 'pause_publication'175# @websocket.put_wsbinary(ssm_frame.to_ack.to_binary_s)176@publication = false177# handle session suspension - backgrounding or general idle178when 'input_stream_data'179# this is supposed to be a one way street180emsg = "SsmChannel received input_stream_data from SSM (!!)"181elog(emsg)182raise emsg183when 'channel_closed'184elog("SsmChannel got closed message #{ssm_frame.uuid}")185close186else187raise Rex::Proto::Http::WebSocket::ConnectionError.new(188msg: "Unknown AWS SSM message type: #{ssm_frame.header.message_type}"189)190end191192nil193end194195def on_data_write(data)196start_publication if not @publication197frame = SsmFrame.create(data)198frame.header.sequence_number = @out_seq_num199@out_seq_num += 1200frame.to_binary_s201end202203def publishing?204@publication205end206end207208def to_ssm_channel(publish_timeout: 10)209chan = SsmChannel.new(self)210211if publish_timeout212# Waiting for the channel to start publishing213(publish_timeout * 2).times do214break if chan.publishing?215216sleep 0.5217end218219raise Rex::TimeoutError.new('Timed out while waiting for the channel to start publishing.') unless chan.publishing?220end221222chan223end224end225226class SsmFrame < BinData::Record227endian :big228229struct :header do230endian :big231232uint32 :header_length, initial_value: 116233string :message_type, length: 32, pad_byte: 0x20, initial_value: 'input_stream_data'234uint32 :schema_version, initial_value: 1235uint64 :created_date, default_value: lambda { (Time.now.to_f * 1000).to_i }236uint64 :sequence_number, initial_value: 0237uint64 :flags, value: 0 #lambda { sequence_number == 0 ? 1 : 0 }238string :message_id, length: 16, initial_value: UUID.pack(UUID.rand)239end240241string :payload_digest, length: 32, default_value: -> { Digest::SHA256.digest(payload_data) }242uint32 :payload_type, default_value: PayloadType::Output243uint32 :payload_length, value: -> { payload_data.length }244string :payload_data, read_length: -> { payload_length }245246class << self247def create(data = nil, mtype = 'input_stream_data')248return data if data.is_a?(SsmFrame)249250frame = SsmFrame.new(header: {251message_type: mtype,252created_date: (Time.now.to_f * 1000).to_i,253message_id: UUID.pack(UUID.rand)254})255if !data.nil?256frame.payload_data = data257frame.payload_digest = Digest::SHA256.digest(data)258frame.payload_length = data.length259frame.payload_type = PayloadType::Output260end261frame262end263264def create_pause_pub265uuid = UUID.rand266time = Time.now267data = JSON.generate({268MessageType: 'pause_publication',269SchemaVersion: 1,270MessageId: uuid,271CreateData: time.strftime("%Y-%m-%dT%T.%LZ")272})273frame = SsmFrame.new( header: {274message_type: 'pause_publication',275created_date: (time.to_f * 1000).to_i,276message_id: UUID.pack(uuid)277})278frame.payload_data = data279frame.payload_digest = Digest::SHA256.digest(data)280frame.payload_length = data.length281frame.payload_type = 0282frame283end284285def create_start_pub286data = 'start_publication'287frame = SsmFrame.new( header: {288message_type: data,289created_date: (Time.now.to_f * 1000).to_i,290message_id: UUID.pack(UUID.rand)291})292frame.payload_data = data293frame.payload_digest = Digest::SHA256.digest(data)294frame.payload_length = data.length295frame.payload_type = 0296frame297end298299def from_ws_frame(wsframe)300SsmFrame.read(wsframe.payload_data)301end302end303304def uuid305UUID.unpack(header.message_id)306end307308def to_ack309data = JSON.generate({310AcknowledgedMessageType: header.message_type.strip,311AcknowledgedMessageId: uuid,312AcknowledgedMessageSequenceNumber: header.sequence_number.to_i,313IsSequentialMessage: true314})315ack = SsmFrame.create(data, 'acknowledge')316ack.header.sequence_number = header.sequence_number317ack.header.flags = header.flags318ack319end320321def length322to_binary_s.length323end324end325#326# Initiates a WebSocket session based on the params of SSM::Client#start_session327#328# @param [Aws::SSM::Types::StartSessionResponse] :session_init Parameters returned by #start_session329# @param [Integer] :timeout330#331# @return [Socket] Socket representing the authenticates SSM WebSocket connection332def connect_ssm_ws(session_init, timeout = 20)333# hack-up a "graceful fail-down" in the caller334# raise Rex::Proto::Http::WebSocket::ConnectionError.new(msg: 'WebSocket sessions still need structs/parsing')335ws_key = session_init.token_value336ssm_id = session_init.session_id337ws_url = URI.parse(session_init.stream_url)338opts = {}339opts['vhost'] = ws_url.host340opts['uri'] = ws_url.to_s.sub(/^.*#{ws_url.host}/, '')341opts['headers'] = {342'Connection' => 'Upgrade',343'Upgrade' => 'WebSocket',344'Sec-WebSocket-Version' => 13,345'Sec-WebSocket-Key' => ws_key346}347ctx = {348'Msf' => framework,349'MsfExploit' => self350}351http_client = Rex::Proto::Http::Client.new(ws_url.host, 443, ctx, true)352raise Rex::Proto::Http::WebSocket::ConnectionError.new if http_client.nil?353354# Send upgrade request355req = http_client.request_raw(opts)356res = http_client.send_recv(req, timeout)357# Verify upgrade358unless res&.code == 101359http_client.close360raise Rex::Proto::Http::WebSocket::ConnectionError.new(http_response: res)361end362# see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-WebSocket-Accept363accept_ws_key = Rex::Text.encode_base64(OpenSSL::Digest::SHA1.digest(ws_key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))364unless res.headers['Sec-WebSocket-Accept'] == accept_ws_key365http_client.close366raise Rex::Proto::Http::WebSocket::ConnectionError.new(msg: 'Invalid Sec-WebSocket-Accept header', http_response: res)367end368# Extract and extend connection object369socket = http_client.conn370socket.extend(Rex::Proto::Http::WebSocket::Interface)371# Send initialization handshake372ssm_wsock_init = JSON.generate({373MessageSchemaVersion: '1.0',374RequestId: UUID.rand,375TokenValue: ws_key376})377socket.put_wstext(ssm_wsock_init)378# Extend with interface379socket.extend(Interface)380end381end382383384