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