CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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