Land #4171, Steam protocol support
commit
79ca0a56f9
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
require 'rex/proto/steam/message'
|
|
@ -0,0 +1,125 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Rex
|
||||
module Proto
|
||||
##
|
||||
#
|
||||
# Steam protocol support, taken from https://developer.valvesoftware.com/wiki/Server_queries
|
||||
#
|
||||
##
|
||||
module Steam
|
||||
# The Steam header ussed when the message is fragmented.
|
||||
FRAGMENTED_HEADER = 0xFFFFFFFE
|
||||
# The Steam header ussed when the message is not fragmented.
|
||||
UNFRAGMENTED_HEADER = 0xFFFFFFFF
|
||||
|
||||
# Decodes a Steam response message.
|
||||
#
|
||||
# @param message [String] the message to decode
|
||||
# @return [Array] the message type and body
|
||||
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
|
||||
[type, message[5, message.length]]
|
||||
end
|
||||
|
||||
# Encodes a Steam message.
|
||||
#
|
||||
# @param type [String, Fixnum] the message type
|
||||
# @param body [String] the message body
|
||||
# @return [String] the encoded Steam message
|
||||
def encode_message(type, body)
|
||||
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') + body
|
||||
end
|
||||
|
||||
# Builds an A2S_INFO message
|
||||
#
|
||||
# @return [String] the A2S_INFO message
|
||||
def a2s_info
|
||||
encode_message('T', "Source Engine Query\x00")
|
||||
end
|
||||
|
||||
# Decodes an A2S_INFO response message
|
||||
#
|
||||
# @parameter response [String] the A2S_INFO resposne to decode
|
||||
# @return [Hash] the fields extracted from the response
|
||||
def a2s_info_decode(response)
|
||||
# abort if it is impossibly short
|
||||
return nil if response.length < 19
|
||||
message_type, body = decode_message(response)
|
||||
# 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 = body.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
|
|
@ -0,0 +1,67 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
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(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'Gather Steam Server Information',
|
||||
'Description' => %q(
|
||||
This module uses the A2S_INFO request to obtain information from a Steam server.
|
||||
),
|
||||
'Author' => 'Jon Hart <jon_hart[at]rapid7.com>',
|
||||
'References' =>
|
||||
[
|
||||
# TODO: add more from https://developer.valvesoftware.com/wiki/Server_queries,
|
||||
# perhaps in different modules
|
||||
['URL', 'https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO']
|
||||
],
|
||||
'License' => MSF_LICENSE
|
||||
)
|
||||
)
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(27015)
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def build_probe
|
||||
@probe ||= a2s_info
|
||||
end
|
||||
|
||||
def scanner_process(response, src_host, src_port)
|
||||
info = a2s_info_decode(response)
|
||||
return unless info
|
||||
@results[src_host] ||= []
|
||||
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
|
||||
|
||||
def scanner_postscan(_batch)
|
||||
@results.each_pair do |host, info|
|
||||
report_host(host: host)
|
||||
report_service(
|
||||
host: host,
|
||||
proto: 'udp',
|
||||
port: rport,
|
||||
name: 'Steam',
|
||||
info: info
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,47 @@
|
|||
# -*- coding: binary -*-
|
||||
require 'spec_helper'
|
||||
require 'rex/proto/steam/message'
|
||||
|
||||
describe Rex::Proto::Steam do
|
||||
subject(:steam) do
|
||||
mod = Module.new
|
||||
mod.extend described_class
|
||||
mod
|
||||
end
|
||||
|
||||
describe '#encode_message' do
|
||||
it 'properly encodes messages' do
|
||||
message = steam.encode_message('T', 'Test')
|
||||
expect(message).to eq("\xFF\xFF\xFF\xFF\x54Test")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#decode_message' do
|
||||
it 'does not decode overly short messages' do
|
||||
expect(steam.decode_message('foo')).to eq(nil)
|
||||
end
|
||||
|
||||
it 'does not decode unknown messages' do
|
||||
expect(steam.decode_message("\xFF\xFF\xFF\x01blahblahblah")).to eq(nil)
|
||||
end
|
||||
|
||||
it 'properly decodes valid messages' do
|
||||
type, message = steam.decode_message("\xFF\xFF\xFF\xFF\x54Test")
|
||||
expect(type).to eq(0x54)
|
||||
expect(message).to eq('Test')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#a2s_info_decode' do
|
||||
it 'extracts a2s_info fields properly' do
|
||||
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 = steam.a2s_info_decode(IO.read(File.join(File.dirname(__FILE__), 'steam_info.bin')))
|
||||
expect(actual_info).to eq(expected_info)
|
||||
end
|
||||
end
|
||||
end
|
Binary file not shown.
Loading…
Reference in New Issue