Skip to content

Commit a521d46

Browse files
committed
Land rapid7#4194, Quake protocol support
2 parents d207345 + ebf6fe4 commit a521d46

File tree

6 files changed

+272
-0
lines changed

6 files changed

+272
-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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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
21+
information from a Quakeserver.
22+
),
23+
'Author' => 'Jon Hart <jon_hart[at]rapid7.com',
24+
'References' =>
25+
[
26+
['URL', 'ftp://ftp.idsoftware.com/idstuff/quake3/docs/server.txt']
27+
],
28+
'License' => MSF_LICENSE,
29+
'Actions' => [
30+
['status', 'Description' => 'Use the getstatus command'],
31+
['info', 'Description' => 'Use the getinfo command']
32+
],
33+
'DefaultAction' => 'status'
34+
)
35+
)
36+
37+
register_options(
38+
[
39+
Opt::RPORT(27960)
40+
], self.class)
41+
end
42+
43+
def build_probe
44+
@probe ||= case action.name
45+
when 'status'
46+
getstatus
47+
when 'info'
48+
getinfo
49+
end
50+
end
51+
52+
def decode_stuff(response)
53+
case action.name
54+
when 'info'
55+
stuff = decode_info(response)
56+
when 'status'
57+
stuff = decode_status(response)
58+
end
59+
60+
if datastore['VERBOSE']
61+
stuff.inspect
62+
else
63+
# try to get the host name, game name and version
64+
stuff.select { |k, _| %w(hostname sv_hostname gamename com_gamename version).include?(k) }
65+
end
66+
end
67+
68+
def scanner_process(response, src_host, src_port)
69+
stuff = decode_stuff(response)
70+
return unless stuff
71+
@results[src_host] ||= []
72+
print_good("#{src_host}:#{src_port} found '#{stuff}'")
73+
@results[src_host] << stuff
74+
end
75+
76+
def scanner_postscan(_batch)
77+
@results.each_pair do |host, stuff|
78+
report_host(host: host)
79+
report_service(
80+
host: host,
81+
proto: 'udp',
82+
port: rport,
83+
name: 'Quake',
84+
info: stuff
85+
)
86+
end
87+
end
88+
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: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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(
40+
'a' => '1', 'b' => '2', 'c' => 'blah'
41+
)
42+
end
43+
end
44+
45+
describe '#decode_response' do
46+
it 'should raise when server-side errors are encountered' do
47+
expect do
48+
subject.decode_response(subject.encode_message("print\nsomeerror\n"))
49+
end.to raise_error(::ArgumentError)
50+
end
51+
end
52+
53+
describe '#decode_info' do
54+
it 'should decode info responses properly' do
55+
expected_info = {
56+
"clients" => "0",
57+
"g_humanplayers" => "0",
58+
"g_needpass" => "0",
59+
"gamename" => "Quake3Arena",
60+
"gametype" => "0",
61+
"hostname" => "noname",
62+
"mapname" => "q3dm2",
63+
"protocol" => "68",
64+
"pure" => "1",
65+
"sv_maxclients" => "8",
66+
"voip" => "1"
67+
}
68+
actual_info = subject.decode_info(IO.read(File.join(File.dirname(__FILE__), 'info_response.bin')))
69+
expect(actual_info).to eq(expected_info)
70+
end
71+
end
72+
73+
describe '#decode_status' do
74+
it 'should decode status responses properly' do
75+
expected_status = {
76+
"bot_minplayers" => "0",
77+
"capturelimit" => "8",
78+
"com_gamename" => "Quake3Arena",
79+
"com_protocol" => "71",
80+
"dmflags" => "0",
81+
"fraglimit" => "30",
82+
"g_gametype" => "0",
83+
"g_maxGameClients" => "0",
84+
"g_needpass" => "0",
85+
"gamename" => "baseq3",
86+
"mapname" => "q3dm2",
87+
"sv_allowDownload" => "0",
88+
"sv_dlRate" => "100",
89+
"sv_floodProtect" => "1",
90+
"sv_hostname" => "noname",
91+
"sv_maxPing" => "0",
92+
"sv_maxRate" => "10000",
93+
"sv_maxclients" => "8",
94+
"sv_minPing" => "0",
95+
"sv_minRate" => "0",
96+
"sv_privateClients" => "0",
97+
"timelimit" => "25",
98+
"version" => "ioq3 1.36+svn2202-1/Ubuntu linux-x86_64 Dec 12 2011"
99+
}
100+
actual_status = subject.decode_status(IO.read(File.join(File.dirname(__FILE__), 'status_response.bin')))
101+
expect(actual_status).to eq(expected_status)
102+
end
103+
end
104+
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)