summaryrefslogtreecommitdiffstats
path: root/scripts/irc-botnet-channels.nse
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--scripts/irc-botnet-channels.nse315
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