Introduction
On July 22, 2021, the popular Apache HTTPD webserver merged in a commit that replaced the function ap_getparents()
with a new function called ap_normalize_path()
. This new function was touted as a more efficient and standard way to deal with the normalization of raw paths found in an HTTP request’s URI. On September 15th, these changes were merged into trunk and tagged version 2.4.49.
What is the Issue?
On September 29th, fourteen days after the release of version 2.4.49, an engineer at cPanel named Ash Daulton reported that an attacker could use this new logic to execute a path-traversal exploit, potentially leading to remote code execution (RCE) when certain configuration conditions are met.
Censys found 75,085 unique hosts running 136,469 services that self-identified as Apache HTTPD version 2.4.49 using this simple search query. And while most installs of this service are not vulnerable out-of-the-box, Censys has identified a few vendors who ship a lax configuration, enabling the bug. One such vendor, Control Webpanel, with over 22,000 hosts running CentOS, seems to be a likely target for this attack due to the configuration it ships.
Some researchers have stated that this vulnerability is being actively scanned for in the wild, though no definitive statement has been made as to whether the scans are resulting in active exploitation.
By default, the following access-control configuration is put in place:
DocumentRoot "/usr/local/apache2/htdocs"
<Directory />
AllowOverride none
Require all denied
</Directory>
<Directory "/usr/local/apache2/htdocs">
AllowOverride None
Require all granted
</Directory>
If a user were to request "/index.html"
, the server would take the DocumentRoot (/usr/local/apache2/htdocs
) and append "/index.html"
to it, resulting in the file "/usr/local/apache2/htdocs/index.html"
. Since a rule allows access to all files within the "/usr/local/apache2/htdocs"
directory, the request would be permitted.
On the other hand, if a user were to request "/../../../../foobar"
, the server would trim out the "/../../../.."
via ap_normalize_path()
and append the result to the DocumentRoot
, resulting in "/usr/local/apache2/htdocs/foobar"
; a file that should not exist on the file system.
But since ap_normalize_path()
does not correctly check for double-dots when encoded as ascii-hex, e.g., "%2e%2e"
, this normalization function will render the dots and not strip the path. But even after the double-dot render, the default access controls should stop most path-transversals from being executed. It is only when an administrator explicitly sets the configuration "Require all granted"
on a protected directory that things can get out of hand; i.e.,
<Directory />
AllowOverride none
Require all granted
</Directory>
This incorrect configuration, combined with Apache’s mod_cgi
module, has the potential to turn this simple path-traversal vulnerability into a full-fledged remote command execution (RCE) attack.
A Deeper Look
While this new ap_normalize_path()
function does play a role here, it is not exactly the cause of the vulnerability. It’s more like this function helped an already-existing problem come to light. I mean, the proof is in the name: it’s “normalize”, not “sanitize”. But this change had unintended side effects for other functions that were relying on this previous iteration that sanitized the data.
The real issue is hidden away in an OS abstracted function called apr_filepath_merge()
, and more specifically, how this function is called. But before we talk about how this function is called, we must understand what this function does. The documentation states that this function will:
“Merge additional file path onto the previously processed rootpath”
The main goal of this function is to take a base path (like an apache DocumentRoot
), and a path passed in a request’s URI, and merge them together in order to map the input URI to a fully qualified path on the server.
Here is a very basic translation of what this function does:
final_path = split("/usr/local/apache2/htdocs", "/") // result: ["usr", "local", "apache2", "htdocs"]
tokens = split("usr/bin/..", "/") // result: ["usr", "bin", ".."]
for each path_part in tokens:
if path_part == "..":
// remove the last segment in the final_path array
final_path[length(final_path)] = null
else:
final_path.append(path_part)
return final_path.Join("/") // returns /usr/local/apache2/htdocs/usr
When this function is called from the default Apache handler (ap_core_translate()
), it will first take the input request path, and skip past the first slash to signify it is a relative path from the DocumentRoot
. For example, if the server is configured with DocumentRoot "/usr/local/apache2/htdocs"
, and the incoming request is:
GET /foo/bar HTTP/1.0
The apr_filename_merge()
will be called like this:
char * path = "/foo/bar";
char * root = "/usr/local/apache2/htdocs";
char * output;
while (*path == '/') {
++path;
}
int rv = apr_filepath_merge(&output, root, path, APR_FILEPATH_SECUREROOT, r->pool));
Which would result in the output
buffer containing: /usr/local/apache2/htdocs/foo/bar
. The reason that this call to apr_filepath_merge
is not vulnerable to the path-traversal attack is mainly due to the flag APR_FILEPATH_SECUREROOT
being set. This will cause the function to fail if the path we’re attempting to add falls outside of the “/usr/local/apache2/htdocs” directory.
But not every request is processed by the default handler, some are processed by modules. For example, the mod_alias
module has to first match the input path to a base-path, and that base-path must be expanded to a real path. Take the following configuration:
<IfModule alias_module>
ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/"
</IfModule>
This states that any incoming URI path that starts with “/cgi-bin/” should actually go to the directory “/usr/local/apache2/cgi-bin/”. So the way the URI is translated to a file is different from what it is in ap_core_translate()
. First, mod_alias
will iterate over a list of alias_entry
‘s with the following format:
alias_entry entry = {
.real = "/usr/local/apache2/cgi-bin/",
.fake = "/cgi-bin/",
};
It will look at the prefix of the incoming request path, and if it matches the fake
element, i.e., “/cgi-bin/”, the contents of incoming URI is appended to the real
element which in turn is used to call apr_filepath_merge
:
char * output_path = NULL;
prefix_len = alias_matches(request->uri, alias->fake);
if (prefix_len > 0) {
// Append the contents of the request URI after the prefix to the end of the "real" element
output_path = apr_pstrcat(request->pool, alias->real, request->uri + prefix_len, NULL);
// if alias->real IS "/usr/local/apache2/cgi-bin/"
// AND
// if request->uri IS "/cgi-bin/../../../etc"
// THEN
// output_path IS "/usr/local/apache2/cgi-bin/../../../etc"
}
if (output_path != NULL) {
char * newpath = NULL;
int ret = apr_filepath_merge(&newpath, "/usr/local/apache2", output_path, 0, p);
if (newpath != NULL && ret == APR_SUCCESS) {
return newpath; // newpath IS /usr/etc
}
}
As you’ve may have noticed, the APR_FILEPATH_SECUREROOT
flag was not passed to apr_filepath_merge
like it was in ap_core_translate()
, meaning that even if the URI contains double-dots, the call would not error – thus mod_alias
is vulnerable to the path-traversal attack.
But to be perfectly clear, just because this function allowed for the traversal to be rendered, the server needs to be configured in such a manner that allows access into the traversed directory. Using the above example of “/usr/etc
“, an administrator must explicitly allow the server access to the “/usr/etc” directory with a “Require all granted” setting. Otherwise, the request will fail at a secondary check in the function ap_run_access_checker_ex()
.
Why does it matter?
If the conditions are right, an attacker can leverage this path-traversal vulnerability to execute any command with the same permissions as the user running the service. An attacker can then use this access to gain elevated administrative permissions if there are any non-patched local vulnerabilities that exist on the system.
Since this vulnerability is easy to execute, and proof-of-concept exploits are readily found on social media and other outlets, we must assume that bad-actors have already started exploiting vulnerable servers in the wild.
What do I do about it?
- Administrators should upgrade to HTTPD version 2.4.51 immediately.
- Check if your host is found in the list of potentially-vulnerable servers.
- Censys ASM customers have been notified via email for any hosts that have been identified as vulnerable. Additionally, ASM customers may find vulnerable assets using the following link.
Update 10-07-2021 (7:30 PM EST): Developers of the CentOS Webpanel have provided an update. Manually running /scripts/update_cwp
will downgrade the Apache HTTPD server back to version 2.4.48.