Skip to content

Commit 07a1653

Browse files
committed
Add gather module for Quake servers
1 parent e05cd95 commit 07a1653

File tree

6 files changed

+269
-0
lines changed

6 files changed

+269
-0
lines changed

lib/rex/proto/quake.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/quake/message'

lib/rex/proto/quake/message.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# -*- coding: binary -*-
2+
3+
module Rex
4+
module Proto
5+
##
6+
#
7+
# Quake 3 protocol, taken from ftp://ftp.idsoftware.com/idstuff/quake3/docs/server.txt
8+
#
9+
##
10+
module Quake
11+
HEADER = 0xFFFFFFFF
12+
13+
def decode_message(message)
14+
# minimum size is header (4) + <command> + <stuff>
15+
return if message.length < 7
16+
header = message.unpack('N')[0]
17+
return if header != HEADER
18+
message[4, message.length]
19+
end
20+
21+
def encode_message(payload)
22+
[HEADER].pack('N') + payload
23+
end
24+
25+
def getstatus
26+
encode_message('getstatus')
27+
end
28+
29+
def getinfo
30+
encode_message('getinfo')
31+
end
32+
33+
def decode_infostring(infostring)
34+
# decode an "infostring", which is just a (supposedly) quoted string of tokens separated
35+
# by backslashes, generally terminated with a newline
36+
token_re = /([^\\]+)\\([^\\]+)/
37+
return nil unless infostring =~ token_re
38+
# remove possibly present leading/trailing double quote
39+
infostring.gsub!(/(?:^"|"$)/, '')
40+
# remove the trailing \n, if present
41+
infostring.gsub!(/\n$/, '')
42+
# split on backslashes and group into key value pairs
43+
infohash = {}
44+
infostring.scan(token_re).each do |kv|
45+
infohash[kv.first] = kv.last
46+
end
47+
infohash
48+
end
49+
50+
def decode_response(message, type)
51+
resp = decode_message(message)
52+
if /^print\n(?<error>.*)\n?/m =~ resp
53+
# XXX: is there a better exception to throw here?
54+
fail ::ArgumentError, "#{type} error: #{error}"
55+
# why doesn't this work?
56+
#elsif /^#{type}Response\n(?<infostring>.*)/m =~ resp
57+
elsif resp =~ /^#{type}Response\n(.*)/m
58+
decode_infostring(Regexp.last_match(1))
59+
else
60+
nil
61+
end
62+
end
63+
64+
def decode_status(message)
65+
decode_response(message, 'status')
66+
end
67+
68+
def decode_info(message)
69+
decode_response(message, 'info')
70+
end
71+
end
72+
end
73+
end
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'rex/proto/quake'
8+
9+
class Metasploit3 < Msf::Auxiliary
10+
include Msf::Auxiliary::Report
11+
include Msf::Auxiliary::UDPScanner
12+
include Rex::Proto::Quake
13+
14+
def initialize(info = {})
15+
super(
16+
update_info(
17+
info,
18+
'Name' => 'Gather Quake Server Information',
19+
'Description' => %q(
20+
This module uses the getstatus or getinfo request to obtain information from a Quakeserver.
21+
),
22+
'Author' => 'Jon Hart <jon_hart[at]rapid7.com',
23+
'References' =>
24+
[
25+
['URL', 'ftp://ftp.idsoftware.com/idstuff/quake3/docs/server.txt']
26+
],
27+
'License' => MSF_LICENSE,
28+
'Actions' => [
29+
['status', 'Description' => 'Use the getstatus command'],
30+
['info', 'Description' => 'Use the getinfo command'],
31+
],
32+
'DefaultAction' => 'status'
33+
)
34+
)
35+
36+
register_options(
37+
[
38+
Opt::RPORT(27960)
39+
], self.class)
40+
end
41+
42+
def build_probe
43+
@probe ||= case action.name
44+
when 'status'
45+
getstatus
46+
when 'info'
47+
getinfo
48+
end
49+
end
50+
51+
def decode_stuff(response)
52+
case action.name
53+
when 'info'
54+
stuff = decode_info(response)
55+
when 'status'
56+
stuff = decode_status(response)
57+
end
58+
59+
if datastore['VERBOSE']
60+
stuff.inspect
61+
else
62+
# try to get the host name, game name and version
63+
stuff.select { |k,v| %w(hostname sv_hostname gamename com_gamename version).include?(k) }
64+
end
65+
end
66+
67+
def scanner_process(response, src_host, src_port)
68+
stuff = decode_stuff(response)
69+
return unless stuff
70+
@results[src_host] ||= []
71+
print_good("#{src_host}:#{src_port} found '#{stuff}'")
72+
@results[src_host] << stuff
73+
end
74+
75+
def scanner_postscan(_batch)
76+
@results.each_pair do |host, stuff|
77+
report_host(host: host)
78+
report_service(
79+
host: host,
80+
proto: 'udp',
81+
port: rport,
82+
name: 'Quake',
83+
info: stuff
84+
)
85+
end
86+
end
87+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
����infoResponse
2+
\voip\1\g_needpass\0\pure\1\gametype\0\sv_maxclients\8\g_humanplayers\0\clients\0\mapname\q3dm2\hostname\noname\protocol\68\gamename\Quake3Arena
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# -*- coding: binary -*-
2+
require 'spec_helper'
3+
require 'rex/proto/quake/message'
4+
5+
describe Rex::Proto::Quake 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('getinfo')
15+
expect(message).to eq("\xFF\xFF\xFF\xFFgetinfo")
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+
expect(subject.decode_message(subject.getstatus)).to eq('getstatus')
30+
end
31+
end
32+
33+
describe '#decode_infostring' do
34+
it 'should not decode things that are not infostrings' do
35+
expect(subject.decode_infostring('this is not an infostring')).to eq(nil)
36+
end
37+
38+
it 'should properly decode infostrings' do
39+
expect(subject.decode_infostring('a\1\b\2\c\blah')).to eq({'a' => '1', 'b' => '2', 'c' => 'blah'})
40+
end
41+
end
42+
43+
describe '#decode_response' do
44+
it 'should raise when server-side errors are encountered' do
45+
expect {
46+
subject.decode_response(subject.encode_message("print\nsomeerror\n"))
47+
}.to raise_error(::ArgumentError)
48+
end
49+
end
50+
51+
describe '#decode_info' do
52+
it 'should decode info responses properly' do
53+
expected_info = {
54+
"clients" => "0",
55+
"g_humanplayers" => "0",
56+
"g_needpass" => "0",
57+
"gamename" => "Quake3Arena",
58+
"gametype" => "0",
59+
"hostname" => "noname",
60+
"mapname" => "q3dm2",
61+
"protocol" => "68",
62+
"pure" => "1",
63+
"sv_maxclients" => "8",
64+
"voip" => "1"
65+
}
66+
actual_info = subject.decode_info(IO.read(File.join(File.dirname(__FILE__), 'info_response.bin')))
67+
expect(actual_info).to eq(expected_info)
68+
end
69+
end
70+
71+
describe '#decode_status' do
72+
it 'should decode status responses properly' do
73+
expected_status = {
74+
"bot_minplayers" => "0",
75+
"capturelimit" => "8",
76+
"com_gamename" => "Quake3Arena",
77+
"com_protocol" => "71",
78+
"dmflags" => "0",
79+
"fraglimit" => "30",
80+
"g_gametype" => "0",
81+
"g_maxGameClients" => "0",
82+
"g_needpass" => "0",
83+
"gamename" => "baseq3",
84+
"mapname" => "q3dm2",
85+
"sv_allowDownload" => "0",
86+
"sv_dlRate" => "100",
87+
"sv_floodProtect" => "1",
88+
"sv_hostname" => "noname",
89+
"sv_maxPing" => "0",
90+
"sv_maxRate" => "10000",
91+
"sv_maxclients" => "8",
92+
"sv_minPing" => "0",
93+
"sv_minRate" => "0",
94+
"sv_privateClients" => "0",
95+
"timelimit" => "25",
96+
"version" => "ioq3 1.36+svn2202-1/Ubuntu linux-x86_64 Dec 12 2011"
97+
}
98+
actual_status = subject.decode_status(IO.read(File.join(File.dirname(__FILE__), 'status_response.bin')))
99+
expect(actual_status).to eq(expected_status)
100+
end
101+
end
102+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
����statusResponse
2+
\capturelimit\8\g_maxGameClients\0\sv_floodProtect\1\sv_maxPing\0\sv_minPing\0\sv_dlRate\100\sv_maxRate\10000\sv_minRate\0\sv_maxclients\8\sv_hostname\noname\timelimit\25\fraglimit\30\dmflags\0\version\ioq3 1.36+svn2202-1/Ubuntu linux-x86_64 Dec 12 2011\com_gamename\Quake3Arena\com_protocol\71\g_gametype\0\mapname\q3dm2\sv_privateClients\0\sv_allowDownload\0\bot_minplayers\0\gamename\baseq3\g_needpass\0

0 commit comments

Comments
 (0)