From 51e84ce548c3f4b45a1a7cfa58253af269688423 Mon Sep 17 00:00:00 2001 From: Jon Hart Date: Mon, 10 Nov 2014 16:45:28 -0800 Subject: [PATCH] Add unit tests, complete extraction/cleanup --- lib/rex/proto/steam.rb | 3 + lib/rex/proto/steam/message.rb | 109 ++++++++++++++++++ .../auxiliary/scanner/steam/server_info.rb | 22 ++-- spec/lib/rex/proto/steam/message_spec.rb | 44 +++++++ 4 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 lib/rex/proto/steam.rb create mode 100644 lib/rex/proto/steam/message.rb create mode 100644 spec/lib/rex/proto/steam/message_spec.rb diff --git a/lib/rex/proto/steam.rb b/lib/rex/proto/steam.rb new file mode 100644 index 0000000000..12552c0449 --- /dev/null +++ b/lib/rex/proto/steam.rb @@ -0,0 +1,3 @@ +# -*- coding: binary -*- + +require 'rex/proto/steam/message' diff --git a/lib/rex/proto/steam/message.rb b/lib/rex/proto/steam/message.rb new file mode 100644 index 0000000000..c76dc33bf1 --- /dev/null +++ b/lib/rex/proto/steam/message.rb @@ -0,0 +1,109 @@ +# -*- coding: binary -*- + + +module Rex +module Proto +## +# +# Steam protocol support, taken from https://developer.valvesoftware.com/wiki/Server_queries +# +## +module Steam + + FRAGMENTED_HEADER = 0xFFFFFFFE + UNFRAGMENTED_HEADER = 0xFFFFFFFF + + def decode_message(message) + # minimum size is header (4) + type (1) + return if message.length < 5 + header, type = message.unpack('NC') + # TODO: handle fragmented responses + return if header != UNFRAGMENTED_HEADER + [header, type, message[5, message.length]] + end + + def encode_message(type, payload) + if type.is_a? Fixnum + type_num = type + elsif type.is_a? String + type_num = type.ord + else + fail ArgumentError, 'type must be a String or Fixnum' + end + + [UNFRAGMENTED_HEADER, type_num ].pack('NC') + payload + end + + def a2s_info + encode_message('T', "Source Engine Query\x00") + end + + def a2s_info_decode(message) + # abort if it is impossibly short + return nil if message.length < 19 + _header, message_type, payload = decode_message(message) + # abort if it isn't a valid Steam response + return nil if message_type != 0x49 # 'I' + info = {} + info[:version], info[:name], info[:map], info[:folder], info[:game_name], + info[:game_id], players, players_max, info[:bots], + type, env, vis, vac, info[:game_version], edf = payload.unpack("CZ*Z*Z*Z*SCCCCCCCZ*C") + + # translate type + case type + when 100 # d + server_type = 'Dedicated' + when 108 # l + server_type = 'Non-dedicated' + when 112 # p + server_type = 'SourceTV relay (proxy)' + else + server_type = "Unknown (#{type})" + end + info[:type] = server_type + + # translate environment + case env + when 108 # l + server_env = 'Linux' + when 119 # w + server_env = 'Windows' + when 109 # m + when 111 # o + server_env = 'Mac' + else + server_env = "Unknown (#{env})" + end + info[:environment] = server_env + + # translate visibility + case vis + when 0 + server_vis = 'public' + when 1 + server_vis = 'private' + else + server_vis = "Unknown (#{vis})" + end + info[:visibility] = server_vis + + # translate VAC + case vac + when 0 + server_vac = 'unsecured' + when 1 + server_vac = 'secured' + else + server_vac = "Unknown (#{vac})" + end + info[:VAC] = server_vac + + # format players/max + info[:players] = "#{players}/#{players_max}" + + # TODO: parse EDF + info + end +end +end +end diff --git a/modules/auxiliary/scanner/steam/server_info.rb b/modules/auxiliary/scanner/steam/server_info.rb index 09cd78c127..7e5d88f544 100644 --- a/modules/auxiliary/scanner/steam/server_info.rb +++ b/modules/auxiliary/scanner/steam/server_info.rb @@ -4,10 +4,12 @@ ## require 'msf/core' +require 'rex/proto/steam' class Metasploit3 < Msf::Auxiliary include Msf::Auxiliary::Report include Msf::Auxiliary::UDPScanner + include Rex::Proto::Steam def initialize(info = {}) super( @@ -32,24 +34,24 @@ class Metasploit3 < Msf::Auxiliary [ Opt::RPORT(27015) ], self.class) - end - # TODO: construct the appropriate probe here. def build_probe - @probe ||= "\xFF\xFF\xFF\xFFTSource Engine Query\x00" + @probe ||= a2s_info end - # Called for each response packet - def scanner_process(response, src_host, _src_port) - return unless response.size >= 19 + def scanner_process(response, src_host, src_port) + info = a2s_info_decode(response) + return unless info @results[src_host] ||= [] - puts "Got something from #{src_host}" - #puts response.unpack("NCCZ*Z*Z*Z*SCCCCCCCZ*C") - + if datastore['VERBOSE'] + print_good("#{src_host}:#{src_port} found '#{info.inspect}'") + else + print_good("#{src_host}:#{src_port} found '#{info[:name]}'") + end + @results[src_host] << info end - # Called after the scan block def scanner_postscan(_batch) @results.each_pair do |host, info| report_host(host: host) diff --git a/spec/lib/rex/proto/steam/message_spec.rb b/spec/lib/rex/proto/steam/message_spec.rb new file mode 100644 index 0000000000..36223575f0 --- /dev/null +++ b/spec/lib/rex/proto/steam/message_spec.rb @@ -0,0 +1,44 @@ +# -*- coding: binary -*- +require 'spec_helper' +require 'rex/proto/steam/message' + +describe Rex::Proto::Steam do + subject do + mod = Module.new + mod.extend described_class + mod + end + + describe '#encode_message' do + it 'should properly encode messages' do + message = subject.encode_message('T', 'Test') + expect(message).to eq("\xFF\xFF\xFF\xFF\x54Test") + end + end + + describe '#decode_message' do + it 'should not decode overly short messages' do + expect(subject.decode_message('foo')).to eq(nil) + end + + it 'should not decode unknown messages' do + expect(subject.decode_message("\xFF\xFF\xFF\x01blahblahblah")).to eq(nil) + end + + it 'should properly decode valid messages' do + header, type, message = subject.decode_message("\xFF\xFF\xFF\xFF\x54Test") + expect(header).to eq(Rex::Proto::Steam::UNFRAGMENTED_HEADER) + expect(type).to eq(0x54) + expect(message).to eq('Test') + end + end + + describe '#a2s_info_decode' do + it 'should extract a2s_info fields properly' do + example_resp = "\xff\xff\xff\xff\x49\x11\x2d\x3d\x54\x48\x45\x20\x42\x41\x54\x54\x4c\x45\x47\x52\x4f\x55\x4e\x44\x53\x20\x2a\x48\x41\x52\x44\x43\x4f\x52\x45\x2a\x3d\x2d\x00\x61\x6f\x63\x5f\x62\x61\x74\x74\x6c\x65\x67\x72\x6f\x75\x6e\x64\x00\x61\x67\x65\x6f\x66\x63\x68\x69\x76\x61\x6c\x72\x79\x00\x41\x67\x65\x20\x6f\x66\x20\x43\x68\x69\x76\x61\x6c\x72\x79\x00\x66\x44\x16\x20\x00\x64\x6c\x00\x01\x31\x2e\x30\x2e\x30\x2e\x36\x00\xb1\x87\x69\x04\x04\x7c\x35\xbe\x12\x40\x01\x48\x4c\x73\x74\x61\x74\x73\x58\x3a\x43\x45\x2c\x69\x6e\x63\x72\x65\x61\x73\x65\x64\x5f\x6d\x61\x78\x70\x6c\x61\x79\x65\x72\x73\x00\x66\x44\x00\x00\x00\x00\x00\x00" + expected_info = {:version=>17, :name=>"-=THE BATTLEGROUNDS *HARDCORE*=-", :map=>"aoc_battleground", :folder=>"ageofchivalry", :game_name=>"Age of Chivalry", :game_id=>17510, :players=>"22/32", :bots=>0, :game_version=>"1.0.0.6", :type=>"Dedicated", :environment=>"Linux", :visibility=>"public", :VAC=>"secured"} + actual_info = subject.a2s_info_decode(example_resp) + expect(actual_info).to eq(expected_info) + end + end +end