summaryrefslogtreecommitdiffstats
path: root/scripts/irc-sasl-brute.nse
blob: 90acbfcb98affaa33137b638d9010e4960bf7afe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
local base64 = require "base64"
local brute = require "brute"
local comm = require "comm"
local creds = require "creds"
local sasl = require "sasl"
local irc = require "irc"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"

description=[[
Performs brute force password auditing against IRC (Internet Relay Chat) servers supporting SASL authentication.
]]

-- You can read more about sasl here:
-- https://github.com/atheme/charybdis/blob/master/doc/sasl.txt
-- http://www.leeh.co.uk/draft-mitchell-irc-capabilities-02.html
-- the first link also explains the meaning of constants used in
-- this script.

---
-- @usage
-- nmap --script irc-sasl-brute -p 6667 <ip>
--
-- @output
-- PORT     STATE SERVICE REASON
-- 6667/tcp open  irc     syn-ack
-- | irc-sasl-brute:
-- |   Accounts
-- |     root:toor - Valid credentials
-- |   Statistics
-- |_    Performed 60 guesses in 29 seconds, average tps: 2
--
-- @args irc-sasl-brute.threads the number of threads to use while brute-forcing.
--       Defaults to 2.



author = "Piotr Olma"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories={"brute","intrusive"}

portrule = irc.portrule

local dbg = stdnse.debug

-- some parts of the following class are taken from irc-brute written by Patrik
Driver = {

  new = function(self, host, port, saslencoder)
    local o = { host = host, port = port, saslencoder = saslencoder}
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  connect = function(self)
    -- the high timeout should take delays from ident into consideration
    local s, r, opts, _ = comm.tryssl(self.host,
      self.port,
      "CAP REQ sasl\r\n",
      { timeout = 10000 } )
    if ( not(s) ) then
      return false, "Failed to connect to server"
    end
    if string.find(r:lower(), "throttled") then
      -- we were reconnecting too fast
      dbg(2, "throttled.")
      return false, "We got throttled."
    end
    local status, _ = s:send("CAP END\r\n")
    if not status then return false, "Send failed." end
    local response
    repeat
      status, response = s:receive_lines(1)
      if not status then return false, "Receive failed." end
      if string.find(response, "ACK") then status = false end
    until (not status)
    self.socket = s
    return true
  end,

  login = function(self, username, password)
    self.socket:send("AUTHENTICATE ".. self.saslencoder:get_mechanism() .."\r\n")
    local status, response, challenge
    repeat
      status, response = self.socket:receive_lines(1)
      if not status then
        local err = brute.Error:new(response)
        err:setRetry(true)
        return false, err
      end
      challenge = string.match(response, "AUTHENTICATE (.*)")
      dbg(3, "challenge found: %s", tostring(challenge))
      if challenge then status = false end
    until (not status)
    local msg = self.saslencoder:encode(username, password, challenge)

    -- SASL PLAIN is supposed to be plaintext, but freenode actually wants it to be base64 encoded
    if self.saslencoder:get_mechanism() == "PLAIN" then
      msg = base64.enc(msg)
    end

    local status, data = self.socket:send("AUTHENTICATE "..msg.."\r\n")
    local success = false

    if ( not(status) ) then
      local err = brute.Error:new( data )
      -- This might be temporary, set the retry flag
      err:setRetry( true )
      return false, err
    end

    repeat
      status, response = self.socket:receive_lines(1)
      if ( status and string.find(response, "90[45]") ) then
        status = false
      end
      if ( status and string.find(response, "90[03]") ) then
        success = true
        status = false
      end
    until (not status)

    if (success) then
      return true, creds.Account:new(username, password, creds.State.VALID)
    end
    return false, brute.Error:new("Incorrect username or password")
  end,

  disconnect = function(self) return self.socket:close() end,
}

-- checks if server supports sasl authentication and if it does, also checks for supported
-- mechanisms
local function check_sasl(host, port)
  local s, r, opts, _ = comm.tryssl(host, port, "CAP REQ sasl\r\n", { timeout = 15000 } )

  repeat
    local status, lines = s:receive_lines(1)
    if string.find(lines, "ACK") then status = false end
    if string.find(lines, "NAK") then
      s:close()
      return false
    end
  until (not status)

  -- we know that sasl is supported, now check which mechanisms can be used
  local to_check = {"PLAIN", "DH-BLOWFISH", "NTLM", "CRAM-MD5", "DIGEST-MD5"}
  local supported = {}
  for _,m in ipairs(to_check) do
    s:send("AUTHENTICATE "..m.."\r\n")
    dbg(3, "checking mechanism %s", m)
    repeat
      local status, lines = s:receive_lines(1)
      if string.find(lines, "AUTHENTICATE") then
        s:send("AUTHENTICATE abort\r\n") -- it's not a real command, just to break the process
        -- wait till we get a message indicating failed authentication
        repeat
          status, lines = s:receive_lines(1)
          if string.find(lines, "90[45]") then status = false end
        until (not status)
        table.insert(supported, m)
        status = false
      elseif string.find(lines, "90[45]") then
        status = false
        break
      end
    until (not status)
  end
  s:close()
  return true, supported
end

action = function(host, port)
  local sasl_supported, mechs = check_sasl(host, port)
  if not sasl_supported then
    return stdnse.format_output(false, "Server doesn't support SASL authentication.")
  end

  local saslencoder = sasl.Helper:new()
  local sasl_mech

  -- check if the library supports any of the mechanisms we identified
  for _,m in ipairs(mechs) do
    if saslencoder:set_mechanism(m) then
      sasl_mech = m
      dbg(2, "supported mechanism found: %s", m)
      break
    end
  end
  local engine = brute.Engine:new(Driver, host, port, saslencoder)
  engine.options.script_name = SCRIPT_NAME
  engine.options.firstonly = true
  -- irc servers seem to be restrictive about too many connection attempts
  -- in a short time thus we need to limit the number of threads
  local threads = stdnse.get_script_args(("%s.threads"):format(SCRIPT_NAME))
  threads = tonumber(threads) or 2
  engine:setMaxThreads(threads)
  local status, accounts = engine:start()
  return accounts
end