local comm = require "comm"
local math = require "math"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
description = [[
Checks an IRC server for channels that are commonly used by malicious botnets.
Control the list of channel names with the irc-botnet-channels.channels
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 http://nmap.org/book/man-legal.html"
categories = {"discovery", "vuln", "safe"}
---
-- @usage
-- nmap -p 6667 --script=irc-botnet-channels
-- @usage
-- nmap -p 6667 --script=irc-botnet-channels --script-args 'irc-botnet-channels.channels={chan1,chan2,chan3}'
--
-- @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 = shortport.port_or_service({6666, 6667, 6697, 6679}, {"irc", "ircs"})
-- 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 stdnse.strjoin(" ", parts) .. "\r\n"
end
local function random_nick()
local nick = {}
for i = 1, 9 do
nick[#nick + 1] = string.char(math.random(string.byte("a"), string.byte("z")))
end
return table.concat(nick)
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 random_nick()
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 stdnse.strjoin(",", 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)
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.print_debug("Got short " .. RPL_LIST .. "response.")
end
elseif code == RPL_LISTEND then
break
end
end
irc_disconnect(irc)
if errorparams then
channels[#channels + 1] = "ERROR: " .. stdnse.strjoin(" ", errorparams)
end
return stdnse.format_output(true, channels)
end