diff --git a/CHANGELOG b/CHANGELOG index c8d3e6533..c581293ef 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,9 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added quake3-info.nse by Toni Ruottu. This script gets + information about games and settings for a Quake 3 (or derived game) + server. + o [NSE] Made irc-info.nse handle the case where the MOTD is missing. Patch by Sebastian Dragomir. diff --git a/scripts/quake3-info.nse b/scripts/quake3-info.nse new file mode 100644 index 000000000..1b6f8dfe6 --- /dev/null +++ b/scripts/quake3-info.nse @@ -0,0 +1,232 @@ +description = [[ +Extracts information from a Quake3-like game server. +]] + +--- +-- @usage +-- nmap -sU -sV -Pn --script quake3-info.nse -p +-- +-- @output +-- PORT STATE SERVICE VERSION +-- 27960/udp open quake3 Quake 3 dedicated server +-- | quake3-info: +-- | +-- | PLAYERS: +-- | 1. cyberix (frags: 0/20, ping: 4) +-- | +-- | BASIC OPTIONS: +-- | capturelimit: 8 +-- | dmflags: 0 +-- | elimflags: 0 +-- | fraglimit: 20 +-- | gamename: baseoa +-- | mapname: oa_dm1 +-- | protocol: 71 +-- | timelimit: 0 +-- | version: ioq3 1.36+svn1933-1/Ubuntu linux-x86_64 Apr 4 2011 +-- | videoflags: 7 +-- | voteflags: 767 +-- | +-- | OTHER OPTIONS: +-- | bot_minplayers: 0 +-- | elimination_roundtime: 120 +-- | g_allowVote: 1 +-- | g_altExcellent: 0 +-- | g_delagHitscan: 0 +-- | g_doWarmup: 0 +-- | g_enableBreath: 0 +-- | g_enableDust: 0 +-- | g_gametype: 0 +-- | g_instantgib: 0 +-- | g_lms_mode: 0 +-- | g_maxGameClients: 0 +-- | g_needpass: 0 +-- | g_obeliskRespawnDelay: 10 +-- | g_rockets: 0 +-- | g_voteGametypes: /0/1/3/4/5/6/7/8/9/10/11/12/ +-- | g_voteMaxFraglimit: 0 +-- | g_voteMaxTimelimit: 0 +-- | g_voteMinFraglimit: 0 +-- | g_voteMinTimelimit: 0 +-- | sv_allowDownload: 0 +-- | sv_floodProtect: 1 +-- | sv_hostname: noname +-- | sv_maxPing: 0 +-- | sv_maxRate: 0 +-- | sv_maxclients: 8 +-- | sv_minPing: 0 +-- | sv_minRate: 0 +-- |_ sv_privateClients: 0 + +author = "Toni Ruottu" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"default", "discovery", "safe"} + +require "shortport" +require "stdnse" +require "comm" +require "bin" + +local function range(first, last) + local list = {} + for i = first, last do + table.insert(list, i) + end + return list +end + +portrule = shortport.port_or_service(range(27960, 27970), {'quake3'}, 'udp') + +local function parsefields(data) + local fields = {} + local parts = stdnse.strsplit("\\", data) + local nullprefix = table.remove(parts, 1) + if nullprefix ~= "" then + stdnse.print_debug(2, "unrecognized field format, skipping options") + return {} + end + for i = 1, #parts, 2 do + local key = parts[i] + local value = parts[i + 1] + fields[key] = value + end + return fields +end + +local function parsename(data) + parts = stdnse.strsplit('"', data) + if #parts ~= 3 then + return nil + end + local e1 = parts[1] + local name = parts[2] + local e2 = parts[3] + local extra = e1 .. e2 + if extra ~= "" then + return nil + end + return name +end + +local function parseplayer(data) + local parts = stdnse.strsplit(" ", data) + if #parts < 3 then + stdnse.print_debug(2, "player info line is missing elements, skipping a player") + return nil + end + if #parts > 3 then + stdnse.print_debug(2, "player info line has unknown elements, skipping a player") + return nil + end + local player = {} + player.frags = parts[1] + player.ping = parts[2] + player.name = parsename(parts[3]) + if player.name == nil then + stdnse.print_debug(2, "invalid player name serialization, skipping a player") + return nil + end + return player +end + +local function parseplayers(data) + local players = {} + for _, p in ipairs(data) do + local player = parseplayer(p) + if player then + table.insert(players, player) + end + end + return players +end + +local function is_leader(a, b) + local collide = a.name == b.name + local even = a.frags == b.frags + local leads = a.frags > b.frags + local alphab = a.name > b.name + local faster = a.ping > b.ping + return leads or (even and alphab) or (even and collide and faster) +end + +local function formatplayers(players, fraglimit) + table.sort(players, is_leader) + local printable = {} + for i, player in ipairs(players) do + local name = player.name + local ping = player.ping + local frags = player.frags + if fraglimit then + frags = string.format("%s/%s", frags, fraglimit) + end + table.insert(printable, string.format("%d. %s (frags: %s, ping: %s)", i, name, frags, ping)) + end + printable["name"] = "\nPLAYERS:" + return printable +end + +local function formatfields(fields, title) + local printable = {} + for key, value in pairs(fields) do + local kv = string.format("%s: %s", key, value) + table.insert(printable, kv) + end + table.sort(printable) + printable["name"] = title + return printable +end + +local function assorted(fields) + local basic = {} + local other = {} + for key, value in pairs(fields) do + if string.find(key, "_") == nil then + basic[key] = value + else + other[key] = value + end + end + return basic, other +end + +action = function(host, port) + local GETSTATUS = bin.pack("CCCCA", 0xff, 0xff, 0xff, 0xff, "getstatus\n") + local STATUSRESP = bin.pack("CCCCA", 0xff, 0xff, 0xff, 0xff, "statusResponse") + + local status, data = comm.exchange(host, port, GETSTATUS, {["proto"] = "udp"}) + if not status then + return + end + local parts = stdnse.strsplit("\n", data) + local header = table.remove(parts, 1) + if header ~= STATUSRESP then + return + end + if #parts < 2 then + stdnse.print_debug(2, "incomplete status response, script abort") + return + end + local nullend = table.remove(parts) + if nullend ~= "" then + stdnse.print_debug(2, "missing terminating endline, script abort") + return + end + local field_data = table.remove(parts, 1) + local player_data = parts + + local fields = parsefields(field_data) + local players = parseplayers(player_data) + + local basic, other = assorted(fields) + + local fraglimit = fields["fraglimit"] + if not fraglimit then + fraglimit = "?" + end + + local response = {} + table.insert(response, formatplayers(players, fraglimit)) + table.insert(response, formatfields(basic, "\nBASIC OPTIONS:")) + table.insert(response, formatfields(other, "\nOTHER OPTIONS:")) + return stdnse.format_output(true, response) +end diff --git a/scripts/script.db b/scripts/script.db index 2930ae22d..dcd904f3d 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -171,6 +171,7 @@ Entry { filename = "pop3-brute.nse", categories = { "auth", "intrusive", } } Entry { filename = "pop3-capabilities.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "pptp-version.nse", categories = { "version", } } Entry { filename = "qscan.nse", categories = { "discovery", "safe", } } +Entry { filename = "quake3-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "quake3-master-getservers.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "realvnc-auth-bypass.nse", categories = { "auth", "default", "safe", } } Entry { filename = "resolveall.nse", categories = { "discovery", "safe", } }