Technical Analysis of ProFTPD Response Pool Use-after-free (CVE-2011-4130) - Part I
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
|