CVE-2021-32030 & CVE-2021-20090
CVE-2021-32030
2021 年 5 月,来自 Atredis 的安全研究者披露了一个影响几乎所有 ASUS 路由器的身份验证绕过漏洞,利用该漏洞,未经授权的攻击者可以直接访问到后台管理接口,由于 ASUS 路由器在后台提供了开启 ssh 的功能,攻击者只需修改管理员密码并开启 ssh 即可获取 root shell。
漏洞分析
固件下载链接:https://dlcdnets.asus.com/pub/ASUS/wireless/RT-AX56U/FW_RT_AX56U_30043848253.zip
我们以 RT-AX56U 为例,使用 binwalk 对固件解包(解包之后有可能只会得到一个 100000.ubi 文件,可以使用 ubi_reader 继续解包)。解包后在 /usr/sbin/ 目录下找到一个叫做 httpd 的程序,此程序用于处理用户发送的 HTTP 请求。
我们用 IDA 分析程序,搜索 httpd_handle_request 字符串可找到处理请求的入口位置,另外,由于 ASUS 路由器采用了一些开源组件,根据 GPL 规则,ASUS 在(数个版本之前)其官网发布了路由器的源代码,并且衍生出梅林等优秀的官改版固件,我们可以在 github 上找到 merlin 固件项目,利用源代码 辅助分析。
httpd_handle_request 函数逻辑简述如下:
首先对请求进行简单解析,利用 strncasecmp 等字符串操作函数从用户请求中匹配出一些结构,并将这些数据赋值到对应的变量。
1 2 3 4 5 6 7 8 9 10 11
| if ( !strncasecmp(v4, "Referer:", 8u) ) { stringp = v4 + 8; v87 = &stringp[strspn(stringp, " \t")]; stringp = v87; v20 = strlen(v87); v17 = v87; v18 = v20 + 1; LABEL_40: v4 = &v17[v18]; }
|
然后过滤 URL,防止出现路径穿越。
1 2 3 4 5 6 7 8 9 10 11 12
| if ( v28 == 47 || !strcmp(v26, "..") || !strncmp(v26, "../", 3u) || strstr(v26, "/../") || (v30 = strcmp(&v26[v29 - 3], "/..")) == 0 ) { v2 = "Illegal filename."; v3 = "Bad Request"; LABEL_60: v24 = 400; return sub_5ED20(v24, v3, v2); }
|
之后根据用户访问的 URL 和 Method 执行不同逻辑,但所有需要身份验证的接口都要经过函数 auth_check(sub_5B560),此函数从 Cookie 中获取 asus_token 参数的值,然后带入 sub_59588 进行检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| int __fastcall sub_59588(const char *a1) { const char *v2; const char *v3; int result;
v2 = nvram_get_1("ifttt_token"); if ( !strcmp(a1, v2) ) { if ( isFileExist("/tmp/IFTTT_ALEXA") > 0 ) Debug2File("/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] IFTTT/ALEXA long token success.\n", "check_ifttt_token", 761); result = 1; } else { if ( isFileExist("/tmp/IFTTT_ALEXA") > 0 ) Debug2File("/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] IFTTT/ALEXA long token fail.\n", "check_ifttt_token", 767); if ( isFileExist("/tmp/IFTTT_ALEXA") > 0 ) Debug2File( "/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] IFTTT/ALEXA long token is %s.\n", "check_ifttt_token", 768, a1); if ( isFileExist("/tmp/IFTTT_ALEXA") > 0 ) { v3 = nvram_get_1("ifttt_token"); Debug2File("/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] httpd long token is %s.\n", "check_ifttt_token", 769, v3); } result = 0; } return result; }
|
我们注意到函数先从配置文件中获取 ifttt_token 参数的值,然后将 asus_token 和它比较,如果两者相等,则输出
1
| IFTTT/ALEXA long token success.
|
同时返回 1,表示身份验证通过。
但是在设备的默认配置下,IFTTT 功能没有开启,这导致 ifttt_token 默认值为空, 此时如果用户传入的 asus_token 也是空值,则 strcmp 会返回 0 表示比较成功,这样就能绕过后续的检查,实现身份验证绕过。
新版 auth_check 函数将代码修改为
1 2 3 4 5 6 7 8 9 10
| if ( *a1 && *nvram_safe_get_0("ifttt_token") && (v2 = nvram_safe_get_0("ifttt_token"), !strcmp(a1, v2)) ) { if ( isFileExist("/tmp/IFTTT_ALEXA") > 0 ) Debug2File( "/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] IFTTT/ALEXA long token success.\n", "check_ifttt_token", 1020); result = 1; }
|
首先判断 asus_token 是否为空,避免用户传入空值的情况出现,从而修复了此漏洞。
参考链接
https://www.atredis.com/blog/2021/4/30/asus-authentication-bypass
CVE-2021-20090
2021 年 4 月,Tenable 安全研究者披露了影响 Arcadyan 系列网络设备的一些漏洞,后续经确认,有多个厂商采用了 Arcadyan 开发的系统套件,从而遭受这些漏洞的影响。
漏洞分析
根据披露信息,Arcadyan 提供的组件被多个网络设备厂商使用,我们以 ASUS DSL-AC88U 为例进行分析。
固件版本:1.10.05_Build502,下载后 binwalk 即可解包。
关键文件:/usr/sbin/httpd
IDA 加载分析,首先可以定位到名为 process_request 的函数,传入参数为解析后的 http 请求结构体,此函数关键代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| url_decode(&req->url); printf("[%s] url=[%s], args=[%s], method=[%s]\n", "process_request", &req->url, req->args, req->method);
req->is_url_valid = sub_DEB0(&req->url); v7 = *(off_54FAC[0] + 5); if ( (!v7 || v7(req) != 2) && (req->is_url_valid || !check_auth_0(&req->url, 0, req)) ) { req->dword798C = 0; v8 = req->method; if ( !strcmp(v8, "HEAD") ) { req->dword798C = 1; if ( req->dword7988 ) { req->dword798C = 0; LOG(req, 400, "Invalid HTTP/0.9 method."); return; } goto LABEL_19; } if ( !strcmp(v8, "GET") ) { LABEL_19: process_get(req); return; } if ( !strcmp(v8, "POST") ) process_post(req); else LOG(req, 400, "Invalid or unsupported method."); }
|
首先调用 url_decode 函数对 url 进行解码,然后调用 sub_DEB0 函数对 url 进行判断,关键代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| int __fastcall sub_DEB0(const char *url) { const char *v2; char **i; size_t v5; const char *v6;
v2 = allow_urls[0]; if ( !allow_urls[0] ) return 0; for ( i = allow_urls; ; ++i ) { v5 = strlen(v2); if ( !strncasecmp(url, v2, v5) ) break; v6 = i[1]; v2 = v6; if ( !v6 ) return 0; } return 1; }
|
此函数从一个 allow_urls 列表中获取值,内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| /images/ /lang/ /js/ /css/ /login.htm /loginpserr.htm /relogin.htm /login.cgi /cgi/cgi_tel_dect_firmware_upgrade_state.js /wz_setpwd.cgi /error_page.htm /cgi/cgi_clients.js /blocking.htm /get_ver.htm
|
先获取 allow_urls 一项的长度,然后用 strncasecmp 函数和用户传入的 URL 比较,如果相等则返回 1,此时 req->is_url_valid 为 1。
之后回到 process_request 函数,判断 req->is_url_valid 的值,如果它等于 1,则不会执行 check_auth_0 函数,相当于当用户访问静态资源时,自动跳过身份验证。
随后判断 method,如果是 POST,则执行 process_post。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| void __fastcall process_post(struct_req *req) { char *url;
url = &req->url; printf("[%s] path: %s, args: %s\n", "process_post", &req->url, req->args); switch ( sub_DF50(url) ) { case -1: LOG(req, 302, url); break; case 0: if ( !sub_14F9C(req) ) LOG(req, 501, "POST to non-script"); break; case 2: exec_cgi_script(req); break; case 3: LOG(req, 400, url); break; default: return; } }
|
将 URL 传入 sub_DF50 函数,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| int __fastcall sub_DF50(const char *url) { int v2; bool v3; int i; int v5; size_t v6;
sub_167A8(url); v2 = *url; v3 = v2 == '/'; if ( v2 != '/' ) v3 = v2 == 0; if ( !v3 ) return 3; for ( i = 0; i != 528; i += 66 ) { if ( sub_17584((maybe_post_handlers + i)) ) break; v5 = maybe_post_handlers + i; v6 = strlen((maybe_post_handlers + i)); if ( !strncmp(url, v5, v6) && (*(v5 + v6 - 1) == '/' || v6 == strlen(url) || url[v6] == '/') ) { sub_166D8(v6, url, (v5 + 32)); return *(maybe_post_handlers + i + 64); } } sub_166D8(0, url, dword_61648); return 0; }
|
再将 URL 传入 sub_167A8,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| int __fastcall sub_167A8(int url) { int j; int k; unsigned __int8 *v3; int v4; bool v5; int v6; int v7; int v8; int v9;
j = 0; k = 0; while ( *(url + k) ) { if ( double_dot[j] == *(url + k) ) { if ( double_dot[++j] ) { ++k; } else { v3 = (url + k + 1); v4 = *v3; v5 = v4 == '/'; if ( v4 != '/' ) v5 = v4 == 0; if ( v5 ) { if ( k <= 3 || *(url + k - 2) == '/' ) { v6 = k + 1; k -= 3; if ( k >= 0 ) { for ( ; k && *(url + k) != '/'; --k ) ; } else { k = 0; } v7 = url + k; v8 = *(url + v6); *(url + k) = v8; if ( v8 ) { do { v9 = *++v3; *++v7 = v9; } while ( v9 ); j = 0; } else { j = 0; } } else { j = 0; } } else { j = 0; } } } else if ( j <= 0 ) { ++k; } else { k += 1 - j; j = 0; } } return url; }
|
简单来讲,此函数会将形如 /aaa/../bbb 转换成 /bbb。
转换之后,回到 sub_DF50 函数,将 URL 和 maybe_post_handlers 列表中定义的 handler 比较,如果比较成功,就回到 process_post 用 exec_cgi_script 执行对应的 handler 函数。
于是我们可以构造如 /images/../apply_abstract.cgi 类似的请求,程序先匹配到 /images,绕过登录,然后替换 /../ 导致后续访问的是 /apply_abstract.cgi 接口,实现身份验证绕过。
参考链接
https://zh-cn.tenable.com/security/research/tra-2021-13?tns_redirect=true
总结
身份验证绕过是一类影响比较严重的漏洞,通过此类漏洞可允许攻击者访问到后台更多敏感接口,通过组合利用漏洞可实现对设备的完全控制,从我们复现过的漏洞来看,身份验证绕过基本有以下几类
- 设备的登录功能对于用户名/密码处理不恰当,攻击者可通过传入空值/硬编码值/可本地计算的值来通过验证流程。
- 设备的会话管理存在问题,攻击者通过控制 Cookie 值绕过会话检查。
- 设备对于用户访问的资源路径处理不恰当,攻击者通过路径穿越/插入特殊字符串等通过身份验证,但实际访问到敏感接口。
- 设备区分了 PC 和 app 请求,app 请求可能存在弱校验。
- 设备直接暴露了敏感接口,攻击者通过访问这些接口得到认证信息。