Skip to content

Commit 51e84ce

Browse files
committed
Add unit tests, complete extraction/cleanup
1 parent a992789 commit 51e84ce

File tree

4 files changed

+168
-10
lines changed

4 files changed

+168
-10
lines changed

lib/rex/proto/steam.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# -*- coding: binary -*-
2+
3+
require 'rex/proto/steam/message'

lib/rex/proto/steam/message.rb

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# -*- coding: binary -*-
2+
3+
4+
module Rex
5+
module Proto
6+
##
7+
#
8+
# Steam protocol support, taken from https://developer.valvesoftware.com/wiki/Server_queries
9+
#
10+
##
11+
module Steam
12+
13+
FRAGMENTED_HEADER = 0xFFFFFFFE
14+
UNFRAGMENTED_HEADER = 0xFFFFFFFF
15+
16+
def decode_message(message)
17+
# minimum size is header (4) + type (1)
18+
return if message.length < 5
19+
header, type = message.unpack('NC')
20+
# TODO: handle fragmented responses
21+
return if header != UNFRAGMENTED_HEADER
22+
[header, type, message[5, message.length]]
23+
end
24+
25+
def encode_message(type, payload)
26+
if type.is_a? Fixnum
27+
type_num = type
28+
elsif type.is_a? String
29+
type_num = type.ord
30+
else
31+
fail ArgumentError, 'type must be a String or Fixnum'
32+
end
33+
34+
[UNFRAGMENTED_HEADER, type_num ].pack('NC') + payload
35+
end
36+
37+
def a2s_info
38+
encode_message('T', "Source Engine Query\x00")
39+
end
40+
41+
def a2s_info_decode(message)
42+
# abort if it is impossibly short
43+
return nil if message.length < 19
44+
_header, message_type, payload = decode_message(message)
45+
# abort if it isn't a valid Steam response
46+
return nil if message_type != 0x49 # 'I'
47+
info = {}
48+
info[:version], info[:name], info[:map], info[:folder], info[:game_name],
49+
info[:game_id], players, players_max, info[:bots],
50+
type, env, vis, vac, info[:game_version], edf = payload.unpack("CZ*Z*Z*Z*SCCCCCCCZ*C")
51+
52+
# translate type
53+
case type
54+
when 100 # d
55+
server_type = 'Dedicated'
56+
when 108 # l
57+
server_type = 'Non-dedicated'
58+
when 112 # p
59+
server_type = 'SourceTV relay (proxy)'
60+
else
61+
server_type = "Unknown (#{type})"
62+
end
63+
info[:type] = server_type
64+
65+
# translate environment
66+
case env
67+
when 108 # l
68+
server_env = 'Linux'
69+
when 119 # w
70+
server_env = 'Windows'
71+
when 109 # m
72+
when 111 # o
73+
server_env = 'Mac'
74+
else
75+
server_env = "Unknown (#{env})"
76+
end
77+
info[:environment] = server_env
78+
79+
# translate visibility
80+
case vis
81+
when 0
82+
server_vis = 'public'
83+
when 1
84+
server_vis = 'private'
85+
else
86+
server_vis = "Unknown (#{vis})"
87+
end
88+
info[:visibility] = server_vis
89+
90+
# translate VAC
91+
case vac
92+
when 0
93+
server_vac = 'unsecured'
94+
when 1
95+
server_vac = 'secured'
96+
else
97+
server_vac = "Unknown (#{vac})"
98+
end
99+
info[:VAC] = server_vac
100+
101+
# format players/max
102+
info[:players] = "#{players}/#{players_max}"
103+
104+
# TODO: parse EDF
105+
info
106+
end
107+
end
108+
end
109+
end

modules/auxiliary/scanner/steam/server_info.rb

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
##
55

66
require 'msf/core'
7+
require 'rex/proto/steam'
78

89
class Metasploit3 < Msf::Auxiliary
910
include Msf::Auxiliary::Report
1011
include Msf::Auxiliary::UDPScanner
12+
include Rex::Proto::Steam
1113

1214
def initialize(info = {})
1315
super(
@@ -32,24 +34,24 @@ def initialize(info = {})
3234
[
3335
Opt::RPORT(27015)
3436
], self.class)
35-
3637
end
3738

38-
# TODO: construct the appropriate probe here.
3939
def build_probe
40-
@probe ||= "\xFF\xFF\xFF\xFFTSource Engine Query\x00"
40+
@probe ||= a2s_info
4141
end
4242

43-
# Called for each response packet
44-
def scanner_process(response, src_host, _src_port)
45-
return unless response.size >= 19
43+
def scanner_process(response, src_host, src_port)
44+
info = a2s_info_decode(response)
45+
return unless info
4646
@results[src_host] ||= []
47-
puts "Got something from #{src_host}"
48-
#puts response.unpack("NCCZ*Z*Z*Z*SCCCCCCCZ*C")
49-
47+
if datastore['VERBOSE']
48+
print_good("#{src_host}:#{src_port} found '#{info.inspect}'")
49+
else
50+
print_good("#{src_host}:#{src_port} found '#{info[:name]}'")
51+
end
52+
@results[src_host] << info
5053
end
5154

52-
# Called after the scan block
5355
def scanner_postscan(_batch)
5456
@results.each_pair do |host, info|
5557
report_host(host: host)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# -*- coding: binary -*-
2+
require 'spec_helper'
3+
require 'rex/proto/steam/message'
4+
5+
describe Rex::Proto::Steam do
6+
subject do
7+
mod = Module.new
8+
mod.extend described_class
9+
mod
10+
end
11+
12+
describe '#encode_message' do
13+
it 'should properly encode messages' do
14+
message = subject.encode_message('T', 'Test')
15+
expect(message).to eq("\xFF\xFF\xFF\xFF\x54Test")
16+
end
17+
end
18+
19+
describe '#decode_message' do
20+
it 'should not decode overly short messages' do
21+
expect(subject.decode_message('foo')).to eq(nil)
22+
end
23+
24+
it 'should not decode unknown messages' do
25+
expect(subject.decode_message("\xFF\xFF\xFF\x01blahblahblah")).to eq(nil)
26+
end
27+
28+
it 'should properly decode valid messages' do
29+
header, type, message = subject.decode_message("\xFF\xFF\xFF\xFF\x54Test")
30+
expect(header).to eq(Rex::Proto::Steam::UNFRAGMENTED_HEADER)
31+
expect(type).to eq(0x54)
32+
expect(message).to eq('Test')
33+
end
34+
end
35+
36+
describe '#a2s_info_decode' do
37+
it 'should extract a2s_info fields properly' do
38+
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"
39+
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"}
40+
actual_info = subject.a2s_info_decode(example_resp)
41+
expect(actual_info).to eq(expected_info)
42+
end
43+
end
44+
end

0 commit comments

Comments
 (0)