diff options
author | Denis Vlasenko <vda.linux@googlemail.com> | 2009-03-16 14:53:54 +0000 |
---|---|---|
committer | Denis Vlasenko <vda.linux@googlemail.com> | 2009-03-16 14:53:54 +0000 |
commit | f2160b6a09f4e4879fb718526106c548d8f8ec23 (patch) | |
tree | ff52e5b8dfb026571302023c39f925092a579f41 | |
parent | 7a6766428e3306c3bdc98e0fff207e5015af8930 (diff) | |
download | busybox-w32-f2160b6a09f4e4879fb718526106c548d8f8ec23.tar.gz busybox-w32-f2160b6a09f4e4879fb718526106c548d8f8ec23.tar.bz2 busybox-w32-f2160b6a09f4e4879fb718526106c548d8f8ec23.zip |
ftpd: security tightened up:
PORT is not allowed on !IPv4
PORT must have IP == peer's IP
upload is allowed only into regular files
function old new delta
ftpd_main 1815 2019 +204
handle_upload_common 260 306 +46
------------------------------------------------------------------------------
(add/remove: 0/0 grow/shrink: 2/0 up/down: 250/0) Total: 250 bytes
-rw-r--r-- | networking/ftpd.c | 196 |
1 files changed, 107 insertions, 89 deletions
diff --git a/networking/ftpd.c b/networking/ftpd.c index 37b340e78..22cec83a8 100644 --- a/networking/ftpd.c +++ b/networking/ftpd.c | |||
@@ -66,9 +66,9 @@ | |||
66 | /* Convert a constant to 3-digit string, packed into uint32_t */ | 66 | /* Convert a constant to 3-digit string, packed into uint32_t */ |
67 | enum { | 67 | enum { |
68 | /* Shift for Nth decimal digit */ | 68 | /* Shift for Nth decimal digit */ |
69 | SHIFT0 = 16 * BB_LITTLE_ENDIAN + 8 * BB_BIG_ENDIAN, | ||
70 | SHIFT1 = 8 * BB_LITTLE_ENDIAN + 16 * BB_BIG_ENDIAN, | ||
71 | SHIFT2 = 0 * BB_LITTLE_ENDIAN + 24 * BB_BIG_ENDIAN, | 69 | SHIFT2 = 0 * BB_LITTLE_ENDIAN + 24 * BB_BIG_ENDIAN, |
70 | SHIFT1 = 8 * BB_LITTLE_ENDIAN + 16 * BB_BIG_ENDIAN, | ||
71 | SHIFT0 = 16 * BB_LITTLE_ENDIAN + 8 * BB_BIG_ENDIAN, | ||
72 | }; | 72 | }; |
73 | #define STRNUM32(s) (uint32_t)(0 \ | 73 | #define STRNUM32(s) (uint32_t)(0 \ |
74 | | (('0' + ((s) / 1 % 10)) << SHIFT0) \ | 74 | | (('0' + ((s) / 1 % 10)) << SHIFT0) \ |
@@ -76,47 +76,6 @@ enum { | |||
76 | | (('0' + ((s) / 100 % 10)) << SHIFT2) \ | 76 | | (('0' + ((s) / 100 % 10)) << SHIFT2) \ |
77 | ) | 77 | ) |
78 | 78 | ||
79 | enum { | ||
80 | OPT_l = (1 << 0), | ||
81 | OPT_1 = (1 << 1), | ||
82 | OPT_v = (1 << 2), | ||
83 | OPT_S = (1 << 3), | ||
84 | OPT_w = (1 << 4), | ||
85 | |||
86 | #define mk_const4(a,b,c,d) (((a * 0x100 + b) * 0x100 + c) * 0x100 + d) | ||
87 | #define mk_const3(a,b,c) ((a * 0x100 + b) * 0x100 + c) | ||
88 | const_ALLO = mk_const4('A', 'L', 'L', 'O'), | ||
89 | const_APPE = mk_const4('A', 'P', 'P', 'E'), | ||
90 | const_CDUP = mk_const4('C', 'D', 'U', 'P'), | ||
91 | const_CWD = mk_const3('C', 'W', 'D'), | ||
92 | const_DELE = mk_const4('D', 'E', 'L', 'E'), | ||
93 | const_EPSV = mk_const4('E', 'P', 'S', 'V'), | ||
94 | const_HELP = mk_const4('H', 'E', 'L', 'P'), | ||
95 | const_LIST = mk_const4('L', 'I', 'S', 'T'), | ||
96 | const_MKD = mk_const3('M', 'K', 'D'), | ||
97 | const_MODE = mk_const4('M', 'O', 'D', 'E'), | ||
98 | const_NLST = mk_const4('N', 'L', 'S', 'T'), | ||
99 | const_NOOP = mk_const4('N', 'O', 'O', 'P'), | ||
100 | const_PASS = mk_const4('P', 'A', 'S', 'S'), | ||
101 | const_PASV = mk_const4('P', 'A', 'S', 'V'), | ||
102 | const_PORT = mk_const4('P', 'O', 'R', 'T'), | ||
103 | const_PWD = mk_const3('P', 'W', 'D'), | ||
104 | const_QUIT = mk_const4('Q', 'U', 'I', 'T'), | ||
105 | const_REST = mk_const4('R', 'E', 'S', 'T'), | ||
106 | const_RETR = mk_const4('R', 'E', 'T', 'R'), | ||
107 | const_RMD = mk_const3('R', 'M', 'D'), | ||
108 | const_RNFR = mk_const4('R', 'N', 'F', 'R'), | ||
109 | const_RNTO = mk_const4('R', 'N', 'T', 'O'), | ||
110 | const_SIZE = mk_const4('S', 'I', 'Z', 'E'), | ||
111 | const_STAT = mk_const4('S', 'T', 'A', 'T'), | ||
112 | const_STOR = mk_const4('S', 'T', 'O', 'R'), | ||
113 | const_STOU = mk_const4('S', 'T', 'O', 'U'), | ||
114 | const_STRU = mk_const4('S', 'T', 'R', 'U'), | ||
115 | const_SYST = mk_const4('S', 'Y', 'S', 'T'), | ||
116 | const_TYPE = mk_const4('T', 'Y', 'P', 'E'), | ||
117 | const_USER = mk_const4('U', 'S', 'E', 'R'), | ||
118 | }; | ||
119 | |||
120 | struct globals { | 79 | struct globals { |
121 | char *p_control_line_buf; | 80 | char *p_control_line_buf; |
122 | len_and_sockaddr *local_addr; | 81 | len_and_sockaddr *local_addr; |
@@ -182,21 +141,16 @@ replace_char(char *str, char from, char to) | |||
182 | return p - str; | 141 | return p - str; |
183 | } | 142 | } |
184 | 143 | ||
144 | /* NB: status_str is char[4] packed into uint32_t */ | ||
185 | static void | 145 | static void |
186 | cmdio_write(uint32_t status_str, const char *str) | 146 | cmdio_write(uint32_t status_str, const char *str) |
187 | { | 147 | { |
188 | union { | ||
189 | char buf[4]; | ||
190 | uint32_t v32; | ||
191 | } u; | ||
192 | char *response; | 148 | char *response; |
193 | int len; | 149 | int len; |
194 | 150 | ||
195 | u.v32 = status_str; | ||
196 | |||
197 | /* FTP allegedly uses telnet protocol for command link. | 151 | /* FTP allegedly uses telnet protocol for command link. |
198 | * In telnet, 0xff is an escape char, and needs to be escaped: */ | 152 | * In telnet, 0xff is an escape char, and needs to be escaped: */ |
199 | response = escape_text(u.buf, str, (0xff << 8) + '\r'); | 153 | response = escape_text((char *) &status_str, str, (0xff << 8) + '\r'); |
200 | 154 | ||
201 | /* ?! does FTP send embedded LFs as NULs? wow */ | 155 | /* ?! does FTP send embedded LFs as NULs? wow */ |
202 | len = replace_char(response, '\n', '\0'); | 156 | len = replace_char(response, '\n', '\0'); |
@@ -444,46 +398,58 @@ handle_epsv(void) | |||
444 | static void | 398 | static void |
445 | handle_port(void) | 399 | handle_port(void) |
446 | { | 400 | { |
447 | unsigned short port; | 401 | unsigned port, port_hi; |
448 | char *raw, *port_part; | 402 | char *raw, *comma; |
449 | len_and_sockaddr *lsa; | 403 | socklen_t peer_ipv4_len; |
404 | struct sockaddr_in peer_ipv4; | ||
405 | struct in_addr port_ipv4_sin_addr; | ||
450 | 406 | ||
451 | port_pasv_cleanup(); | 407 | port_pasv_cleanup(); |
452 | 408 | ||
453 | raw = G.ftp_arg; | 409 | raw = G.ftp_arg; |
454 | 410 | ||
455 | /* Buglets: | 411 | /* PORT command format makes sense only over IPv4 */ |
456 | * xatou16 will accept wrong input, | 412 | if (!raw || G.local_addr->u.sa.sa_family != AF_INET) { |
457 | * xatou16 will exit instead of generating error to peer | 413 | bail: |
458 | */ | 414 | cmdio_write_error(FTP_BADCMD); |
415 | return; | ||
416 | } | ||
459 | 417 | ||
460 | port_part = strrchr(raw, ','); | 418 | comma = strrchr(raw, ','); |
461 | if (port_part == NULL) | 419 | if (comma == NULL) |
420 | goto bail; | ||
421 | *comma = '\0'; | ||
422 | port = bb_strtou(&comma[1], NULL, 10); | ||
423 | if (errno || port > 0xff) | ||
462 | goto bail; | 424 | goto bail; |
463 | port = xatou16(&port_part[1]); | ||
464 | *port_part = '\0'; | ||
465 | 425 | ||
466 | port_part = strrchr(raw, ','); | 426 | comma = strrchr(raw, ','); |
467 | if (port_part == NULL) | 427 | if (comma == NULL) |
468 | goto bail; | 428 | goto bail; |
469 | port |= xatou16(&port_part[1]) << 8; | 429 | *comma = '\0'; |
470 | *port_part = '\0'; | 430 | port_hi = bb_strtou(&comma[1], NULL, 10); |
431 | if (errno || port_hi > 0xff) | ||
432 | goto bail; | ||
433 | port |= port_hi << 8; | ||
471 | 434 | ||
472 | replace_char(raw, ',', '.'); | 435 | replace_char(raw, ',', '.'); |
473 | lsa = xdotted2sockaddr(raw, port); | ||
474 | 436 | ||
475 | if (lsa == NULL) { | 437 | /* We are verifying that PORT's IP matches getpeername(). |
476 | bail: | 438 | * Otherwise peer can make us open data connections |
477 | cmdio_write_error(FTP_BADCMD); | 439 | * to other hosts (security problem!) |
478 | return; | 440 | * This code would be too simplistic: |
479 | } | 441 | * lsa = xdotted2sockaddr(raw, port); |
480 | 442 | * if (lsa == NULL) goto bail; | |
481 | /* Should we verify that lsa matches getpeername(STDIN)? | 443 | */ |
482 | * Otherwise peer can make us open data connections | 444 | if (!inet_aton(raw, &port_ipv4_sin_addr)) |
483 | * to other hosts (security problem!) | 445 | goto bail; |
484 | */ | 446 | peer_ipv4_len = sizeof(peer_ipv4); |
447 | if (getpeername(STDIN_FILENO, &peer_ipv4, &peer_ipv4_len) != 0) | ||
448 | goto bail; | ||
449 | if (memcmp(&port_ipv4_sin_addr, &peer_ipv4.sin_addr, sizeof(struct in_addr)) != 0) | ||
450 | goto bail; | ||
485 | 451 | ||
486 | G.port_addr = lsa; | 452 | G.port_addr = xdotted2sockaddr(raw, port); |
487 | cmdio_write_ok(FTP_PORTOK); | 453 | cmdio_write_ok(FTP_PORTOK); |
488 | } | 454 | } |
489 | 455 | ||
@@ -612,6 +578,8 @@ handle_dir_common(int opts) | |||
612 | if (!(opts & USE_CTRL_CONN) && !port_or_pasv_was_seen()) | 578 | if (!(opts & USE_CTRL_CONN) && !port_or_pasv_was_seen()) |
613 | return; /* port_or_pasv_was_seen emitted error response */ | 579 | return; /* port_or_pasv_was_seen emitted error response */ |
614 | 580 | ||
581 | /* -n prevents user/groupname display, | ||
582 | * which can be problematic in chroot */ | ||
615 | ls_fd = popen_ls((opts & LONG_LISTING) ? "-l" : "-1"); | 583 | ls_fd = popen_ls((opts & LONG_LISTING) ? "-l" : "-1"); |
616 | ls_fp = fdopen(ls_fd, "r"); | 584 | ls_fp = fdopen(ls_fd, "r"); |
617 | if (!ls_fp) /* never happens. paranoia */ | 585 | if (!ls_fp) /* never happens. paranoia */ |
@@ -749,11 +717,12 @@ handle_rnto(void) | |||
749 | static void | 717 | static void |
750 | handle_upload_common(int is_append, int is_unique) | 718 | handle_upload_common(int is_append, int is_unique) |
751 | { | 719 | { |
752 | char *tempname = NULL; | 720 | struct stat statbuf; |
721 | char *tempname; | ||
753 | off_t bytes_transferred; | 722 | off_t bytes_transferred; |
723 | off_t offset; | ||
754 | int local_file_fd; | 724 | int local_file_fd; |
755 | int remote_fd; | 725 | int remote_fd; |
756 | off_t offset; | ||
757 | 726 | ||
758 | offset = G.restart_pos; | 727 | offset = G.restart_pos; |
759 | G.restart_pos = 0; | 728 | G.restart_pos = 0; |
@@ -761,6 +730,7 @@ handle_upload_common(int is_append, int is_unique) | |||
761 | if (!port_or_pasv_was_seen()) | 730 | if (!port_or_pasv_was_seen()) |
762 | return; /* port_or_pasv_was_seen emitted error response */ | 731 | return; /* port_or_pasv_was_seen emitted error response */ |
763 | 732 | ||
733 | tempname = NULL; | ||
764 | local_file_fd = -1; | 734 | local_file_fd = -1; |
765 | if (is_unique) { | 735 | if (is_unique) { |
766 | tempname = xstrdup(" FILE: uniq.XXXXXX"); | 736 | tempname = xstrdup(" FILE: uniq.XXXXXX"); |
@@ -773,13 +743,17 @@ handle_upload_common(int is_append, int is_unique) | |||
773 | flags = O_WRONLY | O_CREAT; | 743 | flags = O_WRONLY | O_CREAT; |
774 | local_file_fd = open(G.ftp_arg, flags, 0666); | 744 | local_file_fd = open(G.ftp_arg, flags, 0666); |
775 | } | 745 | } |
776 | if (local_file_fd < 0) { | 746 | |
747 | if (local_file_fd < 0 | ||
748 | || fstat(local_file_fd, &statbuf) != 0 | ||
749 | || !S_ISREG(statbuf.st_mode) | ||
750 | ) { | ||
777 | cmdio_write_error(FTP_UPLOADFAIL); | 751 | cmdio_write_error(FTP_UPLOADFAIL); |
752 | if (local_file_fd >= 0) | ||
753 | goto close_local_and_bail; | ||
778 | return; | 754 | return; |
779 | } | 755 | } |
780 | 756 | ||
781 | /* TODO: paranoia: fstat it, refuse to do anything if it's not a regular file */ | ||
782 | |||
783 | if (offset) | 757 | if (offset) |
784 | xlseek(local_file_fd, offset, SEEK_SET); | 758 | xlseek(local_file_fd, offset, SEEK_SET); |
785 | 759 | ||
@@ -787,7 +761,7 @@ handle_upload_common(int is_append, int is_unique) | |||
787 | free(tempname); | 761 | free(tempname); |
788 | 762 | ||
789 | if (remote_fd < 0) | 763 | if (remote_fd < 0) |
790 | goto bail; | 764 | goto close_local_and_bail; |
791 | 765 | ||
792 | /* TODO: if we'll implement timeout, this will need more clever handling. | 766 | /* TODO: if we'll implement timeout, this will need more clever handling. |
793 | * Perhaps alarm(N) + checking that current position on local_file_fd | 767 | * Perhaps alarm(N) + checking that current position on local_file_fd |
@@ -800,7 +774,8 @@ handle_upload_common(int is_append, int is_unique) | |||
800 | cmdio_write_error(FTP_BADSENDFILE); | 774 | cmdio_write_error(FTP_BADSENDFILE); |
801 | else | 775 | else |
802 | cmdio_write_ok(FTP_TRANSFEROK); | 776 | cmdio_write_ok(FTP_TRANSFEROK); |
803 | bail: | 777 | |
778 | close_local_and_bail: | ||
804 | close(local_file_fd); | 779 | close(local_file_fd); |
805 | } | 780 | } |
806 | 781 | ||
@@ -838,17 +813,18 @@ cmdio_get_cmd_and_arg(void) | |||
838 | if (!cmd) | 813 | if (!cmd) |
839 | exit(0); | 814 | exit(0); |
840 | 815 | ||
816 | /* Trailing '\n' is already stripped, strip '\r' */ | ||
841 | len = strlen(cmd) - 1; | 817 | len = strlen(cmd) - 1; |
842 | while (len >= 0 && cmd[len] == '\r') { | 818 | while ((ssize_t)len >= 0 && cmd[len] == '\r') { |
843 | cmd[len] = '\0'; | 819 | cmd[len] = '\0'; |
844 | len--; | 820 | len--; |
845 | } | 821 | } |
846 | 822 | ||
847 | G.ftp_arg = strchr(cmd, ' '); | 823 | G.ftp_arg = strchr(cmd, ' '); |
848 | if (G.ftp_arg != NULL) { | 824 | if (G.ftp_arg != NULL) |
849 | *G.ftp_arg = '\0'; | 825 | *G.ftp_arg++ = '\0'; |
850 | G.ftp_arg++; | 826 | |
851 | } | 827 | /* Uppercase and pack into uint32_t first word of the command */ |
852 | cmdval = 0; | 828 | cmdval = 0; |
853 | while (*cmd) | 829 | while (*cmd) |
854 | cmdval = (cmdval << 8) + ((unsigned char)*cmd++ & (unsigned char)~0x20); | 830 | cmdval = (cmdval << 8) + ((unsigned char)*cmd++ & (unsigned char)~0x20); |
@@ -856,6 +832,47 @@ cmdio_get_cmd_and_arg(void) | |||
856 | return cmdval; | 832 | return cmdval; |
857 | } | 833 | } |
858 | 834 | ||
835 | #define mk_const4(a,b,c,d) (((a * 0x100 + b) * 0x100 + c) * 0x100 + d) | ||
836 | #define mk_const3(a,b,c) ((a * 0x100 + b) * 0x100 + c) | ||
837 | enum { | ||
838 | const_ALLO = mk_const4('A', 'L', 'L', 'O'), | ||
839 | const_APPE = mk_const4('A', 'P', 'P', 'E'), | ||
840 | const_CDUP = mk_const4('C', 'D', 'U', 'P'), | ||
841 | const_CWD = mk_const3('C', 'W', 'D'), | ||
842 | const_DELE = mk_const4('D', 'E', 'L', 'E'), | ||
843 | const_EPSV = mk_const4('E', 'P', 'S', 'V'), | ||
844 | const_HELP = mk_const4('H', 'E', 'L', 'P'), | ||
845 | const_LIST = mk_const4('L', 'I', 'S', 'T'), | ||
846 | const_MKD = mk_const3('M', 'K', 'D'), | ||
847 | const_MODE = mk_const4('M', 'O', 'D', 'E'), | ||
848 | const_NLST = mk_const4('N', 'L', 'S', 'T'), | ||
849 | const_NOOP = mk_const4('N', 'O', 'O', 'P'), | ||
850 | const_PASS = mk_const4('P', 'A', 'S', 'S'), | ||
851 | const_PASV = mk_const4('P', 'A', 'S', 'V'), | ||
852 | const_PORT = mk_const4('P', 'O', 'R', 'T'), | ||
853 | const_PWD = mk_const3('P', 'W', 'D'), | ||
854 | const_QUIT = mk_const4('Q', 'U', 'I', 'T'), | ||
855 | const_REST = mk_const4('R', 'E', 'S', 'T'), | ||
856 | const_RETR = mk_const4('R', 'E', 'T', 'R'), | ||
857 | const_RMD = mk_const3('R', 'M', 'D'), | ||
858 | const_RNFR = mk_const4('R', 'N', 'F', 'R'), | ||
859 | const_RNTO = mk_const4('R', 'N', 'T', 'O'), | ||
860 | const_SIZE = mk_const4('S', 'I', 'Z', 'E'), | ||
861 | const_STAT = mk_const4('S', 'T', 'A', 'T'), | ||
862 | const_STOR = mk_const4('S', 'T', 'O', 'R'), | ||
863 | const_STOU = mk_const4('S', 'T', 'O', 'U'), | ||
864 | const_STRU = mk_const4('S', 'T', 'R', 'U'), | ||
865 | const_SYST = mk_const4('S', 'Y', 'S', 'T'), | ||
866 | const_TYPE = mk_const4('T', 'Y', 'P', 'E'), | ||
867 | const_USER = mk_const4('U', 'S', 'E', 'R'), | ||
868 | |||
869 | OPT_l = (1 << 0), | ||
870 | OPT_1 = (1 << 1), | ||
871 | OPT_v = (1 << 2), | ||
872 | OPT_S = (1 << 3), | ||
873 | OPT_w = (1 << 4), | ||
874 | }; | ||
875 | |||
859 | int ftpd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; | 876 | int ftpd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; |
860 | int ftpd_main(int argc, char **argv) | 877 | int ftpd_main(int argc, char **argv) |
861 | { | 878 | { |
@@ -865,6 +882,7 @@ int ftpd_main(int argc, char **argv) | |||
865 | 882 | ||
866 | if (opts & (OPT_l|OPT_1)) { | 883 | if (opts & (OPT_l|OPT_1)) { |
867 | /* Our secret backdoor to ls */ | 884 | /* Our secret backdoor to ls */ |
885 | /* TODO: pass -n too? */ | ||
868 | xchdir(argv[2]); | 886 | xchdir(argv[2]); |
869 | argv[2] = (char*)"--"; | 887 | argv[2] = (char*)"--"; |
870 | return ls_main(argc, argv); | 888 | return ls_main(argc, argv); |