666 lines
25 KiB
Plaintext
666 lines
25 KiB
Plaintext
|
local stdnse = require "stdnse"
|
||
|
local math = require "math"
|
||
|
local nmap = require "nmap"
|
||
|
local strbuf = require "strbuf"
|
||
|
local string = require "string"
|
||
|
local table = require "table"
|
||
|
local shortport = require "shortport"
|
||
|
|
||
|
description = [[
|
||
|
Detects the Ventrilo voice communication server service versions 2.1.2
|
||
|
and above and tries to determine version and configuration
|
||
|
information. Some of the older versions (pre 3.0.0) may not have the
|
||
|
UDP service that this probe relies on enabled by default.
|
||
|
|
||
|
The Ventrilo server listens on a TCP (voice/control) and an UDP (ping/status)
|
||
|
port with the same port number (fixed to 3784 in the free version, otherwise
|
||
|
configurable). This script activates on both a TCP and UDP port version scan.
|
||
|
In both cases probe data is sent only to the UDP port because it allows for a
|
||
|
simple and informative status command as implemented by the
|
||
|
<code>ventrilo_status.exe</code> executable which has shipped alongside the Windows server
|
||
|
package since version 2.1.2 when the UDP status service was implemented.
|
||
|
|
||
|
When run as a version detection script (<code>-sV</code>), the script will report on the
|
||
|
server version, name, uptime, authentication scheme, and OS. When run
|
||
|
explicitly (<code>--script ventrilo-info</code>), the script will additionally report on the
|
||
|
server name phonetic pronunciation string, the server comment, maximum number
|
||
|
of clients, voice codec, voice format, channel and client counts, and details
|
||
|
about channels and currently connected clients.
|
||
|
|
||
|
Original reversing of the protocol was done by Luigi Auriemma
|
||
|
(http://aluigi.altervista.org/papers.htm#ventrilo).
|
||
|
]]
|
||
|
|
||
|
---
|
||
|
-- @usage
|
||
|
-- nmap -sV <target>
|
||
|
-- @usage
|
||
|
-- nmap -Pn -sU -sV --script ventrilo-info -p <port> <target>
|
||
|
--
|
||
|
-- @output
|
||
|
-- PORT STATE SERVICE VERSION
|
||
|
-- 9408/tcp open ventrilo Ventrilo 3.0.3.C (voice port; name: TypeFrag.com; uptime: 152h:56m; auth: pw)
|
||
|
-- | ventrilo-info:
|
||
|
-- | name: TypeFrag.com
|
||
|
-- | phonetic: Type Frag Dot Com
|
||
|
-- | comment: http://www.typefrag.com/
|
||
|
-- | auth: pw
|
||
|
-- | max. clients: 100
|
||
|
-- | voice codec: 3,Speex
|
||
|
-- | voice format: 32,32 KHz%2C 16 bit%2C 10 Qlty
|
||
|
-- | uptime: 152h:56m
|
||
|
-- | platform: WIN32
|
||
|
-- | version: 3.0.3.C
|
||
|
-- | channel count: 14
|
||
|
-- | channel fields: CID, PID, PROT, NAME, COMM
|
||
|
-- | client count: 6
|
||
|
-- | client fields: ADMIN, CID, PHAN, PING, SEC, NAME, COMM
|
||
|
-- | channels:
|
||
|
-- | <top level lobby> (CID: 0, PID: n/a, PROT: n/a, COMM: n/a): <empty>
|
||
|
-- | Group 1 (CID: 719, PID: 0, PROT: 0, COMM: ):
|
||
|
-- | stabya (ADMIN: 0, PHAN: 0, PING: 47, SEC: 206304, COMM:
|
||
|
-- | Group 2 (CID: 720, PID: 0, PROT: 0, COMM: ): <empty>
|
||
|
-- | Group 3 (CID: 721, PID: 0, PROT: 0, COMM: ): <empty>
|
||
|
-- | Group 4 (CID: 722, PID: 0, PROT: 0, COMM: ): <empty>
|
||
|
-- | Group 5 (CID: 723, PID: 0, PROT: 0, COMM: ):
|
||
|
-- | Sir Master Win (ADMIN: 0, PHAN: 0, PING: 32, SEC: 186890, COMM:
|
||
|
-- | waterbukk (ADMIN: 0, PHAN: 0, PING: 31, SEC: 111387, COMM:
|
||
|
-- | likez (ADMIN: 0, PHAN: 0, PING: 140, SEC: 22457, COMM:
|
||
|
-- | Tweet (ADMIN: 0, PHAN: 0, PING: 140, SEC: 21009, COMM:
|
||
|
-- | Group 6 (CID: 724, PID: 0, PROT: 0, COMM: ): <empty>
|
||
|
-- | Raid (CID: 725, PID: 0, PROT: 0, COMM: ): <empty>
|
||
|
-- | Officers (CID: 726, PID: 0, PROT: 1, COMM: ): <empty>
|
||
|
-- | PG 13 (CID: 727, PID: 0, PROT: 0, COMM: ): <empty>
|
||
|
-- | Rated R (CID: 728, PID: 0, PROT: 0, COMM: ): <empty>
|
||
|
-- | Group 7 (CID: 729, PID: 0, PROT: 0, COMM: ): <empty>
|
||
|
-- | Group 8 (CID: 730, PID: 0, PROT: 0, COMM: ): <empty>
|
||
|
-- | Group 9 (CID: 731, PID: 0, PROT: 0, COMM: ): <empty>
|
||
|
-- | AFK - switch to this when AFK (CID: 732, PID: 0, PROT: 0, COMM: ):
|
||
|
-- |_ Eisennacher (ADMIN: 0, PHAN: 0, PING: 79, SEC: 181948, COMM:
|
||
|
-- Service Info: OS: WIN32
|
||
|
--
|
||
|
-- @xmloutput
|
||
|
-- <elem key="phonetic">Type Frag Dot Com</elem>
|
||
|
-- <elem key="comment">http://www.typefrag.com/</elem>
|
||
|
-- <elem key="auth">1</elem>
|
||
|
-- <elem key="maxclients">100</elem>
|
||
|
-- <elem key="voicecodec">3,Speex</elem>
|
||
|
-- <elem key="voiceformat">32,32 KHz%2C 16 bit%2C 10 Qlty</elem>
|
||
|
-- <elem key="uptime">551533</elem>
|
||
|
-- <elem key="platform">WIN32</elem>
|
||
|
-- <elem key="version">3.0.3.C</elem>
|
||
|
-- <elem key="channelcount">14</elem>
|
||
|
-- <table key="channelfields">
|
||
|
-- <elem>CID</elem>
|
||
|
-- <elem>PID</elem>
|
||
|
-- <elem>PROT</elem>
|
||
|
-- <elem>NAME</elem>
|
||
|
-- <elem>COMM</elem>
|
||
|
-- </table>
|
||
|
-- <table key="channels">
|
||
|
-- <table key="0">
|
||
|
-- <elem key="NAME"><top level lobby></elem>
|
||
|
-- <elem key="CID">0</elem>
|
||
|
-- </table>
|
||
|
-- <table key="363">
|
||
|
-- <elem key="CID">363</elem>
|
||
|
-- <elem key="PID">0</elem>
|
||
|
-- <elem key="PROT">0</elem>
|
||
|
-- <elem key="NAME">Group 1</elem>
|
||
|
-- <elem key="COMM"></elem>
|
||
|
-- <table key="clients">
|
||
|
-- <table>
|
||
|
-- <elem key="ADMIN">0</elem>
|
||
|
-- <elem key="CID">363</elem>
|
||
|
-- <elem key="PHAN">0</elem>
|
||
|
-- <elem key="PING">47</elem>
|
||
|
-- <elem key="SEC">207276</elem>
|
||
|
-- <elem key="NAME">stabya</elem>
|
||
|
-- <elem key="COMM"></elem>
|
||
|
-- </table>
|
||
|
-- </table>
|
||
|
-- </table>
|
||
|
-- <!-- Channels other than the first and last cut for brevity -->
|
||
|
-- <table key="376">
|
||
|
-- <elem key="CID">376</elem>
|
||
|
-- <elem key="PID">0</elem>
|
||
|
-- <elem key="PROT">0</elem>
|
||
|
-- <elem key="NAME">AFK - switch to this when AFK</elem>
|
||
|
-- <elem key="COMM"></elem>
|
||
|
-- <table key="clients">
|
||
|
-- <table>
|
||
|
-- <elem key="ADMIN">0</elem>
|
||
|
-- <elem key="CID">376</elem>
|
||
|
-- <elem key="PHAN">0</elem>
|
||
|
-- <elem key="PING">78</elem>
|
||
|
-- <elem key="SEC">182920</elem>
|
||
|
-- <elem key="NAME">Eisennacher</elem>
|
||
|
-- <elem key="COMM"></elem>
|
||
|
-- </table>
|
||
|
-- </table>
|
||
|
-- </table>
|
||
|
-- </table>
|
||
|
-- <elem key="clientcount">6</elem>
|
||
|
-- <table key="clientfields">
|
||
|
-- <elem>ADMIN</elem>
|
||
|
-- <elem>CID</elem>
|
||
|
-- <elem>PHAN</elem>
|
||
|
-- <elem>PING</elem>
|
||
|
-- <elem>SEC</elem>
|
||
|
-- <elem>NAME</elem>
|
||
|
-- <elem>COMM</elem>
|
||
|
-- </table>
|
||
|
|
||
|
author = "Marin Maržić"
|
||
|
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
|
||
|
categories = { "default", "discovery", "safe", "version" }
|
||
|
|
||
|
local crypt_head = {
|
||
|
0x80,0xe5,0x0e,0x38,0xba,0x63,0x4c,0x99,0x88,0x63,0x4c,0xd6,0x54,0xb8,0x65,0x7e,
|
||
|
0xbf,0x8a,0xf0,0x17,0x8a,0xaa,0x4d,0x0f,0xb7,0x23,0x27,0xf6,0xeb,0x12,0xf8,0xea,
|
||
|
0x17,0xb7,0xcf,0x52,0x57,0xcb,0x51,0xcf,0x1b,0x14,0xfd,0x6f,0x84,0x38,0xb5,0x24,
|
||
|
0x11,0xcf,0x7a,0x75,0x7a,0xbb,0x78,0x74,0xdc,0xbc,0x42,0xf0,0x17,0x3f,0x5e,0xeb,
|
||
|
0x74,0x77,0x04,0x4e,0x8c,0xaf,0x23,0xdc,0x65,0xdf,0xa5,0x65,0xdd,0x7d,0xf4,0x3c,
|
||
|
0x4c,0x95,0xbd,0xeb,0x65,0x1c,0xf4,0x24,0x5d,0x82,0x18,0xfb,0x50,0x86,0xb8,0x53,
|
||
|
0xe0,0x4e,0x36,0x96,0x1f,0xb7,0xcb,0xaa,0xaf,0xea,0xcb,0x20,0x27,0x30,0x2a,0xae,
|
||
|
0xb9,0x07,0x40,0xdf,0x12,0x75,0xc9,0x09,0x82,0x9c,0x30,0x80,0x5d,0x8f,0x0d,0x09,
|
||
|
0xa1,0x64,0xec,0x91,0xd8,0x8a,0x50,0x1f,0x40,0x5d,0xf7,0x08,0x2a,0xf8,0x60,0x62,
|
||
|
0xa0,0x4a,0x8b,0xba,0x4a,0x6d,0x00,0x0a,0x93,0x32,0x12,0xe5,0x07,0x01,0x65,0xf5,
|
||
|
0xff,0xe0,0xae,0xa7,0x81,0xd1,0xba,0x25,0x62,0x61,0xb2,0x85,0xad,0x7e,0x9d,0x3f,
|
||
|
0x49,0x89,0x26,0xe5,0xd5,0xac,0x9f,0x0e,0xd7,0x6e,0x47,0x94,0x16,0x84,0xc8,0xff,
|
||
|
0x44,0xea,0x04,0x40,0xe0,0x33,0x11,0xa3,0x5b,0x1e,0x82,0xff,0x7a,0x69,0xe9,0x2f,
|
||
|
0xfb,0xea,0x9a,0xc6,0x7b,0xdb,0xb1,0xff,0x97,0x76,0x56,0xf3,0x52,0xc2,0x3f,0x0f,
|
||
|
0xb6,0xac,0x77,0xc4,0xbf,0x59,0x5e,0x80,0x74,0xbb,0xf2,0xde,0x57,0x62,0x4c,0x1a,
|
||
|
0xff,0x95,0x6d,0xc7,0x04,0xa2,0x3b,0xc4,0x1b,0x72,0xc7,0x6c,0x82,0x60,0xd1,0x0d
|
||
|
}
|
||
|
|
||
|
local crypt_data = {
|
||
|
0x82,0x8b,0x7f,0x68,0x90,0xe0,0x44,0x09,0x19,0x3b,0x8e,0x5f,0xc2,0x82,0x38,0x23,
|
||
|
0x6d,0xdb,0x62,0x49,0x52,0x6e,0x21,0xdf,0x51,0x6c,0x76,0x37,0x86,0x50,0x7d,0x48,
|
||
|
0x1f,0x65,0xe7,0x52,0x6a,0x88,0xaa,0xc1,0x32,0x2f,0xf7,0x54,0x4c,0xaa,0x6d,0x7e,
|
||
|
0x6d,0xa9,0x8c,0x0d,0x3f,0xff,0x6c,0x09,0xb3,0xa5,0xaf,0xdf,0x98,0x02,0xb4,0xbe,
|
||
|
0x6d,0x69,0x0d,0x42,0x73,0xe4,0x34,0x50,0x07,0x30,0x79,0x41,0x2f,0x08,0x3f,0x42,
|
||
|
0x73,0xa7,0x68,0xfa,0xee,0x88,0x0e,0x6e,0xa4,0x70,0x74,0x22,0x16,0xae,0x3c,0x81,
|
||
|
0x14,0xa1,0xda,0x7f,0xd3,0x7c,0x48,0x7d,0x3f,0x46,0xfb,0x6d,0x92,0x25,0x17,0x36,
|
||
|
0x26,0xdb,0xdf,0x5a,0x87,0x91,0x6f,0xd6,0xcd,0xd4,0xad,0x4a,0x29,0xdd,0x7d,0x59,
|
||
|
0xbd,0x15,0x34,0x53,0xb1,0xd8,0x50,0x11,0x83,0x79,0x66,0x21,0x9e,0x87,0x5b,0x24,
|
||
|
0x2f,0x4f,0xd7,0x73,0x34,0xa2,0xf7,0x09,0xd5,0xd9,0x42,0x9d,0xf8,0x15,0xdf,0x0e,
|
||
|
0x10,0xcc,0x05,0x04,0x35,0x81,0xb2,0xd5,0x7a,0xd2,0xa0,0xa5,0x7b,0xb8,0x75,0xd2,
|
||
|
0x35,0x0b,0x39,0x8f,0x1b,0x44,0x0e,0xce,0x66,0x87,0x1b,0x64,0xac,0xe1,0xca,0x67,
|
||
|
0xb4,0xce,0x33,0xdb,0x89,0xfe,0xd8,0x8e,0xcd,0x58,0x92,0x41,0x50,0x40,0xcb,0x08,
|
||
|
0xe1,0x15,0xee,0xf4,0x64,0xfe,0x1c,0xee,0x25,0xe7,0x21,0xe6,0x6c,0xc6,0xa6,0x2e,
|
||
|
0x52,0x23,0xa7,0x20,0xd2,0xd7,0x28,0x07,0x23,0x14,0x24,0x3d,0x45,0xa5,0xc7,0x90,
|
||
|
0xdb,0x77,0xdd,0xea,0x38,0x59,0x89,0x32,0xbc,0x00,0x3a,0x6d,0x61,0x4e,0xdb,0x29
|
||
|
}
|
||
|
|
||
|
local crypt_crc = {
|
||
|
0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
|
||
|
0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,
|
||
|
0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6,
|
||
|
0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de,
|
||
|
0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485,
|
||
|
0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d,
|
||
|
0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4,
|
||
|
0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc,
|
||
|
0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823,
|
||
|
0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b,
|
||
|
0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12,
|
||
|
0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a,
|
||
|
0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41,
|
||
|
0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49,
|
||
|
0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70,
|
||
|
0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78,
|
||
|
0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f,
|
||
|
0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067,
|
||
|
0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e,
|
||
|
0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256,
|
||
|
0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d,
|
||
|
0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405,
|
||
|
0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c,
|
||
|
0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634,
|
||
|
0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab,
|
||
|
0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3,
|
||
|
0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a,
|
||
|
0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92,
|
||
|
0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9,
|
||
|
0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1,
|
||
|
0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8,
|
||
|
0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0
|
||
|
}
|
||
|
|
||
|
-- The probe payload is static as it has proven to be unnecessary to forge a new
|
||
|
-- one every time. The data used includes the following parameters:
|
||
|
-- cmd = 2, password = 0, header len = 20, data len = 16, totlen = 36
|
||
|
-- static 2 byte status request id (time(NULL) in the original protocol)
|
||
|
local static_probe_id = 0x33CF
|
||
|
local static_probe_payload = "\x49\xde\xdf\xd0\x65\xc9\x21\xc4\x90\x0d\xbf\x23\xa2\xc8\x8b\x65\x7d\x43\x15\x9b\x30\xc2\xe2\x23\xd2\x13\xe3\x29\xad\xe8\x63\xff\x17\x31\x33\x50"
|
||
|
|
||
|
-- Returns a string interpretation of the server authentication scheme.
|
||
|
-- @param auth the server authentication scheme code
|
||
|
-- @return string string interpretation of the server authentication scheme
|
||
|
local auth_str = function(auth)
|
||
|
if auth == "0" then
|
||
|
return "none"
|
||
|
elseif auth == "1" then
|
||
|
return "pw"
|
||
|
elseif auth == "2" then
|
||
|
return "user/pw"
|
||
|
else
|
||
|
return auth
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- Formats an uptime string containing a number of seconds.
|
||
|
-- E.g. "3670" -> "1h:1m"
|
||
|
-- @param uptime number of seconds of uptime
|
||
|
-- @return uptime_formatted formatted uptime string (hours and minutes)
|
||
|
local uptime_str = function(uptime)
|
||
|
local uptime_num = tonumber(uptime)
|
||
|
if not uptime_num then
|
||
|
return uptime
|
||
|
end
|
||
|
|
||
|
local h = math.floor(uptime_num/3600)
|
||
|
local m = math.floor((uptime_num - h*3600)/60)
|
||
|
|
||
|
return h .. "h:" .. m .. "m"
|
||
|
end
|
||
|
|
||
|
-- Decrypts the Ventrilo UDP status response header segment.
|
||
|
-- @param str the Ventrilo UDP status response
|
||
|
-- @return id status request id as sent by us
|
||
|
-- @return len length of the data segment of the response
|
||
|
-- @return totlen total length of data segments of all response packets
|
||
|
-- @return pck response packet number (starts with 0)
|
||
|
-- @return totpck total number of response packets to expect
|
||
|
-- @return key key for decrypting the data segment of this response packet
|
||
|
-- @return crc_sum the crc checksum of the full response data segment
|
||
|
local dec_head = function(str)
|
||
|
local head = { string.byte(str, 1, 20) }
|
||
|
|
||
|
head[1], head[2] = head[2], head[1]
|
||
|
local a1 = head[1]
|
||
|
if a1 == 0 then
|
||
|
return table.concat(head)
|
||
|
end
|
||
|
local a2 = head[2]
|
||
|
|
||
|
for i = 3,20 do
|
||
|
head[i] = head[i] - (crypt_head[a2 + 1] + ((i - 3) % 5)) & 0xFF
|
||
|
a2 = (a2 + a1) & 0xFF
|
||
|
end
|
||
|
|
||
|
for i = 3,19,2 do
|
||
|
head[i], head[i + 1] = head[i + 1], head[i]
|
||
|
end
|
||
|
|
||
|
local id = head[7] + (head[8] << 8)
|
||
|
local totlen = head[9] + (head[10] << 8)
|
||
|
local len = head[11] + (head[12] << 8)
|
||
|
local totpck = head[13] + (head[14] << 8)
|
||
|
local pck = head[15] + (head[16] << 8)
|
||
|
local key = head[17] + (head[18] << 8)
|
||
|
local crc_sum = head[19] + (head[20] << 8)
|
||
|
|
||
|
return id, len, totlen, pck, totpck, key, crc_sum
|
||
|
end
|
||
|
|
||
|
-- Decrypts the Ventrilo UDP status response data segment.
|
||
|
-- @param str the Ventrilo UDP status response
|
||
|
-- @param len length of the data segment of this response packet
|
||
|
-- @param key key for decrypting the data segment
|
||
|
local dec_data = function(str, len, key)
|
||
|
-- skip the header (first 20 bytes)
|
||
|
local data = { string.byte(str, 21, 20 + len) }
|
||
|
|
||
|
local a1 = key & 0xFF
|
||
|
if a1 == 0 then
|
||
|
return table.concat(data)
|
||
|
end
|
||
|
local a2 = key >> 8
|
||
|
|
||
|
for i = 1,len do
|
||
|
data[i] = data[i] - (crypt_data[a2 + 1] + ((i - 1) % 72)) & 0xFF
|
||
|
a2 = (a2 + a1) & 0xFF
|
||
|
end
|
||
|
|
||
|
return string.char(table.unpack(data))
|
||
|
end
|
||
|
|
||
|
-- Convenient wrapper for string.find(...). Returns the position of the end of
|
||
|
-- the match, or the previous starting position if no match was found. Also
|
||
|
-- returns the first capture, or "n/a" if one was not found.
|
||
|
-- @param str the string to search
|
||
|
-- @param pattern the pattern to apply for the search
|
||
|
-- @param pos the starting position of the search
|
||
|
-- @return newpos position of the end of the match, or pos if no match found
|
||
|
-- @return cap the first capture, or "n/a" if one was not found
|
||
|
local str_find = function(str, pattern, pos)
|
||
|
local _, newpos, cap = string.find(str, pattern, pos)
|
||
|
return newpos or pos, cap or "n/a"
|
||
|
end
|
||
|
|
||
|
-- Calculates the CRC checksum used for checking the integrity of the received
|
||
|
-- status response data segment.
|
||
|
-- @param data data to calculate the checksum of
|
||
|
-- @return 2 byte CRC checksum as seen in Ventrilo UDP status headers
|
||
|
local crc = function(data)
|
||
|
local sum = 0
|
||
|
for i = 1,#data do
|
||
|
sum = (crypt_crc[(sum >> 8) + 1] ~ data:byte(i) ~ (sum << 8)) & 0xFFFF
|
||
|
end
|
||
|
return sum
|
||
|
end
|
||
|
|
||
|
-- Parses the status response data segment and constructs an output table.
|
||
|
-- @param Ventrilo UDP status response data segment
|
||
|
-- @return info output table representing Ventrilo UDP status response info
|
||
|
local o_table = function(data)
|
||
|
local info = stdnse.output_table()
|
||
|
local pos
|
||
|
|
||
|
pos, info.name = str_find(data, "NAME: ([^\n]*)", 0)
|
||
|
pos, info.phonetic = str_find(data, "PHONETIC: ([^\n]*)", pos)
|
||
|
pos, info.comment = str_find(data, "COMMENT: ([^\n]*)", pos)
|
||
|
pos, info.auth = str_find(data, "AUTH: ([^\n]*)", pos)
|
||
|
pos, info.maxclients = str_find(data, "MAXCLIENTS: ([^\n]*)", pos)
|
||
|
pos, info.voicecodec = str_find(data, "VOICECODEC: ([^\n]*)", pos)
|
||
|
pos, info.voiceformat = str_find(data, "VOICEFORMAT: ([^\n]*)", pos)
|
||
|
pos, info.uptime = str_find(data, "UPTIME: ([^\n]*)", pos)
|
||
|
pos, info.platform = str_find(data, "PLATFORM: ([^\n]*)", pos)
|
||
|
pos, info.version = str_find(data, "VERSION: ([^\n]*)", pos)
|
||
|
|
||
|
-- channels
|
||
|
pos, info.channelcount = str_find(data, "CHANNELCOUNT: ([^\n]*)", pos)
|
||
|
pos, info.channelfields = str_find(data, "CHANNELFIELDS: ([^\n]*)", pos)
|
||
|
|
||
|
-- construct channel fields as a nice list instead of the raw data
|
||
|
local channelfields = {}
|
||
|
for channelfield in string.gmatch(info.channelfields, "[^,\n]+") do
|
||
|
channelfields[#channelfields + 1] = channelfield
|
||
|
end
|
||
|
info.channelfields = channelfields
|
||
|
|
||
|
-- parse and add channels
|
||
|
info.channels = stdnse.output_table()
|
||
|
-- add top level lobby channel (CID = 0)
|
||
|
info.channels["0"] = stdnse.output_table()
|
||
|
info.channels["0"].NAME = "<top level lobby>"
|
||
|
info.channels["0"].CID = "0"
|
||
|
while string.sub(data, pos + 2, pos + 10) == "CHANNEL: " do
|
||
|
local channel = stdnse.output_table()
|
||
|
for _, channelfield in ipairs(info.channelfields) do
|
||
|
pos, channel[channelfield] = str_find(data, channelfield .. "=([^,\n]*)", pos)
|
||
|
end
|
||
|
if channel.CID then
|
||
|
info.channels[channel.CID] = channel
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- clients
|
||
|
pos, info.clientcount = str_find(data, "CLIENTCOUNT: ([^\n]*)", pos)
|
||
|
pos, info.clientfields = str_find(data, "CLIENTFIELDS: ([^\n]*)", pos)
|
||
|
|
||
|
-- construct client fields as a nice list instead of the raw data
|
||
|
local clientfields = {}
|
||
|
for clientfield in string.gmatch(info.clientfields, "[^,\n]+") do
|
||
|
clientfields[#clientfields + 1] = clientfield
|
||
|
end
|
||
|
info.clientfields = clientfields
|
||
|
|
||
|
-- parse and add clients
|
||
|
while string.sub(data, pos + 2, pos + 9) == "CLIENT: " do
|
||
|
local client = stdnse.output_table()
|
||
|
for _, clientfield in ipairs(info.clientfields) do
|
||
|
pos, client[clientfield] = str_find(data, clientfield .. "=([^,\n]*)", pos)
|
||
|
end
|
||
|
if client.CID then
|
||
|
if not info.channels[client.CID] then
|
||
|
-- weird clients with unrecognized CID are put in the -1 channel
|
||
|
if not info.channels["-1"] then
|
||
|
-- add channel for weird clients with unrecognized CIDs
|
||
|
info.channels["-1"] = stdnse.output_table()
|
||
|
info.channels["-1"].NAME = "<clients with unrecognized CIDs>"
|
||
|
info.channels["-1"].CID = "-1"
|
||
|
info.channels["-1"].clients = {}
|
||
|
end
|
||
|
table.insert(info.channels["-1"].clients, client)
|
||
|
elseif not info.channels[client.CID].clients then
|
||
|
-- channel had no clients, create table for the 1st client
|
||
|
info.channels[client.CID].clients = {}
|
||
|
table.insert(info.channels[client.CID].clients, client)
|
||
|
else
|
||
|
table.insert(info.channels[client.CID].clients, client)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return info
|
||
|
end
|
||
|
|
||
|
-- Constructs an output string from an output table for use in normal output.
|
||
|
-- @param info output table
|
||
|
-- @return output_string output string
|
||
|
local o_str = function(info)
|
||
|
local buf = strbuf.new()
|
||
|
buf = buf .. "\nname: "
|
||
|
buf = buf .. info.name
|
||
|
buf = buf .. "\nphonetic: "
|
||
|
buf = buf .. info.phonetic
|
||
|
buf = buf .. "\ncomment: "
|
||
|
buf = buf .. info.comment
|
||
|
buf = buf .. "\nauth: "
|
||
|
buf = buf .. auth_str(info.auth)
|
||
|
buf = buf .. "\nmax. clients: "
|
||
|
buf = buf .. info.maxclients
|
||
|
buf = buf .. "\nvoice codec: "
|
||
|
buf = buf .. info.voicecodec
|
||
|
buf = buf .. "\nvoice format: "
|
||
|
buf = buf .. info.voiceformat
|
||
|
buf = buf .. "\nuptime: "
|
||
|
buf = buf .. uptime_str(info.uptime)
|
||
|
buf = buf .. "\nplatform: "
|
||
|
buf = buf .. info.platform
|
||
|
buf = buf .. "\nversion: "
|
||
|
buf = buf .. info.version
|
||
|
buf = buf .. "\nchannel count: "
|
||
|
buf = buf .. info.channelcount
|
||
|
buf = buf .. "\nchannel fields: "
|
||
|
for i, channelfield in ipairs(info.channelfields) do
|
||
|
buf = buf .. channelfield
|
||
|
if i ~= #info.channelfields then
|
||
|
buf = buf .. ", "
|
||
|
end
|
||
|
end
|
||
|
buf = buf .. "\nclient count: "
|
||
|
buf = buf .. info.clientcount
|
||
|
buf = buf .. "\nclient fields: "
|
||
|
for i, clientfield in ipairs(info.clientfields) do
|
||
|
buf = buf .. clientfield
|
||
|
if i ~= #info.clientfields then
|
||
|
buf = buf .. ", "
|
||
|
end
|
||
|
end
|
||
|
buf = buf .. "\nchannels:"
|
||
|
for i, channel in pairs(info.channels) do
|
||
|
buf = buf .. "\n"
|
||
|
buf = buf .. channel.NAME
|
||
|
buf = buf .. " ("
|
||
|
for j, channelfield in ipairs(info.channelfields) do
|
||
|
if channelfield ~= "NAME" and channelfield ~= "n/a" then
|
||
|
buf = buf .. channelfield
|
||
|
buf = buf .. ": "
|
||
|
buf = buf .. (channel[channelfield] or "n/a")
|
||
|
if j ~= #info.channelfields then
|
||
|
buf = buf .. ", "
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
buf = buf .. "): "
|
||
|
if not channel.clients then
|
||
|
buf = buf .. "<empty>"
|
||
|
else
|
||
|
for j, client in ipairs(channel.clients) do
|
||
|
buf = buf .. "\n "
|
||
|
buf = buf .. client.NAME
|
||
|
buf = buf .. " ("
|
||
|
for k, clientfield in ipairs(info.clientfields) do
|
||
|
if clientfield ~= "NAME" and clientfield ~= "CID" then
|
||
|
buf = buf .. clientfield
|
||
|
buf = buf .. ": "
|
||
|
buf = buf .. client[clientfield]
|
||
|
if k ~= #info.clientfields then
|
||
|
buf = buf .. ", "
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return strbuf.dump(buf, "")
|
||
|
end
|
||
|
|
||
|
portrule = shortport.version_port_or_service({3784}, "ventrilo", {"tcp", "udp"})
|
||
|
|
||
|
action = function(host, port)
|
||
|
local mutex = nmap.mutex("ventrilo-info:" .. host.ip .. ":" .. port.number)
|
||
|
mutex("lock")
|
||
|
|
||
|
if host.registry["ventrilo-info"] == nil then
|
||
|
host.registry["ventrilo-info"] = {}
|
||
|
end
|
||
|
-- Maybe the script already ran for this port number on another protocol
|
||
|
local r = host.registry["ventrilo-info"][port.number]
|
||
|
if r == nil then
|
||
|
r = {}
|
||
|
host.registry["ventrilo-info"][port.number] = r
|
||
|
|
||
|
local socket = nmap.new_socket()
|
||
|
socket:set_timeout(2000)
|
||
|
|
||
|
local cleanup = function()
|
||
|
socket:close()
|
||
|
mutex("done")
|
||
|
end
|
||
|
local try = nmap.new_try(cleanup)
|
||
|
|
||
|
local udpport = { number = port.number, protocol = "udp" }
|
||
|
try(socket:connect(host.ip, udpport))
|
||
|
|
||
|
local status, response
|
||
|
-- try a couple of times on timeout, the service seems to not
|
||
|
-- respond if multiple requests come within a short timeframe
|
||
|
for _ = 1,3 do
|
||
|
try(socket:send(static_probe_payload))
|
||
|
status, response = socket:receive()
|
||
|
if status then
|
||
|
nmap.set_port_state(host, udpport, "open")
|
||
|
break
|
||
|
end
|
||
|
end
|
||
|
if not status then
|
||
|
-- 3 timeouts, no response
|
||
|
cleanup()
|
||
|
return
|
||
|
end
|
||
|
|
||
|
-- received the first packet, process it and others if they come
|
||
|
local fulldata = {}
|
||
|
local fulldatalen = 0
|
||
|
local curlen = 0
|
||
|
local head_crc_sum
|
||
|
while true do
|
||
|
-- decrypt received header and extract relevant information
|
||
|
local id, len, totlen, pck, totpck, key, crc_sum = dec_head(response)
|
||
|
|
||
|
if id == static_probe_id then
|
||
|
curlen = curlen + len
|
||
|
head_crc_sum = crc_sum
|
||
|
|
||
|
-- check for an invalid response
|
||
|
if #response < 20 or pck >= totpck or
|
||
|
len > 492 or curlen > totlen then
|
||
|
stdnse.debug1("Invalid response. Aborting script.")
|
||
|
cleanup()
|
||
|
return
|
||
|
end
|
||
|
|
||
|
-- keep track of the length of fulldata (# isn't applicable)
|
||
|
if fulldata[pck + 1] == nil then
|
||
|
fulldatalen = fulldatalen + 1
|
||
|
end
|
||
|
-- accumulate UDP packets that may not necessarily come in proper
|
||
|
-- order; arrange them by packet id
|
||
|
fulldata[pck + 1] = dec_data(response, len, key)
|
||
|
end
|
||
|
|
||
|
-- check for invalid states in communication
|
||
|
if (fulldatalen > totpck) or (curlen > totlen)
|
||
|
or (fulldatalen == totpck and curlen ~= totlen)
|
||
|
or (curlen == totlen and fulldatalen ~= totpck) then
|
||
|
stdnse.debug1("Invalid state (fulldatalen = " .. fulldatalen ..
|
||
|
"; totpck = " .. totpck .. "; curlen = " .. curlen ..
|
||
|
"; totlen = " .. totlen .. "). Aborting script.")
|
||
|
cleanup()
|
||
|
return
|
||
|
end
|
||
|
|
||
|
-- check for valid end of communication
|
||
|
if fulldatalen == totpck and curlen == totlen then
|
||
|
break
|
||
|
end
|
||
|
|
||
|
-- receive another packet
|
||
|
status, response = socket:receive()
|
||
|
if not status then
|
||
|
stdnse.debug1("Response packets stopped coming midway. Aborting script.")
|
||
|
cleanup()
|
||
|
return
|
||
|
end
|
||
|
end
|
||
|
|
||
|
socket:close()
|
||
|
|
||
|
-- concatenate received data into a single string for further use
|
||
|
local fulldata_str = table.concat(fulldata)
|
||
|
|
||
|
-- check for an invalid checksum on the response data sections (no headers)
|
||
|
local fulldata_crc_sum = crc(fulldata_str)
|
||
|
if fulldata_crc_sum ~= head_crc_sum then
|
||
|
stdnse.debug1("Invalid CRC sum, received = %04X, calculated = %04X", head_crc_sum, fulldata_crc_sum)
|
||
|
cleanup()
|
||
|
return
|
||
|
end
|
||
|
|
||
|
-- parse the received data string into an output table
|
||
|
r.info = o_table(fulldata_str)
|
||
|
end
|
||
|
|
||
|
mutex("done")
|
||
|
|
||
|
-- If the registry is empty the port was probed but Ventrilo wasn't detected
|
||
|
if next(r) == nil then
|
||
|
return
|
||
|
end
|
||
|
|
||
|
port.version.name = "ventrilo"
|
||
|
port.version.name_confidence = 10
|
||
|
port.version.product = "Ventrilo"
|
||
|
port.version.version = r.info.version
|
||
|
port.version.ostype = r.info.platform
|
||
|
port.version.extrainfo = "; name: ".. r.info.name
|
||
|
if port.protocol == "tcp" then
|
||
|
port.version.extrainfo = "voice port" .. port.version.extrainfo
|
||
|
else
|
||
|
port.version.extrainfo = "status port" .. port.version.extrainfo
|
||
|
end
|
||
|
port.version.extrainfo = port.version.extrainfo .. "; uptime: " .. uptime_str(r.info.uptime)
|
||
|
port.version.extrainfo = port.version.extrainfo .. "; auth: " .. auth_str(r.info.auth)
|
||
|
|
||
|
nmap.set_port_version(host, port, "hardmatched")
|
||
|
|
||
|
-- an output table for XML output and a custom string for normal output
|
||
|
return r.info, o_str(r.info)
|
||
|
end
|