Subject: Re: ftp(1) security hole, and suggested fixes
To: None <lm@rmit.edu.au>
From: David Holland <dholland@eecs.harvard.edu>
List: tech-security
Date: 08/17/1997 14:42:28
> Recently someone noted on BUGTRAQ that ftp(1) has two security
> problems:
> [...]
>
> Problem:
> it is possible to trick the client into executing arbitrary code
> on the client's machine by returning a filename such as '|sh',
> whose contents are the list of shell commands to execute.
>
> Suggested fix:
> modify recvrequest() to take an extra argument, which means
> "ignore special names such as '-' and '|*'". use this flag
> when calling recvrequest() from mget().
> I've done this, and it appears to work.
On second thought I don't think this is ideal: you also, if possible,
want to protect against someone seeing a file called '|sh' on an ftp
server and trying to get it. If you do "get |sh", at present, the
contents of the file on the server will be piped to sh since get
copies its argv[1] to argv[2] if the user doesn't supply one.
So I propose instead prepending "./" to any automatically generated
local name, both in get and mget, that begins with '|' or is '-'.
Also, names beginning with '/' should have a '.' prepended to them --
otherwise the server can send "/root/.rhosts" or
"/home/victim/.rhosts" or whatever.
Additionally, everything that mget generates should have ".." path
elements filtered out. I've left off this processing for get, on the
grounds that someone who does "get ../foo" should expect the file to
appear in the parent directory. Maybe this should be changed to be
more consistent.
My fix does reasonably unhappy things if you do "mget ../*".
Suggestions for improvements are welcome.
The following patch fixes the Linux ftp client, and should be pretty
readily adaptable to the NetBSD one.
--- cmds.c 1997/06/13 07:25:11 1.20
+++ cmds.c 1997/08/17 18:19:47
@@ -34,9 +34,9 @@
/*
* from: @(#)cmds.c 5.26 (Berkeley) 3/5/91
*/
char cmds_rcsid[] =
- "$Id: cmds.c,v 1.20 1997/06/13 07:25:11 dholland Exp $";
+ "$Id: cmds.c,v 1.23 1997/08/17 18:18:30 dholland Exp $";
/*
* FTP User Program -- Command Routines.
*/
@@ -90,8 +90,67 @@
static void quote1(const char *initial, int argc, char **argv);
/*
+ * pipeprotect: protect against "special" local filenames by prepending
+ * "./". Special local filenames are "-" and "|..." AND "/...".
+ */
+static char *pipeprotect(char *name)
+{
+ char *nu;
+ if (strcmp(name, "-") && *name!='|' && *name!='/') {
+ return name;
+ }
+
+ /* We're going to leak this memory. XXX. */
+ nu = malloc(strlen(name)+3);
+ if (nu==NULL) {
+ perror("malloc");
+ code = -1;
+ return NULL;
+ }
+ strcpy(nu, ".");
+ if (*name != '/') strcat(nu, "/");
+ strcat(nu, name);
+ return nu;
+}
+
+/*
+ * Look for embedded ".." in a pathname and change it to "!!", printing
+ * a warning.
+ */
+static char *pathprotect(char *name)
+{
+ int gotdots=0, i, len;
+
+ /* Convert null terminator to trailing / to catch a trailing ".." */
+ len = strlen(name)+1;
+ name[len-1] = '/';
+
+ /*
+ * State machine loop. gotdots is < 0 if not looking at dots,
+ * 0 if we just saw a / and thus might start getting dots,
+ * and the count of dots seen so far if we have seen some.
+ */
+ for (i=0; i<len; i++) {
+ if (name[i]=='.' && gotdots>=0) gotdots++;
+ else if (name[i]=='/' && gotdots<0) gotdots=0;
+ else if (name[i]=='/' && gotdots==2) {
+ printf("Warning: embedded .. in %.*s (changing to !!)\n",
+ len-1, name);
+ name[i-1] = '!';
+ name[i-2] = '!';
+ gotdots = 0;
+ }
+ else if (name[i]=='/') gotdots = 0;
+ else gotdots = -1;
+ }
+ name[len-1] = 0;
+ return name;
+}
+
+
+/*
* `Another' gets another argument, and stores the new argc and argv.
* It reverts to the top level (via main.c's intr()) on EOF/error.
*
* Returns false if no new arguments have been added.
@@ -594,9 +653,17 @@
char *oldargv1, *oldargv2;
if (argc == 2) {
argc++;
- argv[2] = argv[1];
+ /*
+ * Protect the user from accidentally retrieving special
+ * local names.
+ */
+ argv[2] = pipeprotect(argv[1]);
+ if (!argv[2]) {
+ code = -1;
+ return 0;
+ }
loc++;
}
if (argc < 2 && !another(&argc, &argv, "remote-file"))
goto usage;
@@ -768,10 +835,21 @@
}
if (mapflag) {
tp = domap(tp);
}
- recvrequest("RETR", tp, cp, "w",
- tp != cp || !interactive);
+ /* Reject embedded ".." */
+ tp = pathprotect(tp);
+
+ /* Prepend ./ to "-" or "!*" or leading "/" */
+ tp = pipeprotect(tp);
+ if (tp == NULL) {
+ /* hmm... how best to handle this? */
+ mflag = 0;
+ }
+ else {
+ recvrequest("RETR", tp, cp, "w",
+ tp != cp || !interactive);
+ }
if (!mflag && fromatty) {
ointer = interactive;
interactive = 1;
if (confirm("Continue with","mget")) {
--
- David A. Holland | VINO project home page:
dholland@eecs.harvard.edu | http://www.eecs.harvard.edu/vino