##
# session.rb
# Created March, 2013
# By Ron Bowes
#
# See: LICENSE.md
#
##
require 'controller/encryptor'
require 'controller/packet'
require 'drivers/driver_command'
require 'drivers/driver_console'
require 'drivers/driver_process'
require 'libs/commander'
require 'libs/dnscat_exception'
require 'libs/swindow'
class Session
class SessionKiller < StandardError
end
@@isn = nil # nil = random
attr_reader :id, :name, :options, :state
attr_reader :window
# The session hasn't seen a SYN packet yet (but may have done some encryption negotiation)
STATE_NEW = 0x00
# After receiving a SYN
STATE_ESTABLISHED = 0x01
# After being manually killed
STATE_KILLED = 0xFF
HANDLERS = {
Packet::MESSAGE_TYPE_SYN => :_handle_syn,
Packet::MESSAGE_TYPE_MSG => :_handle_msg,
Packet::MESSAGE_TYPE_FIN => :_handle_fin,
Packet::MESSAGE_TYPE_ENC => :_handle_enc,
}
def initialize(id, main_window)
@state = STATE_NEW
@kill_reason = nil
@their_seq = 0
@my_seq = @@isn.nil? ? rand(0xFFFF) : @@isn
@options = 0
@id = id
@incoming_data = ''
@outgoing_data = ''
@driver = nil
# Stuff that's displayed after the window's name
@crypto_state = '[cleartext]'
# Create this whether or not we're actually encrypting - it cleans up
# the handler code
@encryptor = Encryptor.new(Settings::GLOBAL.get('secret'))
@settings = Settings.new()
@window = SWindow.new(main_window, false, {:times_out => true})
@settings.create("prompt", Settings::TYPE_NO_STRIP, "not set> ", "Change the prompt (if you want a space, use quotes; 'set prompt=\"a> \"'.") do |old_val, new_val|
@window.prompt = new_val
end
@settings.create("name", Settings::TYPE_NO_STRIP, "(not set)", "Change the name of the window, and how it's displayed on the 'windows' list; this implicitly changes the prompt as well.") do |old_val, new_val|
@window.name = new_val + ' ' + @crypto_state
@settings.set("prompt", "%s %d> " % [new_val, @window.id])
end
@settings.create("history_size", Settings::TYPE_INTEGER, @window.history_size, "Change the number of lines to store in the window's history") do |old_val, new_val|
@window.history_size = new_val
@window.puts("history_size (session) => #{new_val}")
end
end
def kill(reason = "No reason given")
@window.with({:to_ancestors=>true, :to_descendants=>true}) do
if(@state != STATE_KILLED)
@state = STATE_KILLED
@kill_reason = reason
@window.with({:to_ancestors => true}) do
@window.puts("Session #{@window.id} killed: #{reason}")
end
else
@window.puts("Session #{@window.id} killed (again): #{reason}")
end
end
@window.close()
if(@driver)
@driver.shutdown()
end
end
def _next_outgoing(n)
ret = @outgoing_data[0,n-1]
return ret
end
def _ack_outgoing(n)
# "n" is the current ACK value
bytes_acked = (n - @my_seq)
# Handle wraparounds properly
if(bytes_acked < 0)
bytes_acked += 0x10000
end
@outgoing_data = @outgoing_data[bytes_acked..-1]
@my_seq = n
end
def _valid_ack?(ack)
bytes_acked = (ack - @my_seq) & 0xFFFF
return bytes_acked <= @outgoing_data.length
end
def queue_outgoing(data)
@outgoing_data = @outgoing_data + data.force_encoding("ASCII-8BIT")
end
def to_s()
return "id: 0x%04x [internal: %d], state: %d, their_seq: 0x%04x, my_seq: 0x%04x, incoming_data: %d bytes [%s], outgoing data: %d bytes [%s]" % [@id, @window.id, @state, @their_seq, @my_seq, @incoming_data.length, @incoming_data, @outgoing_data.length, @outgoing_data]
end
def _do_display_crypto_values()
@window.with({:to_ancestors => true}) do
if(@encryptor.authenticated?())
@window.puts("Session #{@window.id} Security: ENCRYPTED AND VERIFIED!")
@window.puts("(the security depends on the strength of your pre-shared secret!)")
elsif(@encryptor.ready?())
@window.puts("Session #{@window.id} security: ENCRYPTED BUT *NOT* VALIDATED")
@window.puts("For added security, please ensure the client displays the same string:")
@window.puts()
@window.puts(">> #{@encryptor.get_sas()}")
else
@window.puts("Session #{@window.id} security: UNENCRYPTED")
end
end
end
def _handle_syn(packet, max_length)
options = 0
# Ignore errant SYNs - they are, at worst, retransmissions that we don't care about
if(@state != STATE_NEW)
raise(DnscatException, "Duplicate SYN received!")
end
_do_display_crypto_values()
# Save some of their options
@their_seq = packet.body.seq
@options = packet.body.options
# TODO: We're going to need different driver types
if((@options & Packet::OPT_COMMAND) == Packet::OPT_COMMAND)
@driver = DriverCommand.new(@window, @settings)
else
process = @settings.get("process")
if(process.nil?)
@driver = DriverConsole.new(@window, @settings)
else
@driver = DriverProcess.new(@window, @settings, process)
end
end
if((@options & Packet::OPT_NAME) == Packet::OPT_NAME)
@settings.set("name", packet.body.name)
else
@settings.set("name", "unnamed")
end
if(Settings::GLOBAL.get("auto_attach"))
@window.activate()
end
# Feed the auto_command into the window, as if it was user input
if(auto_command = Settings::GLOBAL.get("auto_command"))
auto_command.split(";").each do |command|
command = command.strip()
window.fake_input(command)
end
end
# Move states (this has to come after the encryption code, otherwise this packet is accidentally encrypted)
@state = STATE_ESTABLISHED
return Packet.create_syn(options, {
:session_id => @id,
:seq => @my_seq
})
end
def _actual_msg_max_length(max_data_length)
return max_data_length - (Packet.header_size(@options) + Packet::MsgBody.header_size(@options))
end
def _handle_msg(packet, max_length)
if(@state != STATE_ESTABLISHED)
raise(DnscatException, "MSG received in invalid state!")
end
# Validate the sequence number
if(@their_seq != packet.body.seq)
@window.puts("Client sent a bad sequence number (expected #{@their_seq}, received #{packet.body.seq}); re-sending")
# Re-send the last packet
old_data = _next_outgoing(_actual_msg_max_length(max_length))
return Packet.create_msg(@options, {
:session_id => @id,
:data => old_data,
:seq => @my_seq,
:ack => @their_seq,
})
end
# Validate the acknowledgement number
if(!_valid_ack?(packet.body.ack))
# Re-send the last packet
old_data = _next_outgoing(_actual_msg_max_length(max_length))
return Packet.create_msg(@options, {
:session_id => @id,
:data => old_data,
:seq => @my_seq,
:ack => @their_seq,
})
end
# Acknowledge the data that has been received so far
# Note: this is where @my_seq is updated
_ack_outgoing(packet.body.ack)
# Write the incoming data to the session
@outgoing_data += @driver.feed(packet.body.data)
# Increment the expected sequence number
@their_seq = (@their_seq + packet.body.data.length) & 0xFFFF;
# Read the next piece of data
new_data = _next_outgoing(_actual_msg_max_length(max_length))
# Create a packet out of it
packet = Packet.create_msg(@options, {
:session_id => @id,
:data => new_data,
:seq => @my_seq,
:ack => @their_seq,
})
return packet
end
def _handle_fin(packet, max_length)
raise(Session::SessionKiller, "Received FIN! Bye!")
end
def _handle_enc(packet, max_length)
params = {
:session_id => @id,
:subtype => packet.body.subtype,
:flags => 0,
}
if(packet.body.subtype == Packet::EncBody::SUBTYPE_INIT)
@encryptor.set_their_public_key(packet.body.public_key_x, packet.body.public_key_y)
# No matter what, respond with our public key
params[:public_key_x] = @encryptor.my_public_key_x()
params[:public_key_y] = @encryptor.my_public_key_y()
elsif(packet.body.subtype == Packet::EncBody::SUBTYPE_AUTH)
if(!@encryptor.ready?())
raise(Session::SessionKiller, "Tried to authenticate before the public key was set")
end
# Check their authenticator
begin
@encryptor.set_their_authenticator(packet.body.authenticator)
rescue Encryptor::Error
raise(Session::SessionKiller, "Invalid authenticator (pre-shared secret)")
end
params[:authenticator] = @encryptor.my_authenticator
else
raise(DnscatException, "Don't know how to parse encryption subtype in: #{packet}")
end
# Update the session info
if(@encryptor.authenticated?())
@crypto_state = "[encrypted and verified]"
elsif(@encryptor.ready?())
@crypto_state = "[encrypted, NOT verified]"
else
@crypto_state = "[cleartext]"
end
# Force a name update so the text gets added
@settings.set('name', @settings.get('name'))
return Packet.create_enc(params)
end
def _get_pcap_window()
id = "pcap#{@window.id}"
if(SWindow.exists?(id))
return SWindow.get(id)
end
return SWindow.new(@window, false, {
:id => id,
:name => "dnscat2 protocol window for session #{@window.id}",
:noinput => true,
})
end
def _check_crypto_options(packet)
# Don't enforce encryption on ENC|INIT packets
if(packet.type == Packet::MESSAGE_TYPE_ENC && packet.body.subtype == Packet::EncBody::SUBTYPE_INIT)
return
end
if(Settings::GLOBAL.get('security') == 'open')
return
end
if(!@encryptor.ready?())
@window.with({:to_ancestors => true}) do
@window.puts("Client attempted to connect with encryption disabled!")
@window.puts("If this was intentional, you can make encryption optional with 'set security=open'")
end
raise(Session::SessionKiller, "This server requires an encrypted connection!")
end
# Don't enforce authentication on AUTH packets
if(packet.type == Packet::MESSAGE_TYPE_ENC && packet.body.subtype == Packet::EncBody::SUBTYPE_AUTH)
return
end
if(Settings::GLOBAL.get('security') == 'encrypted')
return
end
if(!@encryptor.authenticated?())
@window.with({:to_ancestors => true}) do
@window.puts("Client attempted to connect without a pre-shared secret!")
@window.puts("If this was intentional, you can make authentication optional with 'set security=encrypted'")
end
raise(Session::SessionKiller, "This server requires an encrypted and authenticated connection!")
end
end
def _handle_incoming(data, max_length)
packet = Packet.parse(data, @options)
if(Settings::GLOBAL.get("packet_trace"))
window = _get_pcap_window()
window.puts("IN: #{packet}")
end
# We can send a FIN and close right away if the session was killed
if(@state == STATE_KILLED)
raise(Session::SessionKiller, @kill_reason)
end
# Unless it's an encrypted packet (which implies that we're still negotiating stuff), enforce encryption restraints
_check_crypto_options(packet)
# Find the appropriate handler for the packet type
handler = HANDLERS[packet.type]
if(handler.nil?)
raise(DnscatException, "No handler found for that packet type: #{packet.type}")
end
# Handle the packet
return send(handler, packet, max_length)
end
def feed(possibly_encrypted_data, max_length)
# Tell the window that we're still alive
window.kick()
begin
return @encryptor.decrypt_and_encrypt(possibly_encrypted_data) do |data, was_encrypted|
begin
if(@driver && @driver.stopped)
raise(Session::SessionKiller, "The driver requested it be stopped!")
end
if(was_encrypted)
max_length -= 8
end
response_packet = _handle_incoming(data, max_length)
rescue Session::SessionKiller => e
# Kill it
kill(e.message)
# Respond with a FIN
response_packet = Packet.create_fin(@options, {
:session_id => @id,
:reason => "Session killed: #{e.message}",
})
rescue DnscatException => e
# Tell everybody
@window.with({:to_ancestors => true}) do
@window.puts("An error occurred (see window #{@window.id} for stacktrace): #{e.message}")
end
@window.puts()
@window.puts("If you think this might be a bug, please report this trace:")
@window.puts(e.inspect)
e.backtrace.each do |bt|
@window.puts(bt)
end
# Don't respond
response_packet = nil
end
# Print the packet if the user requested a trace
if(Settings::GLOBAL.get("packet_trace"))
window = _get_pcap_window()
if(response_packet.nil?)
window.puts("OUT: <no data>")
else
window.puts("OUT: #{response_packet}")
end
end
if(response_packet)
response_packet.to_bytes()
else
nil
end
end
rescue Encryptor::Error # => e
#@window.puts("There was an error decrypting or encrypting data: #{e}")
return ''
end
end
end