1 //--------------------------------------------------------------------------
2 // Copyright (C) 2014-2021 Cisco and/or its affiliates. All rights reserved.
3 //
4 // This program is free software; you can redistribute it and/or modify it
5 // under the terms of the GNU General Public License Version 2 as published
6 // by the Free Software Foundation. You may not use, modify or distribute
7 // this program under any other version of the GNU General Public License.
8 //
9 // This program is distributed in the hope that it will be useful, but
10 // WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 // General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License along
15 // with this program; if not, write to the Free Software Foundation, Inc.,
16 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 //--------------------------------------------------------------------------
18
19 // ftp_module.cc author Russ Combs <rucombs@cisco.com>
20
21 #ifdef HAVE_CONFIG_H
22 #include "config.h"
23 #endif
24
25 #include "ftp_module.h"
26 #include "ftpp_return_codes.h"
27 #include "ftp_cmd_lookup.h"
28 #include "ftp_parse.h"
29 #include "log/messages.h"
30 #include "utils/util.h"
31 #include "ft_main.h"
32 #include "ftpp_si.h"
33
34 using namespace snort;
35 using namespace std;
36
37 #define ftp_client_help \
38 "FTP client configuration module for use with ftp_server"
39
40 #define ftp_server_help \
41 "main FTP module; ftp_client should also be configured"
42
43 //-------------------------------------------------------------------------
44 // client stuff
45 //-------------------------------------------------------------------------
46
47 static const Parameter client_bounce_params[] =
48 {
49 { "address", Parameter::PT_ADDR, nullptr, "1.0.0.0/32",
50 "allowed IP address in CIDR format" },
51
52 // FIXIT-L port and last_port should be replaced with a port list
53 { "port", Parameter::PT_PORT, nullptr, "20",
54 "allowed port" },
55
56 { "last_port", Parameter::PT_PORT, nullptr, nullptr,
57 "optional allowed range from port to last_port inclusive" },
58
59 { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
60 };
61
62 static const Parameter ftp_client_params[] =
63 {
64 { "bounce", Parameter::PT_BOOL, nullptr, "false",
65 "check for bounces" },
66
67 { "bounce_to", Parameter::PT_LIST, client_bounce_params, nullptr,
68 "allow bounces to CIDRs / ports" },
69
70 { "ignore_telnet_erase_cmds", Parameter::PT_BOOL, nullptr, "false",
71 "ignore erase character and erase line commands when normalizing" },
72
73 { "max_resp_len", Parameter::PT_INT, "0:max32", "4294967295",
74 "maximum FTP response accepted by client" },
75
76 { "telnet_cmds", Parameter::PT_BOOL, nullptr, "false",
77 "detect Telnet escape sequences on FTP control channel" },
78
79 { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
80 };
81
FtpClientModule()82 FtpClientModule::FtpClientModule() :
83 Module(FTP_CLIENT_NAME, ftp_client_help, ftp_client_params)
84 {
85 conf = nullptr;
86 }
87
~FtpClientModule()88 FtpClientModule::~FtpClientModule()
89 {
90 if ( conf )
91 delete conf;
92
93 for ( auto p : bounce_to )
94 delete p;
95 }
96
set(const char *,Value & v,SnortConfig *)97 bool FtpClientModule::set(const char*, Value& v, SnortConfig*)
98 {
99 if ( v.is("address") )
100 {
101 unsigned n;
102 const uint8_t* b = v.get_buffer(n);
103 address.assign((const char*)b, n);
104 }
105 else if ( v.is("bounce") )
106 conf->bounce = v.get_bool();
107
108 else if ( v.is("ignore_telnet_erase_cmds") )
109 conf->ignore_telnet_erase_cmds = v.get_bool();
110
111 else if ( v.is("last_port") )
112 last_port = v.get_uint16();
113
114 else if ( v.is("max_resp_len") )
115 conf->max_resp_len = v.get_uint32();
116
117 else if ( v.is("port") )
118 port = v.get_uint16();
119
120 else if ( v.is("telnet_cmds") )
121 conf->telnet_cmds = v.get_bool();
122
123 return true;
124 }
125
BounceTo(const string & a,Port l,Port h)126 BounceTo::BounceTo(const string& a, Port l, Port h)
127 {
128 address = a;
129 low = l;
130 high = h;
131 }
132
get_bounce(unsigned idx)133 const BounceTo* FtpClientModule::get_bounce(unsigned idx)
134 {
135 if ( idx < bounce_to.size() )
136 return bounce_to[idx];
137 else
138 return nullptr;
139 }
140
get_data()141 FTP_CLIENT_PROTO_CONF* FtpClientModule::get_data()
142 {
143 FTP_CLIENT_PROTO_CONF* tmp = conf;
144 conf = nullptr;
145 return tmp;
146 }
147
begin(const char * fqn,int idx,SnortConfig *)148 bool FtpClientModule::begin(const char* fqn, int idx, SnortConfig*)
149 {
150 if ( !conf )
151 conf = new FTP_CLIENT_PROTO_CONF;
152
153 if ( !strcmp(fqn, "ftp_client.bounce_to") )
154 {
155 if ( idx )
156 {
157 address.clear();
158 port = last_port = 0;
159 }
160 else
161 {
162 for ( auto p : bounce_to )
163 delete p;
164
165 bounce_to.clear();
166 }
167 }
168 return true;
169 }
170
end(const char * fqn,int idx,SnortConfig *)171 bool FtpClientModule::end(const char* fqn, int idx, SnortConfig*)
172 {
173 if ( strcmp(fqn, "ftp_client.bounce_to") )
174 return true;
175
176 if ( idx && !strcmp(fqn, "ftp_client.bounce_to") )
177 {
178 if ( address.empty() || (last_port && (port > last_port)) )
179 {
180 ParseError("bad ftp_client.bounce_to [%d]", idx);
181 return false;
182 }
183 bounce_to.emplace_back(new BounceTo(address, port, last_port));
184 }
185 return true;
186 }
187
188 //-------------------------------------------------------------------------
189 // server stuff
190 //-------------------------------------------------------------------------
191
FtpCmd(const std::string & key,uint32_t flg,int num)192 FtpCmd::FtpCmd(const std::string& key, uint32_t flg, int num)
193 {
194 name = key;
195 flags = flg;
196 number = num;
197 }
198
FtpCmd(const std::string & key,const std::string & fmt,int num)199 FtpCmd::FtpCmd(const std::string& key, const std::string& fmt, int num)
200 {
201 name = key;
202 format = fmt;
203
204 flags = CMD_VALID;
205 number = 0;
206
207 if ( num >= 0 )
208 {
209 number = num;
210 flags |= CMD_LEN;
211 }
212 }
213
214 //-------------------------------------------------------------------------
215
216 #define FTP_TELNET_CMD_STR \
217 "TELNET cmd on FTP command channel"
218 #define FTP_INVALID_CMD_STR \
219 "invalid FTP command"
220 #define FTP_PARAMETER_LENGTH_OVERFLOW_STR \
221 "FTP command parameters were too long"
222 #define FTP_MALFORMED_PARAMETER_STR \
223 "FTP command parameters were malformed"
224 #define FTP_PARAMETER_STR_FORMAT_STR \
225 "FTP command parameters contained potential string format"
226 #define FTP_RESPONSE_LENGTH_OVERFLOW_STR \
227 "FTP response message was too long"
228 #define FTP_ENCRYPTED_STR \
229 "FTP traffic encrypted"
230 #define FTP_BOUNCE_STR \
231 "FTP bounce attempt"
232 #define FTP_EVASIVE_TELNET_CMD_STR \
233 "evasive (incomplete) TELNET cmd on FTP command channel"
234
235 //-------------------------------------------------------------------------
236
237 static const Parameter ftp_server_validity_params[] =
238 {
239 { "command", Parameter::PT_STRING, nullptr, nullptr,
240 "command string" },
241
242 { "format", Parameter::PT_STRING, nullptr, nullptr,
243 "format specification" },
244
245 { "length", Parameter::PT_INT, "0:max32", "0",
246 "specify non-default maximum for command" },
247
248 { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
249 };
250
251 static const Parameter ftp_directory_params[] =
252 {
253 { "dir_cmd", Parameter::PT_STRING, nullptr, nullptr,
254 "directory command" },
255
256 { "rsp_code", Parameter::PT_INT, "200:max32", "200",
257 "expected successful response code for command" },
258
259 { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
260 };
261
262 static const Parameter ftp_server_params[] =
263 {
264 { "chk_str_fmt", Parameter::PT_STRING, nullptr, nullptr,
265 "check the formatting of the given commands" },
266
267 { "data_chan_cmds", Parameter::PT_STRING, nullptr, nullptr,
268 "check the formatting of the given commands" },
269
270 { "data_rest_cmds", Parameter::PT_STRING, nullptr, nullptr,
271 "check the formatting of the given commands" },
272
273 { "data_xfer_cmds", Parameter::PT_STRING, nullptr, nullptr,
274 "check the formatting of the given commands" },
275
276 { "directory_cmds", Parameter::PT_LIST, ftp_directory_params, nullptr,
277 "specify command-response pairs" },
278
279 { "file_put_cmds", Parameter::PT_STRING, nullptr, nullptr,
280 "check the formatting of the given commands" },
281
282 { "file_get_cmds", Parameter::PT_STRING, nullptr, nullptr,
283 "check the formatting of the given commands" },
284
285 { "encr_cmds", Parameter::PT_STRING, nullptr, nullptr,
286 "check the formatting of the given commands" },
287
288 { "login_cmds", Parameter::PT_STRING, nullptr, nullptr,
289 "check the formatting of the given commands" },
290
291 { "check_encrypted", Parameter::PT_BOOL, nullptr, "false",
292 "check for end of encryption" },
293
294 { "cmd_validity", Parameter::PT_LIST, ftp_server_validity_params, nullptr,
295 "specify command formats" },
296
297 { "def_max_param_len", Parameter::PT_INT, "1:max32", "100",
298 "default maximum length of commands handled by server; 0 is unlimited" },
299
300 { "encrypted_traffic", Parameter::PT_BOOL, nullptr, "false",
301 "check for encrypted Telnet and FTP" },
302
303 { "ftp_cmds", Parameter::PT_STRING, nullptr, nullptr,
304 "specify additional commands supported by server beyond RFC 959" },
305
306 { "ignore_data_chan", Parameter::PT_BOOL, nullptr, "false",
307 "do not inspect FTP data channels" },
308
309 { "ignore_telnet_erase_cmds", Parameter::PT_BOOL, nullptr, "false",
310 "ignore erase character and erase line commands when normalizing" },
311
312 { "print_cmds", Parameter::PT_BOOL, nullptr, "false",
313 "print command configurations on start up" },
314
315 { "telnet_cmds", Parameter::PT_BOOL, nullptr, "false",
316 "detect Telnet escape sequences of FTP control channel" },
317
318 { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
319 };
320
321 //-------------------------------------------------------------------------
322
323 static const RuleMap ftp_server_rules[] =
324 {
325 { FTP_TELNET_CMD, FTP_TELNET_CMD_STR },
326 { FTP_INVALID_CMD, FTP_INVALID_CMD_STR },
327 { FTP_PARAMETER_LENGTH_OVERFLOW, FTP_PARAMETER_LENGTH_OVERFLOW_STR },
328 { FTP_MALFORMED_PARAMETER, FTP_MALFORMED_PARAMETER_STR },
329 { FTP_PARAMETER_STR_FORMAT, FTP_PARAMETER_STR_FORMAT_STR },
330 { FTP_RESPONSE_LENGTH_OVERFLOW, FTP_RESPONSE_LENGTH_OVERFLOW_STR },
331 { FTP_ENCRYPTED, FTP_ENCRYPTED_STR },
332 { FTP_BOUNCE, FTP_BOUNCE_STR },
333 { FTP_EVASIVE_TELNET_CMD, FTP_EVASIVE_TELNET_CMD_STR },
334
335 { 0, nullptr }
336 };
337
338 static const PegInfo ftp_pegs[] =
339 {
340 { CountType::SUM, "total_packets", "total packets" },
341 { CountType::SUM, "total_bytes", "total number of bytes processed" },
342 { CountType::NOW, "concurrent_sessions", "total concurrent FTP sessions" },
343 { CountType::MAX, "max_concurrent_sessions", "maximum concurrent FTP sessions" },
344 { CountType::SUM, "start_tls", "total STARTTLS events generated" },
345 { CountType::SUM, "ssl_search_abandoned", "total SSL search abandoned" },
346 { CountType::SUM, "ssl_srch_abandoned_early", "total SSL search abandoned too soon" },
347 { CountType::SUM, "pkt_segment_size_changed", "total number of FTP data packets with segment size change" },
348 { CountType::SUM, "flow_segment_size_changed", "total number of FTP sessions with segment size change" },
349 { CountType::END, nullptr, nullptr }
350 };
351
352 //-------------------------------------------------------------------------
353
FtpServerModule()354 FtpServerModule::FtpServerModule() :
355 Module(FTP_SERVER_NAME, ftp_server_help, ftp_server_params)
356 {
357 conf = nullptr;
358 }
359
~FtpServerModule()360 FtpServerModule::~FtpServerModule()
361 {
362 if ( conf )
363 delete conf;
364
365 for ( auto p : cmds )
366 delete p;
367 }
368
get_rules() const369 const RuleMap* FtpServerModule::get_rules() const
370 { return ftp_server_rules; }
371
get_profile() const372 ProfileStats* FtpServerModule::get_profile() const
373 { return &ftpPerfStats; }
374
add_commands(Value & v,uint32_t flags,int num)375 void FtpServerModule::add_commands(
376 Value& v, uint32_t flags, int num)
377 {
378 string tok;
379 v.set_first_token();
380
381 while ( v.get_next_token(tok) )
382 cmds.emplace_back(new FtpCmd(tok, flags, num));
383 }
384
get_cmd(unsigned idx)385 const FtpCmd* FtpServerModule::get_cmd(unsigned idx)
386 {
387 if ( idx < cmds.size() )
388 return cmds[idx];
389 else
390 return nullptr;
391 }
392
get_data()393 FTP_SERVER_PROTO_CONF* FtpServerModule::get_data()
394 {
395 FTP_SERVER_PROTO_CONF* tmp = conf;
396 conf = nullptr;
397 return tmp;
398 }
399
400 //-------------------------------------------------------------------------
401
set(const char *,Value & v,SnortConfig *)402 bool FtpServerModule::set(const char*, Value& v, SnortConfig*)
403 {
404 if ( v.is("check_encrypted") )
405 conf->detect_encrypted = v.get_bool();
406
407 else if ( v.is("chk_str_fmt") )
408 add_commands(v, CMD_CHECK);
409
410 else if ( v.is("command") )
411 names = v.get_string();
412
413 else if ( v.is("commands") )
414 names = v.get_string();
415
416 else if ( v.is("data_chan_cmds") )
417 add_commands(v, CMD_DATA);
418
419 else if ( v.is("data_rest_cmds") )
420 add_commands(v, CMD_REST);
421
422 else if ( v.is("data_xfer_cmds") )
423 add_commands(v, CMD_XFER);
424
425 else if ( v.is("def_max_param_len") )
426 conf->def_max_param_len = v.get_uint32();
427
428 else if ( v.is("dir_cmd") )
429 names = v.get_string();
430
431 else if ( v.is("encr_cmds") )
432 add_commands(v, CMD_ENCR);
433
434 else if ( v.is("encrypted_traffic") )
435 conf->check_encrypted_data = v.get_bool();
436
437 else if ( v.is("file_get_cmds") )
438 add_commands(v, CMD_XFER|CMD_GET);
439
440 else if ( v.is("file_put_cmds") )
441 add_commands(v, CMD_XFER|CMD_PUT);
442
443 else if ( v.is("format") )
444 format = v.get_string();
445
446 else if ( v.is("ftp_cmds") )
447 add_commands(v, CMD_ALLOW);
448
449 else if ( v.is("ignore_data_chan") )
450 conf->data_chan = v.get_bool();
451
452 else if ( v.is("ignore_telnet_erase_cmds") )
453 conf->ignore_telnet_erase_cmds = v.get_bool();
454
455 else if ( v.is("length") )
456 number = v.get_uint32();
457
458 else if ( v.is("login_cmds") )
459 add_commands(v, CMD_LOGIN);
460
461 else if ( v.is("print_cmds") )
462 conf->print_commands = v.get_bool();
463
464 else if ( v.is("rsp_code") )
465 number = v.get_uint32();
466
467 else if ( v.is("telnet_cmds") )
468 conf->telnet_cmds = v.get_bool();
469
470 return true;
471 }
472
473 //-------------------------------------------------------------------------
474
475 // Recursively sets nodes that allow strings to nodes that check
476 // for a string format attack within the FTP parameter validation tree
477
ResetStringFormat(FTP_PARAM_FMT * Fmt)478 static void ResetStringFormat(FTP_PARAM_FMT* Fmt)
479 {
480 int i;
481 if (!Fmt)
482 return;
483
484 if (Fmt->type == e_unrestricted)
485 Fmt->type = e_strformat;
486
487 ResetStringFormat(Fmt->optional_fmt);
488 for (i=0; i<Fmt->numChoices; i++)
489 {
490 ResetStringFormat(Fmt->choices[i]);
491 }
492 ResetStringFormat(Fmt->next_param_fmt);
493 }
494
ProcessFTPDataChanCmdsList(FTP_SERVER_PROTO_CONF * ServerConf,const FtpCmd * fc)495 static int ProcessFTPDataChanCmdsList(
496 FTP_SERVER_PROTO_CONF* ServerConf, const FtpCmd* fc)
497 {
498 const char* cmd = fc->name.c_str();
499 int iRet = 0;
500
501 FTP_CMD_CONF* FTPCmd =
502 ftp_cmd_lookup_find(ServerConf->cmd_lookup, cmd, strlen(cmd), &iRet);
503
504 if (FTPCmd == nullptr)
505 {
506 /* Add it to the list */
507 // note that struct includes 1 byte for null, so just add len
508 FTPCmd = (FTP_CMD_CONF*)snort_calloc(sizeof(FTP_CMD_CONF)+strlen(cmd));
509 strncpy(FTPCmd->cmd_name, cmd, strlen(cmd) + 1);
510
511 // FIXIT-L make sure pulled from server conf when used if not overridden
512 //FTPCmd->max_param_len = ServerConf->def_max_param_len;
513
514 ftp_cmd_lookup_add(ServerConf->cmd_lookup, cmd,
515 strlen(cmd), FTPCmd);
516 iRet = 0;
517 }
518 if ( fc->flags & CMD_DIR )
519 FTPCmd->dir_response = fc->number;
520
521 if ( fc->flags & CMD_LEN )
522 {
523 FTPCmd->max_param_len = fc->number;
524 FTPCmd->max_param_len_overridden = 1;
525 }
526 if ( fc->flags & CMD_DATA )
527 FTPCmd->data_chan_cmd = true;
528
529 if ( fc->flags & CMD_REST )
530 FTPCmd->data_rest_cmd = true;
531
532 if ( fc->flags & CMD_XFER )
533 FTPCmd->data_xfer_cmd = true;
534
535 if ( fc->flags & CMD_PUT )
536 FTPCmd->file_put_cmd = true;
537
538 if ( fc->flags & CMD_GET )
539 FTPCmd->file_get_cmd = true;
540
541 if ( fc->flags & CMD_CHECK )
542 {
543 FTP_PARAM_FMT* Fmt = FTPCmd->param_format;
544 if (Fmt)
545 {
546 ResetStringFormat(Fmt);
547 }
548 else
549 {
550 Fmt = (FTP_PARAM_FMT*)snort_calloc(sizeof(FTP_PARAM_FMT));
551 Fmt->type = e_head;
552 FTPCmd->param_format = Fmt;
553
554 Fmt = (FTP_PARAM_FMT*)snort_calloc(sizeof(FTP_PARAM_FMT));
555 Fmt->type = e_strformat;
556 FTPCmd->param_format->next_param_fmt = Fmt;
557 Fmt->prev_param_fmt = FTPCmd->param_format;
558 }
559 FTPCmd->check_validity = true;
560 }
561 if ( fc->flags & CMD_VALID && !fc->format.empty())
562 {
563 char err[1024];
564 iRet = ProcessFTPCmdValidity(
565 ServerConf, cmd, fc->format.c_str(), err, sizeof(err));
566 }
567 if ( fc->flags & CMD_ENCR )
568 FTPCmd->encr_cmd = true;
569
570 if ( fc->flags & CMD_PROT )
571 FTPCmd->prot_cmd = true;
572
573 if ( fc->flags & CMD_LOGIN )
574 FTPCmd->login_cmd = true;
575
576 return iRet;
577 }
578
begin(const char * fqn,int,SnortConfig *)579 bool FtpServerModule::begin(const char* fqn, int, SnortConfig*)
580 {
581 names.clear();
582 format.clear();
583 number = -1;
584
585 if ( !conf )
586 conf = new FTP_SERVER_PROTO_CONF;
587
588 if ( !strcmp(fqn, "ftp_server") )
589 {
590 for ( auto cmd : cmds )
591 delete cmd;
592
593 cmds.clear();
594 }
595 return true;
596 }
597
end(const char * fqn,int idx,SnortConfig *)598 bool FtpServerModule::end(const char* fqn, int idx, SnortConfig*)
599 {
600
601 if ( !idx && !strcmp(fqn, "ftp_server") )
602 {
603 cmds.emplace_back(new FtpCmd("PROT", CMD_PROT, 0));
604 for( auto cmd : cmds)
605 {
606 if ( FTPP_SUCCESS != ProcessFTPDataChanCmdsList(conf, cmd) )
607 return false;
608 }
609 return true;
610 }
611
612 if ( !strcmp(fqn, "ftp_server.cmd_validity") )
613 cmds.emplace_back(new FtpCmd(names, format, number));
614
615 else if ( !strcmp(fqn, "ftp_server.directory_cmds") )
616 {
617 Value v(names.c_str());
618 add_commands(v, CMD_DIR, number);
619 }
620 return true;
621 }
622
get_pegs() const623 const PegInfo* FtpServerModule::get_pegs() const
624 { return ftp_pegs; }
625
get_counts() const626 PegCount* FtpServerModule::get_counts() const
627 { return (PegCount*)&ftstats; }
628
629