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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
|
---
-- FTP functions.
--
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
local comm = require "comm"
local stdnse = require "stdnse"
local string = require "string"
local ipOps = require "ipOps"
local nmap = require "nmap"
_ENV = stdnse.module("ftp", stdnse.seeall)
local ERROR_MESSAGES = {
["EOF"] = "connection closed",
["TIMEOUT"] = "connection timeout",
["ERROR"] = "failed to receive data"
}
local crlf_pattern = "\r?\n"
--- Connects to the FTP server based on the provided options and returns the parsed banner.
--
-- @param host The host table
-- @param port The port table
-- @param opts The connection option table, from comm.lua.
-- @return socket The socket descriptor, or nil on errors
-- @return code The numeric response code, as returned by read_reply, or error message if socket is nil.
-- @return message The response message
-- @return buffer The socket read buffer function, to be passed to read_reply.
-- @see comm.lua
connect = function(host, port, opts)
opts = opts or {}
opts.recv_before = true
local socket, err, proto, ret = comm.tryssl(host, port, '', opts)
if not socket then
return socket, (ERROR_MESSAGES[ret] or 'unspecified error')
end
local buffer = stdnse.make_buffer(socket, crlf_pattern)
local pos = 1
-- Should we just pass the output of buffer()?
local usebuf = false
-- Since we already read the first chunk of banner from the socket,
-- we have to supply it line-by-line to read_reply.
local code, message = read_reply(function()
if usebuf then
-- done reading the initial banner; pass along the socket buffer.
return buffer()
end
-- Look for CRLF
local i, j = ret:find(crlf_pattern, pos)
if not i then
-- Didn't find it! Grab another chunk (up to CRLF) and return it
usebuf = true
local chunk = buffer()
return ret:sub(pos) .. chunk
end
local oldpos = pos
-- start the next search just after CRLF
pos = j + 1
if pos >= #ret then
-- We consumed the whole thing! Start calling buffer() next.
usebuf = true
end
return ret:sub(oldpos, i - 1)
end)
return socket, code, message, buffer
end
---
-- Read an FTP reply and return the numeric code and the message. See RFC 959,
-- section 4.2.
-- @param buffer The buffer returned by ftp.connect or created with
-- <code>stdnse.make_buffer(socket, "\r?\n")</code>.
-- @return numeric code or <code>nil</code>.
-- @return text reply or error message.
function read_reply(buffer)
local readline
local line, err
local code, message
local _, p, tmp
line, err = buffer()
if not line then
return line, err
end
-- Single-line response?
code, message = string.match(line, "^(%d%d%d) (.*)$")
if code then
return tonumber(code), message
end
-- Multi-line response?
_, p, code, message = string.find(line, "^(%d%d%d)%-(.*)$")
if p then
while true do
line, err = buffer()
if not line then
return line, err
end
tmp = string.match(line, "^%d%d%d (.*)$")
if tmp then
message = message .. "\n" .. tmp
break
end
message = message .. "\n" .. line
end
return tonumber(code), message
end
return nil, string.format("Unparseable response: %q", line)
end
--- Close an FTP command connection
--
-- @param socket The socket with the live connection
function close(socket)
socket:send("QUIT\r\n")
socket:close()
end
--- Issue a STARTTLS command.
--
-- @param socket The connected command socket
-- @param buffer The socket read buffer
-- @return Boolean true if AUTH TLS succeeded, false otherwise
-- @return error string on failure
function starttls(socket, buffer)
-- Send AUTH TLS command, ask the service to start encryption
local status, err = socket:send("AUTH TLS\r\n")
if not status then
return nil, err
end
local code, result = read_reply(buffer)
return code == 234, result
end
local function is_ssl(socket)
return pcall(socket.get_ssl_certificate, socket)
end
-- Should we try STARTTLS based on this error?
local function should_try_ssl(code, message)
return code and code >= 400 and (
message:match('[Ss][Ss][Ll]') or
message:match('[Tt][Ll][Ss]') or
message:match('[Ss][Ee][Cc][Uu][Rr]')
)
end
-- Try to reconnect over STARTTLS.
local function reconnect_ssl(socket, buffer)
local status, err = starttls(socket, buffer)
if status then
status, err = socket:reconnect_ssl()
if status then
return true
end
end
return nil, err
end
--- Authenticate with username and password
--
-- May negotiate AUTH TLS if required
-- @param socket The connected command socket
-- @param buffer The socket read buffer
-- @param username The username to send
-- @param password The password to send
-- @param acct (optional) If the server requires it, send this account name. Default: username
-- @return Boolean true if auth succeeded, false otherwise
-- @return FTP response code
-- @return FTP response message
function auth(socket, buffer, username, password, acct)
local already_ssl = is_ssl(socket)
::TRY_AGAIN::
local status, err = socket:send(("USER %s\r\n"):format(username))
if not status then
return nil, err
end
local code, message = read_reply(buffer)
if code == 331 then
-- 331: User name okay, need password.
status, err =socket:send(("PASS %s\r\n"):format(password))
if not status then
return nil, err
end
code, message = read_reply(buffer)
elseif not already_ssl and should_try_ssl(code, message) then
if not reconnect_ssl(socket, buffer) then
return nil
end
already_ssl = true
goto TRY_AGAIN
end
if code == 332 then
-- 332: Need account for login.
-- This is rarely seen but may come in response to a
-- USER or PASS command.
status, err = socket:send("ACCT %s\r\n"):format(acct or username)
if not status then
return nil, err
end
code, message = read_reply(buffer)
if code == 331 then
-- 331: User name okay, need password.
status, err = socket:send("PASS %s\r\n"):format(password)
if not status then
return nil, err
end
code, message = read_reply(buffer)
elseif not already_ssl and should_try_ssl(code, message) then
if not reconnect_ssl(socket, buffer) then
return nil
end
already_ssl = true
goto TRY_AGAIN
end
end
if code and code >= 200 and code < 300 then
-- We are primarily looking for 230: User logged in, proceed.
return true, code, message
else
if code and not already_ssl and should_try_ssl(code, message) then
if not reconnect_ssl(socket, buffer) then
return nil
end
already_ssl = true
goto TRY_AGAIN
end
return nil, code, message
end
end
--- Start PASV mode
--
-- For IPv6 connections, attempts to use EPSV (RFC 2428). If the server sends an address that is not the target address, then this is an error.
-- @param socket The connected command socket
-- @param buffer The receive buffer
-- @return The connected data socket, or nil on error
-- @return Error message if data socket is nil
function pasv(socket, buffer)
local epsv = false
local status, lhost, lport, rhost, rport = socket:get_info()
if not status then
return nil, ("Can't determine remote host IP: %s"):format(lhost)
end
epsv = #ipOps.ip_to_str(rhost) > 4
::TRY_AGAIN::
local cmd = epsv and "EPSV" or "PASV"
-- ask the server for a Passive Mode: it should give us a port to
-- listen to, where it will dump the directory listing
local status, err = socket:send(cmd .. "\r\n")
if not status then
return status, err
end
local code, message = read_reply(buffer)
local pasv_port
if epsv then
if not code then
return nil, ("EPSV failed: socket %s"):format(message)
elseif code ~= 229 then
stdnse.debug2("Falling back to PASV. EPSV: %d %s", code, message)
epsv = false
goto TRY_AGAIN
end
status, pasv_port = string.match(message, "%((.)%1%1(%d+)%1%)")
if not status then
stdnse.debug2("Can't parse EPSV response: %s", message)
epsv = false
goto TRY_AGAIN
end
else
if not code or code >= 300 then
return nil, ("PASV failed: %s %s"):format(code or "socket", message)
end
-- Compute the PASV port as given by the server
-- The server should answer with something like
-- 2xx Entering Passive Mode (a,b,c,d,hp,lp)
-- (-- IP--,PORT)
-- PORT is (hp x 256) + lp
local ip, high, low = string.match(message, "%((%d+,%d+,%d+,%d+),(%d+),(%d+)%)")
if not ip then
return nil, string.format("Can't parse PASV response: %q", message)
end
ip = ip:gsub(",", ".")
if not (ipOps.compare_ip(ip, "eq", rhost) or ipOps.compare_ip(ip, "eq", "0.0.0.0")) then
return nil, ("PASV IP %s is not the same as %s"):format(ip, rhost)
end
pasv_port = high * 256 + low
end
local list_socket = nmap.new_socket()
status, err = list_socket:connect(rhost, pasv_port, "tcp")
if not status then
return status, err
end
return list_socket
end
return _ENV;
|