Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/fuzzers/ftp/client_ftp.rb
19664 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
##
7
# Fuzzer written by corelanc0d3r - <peter.ve [at] corelan.be>
8
# http://www.corelan.be:8800/index.php/2010/10/12/death-of-an-ftp-client/
9
#
10
##
11
12
class MetasploitModule < Msf::Auxiliary
13
include Exploit::Remote::TcpServer
14
15
def initialize
16
super(
17
'Name' => 'Simple FTP Client Fuzzer',
18
'Description' => %q{
19
This module will serve an FTP server and perform FTP client interaction fuzzing
20
},
21
'Author' => [ 'corelanc0d3r <peter.ve[at]corelan.be>' ],
22
'License' => MSF_LICENSE,
23
'References' => [
24
[ 'URL', 'http://www.corelan.be:8800/index.php/2010/10/12/death-of-an-ftp-client/' ],
25
],
26
'Notes' => {
27
'Stability' => [CRASH_SERVICE_DOWN],
28
'SideEffects' => [],
29
'Reliability' => []
30
}
31
)
32
register_options(
33
[
34
OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 21 ]),
35
OptString.new('FUZZCMDS', [ true, 'Comma separated list of commands to fuzz (Uppercase).', 'LIST,NLST,LS,RETR', nil, /(?:[A-Z]+,?)+/ ]),
36
OptInt.new('STARTSIZE', [ true, 'Fuzzing string startsize.', 1000]),
37
OptInt.new('ENDSIZE', [ true, 'Max Fuzzing string size.', 200000]),
38
OptInt.new('STEPSIZE', [ true, 'Increment fuzzing string each attempt.', 1000]),
39
OptBool.new('RESET', [ true, 'Reset fuzzing values after client disconnects with QUIT cmd.', true]),
40
OptString.new('WELCOME', [ true, 'FTP Server welcome message.', 'Evil FTP Server Ready']),
41
OptBool.new('CYCLIC', [ true, "Use Cyclic pattern instead of A's (fuzzing payload).", true]),
42
OptBool.new('ERROR', [ true, 'Reply with error codes only', false]),
43
OptBool.new('EXTRALINE', [ true, "Add extra CRLF's in response to LIST", true])
44
]
45
)
46
end
47
48
# Not compatible today
49
def support_ipv6?
50
false
51
end
52
53
def setup
54
super
55
@state = {}
56
end
57
58
def run
59
@fuzzsize = datastore['STARTSIZE'].to_i
60
exploit
61
end
62
63
# Handler for new FTP client connections
64
def on_client_connect(client)
65
@state[client] = {
66
name: "#{client.peerhost}:#{client.peerport}",
67
ip: client.peerhost,
68
port: client.peerport,
69
user: nil,
70
pass: nil
71
}
72
# set up an active data port on port 20
73
print_status("Client connected : #{client.peerhost}")
74
active_data_port_for_client(client, 20)
75
send_response(client, '', 'WELCOME', 220, ' ' + datastore['WELCOME'])
76
# from this point forward, on_client_data() will take over
77
end
78
79
def on_client_close(client)
80
@state.delete(client)
81
end
82
83
# Active and Passive data connections
84
def passive_data_port_for_client(client)
85
@state[client][:mode] = :passive
86
if !(@state[client][:passive_sock])
87
s = Rex::Socket::TcpServer.create(
88
'LocalHost' => '0.0.0.0',
89
'LocalPort' => 0,
90
'Context' => { 'Msf' => framework, 'MsfExploit' => self }
91
)
92
dport = s.getsockname[2]
93
@state[client][:passive_sock] = s
94
@state[client][:passive_port] = dport
95
print_status(" - Set up passive data port #{dport}")
96
end
97
@state[client][:passive_port]
98
end
99
100
def active_data_port_for_client(client, port)
101
@state[client][:mode] = :active
102
connector = proc do
103
host = client.peerhost.dup
104
Rex::Socket::Tcp.create(
105
'PeerHost' => host,
106
'PeerPort' => port,
107
'Context' => { 'Msf' => framework, 'MsfExploit' => self }
108
)
109
end
110
@state[client][:active_connector] = connector
111
@state[client][:active_port] = port
112
print_status(" - Set up active data port #{port}")
113
end
114
115
def establish_data_connection(client)
116
print_status(" - Establishing #{@state[client][:mode]} data connection")
117
begin
118
Timeout.timeout(20) do
119
if (@state[client][:mode] == :active)
120
return @state[client][:active_connector].call
121
end
122
if (@state[client][:mode] == :passive)
123
return @state[client][:passive_sock].accept
124
end
125
end
126
print_status(' - Data connection active')
127
rescue StandardError => e
128
print_error("Failed to establish data connection: #{e.class} #{e}")
129
end
130
nil
131
end
132
133
# FTP Client-to-Server Command handlers
134
def on_client_data(client)
135
# get the client data
136
data = client.get_once
137
return if !data
138
139
# split data into command and arguments
140
cmd, arg = data.strip.split(/\s+/, 2)
141
arg ||= ''
142
143
return if !cmd
144
145
# convert commands to uppercase and strip spaces
146
case cmd.upcase.strip
147
148
when 'USER'
149
@state[client][:user] = arg
150
send_response(client, arg, 'USER', 331, ' User name okay, need password')
151
return
152
153
when 'PASS'
154
@state[client][:pass] = arg
155
send_response(client, arg, 'PASS', 230, "-Password accepted.\r\n230 User logged in.")
156
return
157
158
when 'QUIT'
159
if datastore['RESET']
160
print_status('Resetting fuzz settings')
161
@fuzzsize = datastore['STARTSIZE']
162
@stepsize = datastore['STEPSIZE']
163
end
164
print_status('** Client disconnected **')
165
send_response(client, arg, 'QUIT', 221, ' User logged out')
166
return
167
168
when 'SYST'
169
send_response(client, arg, 'SYST', 215, ' UNIX Type: L8')
170
return
171
172
when 'TYPE'
173
send_response(client, arg, 'TYPE', 200, " Type set to #{arg}")
174
return
175
176
when 'CWD'
177
send_response(client, arg, 'CWD', 250, ' CWD Command successful')
178
return
179
180
when 'PWD'
181
send_response(client, arg, 'PWD', 257, ' "/" is current directory.')
182
return
183
184
when 'REST'
185
send_response(client, arg, 'REST', 200, ' OK')
186
return
187
188
when 'XPWD'
189
send_response(client, arg, 'PWD', 257, ' "/" is current directory')
190
return
191
192
when 'SIZE'
193
send_response(client, arg, 'SIZE', 213, ' 1')
194
return
195
196
when 'MDTM'
197
send_response(client, arg, 'MDTM', 213, " #{Time.now.strftime('%Y%m%d%H%M%S')}")
198
return
199
200
when 'CDUP'
201
send_response(client, arg, 'CDUP', 257, ' "/" is current directory')
202
return
203
204
when 'PORT'
205
port = arg.split(',')[4, 2]
206
if !port && (port.length == 2)
207
client.put("500 Illegal PORT command.\r\n")
208
return
209
end
210
port = port.map(&:to_i).pack('C*').unpack('n')[0]
211
active_data_port_for_client(client, port)
212
send_response(client, arg, 'PORT', 200, ' PORT command successful')
213
return
214
215
when 'PASV'
216
print_status("Handling #{cmd.upcase} command")
217
daddr = Rex::Socket.source_address(client.peerhost)
218
dport = passive_data_port_for_client(client)
219
@state[client][:daddr] = daddr
220
@state[client][:dport] = dport
221
pasv = (daddr.split('.') + [dport].pack('n').unpack('CC')).join(',')
222
dofuzz = fuzz_this_cmd('PASV')
223
code = 227
224
if datastore['ERROR']
225
code = 557
226
end
227
if (dofuzz == 1)
228
print_status(" * Fuzzing response for PASV, payload length #{@fuzzdata.length}")
229
send_response(client, arg, 'PASV', code, " Entering Passive Mode (#{@fuzzdata},1,1,1,1,1)\r\n")
230
incr_fuzzsize
231
else
232
send_response(client, arg, 'PASV', code, " Entering Passive Mode (#{pasv})")
233
end
234
return
235
236
when /^(LIST|NLST|LS)$/
237
# special case - requires active/passive connection
238
print_status("Handling #{cmd.upcase} command")
239
conn = establish_data_connection(client)
240
if !conn
241
client.put("425 Can't build data connection\r\n")
242
return
243
end
244
print_status(' - Data connection set up')
245
code = 150
246
if datastore['ERROR']
247
code = 550
248
end
249
client.put("#{code} Here comes the directory listing.\r\n")
250
code = 226
251
if datastore['ERROR']
252
code = 550
253
end
254
client.put("#{code} Directory send ok.\r\n")
255
strfile = 'passwords.txt'
256
strfolder = 'Secret files'
257
dofuzz = fuzz_this_cmd('LIST')
258
if (dofuzz == 1)
259
strfile = @fuzzdata + '.txt'
260
strfolder = @fuzzdata
261
paylen = @fuzzdata.length
262
print_status("* Fuzzing response for LIST, payload length #{paylen}")
263
incr_fuzzsize
264
end
265
print_status(' - Sending directory list via data connection')
266
if datastore['EXTRALINE']
267
extra = "\r\n"
268
else
269
extra = ''
270
end
271
dirlist = "drwxrwxrwx 1 100 0 11111 Jun 11 21:10 #{strfolder}\r\n" + extra
272
dirlist << "-rw-rw-r-- 1 1176 1176 1060 Aug 16 22:22 #{strfile}\r\n" + extra
273
conn.put("total 2\r\n" + dirlist)
274
conn.close
275
return
276
277
when 'RETR'
278
# special case - requires active/passive connection
279
print_status("Handling #{cmd.upcase} command")
280
conn = establish_data_connection(client)
281
if !conn
282
client.put("425 Can't build data connection\r\n")
283
return
284
end
285
print_status(' - Data connection set up')
286
strcontent = 'blahblahblah'
287
dofuzz = fuzz_this_cmd('LIST')
288
if (dofuzz == 1)
289
strcontent = @fuzzdata
290
paylen = @fuzzdata.length
291
print_status("* Fuzzing response for RETR, payload length #{paylen}")
292
incr_fuzzsize
293
end
294
client.put("150 Opening BINARY mode data connection #{strcontent}\r\n")
295
print_status(' - Sending data via data connection')
296
conn.put(strcontent)
297
client.put("226 Transfer complete\r\n")
298
conn.close
299
return
300
301
when /^(STOR|MKD|REM|DEL|RMD)$/
302
send_response(client, arg, cmd.upcase, 500, ' Access denied')
303
return
304
305
when 'FEAT'
306
send_response(client, arg, 'FEAT', '', "211-Features:\r\n211 End")
307
return
308
309
when 'HELP'
310
send_response(client, arg, 'HELP', 214, " Syntax: #{arg} - (#{arg}-specific commands)")
311
312
when 'SITE'
313
send_response(client, arg, 'SITE', 200, ' OK')
314
return
315
316
when 'NOOP'
317
send_response(client, arg, 'NOOP', 200, ' OK')
318
return
319
320
when 'ABOR'
321
send_response(client, arg, 'ABOR', 225, ' Abor command successful')
322
return
323
324
when 'ACCT'
325
send_response(client, arg, 'ACCT', 200, ' OK')
326
return
327
328
when 'RNFR'
329
send_response(client, arg, 'RNRF', 350, ' File.exist')
330
return
331
332
when 'RNTO'
333
send_response(client, arg, 'RNTO', 350, ' File.exist')
334
return
335
336
else
337
send_response(client, arg, cmd.upcase, 200, ' Command not understood')
338
return
339
end
340
341
return
342
end
343
344
# Fuzzer functions
345
346
# Do we need to fuzz this command ?
347
def fuzz_this_cmd(cmd)
348
@fuzzcommands = datastore['FUZZCMDS'].split(',')
349
350
fuzzme = 0
351
@fuzzcommands.each do |thiscmd|
352
if ((cmd.upcase == thiscmd.upcase) || (thiscmd == '*')) && (fuzzme == 0)
353
fuzzme = 1
354
break
355
end
356
end
357
358
if fuzzme == 1
359
# should we use a cyclic pattern, or just A's ?
360
if datastore['CYCLIC']
361
@fuzzdata = Rex::Text.pattern_create(@fuzzsize)
362
else
363
@fuzzdata = 'A' * @fuzzsize
364
end
365
end
366
367
return fuzzme
368
end
369
370
def incr_fuzzsize
371
@stepsize = datastore['STEPSIZE'].to_i
372
@fuzzsize += @stepsize
373
print_status("(i) Setting next payload size to #{@fuzzsize}")
374
if (@fuzzsize > datastore['ENDSIZE'].to_i)
375
@fuzzsize = datastore['ENDSIZE'].to_i
376
end
377
end
378
379
# Send data back to the server
380
def send_response(client, arg, cmd, code, msg)
381
if arg.length > 40
382
showarg = arg[0, 40] + '...'
383
else
384
showarg = arg
385
end
386
387
if cmd.length > 40
388
showcmd = cmd[0, 40] + '...'
389
else
390
showcmd = cmd
391
end
392
393
print_status("Sending response for '#{showcmd}' command, arg #{showarg}")
394
dofuzz = fuzz_this_cmd(cmd)
395
396
## Fuzz this command ? (excluding PASV, which is handled in the command handler)
397
if (dofuzz == 1) && (cmd.upcase != 'PASV')
398
paylen = @fuzzdata.length
399
print_status("* Fuzzing response for #{cmd.upcase}, payload length #{paylen}")
400
if datastore['ERROR']
401
code = '550 '
402
end
403
if cmd == 'FEAT'
404
@fuzzdata = "211-Features:\r\n " + @fuzzdata + "\r\n211 End"
405
end
406
if cmd == 'PWD'
407
@fuzzdata = ' "/' + @fuzzdata + '" is current directory'
408
end
409
cmsg = code.to_s + ' ' + @fuzzdata
410
client.put("#{cmsg}\r\n")
411
print_status('* Fuzz data sent')
412
incr_fuzzsize
413
else
414
# Do not fuzz
415
cmsg = code.to_s + msg
416
cmsg = cmsg.strip
417
client.put("#{cmsg}\r\n")
418
end
419
end
420
end
421
422