About Us | Contact Us    

 


 

VUPEN Research

 
  VUPEN Research Team
  VUPEN Research Blog
  VUPEN Research Videos
 
   

VUPEN Vulnerability Research Team (VRT) Blog

 
Technical Analysis of ProFTPD Response Pool Use-after-free (CVE-2011-4130) - Part I
 
Published on 2012-01-10 16:21:11 UTC by Jordan Gruskovnjak, Security Researcher @ VUPEN

Twitter LinkedIn Delicious Digg   

Hi everyone,

In this blog, we will share our technical analysis of a critical vulnerability affecting ProFTPD server (CVE-2011-4130) which was reported by TippingPoint [1] and fixed in ProFTPD versions 1.3.3g and 1.3.4 [2]. We will also explain how we managed to trigger and create a reliable exploit for this flaw (in Part II).

Exploiting this vulnerability was very challenging as it is a remote use-after-free in a server component on Linux.


1. Technical Analysis of the Vulnerability

As documented by TippingPoint in the ProFTPD bug entry [3], this vulnerability is located in the code that manages pools used for responses sent by the server to the client. The response pool is set on each new command received by the server.

The code responsible for dispatching the command is the "cmd_loop()" function located in "src/main.c":

 
static void cmd_loop(server_rec *server, conn_t *c) {

while (TRUE) {
  int res = 0;
  cmd_rec *cmd = NULL;

  pr_signals_handle();

  res = pr_cmd_read(&cmd);
  [...]
  if (cmd) {
     pr_cmd_dispatch(cmd);
     destroy_pool(cmd->pool);
  [...]
}

 

The "cmd_rec" structure is defined in include/dirtree.h:

 
typedef struct cmd_struc {
  pool *pool;
  [...]
  pool *tmp_pool; /* Temporary pool which only exists
                           * while the cmd's handler is running
                           */
  int argc;

  char *arg;         /* entire argument (excluding command) */
  char **argv;
  char *group;     /* Command grouping */
  [...]
  int cmd_id;       /* Index into commands list, for faster comparisons */
  } cmd_rec;

 

Each "cmd_rec" instance has its own allocation pool.

The "pr_cmd_dispatch()" function is a wrapper arround "pr_cmd_dispatch_phase()" located in src/main.c:

 
int pr_cmd_dispatch_phase(cmd_rec *cmd, int phase, int flags) {
  char *cp = NULL;
  int success = 0, xerrno = 0;
  pool *resp_pool = NULL;
  [...]
  resp_pool = pr_response_get_pool();                                    // backup current response pool

  /* Set the pool used by the Response API for this command. */
  pr_response_set_pool(cmd->pool);
  [...]
  if (phase == 0) {

  /* First, dispatch to wildcard PRE_CMD handlers. */
    success = _dispatch(cmd, PRE_CMD, FALSE, C_ANY);             // performing PRE_CMD tasks

    if (!success) /* run other pre_cmd */
    success = _dispatch(cmd, PRE_CMD, FALSE, NULL);

    [...]
    success = _dispatch(cmd, CMD, FALSE, C_ANY);                      // performing CMD tasks
    [...]

    /* Restore any previous pool to the Response API. */
    pr_response_set_pool(resp_pool);                                     // restoring previous response pool

    errno = xerrno;
    return success;
}

 

The "pr_response_set_pool()" and "pr_response_get_pool()" functions are just accessors to the static variable "resp_pool" located in "src/response.c". This static "resp_pool" variable is used to perform allocations during the response process.

 
static pool *resp_pool = NULL;                                                // used to perform allocations

pool *pr_response_get_pool(void) {
  return resp_pool;
}

void pr_response_set_pool(pool *p) {
  resp_pool = p;
  [...]
}

 

Under normal circumstances, the code first saves its current resp_pool. The resp_pool static variable is then set to the current cmd->pool, therefore all further allocations made to handle responses are performed using the cmd->pool pointer as the current resp_pool pointer.

When the handling of the command is achieved, the response pool is restored back to its previous value using "pr_set_response_pool()".

The "_dispatch()" function calls handlers associated with the requested command at different phases of the command handling.

There are 3 main phases:

 PRE_CMD: perform inputs sanitization and preliminary verifications before calling the main command handler.
 CMD: Call the command handler.
 POST_CMD: Perform cleanups after command handling.

The wildcard PRE_CMD handlers are called regardless of the function being requested by the client. (4th argument is C_ANY).

By taking a closer look at the function, it can be seen that if the "_dispatch()" function fails when dispatching the PRE_CMD handlers, the following code is reached:

 
if (phase == 0) {

   /* First, dispatch to wildcard PRE_CMD handlers. */
   success = _dispatch(cmd, PRE_CMD, FALSE, C_ANY);

  if (!success) /* run other pre_cmd */
    success = _dispatch(cmd, PRE_CMD, FALSE, NULL);

  if (success < 0) {
    /* Dispatch to POST_CMD_ERR handlers as well. */

    _dispatch(cmd, POST_CMD_ERR, FALSE, C_ANY);
    _dispatch(cmd, POST_CMD_ERR, FALSE, NULL);

  _dispatch(cmd, LOG_CMD_ERR, FALSE, C_ANY);
  _dispatch(cmd, LOG_CMD_ERR, FALSE, NULL);

  xerrno = errno;
  pr_trace_msg("response", 9, "flushing error response list for '%s'",
  cmd->argv[0]);
  pr_response_flush(&resp_err_list);

  errno = xerrno;
  return success;                                                         // funct returns without restoring previous
                                                                                    // pool using pr_response_set_pool() !!     
}

 

The vulnerability lies here: if the function returns after an error on a PRE_CMD handler, it fails to restore the previous response pool. While under normal circumstances, the bug looks harmless since the stale pointer is not reused and is replaced as soon as another command is issued, it can in fact result in a critical and exploitable use-after-free.


2. Triggering the Vulnerability

In order to trigger the use-after-free condition, one needs to put the server into a state where more than one pool can exist.

Strictly looking at the code, such condition can be achieved with two nested calls to the "pr_cmd_dispatch()" function. For instance, when performing a data transfer by downloading or uploading a file to the server. Indeed, during a data transfer the control channel is polled in case additional commands are sent by the client and need to be handled with another call to "pr_cmd_dispatch()".

For example, when issuing the "STOR" command, the "xfer_stor()" CMD handler located in "modules/mod_xfer.c" is called by the dispatcher. The function "pr_data_open()" will be executed to start the transfer at the beginning. Then, "pr_data_xfer()" will be called as we will see in the next snippet. At the end, "pr_data_close()" is called to close the transfer.

As aforementioned, when performing a data transfer, the "pr_data_xfer()" function located in "src/data.c" polls the control channel for any additional command sent during the transfer.

Since the call to "pr_data_xfer()" is the result of a command like "STOR", "RETR", ..., the current response pool is set to the pool of the "cmd_rec" structure handling this command. The "pr_data_xfer()" function allows commands to be sent to the server while performing the data transfer on the data port:

 
int pr_data_xfer(char *cl_buf, int cl_size) {
  int len = 0;
  int total = 0;
  int res = 0;

  /* Poll the control channel for any commands we should handle, like
  * QUIT or ABOR.
  */
  [...]
  res = pr_cmd_read(&cmd);                                       // Allocation of the pool associated with the
                                                                                   // command issued during data transfer.
  [...]
  else if (cmd != NULL) {
  char *ch;

  for (ch = cmd->argv[0]; *ch; ch++)
    *ch = toupper(*ch);

  cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);

  [...]

  if (pr_cmd_cmp(cmd, PR_CMD_APPE_ID) == 0 ||        // commands that cannot be
    [...]                                                                        // used during data transfer
      pr_cmd_cmp(cmd, PR_CMD_EPSV_ID) == 0) {
    [...]
  else {                                                                       // commands that can be
    [...]                                                                        // used during data transfer

  pr_cmd_dispatch(cmd);                                         // [1] nested call to
  [...]                                                                         // pr_cmd_dispatch
  destroy_pool(cmd->pool);                                      // [2]
  [...]
}

 

If the issued command is not a blacklisted one, the "pr_cmd_dispatch()" is called for the second time in [1], thus setting the new command pool as the response pool. If the issued command fails on a PRE_CMD handler, the function will return without restoring the response pool to its previous state.

The response pool now still points to the cmd->pool pointer which is destroyed right after the function returns by calling the "destroy_pool()" function in [2]. Now, all further responses performed inside the first call to "pr_cmd_dispatch()" (responsible for the CMD handler execution), will use the stale response pool pointer.

Indeed, the "pr_data_close()" function located in src/data.c will be called just after "pr_data_xfer()" returns. This function will call the "pr_response_add()" function which will reuse the stale pointer as we can see:

 
/* close == successful transfer */
void pr_data_close(int quiet) {
nstrm = NULL;

[...]

if (!quiet)
  pr_response_add(R_226, _("Transfer complete"));  // Use of stale pool pointer
}

 

Looking at the "pr_response_add()" function located in src/response.c:

 
void pr_response_add(const char *numeric, const char *fmt, ...) {
  pr_response_t *resp = NULL, **head = NULL;
  va_list msg;

  va_start(msg, fmt);
  vsnprintf(resp_buf, sizeof(resp_buf), fmt, msg);
  va_end(msg);

  resp_buf[sizeof(resp_buf) - 1] = '\0';

  resp = (pr_response_t *) pcalloc(resp_pool, sizeof(pr_response_t));
  resp->num = (numeric ? pstrdup(resp_pool, numeric) : NULL);
  resp->msg = pstrdup(resp_pool, resp_buf);

 

As we can see, this function can use the stale resp_pool pointer to perform allocations which could result in a use-after-free condition.

Note: The "pcalloc()" and "pstrdup()" functions are allocation functions related to the ProFTPD pool allocator which will be covered later.

To trigger the use-after-free condition, the server has to be in a state where it performs a data transfer using the "pr_data_xfer()" function. This latter function will poll the control channel for other commands and therefore call the "pr_cmd_dispatch()" function within the context of another dispatcher. If a PRE_CMD handler associated with the incoming command fails, the response pool will not be restored on return.

Since incoming commands are restricted because otherwise they could mess up data transfer, only a few sets are allowed. Searching the sources inside the "module" directory for the "PRE_CMD" pattern gives us the list of registered PRE_CMD handlers:

 
  mod_auth.c: {PRE_CMD, C_USER, G_NONE, auth_pre_user, FALSE, FALSE, CL_AUTH },
  mod_auth.c: {PRE_CMD, C_PASS, G_NONE, auth_pre_pass, FALSE, FALSE, CL_AUTH },
  mod_core.c: {PRE_CMD, C_ANY, G_NONE, regex_filters, FALSE, FALSE, CL_NONE },
  mod_core.c: {PRE_CMD, C_ANY, G_NONE, core_clear_fs,FALSE, FALSE, CL_NONE },
  mod_delay.c: {PRE_CMD, C_PASS, G_NONE, delay_pre_pass, FALSE, FALSE },
  mod_delay.c: {PRE_CMD, C_USER, G_NONE, delay_pre_user, FALSE, FALSE },
  mod_exec.c: {PRE_CMD, C_ANY, G_NONE, exec_pre_cmd, FALSE, FALSE },
  mod_log.c: {PRE_CMD, C_DELE, G_NONE, log_pre_dele, FALSE, FALSE },
  mod_rewrite.c: { PRE_CMD, C_ANY, G_NONE, rewrite_fixup, FALSE, FALSE },
  mod_site.c: {PRE_CMD, C_SITE, G_NONE, site_pre_cmd, FALSE, FALSE },
  mod_tls.c: {PRE_CMD, C_ANY, G_NONE, tls_any, FALSE, FALSE },
  mod_xfer.c: {PRE_CMD, C_RETR, G_READ, xfer_pre_retr, TRUE, FALSE },
  mod_xfer.c: {PRE_CMD, C_STOR, G_WRITE, xfer_pre_stor, TRUE, FALSE },
  mod_xfer.c: {PRE_CMD, C_STOU, G_WRITE, xfer_pre_stou, TRUE, FALSE },
  mod_xfer.c: {PRE_CMD, C_APPE, G_WRITE, xfer_pre_appe, TRUE, FALSE },

 

After discarding transfer related PRE_CMD handlers and handlers not returning an error code, only a single handler remains: the "regex_filters()" function located in "modules/mod_core.c" which is a wildcard (C_ANY) handler. Wilcard PRE_CMD handlers are automatically called for every command supplied by an FTP client EVEN if this command does not exist:

 
MODRET regex_filters(cmd_rec *cmd) {
  pr_regex_t *allow_regex = NULL, *deny_regex = NULL;

  [...]
  /* Check for an AllowFilter */
  allow_regex = get_param_ptr(CURRENT_CONF, "AllowFilter", FALSE);
  if (allow_regex != NULL &&
    cmd->arg != NULL &&
    pr_regexp_exec(allow_regex, cmd->arg, 0, NULL, 0, 0, 0) != 0) {
    pr_log_debug(DEBUG2, "'%s %s' denied by AllowFilter", cmd->argv[0],
    cmd->arg);
    pr_response_add_err(R_550, _("%s: Forbidden command argument"), cmd->arg);
    errno = EACCES;
    return PR_ERROR(cmd);                                         // returns a negative value
  }

  /* Check for a DenyFilter */
  deny_regex = get_param_ptr(CURRENT_CONF, "DenyFilter", FALSE);
  if (deny_regex != NULL &&
    cmd->arg != NULL &&
    pr_regexp_exec(deny_regex, cmd->arg, 0, NULL, 0, 0, 0) == 0) {
    pr_log_debug(DEBUG2, "'%s %s' denied by DenyFilter", cmd->argv[0],
    cmd->arg);
    pr_response_add_err(R_550, _("%s: Forbidden command argument"), cmd->arg);
    errno = EACCES;
    return PR_ERROR(cmd);                                        // returns a negative value
  }

  return PR_DECLINED(cmd);
  }

 

The "regex_filter()" function parses arguments using regular expressions specified via the "AllowFilter" and "DenyFilter" directives located in ProFTPD configuration files.

If an argument does not match one of the "AllowFilter" or "DenyFilter" directives, an error is returned, and hence this can be used to trigger the vulnerability.

Searching for "AllowFilter" in the default installation gives no results, but "DenyFilter" gives a single result:

 
proftpd.conf: DenyFilter \*.*/
 

Ironically, this regex which is present by default e.g. on Ubuntu and Debian was added as a security measure to prevent certain attacks related to file paths.

We now have all the tricks to trigger the bug. Creating a proof-of-concept issuing the non-existent "SEGV" command with the "../*/.." argument during the transfer of the "foo" file gives the following response:

 
220 ProFTPD 1.3.4rc2 (Debian) [127.0.0.1]
[...]
RETR foo
150 Opening ASCII mode data connection for foo
SEGV ../*/..
550 ../*/..: Forbidden command argument

 

And leads to a crash of the server:

 
Program received signal SIGSEGV, Segmentation fault.
0x080595a5 in ?? ()
(gdb) x/i $eip
=> 0x80595a5: mov 0x8(%eax),%edi
(gdb) i r eax
eax 0x0 0                                                                 // dereferencing a pointer !
(gdb) bt 4
#0 0x080595a5 in ?? ()
#1 0x0805ab16 in pstrdup ()
#2 0x08074444 in pr_response_add()
#3 0x0807571c in pr_data_close ()
[...]

 

As we can see, ProFTPD crashed when trying to dereference a pointer.

In this first part of the blog, we saw the technical analysis of the flaw and how to trigger it. The second part will include an in-depth overview of the ProFTPD allocator, our methods to fully control memory allocations and data, and finally the detailed description of our tricks to achieve a highly reliable exploitation.


3. References and Links

[1]
http://zerodayinitiative.com/advisories/ZDI-11-328/
[2] http://www.proftpd.org/docs/NEWS-1.3.4

[1] http://bugs.proftpd.org/show_bug.cgi?id=3711

Copyright VUPEN Security



 

VUPEN Solutions  

 


 

 

 

 

 

 

 

 

 

2004-2014 VUPEN Security - Copyright - Privacy Policy