Lucene search

K
nessusThis script is Copyright (C) 2005-2022 and is owned by Tenable, Inc. or an Affiliate thereof.FTP_WRITEABLE_DIRECTORIES.NASL
HistoryOct 04, 2005 - 12:00 a.m.

FTP Writable Directories

2005-10-0400:00:00
This script is Copyright (C) 2005-2022 and is owned by Tenable, Inc. or an Affiliate thereof.
www.tenable.com
752

By crawling through the remote FTP server, Nessus discovered several directories were marked as being world-writable.

This could have several negative impacts :

  • Temporary file uploads are sometimes immediately available to all anonymous users, allowing the FTP server to be used as a ‘drop’ point. This may facilitate trading copyrighted, pornographic, or questionable material.

  • A user may be able to upload large files that consume disk space, resulting in a denial of service condition.

  • A user can upload a malicious program. If an administrator routinely checks the ‘incoming’ directory, they may load a document or run a program that exploits a vulnerability in client software.

#TRUSTED 913876aa723f886be3648f6010d3881f4059eb306f3cd8148a53b3024160ef850e908e0588c06e8047ee88e02374441926d6dc72aa4719ec6b26549de2a4a1f13910ec07c796126c919e97d6483ff5d898b61b535fe8fd365160ef201a9a057a142eb31adf95700f678a810d3539588fd60612e1ed85ca28df77d2f681da9eb3ba443ee41ddb38f555d2d3f6431691d82b5582fedf19509e6a623f1ea907646e23a89ace3d3cf25acdb18d18609fa4a375b0ad53b0d3fa798d93082b531af913e1cb3829042410feacd6d4f4fda9655c0c08ee5c55d301fd69cda8ae404bc46efebf7ebaa66ef0c322dde25b4984612c9100b12752f7d43f4662ee95ee24dba6c3673738e220d6f5b8160e563a4a05d65439f935a0486a20616e95441f13a37aff059e6ca5e236a06bc765437f5cc776d1abb766ad0768acc3d8f24150dbf299b87b12d3a64a5963b2a227823c909cf5bc6cc3cf5a43bbd31782e251bb15bc1897fe95ce20d647eb3753c38e4e41da85261f9eceae7099ddba3bb508bc3bde633e3c485749aa34d7713bfa0f5148872da8e7dbc2a7191149fb5fe1b5e4680740608f607f928cf7a462d1b14d57f6f485196dff5d524b8dfe7a8f068bed81a72e764220b6a63f304817c764a523664dc173fb69c6432eb370c8c5399c21f59449240004150a5b58a956575dec53558c9b28151969c8644da5eced81ced87e97bb
#
# (C) Tenable Network Security, Inc.
#

include('compat.inc');

