Land #4194, Quake protocol support
commit
a521d469ed
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
require 'rex/proto/quake/message'
|
|
@ -0,0 +1,73 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
module Rex
|
||||
module Proto
|
||||
##
|
||||
#
|
||||
# Quake 3 protocol, taken from ftp://ftp.idsoftware.com/idstuff/quake3/docs/server.txt
|
||||
#
|
||||
##
|
||||
module Quake
|
||||
HEADER = 0xFFFFFFFF
|
||||
|
||||
def decode_message(message)
|
||||
# minimum size is header (4) + <command> + <stuff>
|
||||
return if message.length < 7
|
||||
header = message.unpack('N')[0]
|
||||
return if header != HEADER
|
||||
message[4, message.length]
|
||||
end
|
||||
|
||||
def encode_message(payload)
|
||||
[HEADER].pack('N') + payload
|
||||
end
|
||||
|
||||
def getstatus
|
||||
encode_message('getstatus')
|
||||
end
|
||||
|
||||
def getinfo
|
||||
encode_message('getinfo')
|
||||
end
|
||||
|
||||
def decode_infostring(infostring)
|
||||
# decode an "infostring", which is just a (supposedly) quoted string of tokens separated
|
||||
# by backslashes, generally terminated with a newline
|
||||
token_re = /([^\\]+)\\([^\\]+)/
|
||||
return nil unless infostring =~ token_re
|
||||
# remove possibly present leading/trailing double quote
|
||||
infostring.gsub!(/(?:^"|"$)/, '')
|
||||
# remove the trailing \n, if present
|
||||
infostring.gsub!(/\n$/, '')
|
||||
# split on backslashes and group into key value pairs
|
||||
infohash = {}
|
||||
infostring.scan(token_re).each do |kv|
|
||||
infohash[kv.first] = kv.last
|
||||
end
|
||||
infohash
|
||||
end
|
||||
|
||||
def decode_response(message, type)
|
||||
resp = decode_message(message)
|
||||
if /^print\n(?<error>.*)\n?/m =~ resp
|
||||
# XXX: is there a better exception to throw here?
|
||||
fail ::ArgumentError, "#{type} error: #{error}"
|
||||
# why doesn't this work?
|
||||
# elsif /^#{type}Response\n(?<infostring>.*)/m =~ resp
|
||||
elsif resp =~ /^#{type}Response\n(.*)/m
|
||||
decode_infostring(Regexp.last_match(1))
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def decode_status(message)
|
||||
decode_response(message, 'status')
|
||||
end
|
||||
|
||||
def decode_info(message)
|
||||
decode_response(message, 'info')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,88 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'rex/proto/quake'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Auxiliary::UDPScanner
|
||||
include Rex::Proto::Quake
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'Gather Quake Server Information',
|
||||
'Description' => %q(
|
||||
This module uses the getstatus or getinfo request to obtain
|
||||
information from a Quakeserver.
|
||||
),
|
||||
'Author' => 'Jon Hart <jon_hart[at]rapid7.com',
|
||||
'References' =>
|
||||
[
|
||||
['URL', 'ftp://ftp.idsoftware.com/idstuff/quake3/docs/server.txt']
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'Actions' => [
|
||||
['status', 'Description' => 'Use the getstatus command'],
|
||||
['info', 'Description' => 'Use the getinfo command']
|
||||
],
|
||||
'DefaultAction' => 'status'
|
||||
)
|
||||
)
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(27960)
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def build_probe
|
||||
@probe ||= case action.name
|
||||
when 'status'
|
||||
getstatus
|
||||
when 'info'
|
||||
getinfo
|
||||
end
|
||||
end
|
||||
|
||||
def decode_stuff(response)
|
||||
case action.name
|
||||
when 'info'
|
||||
stuff = decode_info(response)
|
||||
when 'status'
|
||||
stuff = decode_status(response)
|
||||
end
|
||||
|
||||
if datastore['VERBOSE']
|
||||
stuff.inspect
|
||||
else
|
||||
# try to get the host name, game name and version
|
||||
stuff.select { |k, _| %w(hostname sv_hostname gamename com_gamename version).include?(k) }
|
||||
end
|
||||
end
|
||||
|
||||
def scanner_process(response, src_host, src_port)
|
||||
stuff = decode_stuff(response)
|
||||
return unless stuff
|
||||
@results[src_host] ||= []
|
||||
print_good("#{src_host}:#{src_port} found '#{stuff}'")
|
||||
@results[src_host] << stuff
|
||||
end
|
||||
|
||||
def scanner_postscan(_batch)
|
||||
@results.each_pair do |host, stuff|
|
||||
report_host(host: host)
|
||||
report_service(
|
||||
host: host,
|
||||
proto: 'udp',
|
||||
port: rport,
|
||||
name: 'Quake',
|
||||
info: stuff
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,2 @@
|
|||
ÿÿÿÿinfoResponse
|
||||
\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
|
|
@ -0,0 +1,104 @@
|
|||
# -*- coding: binary -*-
|
||||
require 'spec_helper'
|
||||
require 'rex/proto/quake/message'
|
||||
|
||||
describe Rex::Proto::Quake 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('getinfo')
|
||||
expect(message).to eq("\xFF\xFF\xFF\xFFgetinfo")
|
||||
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
|
||||
expect(subject.decode_message(subject.getstatus)).to eq('getstatus')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#decode_infostring' do
|
||||
it 'should not decode things that are not infostrings' do
|
||||
expect(subject.decode_infostring('this is not an infostring')).to eq(nil)
|
||||
end
|
||||
|
||||
it 'should properly decode infostrings' do
|
||||
expect(subject.decode_infostring('a\1\b\2\c\blah')).to eq(
|
||||
'a' => '1', 'b' => '2', 'c' => 'blah'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#decode_response' do
|
||||
it 'should raise when server-side errors are encountered' do
|
||||
expect do
|
||||
subject.decode_response(subject.encode_message("print\nsomeerror\n"))
|
||||
end.to raise_error(::ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#decode_info' do
|
||||
it 'should decode info responses properly' do
|
||||
expected_info = {
|
||||
"clients" => "0",
|
||||
"g_humanplayers" => "0",
|
||||
"g_needpass" => "0",
|
||||
"gamename" => "Quake3Arena",
|
||||
"gametype" => "0",
|
||||
"hostname" => "noname",
|
||||
"mapname" => "q3dm2",
|
||||
"protocol" => "68",
|
||||
"pure" => "1",
|
||||
"sv_maxclients" => "8",
|
||||
"voip" => "1"
|
||||
}
|
||||
actual_info = subject.decode_info(IO.read(File.join(File.dirname(__FILE__), 'info_response.bin')))
|
||||
expect(actual_info).to eq(expected_info)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#decode_status' do
|
||||
it 'should decode status responses properly' do
|
||||
expected_status = {
|
||||
"bot_minplayers" => "0",
|
||||
"capturelimit" => "8",
|
||||
"com_gamename" => "Quake3Arena",
|
||||
"com_protocol" => "71",
|
||||
"dmflags" => "0",
|
||||
"fraglimit" => "30",
|
||||
"g_gametype" => "0",
|
||||
"g_maxGameClients" => "0",
|
||||
"g_needpass" => "0",
|
||||
"gamename" => "baseq3",
|
||||
"mapname" => "q3dm2",
|
||||
"sv_allowDownload" => "0",
|
||||
"sv_dlRate" => "100",
|
||||
"sv_floodProtect" => "1",
|
||||
"sv_hostname" => "noname",
|
||||
"sv_maxPing" => "0",
|
||||
"sv_maxRate" => "10000",
|
||||
"sv_maxclients" => "8",
|
||||
"sv_minPing" => "0",
|
||||
"sv_minRate" => "0",
|
||||
"sv_privateClients" => "0",
|
||||
"timelimit" => "25",
|
||||
"version" => "ioq3 1.36+svn2202-1/Ubuntu linux-x86_64 Dec 12 2011"
|
||||
}
|
||||
actual_status = subject.decode_status(IO.read(File.join(File.dirname(__FILE__), 'status_response.bin')))
|
||||
expect(actual_status).to eq(expected_status)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,2 @@
|
|||
ÿÿÿÿstatusResponse
|
||||
\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
|
Loading…
Reference in New Issue