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
|