From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- scripts/quake1-info.nse | 319 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 scripts/quake1-info.nse (limited to 'scripts/quake1-info.nse') diff --git a/scripts/quake1-info.nse b/scripts/quake1-info.nse new file mode 100644 index 0000000..5bacbaa --- /dev/null +++ b/scripts/quake1-info.nse @@ -0,0 +1,319 @@ +local comm = require "comm" +local nmap = require "nmap" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" + +description = [[ +Extracts information from Quake game servers and other game servers +which use the same protocol. + +Quake uses UDP packets, which because of source spoofing can be used to amplify +a denial-of-service attack. For each request, the script reports the payload +amplification as a ratio. The format used is +response_bytes/request_bytes=ratio + +http://www.gamers.org/dEngine/quake/QDP/qnp.html +]] + +--- +-- @usage +-- nmap -n -sU -Pn --script quake1-info -pU:26000-26004 -- +-- +-- @output +-- PORT STATE SERVICE +-- 26000/udp open quake +-- | quake1-info: +-- | server info exchange payload amplification: 59/12=4.916667 +-- | listen address: 10.200.200.10:26000 +-- | server name: An anonymous Debian server +-- | level name: dm1 +-- | players: 1/8 +-- | player table +-- | player 1: fragmeister +-- | player info exchange payload amplification: 49/6=8.166667 +-- | client address: 192.168.0.10:40430 +-- | connect time: 55587 secs +-- | frags: -1 +-- | shirt: green3 +-- | pants: orange6 +-- |_ protocol version: released (0x3) +-- +-- @xmloutput +-- 59/12=4.916667 +-- 10.200.200.10:26000 +-- An anonymous Debian server +-- dm1 +-- 1/8 +-- +--
+-- 49/6=8.166667 +-- fragmeister +-- 192.168.0.10:40430 +-- 55587 secs +-- -1 +-- green3 +-- orange6 +--
+-- +-- released (0x3) + + +categories = {"default", "discovery", "safe", "version"} +author = "Ulrik Haugen" +copyright = "Linköpings universitet 2014, Ulrik Haugen 2014" +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" + + +--- Proceed with action on open/open|filtered udp ports in interval +-- [26000, 26004] and whatever Quake is listed under in nmap-services. +function portrule(host, port) + return (port.state == 'open' or port.state == 'open|filtered') + and port.protocol == 'udp' + and ((26000 <= port.number and port.number <= 26004) + or port.service == 'quake') + and nmap.version_intensity() >= 7 +end + + +--- Like assert but put /message/ in the ERROR key in /results_table/ to +-- better suit collate_results and pass 0 as level to error to ensure +-- the error message will not be prefixed with file and line number. +-- /results_table/ may be left out. +local function assert_w_table(condition, message, results_table) + if condition then + return condition + else + results_table = results_table or {} + results_table.ERROR = message + error(results_table, 0) + end +end + + +-- Protocol constants and tables. +local ctrl_pkt_type = 0x8000 +local ccreq_server_info = 0x02 +local ccrep_server_info = 0x83 +local ccreq_player_info = 0x03 +local ccrep_player_info = 0x84 +local game_name = "QUAKE" +local net_protocol_versions = { + [ 0x01 ] = "qtest1", + [ 0x02 ] = "unknown", + [ 0x03 ] = "released" +} +local net_protocol_released = 0x03 +local color_codes = { + [ 0x0 ] = "gray0", + [ 0x1 ] = "brown1", + [ 0x2 ] = "lavender2", + [ 0x3 ] = "green3", + [ 0x4 ] = "red4", + [ 0x5 ] = "light green5", + [ 0x6 ] = "orange6", + [ 0x7 ] = "light brown7", + [ 0x8 ] = "violet8", + [ 0x9 ] = "pink9", + [ 0xa ] = "beige10", + [ 0xb ] = "green11", + [ 0xc ] = "yellow12", + [ 0xd ] = "blue13" +} + + +--- Request player info from /host/:/port/ for player /id/, return +-- player info as a table on success and raise an error on failure. +local function get_player_info(host, port, id) + local player_info = stdnse.output_table() + local req_pl = string.pack('>I2 I2 BB', + ctrl_pkt_type, -- packet type + 2+2+1+1, -- packet length + ccreq_player_info, -- operation code + id - 1) -- player number (0 indexed) + -- iptables -m u32 --u32 '0x1c=0x80000006&&0x1d&0xff=0x03' + + local status, rep_pl = comm.exchange(host, port, req_pl) + assert_w_table(status, "No response to request for player info") + assert_w_table(#rep_pl >= 4, "Response too small for packet header") + + player_info.player_ratio = string.format("%d/%d=%f", + rep_pl:len(), req_pl:len(), + rep_pl:len()/req_pl:len() ) + + local rep_pkt_type, rep_pl_len, pos = string.unpack('>I2 I2', rep_pl) + assert_w_table(rep_pl_len == rep_pl:len(), + string.format("Incorrect reply packet length: %d" + .. " received, %d bytes in packet", + rep_pl_len, rep_pl:len()), + player_info) + local term_pos = rep_pl_len + 1 + assert_w_table(rep_pkt_type == ctrl_pkt_type, + "Bad reply packet type", player_info) + + -- frags and connect_time are sent little endian: + local rep_opc, player_id, name, colors, frags, connect_time, client_address, pos = string.unpack('>BBzBxxxz', rep_pl, pos) + assert_w_table(pos == term_pos, "Error parsing reply (packet type/ length)", + player_info) + assert_w_table(rep_opc == ccrep_player_info, + string.format("Incorrect operation code 0x%x in reply," + .. " should be 0x%x", + rep_opc, ccrep_player_info), + player_info) + + player_info.name = name + player_info.client_address = client_address + player_info.connect_time = string.format("%d secs", connect_time) + player_info.frags = frags + player_info.shirt = color_codes[colors >> 4] or "INVALID" + player_info.pants = color_codes[colors & 0x0f] or "INVALID" + return player_info +end + + +--- Request player info from /host/:/port/ for players [1, +-- /cur_players/], return player infos or errors in a table. +local function get_player_table(host, port, cur_players) + local player_table = stdnse.output_table() + for id = 1, cur_players do + -- At this point we have established that the target is a Quake + -- game server so lost ccreq or ccrep player info packets are + -- merely noted in the output, they don't abort the script. + local status, player_info = pcall(get_player_info, host, port, id) + player_table[string.format("player %d", id)] = player_info + end + return player_table +end + + +--- Request server info and possibly player infos from /host/:/port/, +-- return server info and any player infos as a table on success and +-- raise an error on failure. +local function get_server_info(host, port) + local server_info = stdnse.output_table() + local req_pl = string.pack('>I2I2BzB', + ctrl_pkt_type, -- packet type + 2+2+1+game_name:len()+1+1, -- packet length + ccreq_server_info, -- operation code + game_name, + net_protocol_released) -- net protocol version + -- iptables -m u32 --u32 '0x1c=0x8000000c&&0x20=0x02515541&&0x24=0x4b450003' + + local status, rep_pl = comm.exchange(host, port, req_pl) + assert_w_table(status, "No response to request for server info") + assert_w_table(#rep_pl >= 4, "Response too small for packet header") + + nmap.set_port_state(host, port, 'open') + server_info.server_ratio = string.format("%d/%d=%f", + rep_pl:len(), req_pl:len(), + rep_pl:len()/req_pl:len()) + + local rep_pkt_type, rep_pl_len, pos = string.unpack('>I2 I2', rep_pl) + assert_w_table(rep_pkt_type == ctrl_pkt_type, + string.format("Bad reply packet type 0x%x, expected 0x%x", + rep_pkt_type, ctrl_pkt_type), server_info) + assert_w_table(rep_pl_len == rep_pl:len(), + string.format("Bad reply packet length: %d received," + .. " %d bytes in packet", + rep_pl_len, rep_pl:len()), server_info) + local term_pos = rep_pl_len + 1 + + local rep_opc, pos = string.unpack('>B', rep_pl, pos) + assert_w_table(rep_opc == ccrep_server_info, + string.format("Bad operation code 0x%x in reply," + .. " expected 0x%x", + rep_opc, ccrep_server_info), server_info) + local server_address, server_host_name, level_name, cur_players, max_players, net_protocol_version, pos = string.unpack('>zzzBBB', rep_pl, pos) + assert_w_table(pos == term_pos, "Error parsing reply (packet type/length)", + server_info) + + port.version.name = "quake" + port.version.product = "Quake 1 server" + port.version.version = net_protocol_versions[net_protocol_version] + nmap.set_port_version(host, port) + + local player_table = get_player_table(host, port, cur_players) + + server_info.listen_address = server_address + server_info.server_name = server_host_name + server_info.level_name = level_name + server_info.players = string.format("%d/%d", cur_players, max_players) + server_info.player_table = player_table + server_info.protocol_version = string.format( + "%s (0x%x)", + net_protocol_versions[net_protocol_version], net_protocol_version) + return server_info +end + + +--- Return a function from structured to unstructured output indenting +-- nested tables /offset/ or two spaces with special treatment of name +-- keys and optionally using /xlate_key/ to format keys. +local function make_formatter(offset, xlate_key) + offset = offset or 2 + xlate_key = xlate_key or function(key) return key:gsub("_", " ") end + + --- Format /results_table/ as a string starting /indent/ or zero + -- steps from the margin for the name key and adding offset steps + -- for other table contents and again for the contents of nested + -- tables. + local function formatter(results_table, indent) + indent = indent or 0 + local output = {} + + if results_table.name then + table.insert(output, + string.format("%s%s", ({ [ false ] = ": ", + [ true ] = "\n" })[indent == 0], + results_table.name)) + end + + for key, value in pairs(results_table) do + -- name is printed already + if key ~= 'name' then + if type(value) == 'table' then + table.insert(output, + string.format("\n%s%s", + string.rep(" ", indent + offset), + xlate_key(key))) + table.insert(output, formatter(value, indent + offset)) + else + table.insert(output, + string.format("\n%s%s: %s", + string.rep(" ", indent + offset), + xlate_key(key), value)) + end + end + end + return table.concat(output, '') + end + + return formatter +end + + +--- Use /formatter/ to produce unstructured output from +-- /results_table/ considering /status/. Return structured and +-- unstructured output. +local function collate_results(formatter, status, results_table) + if not status and nmap.debugging() < 1 then + return nil + end + return results_table, formatter(results_table) +end + + +--- Nmap entry point. +function action(host, port) + local xlate_table = { + player_ratio = "player info exchange payload amplification", + server_ratio = "server info exchange payload amplification", + } + + local function xlate_key(key) + return xlate_table[key] or key:gsub("_", " ") + end + + return collate_results(make_formatter(nil, xlate_key), + pcall(get_server_info, host, port)) +end -- cgit v1.2.3