1 /*
2  * ProFTPD - FTP server daemon
3  * Copyright (c) 2009-2020 The ProFTPD Project team
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
18  *
19  * As a special exemption, The ProFTPD Project team and other respective
20  * copyright holders give permission to link this program with OpenSSL, and
21  * distribute the resulting executable, without including the source code for
22  * OpenSSL in the source distribution.
23  */
24 
25 #include "conf.h"
26 
27 /* This struct and the list of such structs are used to try to reduce
28  * the use of the following idiom to identify which command a given
29  * cmd_rec is:
30  *
31  *  if (strcmp(cmd->argv[0], C_USER) == 0)
32  *
33  * Rather than using strcmp(3) so freely, try to reduce the command to
34  * a fixed ID (an index into the struct list); this ID can then be compared
35  * rather than using strcmp(3).  For commands not in the list, strcmp(3)
36  * can always be used as a fallback.
37  *
38  * A future improvement would be to sort the entries in the table so that
39  * the most common commands appear earlier in the table, and make the
40  * linear scan even shorter.  But I'd need to collect better metrics in
41  * order to do that.
42  */
43 
44 struct cmd_entry {
45   const char *cmd_name;
46   size_t cmd_namelen;
47 };
48 
49 static struct cmd_entry cmd_ids[] = {
50   { " ",	1 },	/* Index 0 is intentionally filled with a sentinel */
51   { C_USER,	4 },	/* PR_CMD_USER_ID (1) */
52   { C_PASS,	4 },	/* PR_CMD_PASS_ID (2) */
53   { C_ACCT,	4 },	/* PR_CMD_ACCT_ID (3) */
54   { C_CWD,	3 },	/* PR_CMD_CWD_ID (4) */
55   { C_XCWD,	4 },	/* PR_CMD_XCWD_ID (5) */
56   { C_CDUP,	4 },	/* PR_CMD_CDUP_ID (6) */
57   { C_XCUP,	4 },	/* PR_CMD_XCUP_ID (7) */
58   { C_SMNT,	4 },	/* PR_CMD_SMNT_ID (8) */
59   { C_REIN,	4 },	/* PR_CMD_REIN_ID (9) */
60   { C_QUIT,	4 },	/* PR_CMD_QUIT_ID (10) */
61   { C_PORT,	4 },	/* PR_CMD_PORT_ID (11) */
62   { C_EPRT,	4 },	/* PR_CMD_EPRT_ID (12) */
63   { C_PASV,	4 },	/* PR_CMD_PASV_ID (13) */
64   { C_EPSV,	4 },	/* PR_CMD_EPSV_ID (14) */
65   { C_TYPE,	4 },	/* PR_CMD_TYPE_ID (15) */
66   { C_STRU,	4 },	/* PR_CMD_STRU_ID (16) */
67   { C_MODE,	4 },	/* PR_CMD_MODE_ID (17) */
68   { C_RETR,	4 },	/* PR_CMD_RETR_ID (18) */
69   { C_STOR,	4 },	/* PR_CMD_STOR_ID (19) */
70   { C_STOU,	4 },	/* PR_CMD_STOU_ID (20) */
71   { C_APPE,	4 },	/* PR_CMD_APPE_ID (21) */
72   { C_ALLO,	4 },	/* PR_CMD_ALLO_ID (22) */
73   { C_REST,	4 },	/* PR_CMD_REST_ID (23) */
74   { C_RNFR,	4 },	/* PR_CMD_RNFR_ID (24) */
75   { C_RNTO,	4 },	/* PR_CMD_RNTO_ID (25) */
76   { C_ABOR,	4 },	/* PR_CMD_ABOR_ID (26) */
77   { C_DELE,	4 },	/* PR_CMD_DELE_ID (27) */
78   { C_MDTM,	4 },	/* PR_CMD_MDTM_ID (28) */
79   { C_RMD,	3 },	/* PR_CMD_RMD_ID (29) */
80   { C_XRMD,	4 },	/* PR_CMD_XRMD_ID (30) */
81   { C_MKD,	3 },	/* PR_CMD_MKD_ID (31) */
82   { C_MLSD,	4 },	/* PR_CMD_MLSD_ID (32) */
83   { C_MLST,	4 },	/* PR_CMD_MLST_ID (33) */
84   { C_XMKD,	4 },	/* PR_CMD_XMKD_ID (34) */
85   { C_PWD,	3 },	/* PR_CMD_PWD_ID (35) */
86   { C_XPWD,	4 },	/* PR_CMD_XPWD_ID (36) */
87   { C_SIZE,	4 },	/* PR_CMD_SIZE_ID (37) */
88   { C_LIST,	4 },	/* PR_CMD_LIST_ID (38) */
89   { C_NLST,	4 },	/* PR_CMD_NLST_ID (39) */
90   { C_SITE,	4 },	/* PR_CMD_SITE_ID (40) */
91   { C_SYST,	4 },	/* PR_CMD_SYST_ID (41) */
92   { C_STAT,	4 },	/* PR_CMD_STAT_ID (42) */
93   { C_HELP,	4 },	/* PR_CMD_HELP_ID (43) */
94   { C_NOOP,	4 },	/* PR_CMD_NOOP_ID (44) */
95   { C_FEAT,	4 },	/* PR_CMD_FEAT_ID (45) */
96   { C_OPTS,	4 },	/* PR_CMD_OPTS_ID (46) */
97   { C_LANG,	4 },	/* PR_CMD_LANG_ID (47) */
98   { C_ADAT,	4 },	/* PR_CMD_ADAT_ID (48) */
99   { C_AUTH,	4 },	/* PR_CMD_AUTH_ID (49) */
100   { C_CCC,	3 },	/* PR_CMD_CCC_ID (50) */
101   { C_CONF,	4 },	/* PR_CMD_CONF_ID (51) */
102   { C_ENC,	3 },	/* PR_CMD_ENC_ID (52) */
103   { C_MIC,	3 },	/* PR_CMD_MIC_ID (53) */
104   { C_PBSZ,	4 },	/* PR_CMD_PBSZ_ID (54) */
105   { C_PROT,	4 },	/* PR_CMD_PROT_ID (55) */
106   { C_MFF,	3 },	/* PR_CMD_MFF_ID (56) */
107   { C_MFMT,	4 },	/* PR_CMD_MFMT_ID (57) */
108   { C_HOST,	4 },	/* PR_CMD_HOST_ID (58) */
109   { C_CLNT,	4 },	/* PR_CMD_CLNT_ID (59) */
110   { C_RANG,	4 },	/* PR_CMD_RANG_ID (60) */
111 
112   { NULL,	0 }
113 };
114 
115 /* Due to potential XSS issues (see Bug#4143), we want to explicitly
116  * check for commands from other text-based protocols (e.g. HTTP and SMTP);
117  * if we see these, we want to close the connection with extreme prejudice.
118  */
119 
120 static struct cmd_entry http_ids[] = {
121   { " ",	1 },    /* Index 0 is intentionally filled with a sentinel */
122   { "CONNECT",	7 },
123   { "DELETE",	6 },
124   { "GET",	3 },
125   { "HEAD",	4 },
126   { "OPTIONS",	7 },
127   { "PATCH",	5 },
128   { "POST",	4 },
129   { "PUT",	3 },
130 
131   { NULL,	0 }
132 };
133 
134 static struct cmd_entry smtp_ids[] = {
135   { " ",	1 },    /* Index 0 is intentionally filled with a sentinel */
136   { "DATA",	4 },
137   { "EHLO",	4 },
138   { "HELO",	4 },
139   { "MAIL",	4 },
140   { "RCPT",	4 },
141   { "RSET",	4 },
142   { "VRFY",	4 },
143 
144   { NULL,	0 }
145 };
146 
147 static const char *trace_channel = "command";
148 
pr_cmd_alloc(pool * p,unsigned int argc,...)149 cmd_rec *pr_cmd_alloc(pool *p, unsigned int argc, ...) {
150   pool *newpool = NULL;
151   cmd_rec *cmd = NULL;
152   int *xerrno = NULL;
153   va_list args;
154 
155   if (p == NULL) {
156     errno = EINVAL;
157     return NULL;
158   }
159 
160   newpool = make_sub_pool(p);
161   pr_pool_tag(newpool, "cmd_rec pool");
162 
163   cmd = pcalloc(newpool, sizeof(cmd_rec));
164   cmd->argc = argc;
165   cmd->stash_index = -1;
166   cmd->stash_hash = 0;
167   cmd->pool = newpool;
168   cmd->tmp_pool = make_sub_pool(cmd->pool);
169   pr_pool_tag(cmd->tmp_pool, "cmd_rec tmp pool");
170 
171   if (argc > 0) {
172     register unsigned int i = 0;
173 
174     cmd->argv = pcalloc(cmd->pool, sizeof(void *) * (argc + 1));
175     va_start(args, argc);
176 
177     for (i = 0; i < argc; i++) {
178       cmd->argv[i] = va_arg(args, void *);
179     }
180 
181     va_end(args);
182     cmd->argv[argc] = NULL;
183 
184     pr_pool_tag(cmd->pool, cmd->argv[0]);
185   }
186 
187   /* This table will not contain that many entries, so a low number
188    * of chains should suffice.
189    */
190   cmd->notes = pr_table_nalloc(cmd->pool, 0, 8);
191 
192   /* Initialize the "errno" note to be zero, so that it is always present. */
193   xerrno = palloc(cmd->pool, sizeof(int));
194   *xerrno = 0;
195   (void) pr_table_add(cmd->notes, "errno", xerrno, sizeof(int));
196 
197   return cmd;
198 }
199 
pr_cmd_clear_cache(cmd_rec * cmd)200 int pr_cmd_clear_cache(cmd_rec *cmd) {
201   if (cmd == NULL) {
202     errno = EINVAL;
203     return -1;
204   }
205 
206   /* Clear the strings that have been cached for this command in the
207    * notes table.
208    */
209 
210   (void) pr_table_remove(cmd->notes, "displayable-str", NULL);
211   (void) pr_cmd_set_errno(cmd, 0);
212 
213   return 0;
214 }
215 
pr_cmd_cmp(cmd_rec * cmd,int cmd_id)216 int pr_cmd_cmp(cmd_rec *cmd, int cmd_id) {
217   if (cmd == NULL ||
218       cmd_id <= 0) {
219     errno = EINVAL;
220     return -1;
221   }
222 
223   if (cmd->argc == 0 ||
224       cmd->argv == NULL) {
225     return 1;
226   }
227 
228   /* The cmd ID is unknown; look it up. */
229   if (cmd->cmd_id == 0) {
230     cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
231   }
232 
233   /* The cmd ID is known to be unknown. */
234   if (cmd->cmd_id < 0) {
235     return 1;
236   }
237 
238   if (cmd->cmd_id == cmd_id) {
239     return 0;
240   }
241 
242   return cmd->cmd_id < cmd_id ? -1 : 1;
243 }
244 
pr_cmd_get_errno(cmd_rec * cmd)245 int pr_cmd_get_errno(cmd_rec *cmd) {
246   void *v;
247   int *xerrno;
248 
249   if (cmd == NULL) {
250     errno = EINVAL;
251     return -1;
252   }
253 
254   v = (void *) pr_table_get(cmd->notes, "errno", NULL);
255   if (v == NULL) {
256     errno = ENOENT;
257     return -1;
258   }
259 
260   xerrno = v;
261   return *xerrno;
262 }
263 
pr_cmd_set_errno(cmd_rec * cmd,int xerrno)264 int pr_cmd_set_errno(cmd_rec *cmd, int xerrno) {
265   void *v;
266 
267   if (cmd == NULL ||
268       cmd->notes == NULL) {
269     errno = EINVAL;
270     return -1;
271   }
272 
273   v = (void *) pr_table_get(cmd->notes, "errno", NULL);
274   if (v == NULL) {
275     errno = ENOENT;
276     return -1;
277   }
278 
279   *((int *) v) = xerrno;
280   return 0;
281 }
282 
pr_cmd_set_name(cmd_rec * cmd,const char * cmd_name)283 int pr_cmd_set_name(cmd_rec *cmd, const char *cmd_name) {
284   if (cmd == NULL ||
285       cmd_name == NULL) {
286     errno = EINVAL;
287     return -1;
288   }
289 
290   cmd->argv[0] = (char *) cmd_name;
291   cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
292 
293   return 0;
294 }
295 
pr_cmd_strcmp(cmd_rec * cmd,const char * cmd_name)296 int pr_cmd_strcmp(cmd_rec *cmd, const char *cmd_name) {
297   int cmd_id;
298   size_t cmd_namelen;
299 
300   if (cmd == NULL ||
301       cmd_name == NULL) {
302     errno = EINVAL;
303     return -1;
304   }
305 
306   if (cmd->argc == 0 ||
307       cmd->argv == NULL) {
308     return 1;
309   }
310 
311   /* The cmd ID is unknown; look it up. */
312   if (cmd->cmd_id == 0) {
313     cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
314   }
315 
316   if (cmd->cmd_id > 0) {
317     int res;
318 
319     cmd_id = pr_cmd_get_id(cmd_name);
320 
321     res = pr_cmd_cmp(cmd, cmd_id);
322     if (res == 0) {
323       return 0;
324     }
325 
326     return strncasecmp(cmd_name, cmd->argv[0],
327       cmd_ids[cmd->cmd_id].cmd_namelen + 1);
328   }
329 
330   cmd_namelen = strlen(cmd_name);
331   return strncmp(cmd->argv[0], cmd_name, cmd_namelen + 1);
332 }
333 
pr_cmd_get_displayable_str(cmd_rec * cmd,size_t * str_len)334 const char *pr_cmd_get_displayable_str(cmd_rec *cmd, size_t *str_len) {
335   const char *res;
336   unsigned int argc;
337   void **argv;
338   pool *p;
339 
340   if (cmd == NULL) {
341     errno = EINVAL;
342     return NULL;
343   }
344 
345   res = pr_table_get(cmd->notes, "displayable-str", NULL);
346   if (res != NULL) {
347     if (str_len != NULL) {
348       *str_len = strlen(res);
349     }
350 
351     return res;
352   }
353 
354   argc = cmd->argc;
355   argv = cmd->argv;
356   p = cmd->pool;
357 
358   res = "";
359 
360   /* Check for "sensitive" commands. */
361   if (pr_cmd_cmp(cmd, PR_CMD_PASS_ID) == 0 ||
362       pr_cmd_cmp(cmd, PR_CMD_ADAT_ID) == 0) {
363     argc = 2;
364     argv[1] = "(hidden)";
365   }
366 
367   if (argc > 0) {
368     register unsigned int i;
369 
370     res = pstrcat(p, res, pr_fs_decode_path(p, argv[0]), NULL);
371 
372     for (i = 1; i < argc; i++) {
373       res = pstrcat(p, res, " ", pr_fs_decode_path(p, argv[i]), NULL);
374     }
375   }
376 
377   if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
378       pstrdup(cmd->pool, res), 0) < 0) {
379     if (errno != EEXIST) {
380       pr_trace_msg(trace_channel, 4,
381         "error setting 'displayable-str' command note: %s", strerror(errno));
382     }
383   }
384 
385   if (str_len != NULL) {
386     *str_len = strlen(res);
387   }
388 
389   return res;
390 }
391 
pr_cmd_get_id(const char * cmd_name)392 int pr_cmd_get_id(const char *cmd_name) {
393   register unsigned int i;
394   size_t cmd_namelen;
395   char first_letter;
396 
397   if (cmd_name == NULL) {
398     errno = EINVAL;
399     return -1;
400   }
401 
402   cmd_namelen = strlen(cmd_name);
403 
404   /* Take advantage of the fact that we know, a priori, that the shortest
405    * command name in the list is 3 characters, and that the longest is 4
406    * characters.  No need to scan the list if we know that the given name
407    * is not within that length range.
408    */
409   if (cmd_namelen < PR_CMD_MIN_NAMELEN ||
410       cmd_namelen > PR_CMD_MAX_NAMELEN) {
411     errno = ENOENT;
412     return -1;
413   }
414 
415   first_letter = toupper(cmd_name[0]);
416 
417   for (i = 1; cmd_ids[i].cmd_name != NULL; i++) {
418     if (cmd_ids[i].cmd_namelen != cmd_namelen) {
419       continue;
420     }
421 
422     if (cmd_ids[i].cmd_name[0] != first_letter) {
423       continue;
424     }
425 
426     if (strcasecmp(cmd_ids[i].cmd_name, cmd_name) == 0) {
427       return i;
428     }
429   }
430 
431   errno = ENOENT;
432   return -1;
433 }
434 
is_known_cmd(struct cmd_entry * known_cmds,const char * cmd_name,size_t cmd_namelen)435 static int is_known_cmd(struct cmd_entry *known_cmds, const char *cmd_name,
436     size_t cmd_namelen) {
437   register unsigned int i;
438   int known = FALSE;
439 
440   for (i = 0; known_cmds[i].cmd_name != NULL; i++) {
441     if (cmd_namelen == known_cmds[i].cmd_namelen) {
442       if (strncmp(cmd_name, known_cmds[i].cmd_name, cmd_namelen + 1) == 0) {
443         known = TRUE;
444         break;
445       }
446     }
447   }
448 
449   return known;
450 }
451 
pr_cmd_is_http(cmd_rec * cmd)452 int pr_cmd_is_http(cmd_rec *cmd) {
453   const char *cmd_name;
454   size_t cmd_namelen;
455 
456   if (cmd == NULL) {
457     errno = EINVAL;
458     return -1;
459   }
460 
461   cmd_name = cmd->argv[0];
462   if (cmd_name == NULL) {
463     errno = EINVAL;
464     return -1;
465   }
466 
467   if (cmd->cmd_id == 0) {
468     cmd->cmd_id = pr_cmd_get_id(cmd_name);
469   }
470 
471   if (cmd->cmd_id >= 0) {
472     return FALSE;
473   }
474 
475   cmd_namelen = strlen(cmd_name);
476   return is_known_cmd(http_ids, cmd_name, cmd_namelen);
477 }
478 
pr_cmd_is_smtp(cmd_rec * cmd)479 int pr_cmd_is_smtp(cmd_rec *cmd) {
480   const char *cmd_name;
481   size_t cmd_namelen;
482 
483   if (cmd == NULL) {
484     errno = EINVAL;
485     return -1;
486   }
487 
488   cmd_name = cmd->argv[0];
489   if (cmd_name == NULL) {
490     errno = EINVAL;
491     return -1;
492   }
493 
494   if (cmd->cmd_id == 0) {
495     cmd->cmd_id = pr_cmd_get_id(cmd_name);
496   }
497 
498   if (cmd->cmd_id >= 0) {
499     return FALSE;
500   }
501 
502   cmd_namelen = strlen(cmd_name);
503   return is_known_cmd(smtp_ids, cmd_name, cmd_namelen);
504 }
505 
pr_cmd_is_ssh2(cmd_rec * cmd)506 int pr_cmd_is_ssh2(cmd_rec *cmd) {
507   const char *cmd_name;
508 
509   if (cmd == NULL) {
510     errno = EINVAL;
511     return -1;
512   }
513 
514   cmd_name = cmd->argv[0];
515   if (cmd_name == NULL) {
516     errno = EINVAL;
517     return -1;
518   }
519 
520   if (cmd->cmd_id == 0) {
521     cmd->cmd_id = pr_cmd_get_id(cmd_name);
522   }
523 
524   if (cmd->cmd_id >= 0) {
525     return FALSE;
526   }
527 
528   if (strncmp(cmd_name, "SSH-2.0-", 8) == 0 ||
529       strncmp(cmd_name, "SSH-1.99-", 9) == 0) {
530     return TRUE;
531   }
532 
533   return FALSE;
534 }
535