summaryrefslogtreecommitdiffstats
path: root/nselib/imap.lua
blob: 9e786a6840aa05461e79153309276b28701b4fae (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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
---
-- A library implementing a minor subset of the IMAP protocol, currently the
-- CAPABILITY, LOGIN and AUTHENTICATE functions. The library was initially
-- written by Brandon Enright and later extended and converted to OO-form by
-- Patrik Karlsson <patrik@cqure.net>
--
-- The library consists of a <code>Helper</code>, class which is the main
-- interface for script writers, and the <code>IMAP</code> class providing
-- all protocol-level functionality.
--
-- The following example illustrates the recommended use of the library:
-- <code>
-- local helper = imap.Helper:new(host, port)
-- helper:connect()
-- helper:login("user","password","PLAIN")
-- helper:close()
-- </code>
--
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
-- @author Brandon Enright
-- @author Patrik Karlsson

-- Version 0.2
-- Revised 07/15/2011 - v0.2 - added the IMAP and Helper classes
--                             added support for LOGIN and AUTHENTICATE
--                             <patrik@cqure.net>

local base64 = require "base64"
local comm = require "comm"
local match = require "match"
local sasl = require "sasl"
local stdnse = require "stdnse"
local table = require "table"
_ENV = stdnse.module("imap", stdnse.seeall)


IMAP = {

  --- Creates a new instance of the IMAP class
  --
  -- @param host table as received by the script action method
  -- @param port table as received by the script action method
  -- @param options table containing options, currently
  --  <code>timeout<code> - number containing the seconds to wait for
  --                        a response
  new = function(self, host, port, options)
    local o = {
      host = host,
      port = port,
      counter = 1,
      timeout = ( options and options.timeout ) or 10000
    }
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  --- Receives a response from the IMAP server
  --
  -- @return status true on success, false on failure
  -- @return data string containing the received data
  receive = function(self)
    local data = ""
    repeat
      local status, tmp = self.socket:receive_buf(match.pattern_limit("\r\n", 1024), false)
      if( not(status) ) then return false, tmp end
      data = data .. tmp
    until( tmp:match(("^A%04d"):format(self.counter - 1)) or tmp:match("^%+"))

    return true, data
  end,

  --- Sends a request to the IMAP server
  --
  -- @param cmd string containing the command to send to the server eg.
  --            eg. (AUTHENTICATE, LOGIN)
  -- @param params string containing the command parameters
  -- @return true on success, false on failure
  -- @return err string containing the error if status was false
  send = function(self, cmd, params)
    local data
    if ( not(params) ) then
      data = ("A%04d %s\r\n"):format(self.counter, cmd)
    else
      data = ("A%04d %s %s\r\n"):format(self.counter, cmd, params)
    end
    local status, err = self.socket:send(data)
    if ( not(status) ) then return false, err end
    self.counter = self.counter + 1
    return true
  end,

  --- Connect to the server
  --
  -- @return status true on success, false on failure
  -- @return banner string containing the server banner
  connect = function(self)
    local socket, banner, opt = comm.tryssl( self.host, self.port, "", { request_timeout=self.timeout, recv_before=true } )
    if ( not(socket) or not(banner) ) then return false, "ERROR: Failed to connect to server" end
    self.socket = socket
    return true, banner
  end,

  --- Authenticate to the server (non PLAIN text mode)
  -- Currently supported algorithms are CRAM-MD5 and CRAM-SHA1
  --
  -- @param username string containing the username
  -- @param pass string containing the password
  -- @param mech string containing a authentication mechanism, currently
  --             CRAM-MD5 or CRAM-SHA1
  -- @return status true if login was successful, false on failure
  -- @return err string containing the error message if status was false
  authenticate = function(self, username, pass, mech)
    assert( mech == "NTLM" or
      mech == "DIGEST-MD5" or
      mech == "CRAM-MD5" or
      mech == "PLAIN",
      "Unsupported authentication mechanism")

    local status, err = self:send("AUTHENTICATE", mech)

    if( not(status) ) then return false, "ERROR: Failed to send data" end

    local status, data = self:receive()
    if( not(status) ) then return false, "ERROR: Failed to receive challenge" end

    if ( mech == "NTLM" ) then
      -- sniffed of the wire, seems to always be the same
      -- decodes to some NTLMSSP blob greatness
      status, data = self.socket:send("TlRMTVNTUAABAAAAB7IIogYABgA3AAAADwAPACgAAAAFASgKAAAAD0FCVVNFLUFJUi5MT0NBTERPTUFJTg==\r\n")
      if ( not(status) ) then return false, "ERROR: Failed to send NTLM packet" end
      status, data = self:receive()
      if ( not(status) ) then return false, "ERROR: Failed to receive NTLM challenge" end
    end

    if ( data:match(("^A%04d "):format(self.counter-1)) ) then
      return false, "ERROR: Authentication mechanism not supported"
    end

    local digest, auth_data
    if ( not(data:match("^+")) ) then
      return false, "ERROR: Failed to receive proper response from server"
    end
    data = base64.dec(data:match("^+ (.*)"))

    -- All mechanisms expect username and pass
    -- add the otheronce for those who need them
    local mech_params = { username, pass, data, "imap" }
    auth_data = sasl.Helper:new(mech):encode(table.unpack(mech_params))
    auth_data = base64.enc(auth_data) .. "\r\n"

    status, data = self.socket:send(auth_data)
    if( not(status) ) then return false, "ERROR: Failed to send data" end

    status, data = self:receive()
    if( not(status) ) then return false, "ERROR: Failed to receive data" end

    if ( mech == "DIGEST-MD5" ) then
      local rspauth = data:match("^+ (.*)")
      if ( rspauth ) then
        rspauth = base64.dec(rspauth)
        status, data = self.socket:send("\r\n")
        status, data = self:receive()
      end
    end
    if ( data:match(("^A%04d OK"):format(self.counter - 1)) ) then
      return true
    end
    return false, "Login failed"
  end,

  --- Login to the server using PLAIN text authentication
  --
  -- @param username string containing the username
  -- @param password string containing the password
  -- @return status true on success, false on failure
  -- @return err string containing the error message if status was false
  login = function(self, username, password)
    local status, err = self:send("LOGIN", ("\"%s\" \"%s\""):format(username, password))
    if( not(status) ) then return false, "ERROR: Failed to send data" end

    local status, data = self:receive()
    if( not(status) ) then return false, "ERROR: Failed to receive data" end

    if ( data:match(("^A%04d OK"):format(self.counter - 1)) ) then
      return true
    end
    return false, "Login failed"
  end,

  --- Retrieves a list of server capabilities (eg. supported authentication
  --  mechanisms, QUOTA, UIDPLUS, ACL ...)
  --
  -- @return status true on success, false on failure
  -- @return capas array containing the capabilities that are supported
  capabilities = function(self)
    local capas = {}
    local proto = (self.port.version and self.port.version.service_tunnel == "ssl" and "ssl") or "tcp"
    local status, err = self:send("CAPABILITY")
    if( not(status) ) then return false, err end

    local status, line = self:receive()
    if (not(status)) then
      capas.CAPABILITY = false
    else
      while status do
        if ( line:match("^%*%s+CAPABILITY") ) then
          line = line:gsub("^%*%s+CAPABILITY", "")
          for capability in line:gmatch("[%w%+=-]+") do
            capas[capability] = true
          end
          break
        end
        status, line = self.socket:receive()
      end
    end
    return true, capas
  end,

  --- Closes the connection to the IMAP server
  -- @return true on success, false on failure
  close = function(self) return self.socket:close() end

}


-- The helper class, that servers as interface to script writers
Helper = {

  -- @param host table as received by the script action method
  -- @param port table as received by the script action method
  -- @param options table containing options, currently
  -- <code>timeout<code> -  number containing the seconds to wait for
  --                        a response
  new = function(self, host, port, options)
    local o = { client = IMAP:new( host, port, options ) }
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  --- Connects to the IMAP server
  -- @return status true on success, false on failure
  connect = function(self)
    return self.client:connect()
  end,

  --- Login to the server using either plain-text or using the authentication
  -- mechanism provided in the mech argument.
  --
  -- @param username string containing the username
  -- @param password string containing the password
  -- @param mech [optional] containing the authentication mechanism to use
  -- @return status true on success, false on failure
  login = function(self, username, password, mech)
    if ( not(mech) or mech == "LOGIN" ) then
      return self.client:login(username, password)
    else
      return self.client:authenticate(username, password, mech)
    end
  end,

  --- Retrieves a list of server capabilities (eg. supported authentication
  --  mechanisms, QUOTA, UIDPLUS, ACL ...)
  --
  -- @return status true on success, false on failure
  -- @return capas array containing the capabilities that are supported
  capabilities = function(self)
    return self.client:capabilities()
  end,

  --- Closes the connection to the IMAP server
  -- @return true on success, false on failure
  close = function(self)
    return self.client:close()
  end,

}

return _ENV;