summaryrefslogtreecommitdiffstats
path: root/scripts/quake3-master-getservers.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/quake3-master-getservers.nse')
-rw-r--r--scripts/quake3-master-getservers.nse249
1 files changed, 249 insertions, 0 deletions
diff --git a/scripts/quake3-master-getservers.nse b/scripts/quake3-master-getservers.nse
new file mode 100644
index 0000000..aeade67
--- /dev/null
+++ b/scripts/quake3-master-getservers.nse
@@ -0,0 +1,249 @@
+local ipOps = require "ipOps"
+local nmap = require "nmap"
+local shortport = require "shortport"
+local stdnse = require "stdnse"
+local string = require "string"
+local stringaux = require "stringaux"
+local tab = require "tab"
+local table = require "table"
+
+description = [[
+Queries Quake3-style master servers for game servers (many games other than Quake 3 use this same protocol).
+]]
+
+---
+-- @usage
+-- nmap -sU -p 27950 --script=quake3-master-getservers <target>
+-- @output
+-- PORT STATE SERVICE REASON
+-- 27950/udp open quake3-master
+-- | quake3-master-getservers:
+-- | 192.0.2.22:26002 Xonotic (Xonotic 3)
+-- | 203.0.113.37:26000 Nexuiz (Nexuiz 3)
+-- |_ Only 2 shown. Use --script-args quake3-master-getservers.outputlimit=-1 to see all.
+--
+-- @args quake3-master-getservers.outputlimit If set, limits the amount of
+-- hosts returned by the script. All discovered hosts are still
+-- stored in the registry for other scripts to use. If set to 0 or
+-- less, all files are shown. The default value is 10.
+
+author = "Toni Ruottu"
+license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
+categories = {"default", "discovery", "safe"}
+
+portrule = shortport.port_or_service ({20110, 20510, 27950, 30710}, "quake3-master", {"udp"})
+postrule = function()
+ return (nmap.registry.q3m_servers ~= nil)
+end
+
+-- There are various sources for this information. These include:
+-- - http://svn.icculus.org/twilight/trunk/dpmaster/readme.txt?view=markup
+-- - http://openarena.wikia.com/wiki/Changes
+-- - http://dpmaster.deathmask.net/
+-- - qstat-2.11, qstat.cfg
+-- - scanning master servers
+-- - looking at game traffic with Wireshark
+local KNOWN_PROTOCOLS = {
+ ["5"] = "Call of Duty",
+ ["10"] = "unknown",
+ ["43"] = "unknown",
+ ["48"] = "unknown",
+ ["50"] = "Return to Castle Wolfenstein",
+ ["57"] = "unknown",
+ ["59"] = "Return to Castle Wolfenstein",
+ ["60"] = "Return to Castle Wolfenstein",
+ ["66"] = "Quake III Arena",
+ ["67"] = "Quake III Arena",
+ ["68"] = "Quake III Arena, or Urban Terror",
+ ["69"] = "OpenArena, or Tremulous",
+ ["70"] = "unknown",
+ ["71"] = "OpenArena",
+ ["72"] = "Wolfenstein: Enemy Territory",
+ ["80"] = "Wolfenstein: Enemy Territory",
+ ["83"] = "Wolfenstein: Enemy Territory",
+ ["84"] = "Wolfenstein: Enemy Territory",
+ ["2003"] = "Soldier of Fortune II: Double Helix",
+ ["2004"] = "Soldier of Fortune II: Double Helix",
+ ["DarkPlaces-Quake 3"] = "DarkPlaces Quake",
+ ["Nexuiz 3"] = "Nexuiz",
+ ["Transfusion 3"] = "Transfusion",
+ ["Warsow 8"] = "Warsow",
+ ["Xonotic 3"] = "Xonotic",
+}
+
+local function getservers(host, port, q3protocol)
+ local socket = nmap.new_socket()
+ socket:set_timeout(10000)
+ local status, err = socket:connect(host, port)
+ if not status then
+ return {}
+ end
+ local probe = string.format("\xff\xff\xff\xffgetservers %s empty full\n", q3protocol)
+ socket:send(probe)
+
+ local data
+ status, data = socket:receive() -- get some data
+ if not status then
+ return {}
+ end
+ nmap.set_port_state(host, port, "open")
+
+ local magic = "\xff\xff\xff\xffgetserversResponse"
+ local tmp
+ while #data < #magic do -- get header
+ status, tmp = socket:receive()
+ if status then
+ data = data .. tmp
+ end
+ end
+ if string.sub(data, 1, #magic) ~= magic then -- no match
+ return {}
+ end
+
+ port.version.name = "quake3-master"
+ nmap.set_port_version(host, port)
+
+ local EOT = "EOT\0\0\0"
+ local pieces = stringaux.strsplit("\\", data)
+ while pieces[#pieces] ~= EOT do -- get all data
+ status, tmp = socket:receive()
+ if status then
+ data = data .. tmp
+ pieces = stringaux.strsplit("\\", data)
+ end
+ end
+
+ table.remove(pieces, 1) --remove magic
+ table.remove(pieces, #pieces) --remove EOT
+
+ local servers = {}
+ for _, value in ipairs(pieces) do
+ local ip, port = string.unpack("c4 >I2", value)
+ table.insert(servers, {ipOps.str_to_ip(ip), port})
+ end
+ socket:close()
+ return servers
+end
+
+local function formatresult(servers, outputlimit, protocols)
+ local t = tab.new()
+
+ if not outputlimit then
+ outputlimit = #servers
+ end
+ for i = 1, outputlimit do
+ if not servers[i] then
+ break
+ end
+ local node = servers[i]
+ local protocol = node.protocol
+ local ip = node.ip
+ local portnum = node.port
+ tab.addrow(t, string.format('%s:%d', ip, portnum), string.format('%s (%s)', protocols[protocol], protocol))
+ end
+
+ return tab.dump(t)
+end
+
+local function dropdupes(tables, stringify)
+ local unique = {}
+ local dupe = {}
+ local s
+ for _, v in ipairs(tables) do
+ s = stringify(v)
+ if not dupe[s] then
+ table.insert(unique, v)
+ dupe[s] = true
+ end
+ end
+ return unique
+end
+
+local function scan(host, port, protocols)
+ local discovered = {}
+ for protocol, _ in pairs(protocols) do
+ for _, node in ipairs(getservers(host, port, protocol)) do
+ local entry = {
+ protocol = protocol,
+ ip = node[1],
+ port = node[2],
+ masterip = host.ip,
+ masterport = port.number
+ }
+ table.insert(discovered, entry)
+ end
+ end
+ return discovered
+end
+
+local function store(servers)
+ if not nmap.registry.q3m_servers then
+ nmap.registry.q3m_servers = {}
+ end
+ for _, server in ipairs(servers) do
+ table.insert(nmap.registry.q3m_servers, server)
+ end
+end
+
+local function protocols()
+ local filter = {}
+ local count = {}
+ for _, advert in ipairs(nmap.registry.q3m_servers) do
+ local key = table.concat({advert.ip, advert.port, advert.protocol}, ":")
+ if filter[key] == nil then
+ if count[advert.protocol] == nil then
+ count[advert.protocol] = 0
+ end
+ count[advert.protocol] = count[advert.protocol] + 1
+ filter[key] = true
+ end
+ local mkey = table.concat({advert.masterip, advert.masterport}, ":")
+ end
+ local sortable = {}
+ for k, v in pairs(count) do
+ table.insert(sortable, {k, v})
+ end
+ table.sort(sortable, function(a, b) return a[2] > b[2] or (a[2] == b[2] and a[1] > b[1]) end)
+ local t = tab.new()
+ tab.addrow(t, '#', 'PROTOCOL', 'GAME', 'SERVERS')
+ for i, p in ipairs(sortable) do
+ local pos = i .. '.'
+ local protocol = p[1]
+ count = p[2]
+ local game = KNOWN_PROTOCOLS[protocol]
+ if game == "unknown" then
+ game = ""
+ end
+ tab.addrow(t, pos, protocol, game, count)
+ end
+ return '\n' .. tab.dump(t)
+end
+
+action = function(host, port)
+ if SCRIPT_TYPE == "postrule" then
+ return protocols()
+ end
+ local outputlimit = nmap.registry.args[SCRIPT_NAME .. ".outputlimit"]
+ if not outputlimit then
+ outputlimit = 10
+ else
+ outputlimit = tonumber(outputlimit)
+ end
+ if outputlimit < 1 then
+ outputlimit = nil
+ end
+ local servers = scan(host, port, KNOWN_PROTOCOLS)
+ store(servers)
+ local unique = dropdupes(servers, function(t) return string.format("%s: %s:%d", t.protocol, t.ip, t.port) end)
+ local formatted = formatresult(unique, outputlimit, KNOWN_PROTOCOLS)
+ if #formatted < 1 then
+ return
+ end
+ local response = {}
+ table.insert(response, formatted)
+ if outputlimit and outputlimit < #servers then
+ table.insert(response, string.format('Only %d/%d shown. Use --script-args %s.outputlimit=-1 to see all.', outputlimit, #servers, SCRIPT_NAME))
+ end
+ return stdnse.format_output(true, response)
+end
+