diff options
Diffstat (limited to '')
-rw-r--r-- | scripts/irc-botnet-channels.nse | 315 |
1 files changed, 315 insertions, 0 deletions
diff --git a/scripts/irc-botnet-channels.nse b/scripts/irc-botnet-channels.nse new file mode 100644 index 0000000..73da1f2 --- /dev/null +++ b/scripts/irc-botnet-channels.nse @@ -0,0 +1,315 @@ +local comm = require "comm" +local irc = require "irc" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" +local rand = require "rand" + +description = [[ +Checks an IRC server for channels that are commonly used by malicious botnets. + +Control the list of channel names with the <code>irc-botnet-channels.channels</code> +script argument. The default list of channels is +* loic +* Agobot +* Slackbot +* Mytob +* Rbot +* SdBot +* poebot +* IRCBot +* VanBot +* MPack +* Storm +* GTbot +* Spybot +* Phatbot +* Wargbot +* RxBot +]] + +author = {"David Fifield", "Ange Gutek"} + +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" + +categories = {"discovery", "vuln", "safe"} + +--- +-- @usage +-- nmap -p 6667 --script=irc-botnet-channels <target> +-- @usage +-- nmap -p 6667 --script=irc-botnet-channels --script-args 'irc-botnet-channels.channels={chan1,chan2,chan3}' <target> +-- +-- @args irc-botnet-channels.channels a list of channel names to check for. +-- +-- @output +-- | irc-botnet-channels: +-- | #loic +-- |_ #RxBot + + +-- See RFC 2812 for protocol documentation. + +-- Section 5.1 for protocol replies. +local RPL_TRYAGAIN = "263" +local RPL_LIST = "322" +local RPL_LISTEND = "323" + +local DEFAULT_CHANNELS = { + "loic", + "Agobot", + "Slackbot", + "Mytob", + "Rbot", + "SdBot", + "poebot", + "IRCBot", + "VanBot", + "MPack", + "Storm", + "GTbot", + "Spybot", + "Phatbot", + "Wargbot", + "RxBot", +} + +portrule = irc.portrule + +-- Parse an IRC message. Returns nil, errmsg in case of error. Otherwise returns +-- true, prefix, command, params. prefix may be nil. params is an array of +-- strings. The final param has the ':' stripped from the beginning. +-- +-- The special return value true, nil indicates an empty message to be ignored. +-- +-- See RFC 2812, section 2.3.1 for BNF of a message. +local function irc_parse_message(s) + local prefix, command, params + local _, p, t + + s = string.gsub(s, "\r?\n$", "") + if string.match(s, "^ *$") then + return true, nil + end + + p = 0 + _, t, prefix = string.find(s, "^:([^ ]+) +", p + 1) + if t then + p = t + end + + -- We do not check for any special format of the command name or + -- number. + _, p, command = string.find(s, "^([^ ]+)", p + 1) + if not p then + return nil, "Presumed message is missing a command." + end + + params = {} + while p + 1 <= #s do + local param + + _, p = string.find(s, "^ +", p + 1) + if not p then + return nil, "Missing a space before param." + end + -- We don't do any checks on the contents of params. + if #params == 14 then + params[#params + 1] = string.sub(s, p + 1) + break + elseif string.match(s, "^:", p + 1) then + params[#params + 1] = string.sub(s, p + 2) + break + else + _, p, param = string.find(s, "^([^ ]+)", p + 1) + if not p then + return nil, "Missing a param." + end + params[#params + 1] = param + end + end + + return true, prefix, command, params +end + +local function irc_compose_message(prefix, command, ...) + local parts, params + + parts = {} + if prefix then + parts[#parts + 1] = prefix + end + + if string.match(command, "^:") then + return nil, "Command may not begin with ':'." + end + parts[#parts + 1] = command + + params = {...} + for i, param in ipairs(params) do + if not string.match(param, "^[^\0\r\n :][^\0\r\n ]*$") then + if i < #params then + return nil, "Bad format for param." + else + parts[#parts + 1] = ":" .. param + end + else + parts[#parts + 1] = param + end + end + + return table.concat(parts, " ") .. "\r\n" +end + +local function splitlines(s) + local lines = {} + local _, i, j + + i = 1 + while i <= #s do + _, j = string.find(s, "\r?\n", i) + lines[#lines + 1] = string.sub(s, i, j) + if not j then + break + end + i = j + 1 + end + + return lines +end + +local function irc_connect(host, port, nick, user, pass) + local commands = {} + local irc = {} + local banner + + -- Section 3.1.1. + if pass then + commands[#commands + 1] = irc_compose_message(nil, "PASS", pass) + end + nick = nick or rand.random_alpha(9) + commands[#commands + 1] = irc_compose_message(nil, "NICK", nick) + user = user or nick + commands[#commands + 1] = irc_compose_message(nil, "USER", user, "8", "*", user) + + irc.sd, banner = comm.tryssl(host, port, table.concat(commands)) + if not irc.sd then + return nil, "Unable to open connection." + end + + irc.sd:set_timeout(60 * 1000) + + -- Buffer these initial lines for irc_readline. + irc.linebuf = splitlines(banner) + + irc.buf = stdnse.make_buffer(irc.sd, "\r?\n") + + return irc +end + +local function irc_disconnect(irc) + irc.sd:close() +end + +local function irc_readline(irc) + local line + + if next(irc.linebuf) then + line = table.remove(irc.linebuf, 1) + if string.match(line, "\r?\n$") then + return line + else + -- We had only half a line buffered. + return line .. irc.buf() + end + else + return irc.buf() + end +end + +local function irc_read_message(irc) + local line, err + + line, err = irc_readline(irc) + if not line then + return nil, err + end + + return irc_parse_message(line) +end + +local function irc_send_message(irc, prefix, command, ...) + local line + + line = irc_compose_message(prefix, command, ...) + irc.sd:send(line) +end + +-- Prefix channel names with '#' if necessary and concatenate into a +-- comma-separated list. +local function concat_channel_list(channels) + local mod = {} + + for _, channel in ipairs(channels) do + if not string.match(channel, "^#") then + channel = "#" .. channel + end + mod[#mod + 1] = channel + end + + return table.concat(mod, ",") +end + +function action(host, port) + local irc + local search_channels + local channels + local errorparams + + search_channels = stdnse.get_script_args(SCRIPT_NAME .. ".channels") + if not search_channels then + search_channels = DEFAULT_CHANNELS + elseif type(search_channels) == "string" then + search_channels = {search_channels} + end + + irc = irc_connect(host, port) + if not irc then + stdnse.debug1("Could not connect") + return nil + end + irc_send_message(irc, "LIST", concat_channel_list(search_channels)) + + channels = {} + while true do + local status, prefix, code, params + + status, prefix, code, params = irc_read_message(irc) + if not status then + -- Error message from irc_read_message. + errorparams = {prefix} + break + elseif code == "ERROR" then + errorparams = params + break + elseif code == RPL_TRYAGAIN then + errorparams = params + break + elseif code == RPL_LIST then + if #params >= 2 then + channels[#channels + 1] = params[2] + else + stdnse.debug1("Got short " .. RPL_LIST .. "response.") + end + elseif code == RPL_LISTEND then + break + end + end + irc_disconnect(irc) + + if errorparams then + channels[#channels + 1] = "ERROR: " .. table.concat(errorparams, " ") + end + + return stdnse.format_output(true, channels) +end |