if (description)
{
  script_id(19782);
  script_version("1.25");
  script_set_attribute(attribute:"plugin_modification_date", value:"2022/02/11");

  script_name(english:"FTP Writable Directories");
  script_summary(english:"Checks for FTP directories which are world-writable.");

  script_set_attribute(attribute:"synopsis", value:
"The remote FTP server contains world-writable directories.");
  script_set_attribute( attribute:"description", value:
"By crawling through the remote FTP server, Nessus discovered several
directories were marked as being world-writable.

This could have several negative impacts :
  - Temporary file uploads are sometimes immediately available to
    all anonymous users, allowing the FTP server to be used as
    a 'drop' point. This may facilitate trading copyrighted,
    pornographic, or questionable material.

  - A user may be able to upload large files that consume disk
    space, resulting in a denial of service condition.

  - A user can upload a malicious program. If an administrator
    routinely checks the 'incoming' directory, they may load a
    document or run a program that exploits a vulnerability
    in client software.");
  script_set_attribute(attribute:"solution",  value:
"Configure the remote FTP directories so that they are not world-
writable.");
  script_set_cvss_base_vector("CVSS2#AV:N/AC:L/Au:N/C:N/I:P/A:P");
  script_set_cvss3_base_vector("CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L");
  script_set_attribute(attribute:"cvss_score_source", value:"manual");
  script_set_attribute(attribute:"cvss_score_rationale", value:"Score based on manual analysis");

  script_set_attribute(attribute:"plugin_publication_date", value:"2005/10/04");
  script_set_attribute(attribute:"vuln_publication_date", value:"1997/10/08");
  script_set_attribute(attribute:"plugin_type", value:"remote");
  script_end_attributes();

  script_category(ACT_GATHER_INFO);
  script_family(english:"FTP");

  script_copyright(english:"This script is Copyright (C) 2005-2022 and is owned by Tenable, Inc. or an Affiliate thereof.");

  script_dependencies("ftp_anonymous.nasl", "ftpserver_detect_type_nd_version.nasl");
  script_exclude_keys("global_settings/supplied_logins_only");
  script_require_ports("Services/ftp", 21);
  exit(0);
}

include('audit.inc');
include('ftp_func.inc');
include('misc_func.inc');
include('global_settings.inc');
include('string.inc');
include('spad_log_func.inc');
include('lists.inc');

##
# Get FTP LIST command output via PASV mode of FTP
#
# @param [path:string] current FTP path to look for directories
# @param [commands_socket:socket] socket for FTP comands
#
# @return string output containing directories listing
##
function get_ftp_list_output(path, commands_socket)
{
  var res_code, data_port, data_socket, ls_command;
  var res_data = '';

  if (empty_or_null(path))
    ls_command = 'LIST /';
  else
    ls_command = sprintf('LIST %s', path);

  debug_log(message:'LIST command to execute: ' + ls_command);

  # Switch FTP server to PASV mode
  data_port = ftp_pasv(socket:commands_socket);

  debug_log(message:'PASV FTP Port: ' + data_port);

  # Cann't get PASV port from FTP server
  if (data_port == 0)
    audit(AUDIT_SVC_ERR, commands_port);

  data_socket = open_sock_tcp(data_port);

  if (empty_or_null(data_socket))
    audit(AUDIT_SOCK_FAIL, data_port);

  # Run LIST command to start folder's investigation
  res_code = ftp_send_cmd(
               socket:commands_socket,
               cmd:ls_command
             );

  debug_log(message:'FTP Command result code: ' + res_code);

  # FTP transfer confirmation code:
  # 150 Here comes the directory listing.
  # 125 Data connection already open; transfer starting.
  if (empty_or_null(res_code) || (('125' >!< res_code) && ('150' >!< res_code)))
    return res_data;

  res_data = ftp_recv_listing(socket:data_socket);

  debug_log(message:'FTP LIST command output:\n' + res_data);

  # Get transfer complete confirmation from commands FTP socket
  # 226 Transfer complete
  res_code = ftp_recv_line(socket:commands_socket);

  debug_log(message:'FTP Transaction code: ' + res_code);

  if (empty_or_null(res_code) || ('226' >!< res_code))
    audit(AUDIT_SVC_ERR, commands_port);

  # Data transfer completed. Close Data socket
  close(data_socket);

  return res_data;
}

##
# Parse FTP LIST command output and get 
#
# @param [line:string] a line from FTP LIST command output
#
# @return string with directory name / '' if the wrong input 
##
function get_dir_name(line)
{
  var result = '';

  # FTP Windows (MS-DOS style)
  # 02-19-19  05:50AM       <DIR>          1
  if ('<DIR>' >< line)
  {
    debug_log(message:'Line to parse: ' + line);
    result = pregmatch(pattern:"<DIR>\s+(.+)$", string:line);

    if (!empty_or_null(result))
    {
      debug_log(message:'Folder name: ' + result[1]);
      return result[1];
    }
  }
  # FTP Unix
  # drwxrwxrwx   1 owner    group            0 Feb 19 05:50 1
  if (line[0] == 'd')
  {
    debug_log(message:'Line to parse: ' + line);
    result = pregmatch(
               pattern:"(.+?\:\d{2}\s)(.+)",
               string:line
             );

    if (!empty_or_null(result))
    {
      debug_log(message:'Folder name: ' + result[2]);
      return result[2];
    }
  }

  debug_log(message:'Folder name: ' + result);
  return result;
}

##
# Provides the list of all directories on the target FTP server
#
# @param [path:string] current FTP path to look for directories
# @param [commands_socket:socket] socket for FTP commands
# @param [depth_limit:int] limits the depth of search for folders
#
# @return list of paths to folders
##
function dir_crawler(path, commands_socket, depth_limit)
{
  var res_data, line, dir_name;
  var directories = make_list();

  if (write_test(path:path, commands_socket:commands_socket))
    directories = make_list(directories, path);

  # Stop directory search if we've reached a limit
  if (depth_limit == 0)
    return directories;

  res_data = get_ftp_list_output(path:path, commands_socket:commands_socket);

  if (empty_or_null(res_data))
    return directories;

  # Split the FTP LIST command output and work with each directory separately
  foreach line (split(res_data, sep:'\r\n', keep:FALSE))
    if (('<DIR>' >< line) || (line[0] == 'd'))
    {
      dir_name = get_dir_name(line:line);

      if (!empty_or_null(dir_name))
        # Use a recursion to browse through the whole tree
        directories = make_list(directories, dir_crawler(
                 path:path + dir_name + '/',
                 commands_socket:commands_socket,
                 depth_limit:depth_limit-1
               ));
    }

  return directories;
}

##
# Run all the possible tests to detect if the path is writable
#
# @param [path:string] current FTP path to look for directories
# @param [commands_socket:socket] socket for FTP commands
#
# @return boolean true if path is writeable
##
function write_test(path, commands_socket)
{
  # We need a couple of tests as FTP configuation allows to configure
  # directories and files premissions separately

  # Test 1. An attempt to write a file
  if (file_write_test(path:path, commands_socket:commands_socket))
  {
    debug_log(message:'Writeable as file can be uploaded: ' + path);
    return true;
  }

  # Test 2. An attempt to create a directory
  if (directory_write_test(path:path, commands_socket:commands_socket))
  {
    debug_log(message:'Writeable as directory can be created: ' + path);
    return true;
  }

  return false;
}

##
# Attempt to create a file
#
# @param [path:string] current FTP path to look for directories
# @param [commands_socket:socket] socket for FTP commands
#
# @return boolean true if directory is writeable
##
function file_write_test(path, commands_socket)
{
  var file_name = rand_str(length:8) + '.nes';
  var file_body = 'Nessus TEST\r\n\r\n';
  var file_check = sprintf('SIZE %s', file_name);
  var file_create, file_remove, res_code, data_port, data_socket;
  var cwd = sprintf('CWD %s', path);
  var res_data = '';
  var result = false;

  debug_log(message:'FTP CWD Command: ' + cwd);
  res_code = ftp_send_cmd(
               socket:commands_socket,
               cmd:cwd
             );

  debug_log(message:'FTP CWD Command result code: ' + res_code);

  # Check the FTP CWD command results
  # 250: CWD command successful.
  if ('250' >!< res_code)
    return result;

  # Check the unlucky event of file already exists
  while (true)
  {
    debug_log(message:'File existence check command: ' + file_check);
    # Run SIZE command
    res_code = ftp_send_cmd(
                 socket:commands_socket,
                 cmd:file_check
               );

    debug_log(message:'File existence check result: ' + res_code);
    # File doesn't exist
    # 550: The system cannot find the file specified.
    if (empty_or_null(res_code))
      return false;
    else if ('550' >!< res_code)
    {
      file_name = rand_str(length:8) + '.nes';
      file_check = sprintf('SIZE %s', file_name);
    }
    else
      break;
  }

  file_create = sprintf('STOR %s', file_name);
  file_remove = sprintf('DELE %s', file_name);

  # Swith to ASCII transfer mode
  debug_log(message:'FTP TYPE Command: TYPE I');
  # Run STOR command to start folder's investigation
  res_code = ftp_send_cmd(
               socket:commands_socket,
               cmd:'TYPE I'
             );

  debug_log(message:'FTP TYPE Command result code: ' + res_code);

  # Check the FTP TYPE command results
  # 200: Type set to I.
  if ('200' >!< res_code)
    return result;

  # Switch FTP server to PASV mode
  data_port = ftp_pasv(socket:commands_socket);

  debug_log(message:'PASV FTP Port: ' + data_port);

  # Cann't get PASV port from FTP server
  if (data_port == 0)
    audit(AUDIT_SVC_ERR, commands_port);

  data_socket = open_sock_tcp(data_port);

  if (empty_or_null(data_socket))
    audit(AUDIT_SOCK_FAIL, data_port);

  # Run STOR command to start folder's investigation
  res_code = ftp_send_cmd(
               socket:commands_socket,
               cmd:file_create
             );

  debug_log(message:'FTP STOR Command result code: ' + res_code);

  # FTP transfer confirmation code:
  # 125 Data connection already open; transfer starting.
  # 150 Ok to send data.
  if (!empty_or_null(res_code) && (('125' >< res_code) || ('150' >< res_code)))
  {
    # Upload our test file
    send(socket:data_socket, data:file_body);
    shutdown(socket:data_socket, how:2);

    # Get transfer complete confirmation from commands FTP socket
    # 226 Transfer complete
    res_code = ftp_recv_line(socket:commands_socket);

    debug_log(message:'FTP Transaction code: ' + res_code);

    debug_log(message:'File existence check command: ' + file_check);
    # Run SIZE command
    res_code = ftp_send_cmd(
                 socket:commands_socket,
                 cmd:file_check
               );

    debug_log(message:'File existence check result: ' + res_code);
    # File does exist
    # 213: <SIZE>
    if ('213' >< res_code)
      result = true;

    # Cleaning procedures
    # 1) Delete file anyway
    debug_log(message:'File DELE command: ' + file_remove);
    # Run SIZE command
    res_code = ftp_send_cmd(
                 socket:commands_socket,
                 cmd:file_remove
               );

    debug_log(message:'File DELE result: ' + res_code);

    # 2) FTP CWD back to /
    debug_log(message:'FTP CWD Command: CWD /');
    res_code = ftp_send_cmd(
               socket:commands_socket,
               cmd:'CWD /'
             );

    debug_log(message:'FTP CWD Command result code: ' + res_code);

    return result;
  }

  close(data_socket);
  return result;
}

##
# Attempt to create a directory
#
# @param [path:string] current FTP path to look for directories
# @param [commands_socket:socket] socket for FTP commands
#
# @return boolean true if directory is writeable
##
function directory_write_test(path, commands_socket)
{
  var folder_name = 'Nessus' + rand_str(length:10);
  var dir_check = sprintf('CWD %s%s', path, folder_name);
  var dir_create, dir_remove;
  var result = false;
  var res_code;

  # Check the unlucky event of folder with the same name exists
  while (true)
  {
    debug_log(message:'Dir existence check command: ' + dir_check);
    # Run CWD command
    res_code = ftp_send_cmd(
                 socket:commands_socket,
                 cmd:dir_check
               );

    debug_log(message:'Dir existence check result: ' + res_code);
    # Folder doesn't exist
    # 550: The system cannot find the file specified.
    if (empty_or_null(res_code))
      return false;
    else if ('550' >!< res_code)
    {
      folder_name = 'Nessus' + rand_str(length:10);
      dir_check = sprintf('CWD %s%s', path, folder_name);
    }
    else
      break;
  }

  dir_create = sprintf('MKD %s%s', path, folder_name);
  dir_remove = sprintf('RMD %s%s', path, folder_name);

  debug_log(message:'Dir creation command: ' + dir_create);
  # Run MKD command
  res_code = ftp_send_cmd(
               socket:commands_socket,
               cmd:dir_create
             );

  debug_log(message:'Dir creation result: ' + res_code);

  # FTP response should be:
  # 257 - "DIR NAME" directory created.
  if (!empty_or_null(res_code) && '257' >< res_code)
    result = true;

  # Cleaning:
  debug_log(message:'FTP Dir remove command: ' + dir_remove);

  # 1) Run RMD command
  res_code = ftp_send_cmd(
               socket:commands_socket,
               cmd:dir_remove
             );

  debug_log(message:'Dir removal result: ' + dir_remove);

  # 2) Run CWD command
  debug_log(message:'FTP CWD Command: CWD /');
  res_code = ftp_send_cmd(
               socket:commands_socket,
               cmd:'CWD /'
             );

  debug_log(message:'FTP CWD Command result code: ' + res_code);

  return result;
}

##
# Put information to debug log if it's enabled 
#
# @param [message:string] string to put in debug log
##
function debug_log(message)
{
  if (!empty_or_null(message))
    spad_log(message:message, name:'ftp_writeable_directories.log');
}

#
# Main
#

var commands_port, commands_socket, report, directories, depth_limit;

# Limit for dir crawler
depth_limit = 10;
debug_log(message:'Depth limit: ' + depth_limit);

commands_port = get_ftp_port(default: 21);

# Anonymous access to FTP is not possible
get_kb_item_or_exit('ftp/' + commands_port + '/anonymous');

if (supplied_logins_only)
  audit(AUDIT_SUPPLIED_LOGINS_ONLY);

if (safe_checks())
  # No corresponding audit message
  exit(0, "This plugin requires safe checks to be disabled.");

# Open connection to commands port
commands_socket = ftp_open_and_authenticate(
                   user:'anonymous',
                   pass:get_kb_item('ftp/password'),
                   port:commands_port
                 );

if (empty_or_null(commands_socket))
  audit(AUDIT_SOCK_FAIL, commands_port);

# Unfortunately we can not check writable directories based on
# permissions provided as part of FTP LIST command. Because:
# - MS Windows FTP Server (part of IIS) doesn't provide information
#   about permissions if "Directory Listing Style" setting is set 
#   to MS-DOS (default option).
# - There is no way to get current users' permissions / id / group
#   to match them with files' / folders' permissions.
# - FTP could be configured as anonymous read only, which doesn't
#   influence displayed permissions for files / folders but blocks
#   writting completely.
# We have to just try actual writting in order to be sure.
directories = dir_crawler(
                path:'/',
                commands_socket:commands_socket,
                depth_limit:depth_limit
              );

# Close FTP commands socket
ftp_close(socket:commands_socket);

# No writable directories found. FTP Server is not vulnerable
if (max_index(directories) < 1)
  audit(AUDIT_LISTEN_NOT_VULN, 'FTP Server', commands_port);

report = 'By writing on the remote FTP server, it was possible to ' +
  'gather the following list of writable directories:\n';

report += join(directories, sep:'\n');

debug_log(message:'Report Message: \n' + report);

# Set the KBs
replace_kb_item(name:'ftp/writeable_dir', value:directories[0]);
replace_kb_item(name:'ftp/tested_writeable_dir', value:directories[0]);
replace_kb_item(name:'ftp/' + commands_port + '/writeable_dir', value:directories[0]);
replace_kb_item(name:'ftp/' + commands_port + '/tested_writable_dir', value:directories[0]);

security_report_v4(
  port:commands_port,
  severity:SECURITY_WARNING,
  extra:report
);