写点什么

身份验证绕过漏洞分析

发布于: 2 小时前

0x01 前言

最近 Tenable 披露了 Arcadyna 网络设备身份验证绕过漏洞,并且很多的厂商都采用产生漏洞的组件,由于 Arcadyan 设备固件厂商并没有开源出来,在官网支持里面下载的文件是 window 和 linux 下和设备连接的客户端软件,无法对漏洞点开展分析,这里我们使用同样受影响的华硕产品 DSL-AC3100 的固件来进行设备分析。并且复现在网络设备中网络检测 ping 功能的远程命令执行漏洞,从而开启设备 telentd。

0x02 华硕 DSL-AC3100 固件

我们从华硕的官网中下载固件。设备名称: DSL-AC3100 固件版本: DSL-AC3100_v1.10.05_build503

0x03 身份验证绕过漏洞分析

提取固件包

从华硕的官网下载到固件包 DSL-AC3100_v1.10.05_build503.w ,这是一个是用.w 为后缀的固件文件,使用 binwalk 可以提取出来。根据漏洞信息,可以确定这是一个在 http 服务中存在的漏洞,可以确定到 httpd 文件,本固件的 httpd 文件在 /usr/sbin/httpd 中。


httpd 二进制文件分析

在 ghidra 导入 httpd 文件,自动对文件进行分析,识别文件的各种函数。


由于漏洞是身份认证绕过漏洞,因此首先要确定设备的身份验证相关的函数有哪些,在 ghidra 对 httpd 文件中的字符串进行搜寻,根据字符串 “check_auth” ,定位到函数 FUN_0001d0c0(),


undefined4 FUN_0001d0c0(int iParm1)
{ int iVar1; undefined4 uVar2; int iVar3; undefined4 local_52c; undefined4 local_528; undefined4 local_524; undefined4 uStack1312; undefined4 local_51c; char acStack1304 [1024]; char acStack280 [260];
memset(acStack280,0,0x100); memset(acStack1304,0,0x400); local_52c = 0; local_528 = 0; local_524 = 0; uStack1312 = 0; local_51c = 0; iVar1 = FUN_00017df0(); if (iVar1 == -1) { uVar2 = 1; } else { iVar3 = mapi_ccfg_match_str(iVar1,"ARC_SYS_LogEnable",&DAT_00046b48); iVar1 = mapi_ccfg_match_str(iVar1,"ARC_SYS_MPTEST",&DAT_00046b48); if (iVar1 == 0) { if (iVar3 != 0) { iVar3 = 1; } if (iVar3 != 0) { FUN_00017738(iParm1 + 0x76f0,&local_52c); } if (*(int *)(iParm1 + 0x774c) == 0) { uVar2 = FUN_0001b6f4(iParm1 + 0x771e,*(undefined4 *)(iParm1 + 0x76ec)); FUN_0001b8c8(iParm1,uVar2); } iVar1 = FUN_0001ce8c(*(undefined4 *)(iParm1 + 0x774c),*(undefined4 *)(iParm1 + 0x76b0), *(undefined4 *)(iParm1 + 0x76b4),*(undefined4 *)(iParm1 + 0x76b8), *(undefined4 *)(iParm1 + 0x76bc),*(undefined4 *)(iParm1 + 0x76c0), *(undefined4 *)(iParm1 + 0x76c4),*(undefined4 *)(iParm1 + 0x76c8), *(undefined4 *)(iParm1 + 0x7b34)); if (iVar1 == 1) { printf("[%s] %s login time out, reauth\n","check_auth",iParm1 + 0x76f0); FUN_00039088(1); snprintf(acStack1304,0x400,"Location: /relogin.htm\n\n"); } else { if (iVar1 == 2) { printf("[%s] new user %s(%s) comes to login, check user and auth\n","check_auth", iParm1 + 0x76f0,iParm1 + 0x4c); snprintf(acStack1304,0x400,"Location: /relogin.htm\n\n"); } else { if (iVar1 == 0) { printf("[%s] %s has already granted, pass\n","check_auth",iParm1 + 0x76f0); return 0; } } } if (iVar3 != 0) { snprintf(acStack280,0x100,"User from %s(%s) authentication fail.",&local_52c,iParm1 +0x76f0 ); append_to_file("/tmp/security_log.txt",acStack280); } FUN_00015338(iParm1,acStack1304); uVar2 = 1; } else { uVar2 = 0; } } return uVar2;}
复制代码


根据函数代码的一些细节,可以看出这个函数检查认认证是否符合的功能函数,其中 FUN_0001ce8c 函数的返回值 iVar1,在函数中 iVar1 的值为 2 时,说明是新用户登录,需要检查用户名和验证。iVar1 的值为 0 的时候,则显示验证通过。iVar1 的值为 1 的时候,则表示说明验证超时,并且重新返回到登录界面。


接下来,查看 FUN_0001d0c0() 函数在 FUN_0001d578() 中被引用。而 FUN_0001d0c0() 函数就是漏洞的 evaluate_access() 函数。


// evaluate_access()undefined4 FUN_0001d578(undefined4 uParm1,undefined4 uParm2,int iParm3)
{ int iVar1; undefined4 uVar2;
if (iParm3 == 0) { return 0; } iVar1 = FUN_0001d2e0(iParm3); if (iVar1 != 0) { if (*(int *)(iParm3 + 0x76a8) != 0) { return 0; } uVar2 = FUN_0001d0c0(iParm3); return uVar2; } FUN_00014510(iParm3,0x193,"Unauthorized."); return 1;}
复制代码


FUN_0001d578() 函数中的 FUN_0001d2e0() 是使用正则表达式来校验 URL 中的 IP,端口是否符合规范。以及 FUN_0001d0c0() 函数也在其中,因此这个函数是 httpd 中来做身份验证的函数,也就是漏洞分析中的 evaluate_access()。


接下来我们来查看调用 evaluate_access() 函数的地方,真正的漏洞点在这个函数,我们来看漏洞点是如何绕过身份验证的。我们来到了 FUN_00015058 函数,这就是 process_request 的函数。


void FUN_00015058(int iParm1)
{ undefined4 uVar1; char *pcVar2; char *__src; int iVar3; char *__dest;
iVar3 = iParm1 + 0xd5; uVar1 = FUN_00016a84(iVar3,0xd); *(undefined4 *)(iParm1 + 0x27f0) = uVar1; *(undefined4 *)(iParm1 + 0x76a4) = 0xffffffff; *(undefined4 *)(iParm1 + 0x76ac) = 0xffffffff; __src = (char *)FUN_00016a84(iVar3,0x20); *(int *)(iParm1 + 0x7b18) = iVar3; pcVar2 = (char *)FUN_00016a84(__src,0x20); uVar1 = FUN_00016a84(__src,0x3f); *(undefined4 *)(iParm1 + 0x7b14) = uVar1; __dest = (char *)(iParm1 + 0x7994); strncpy(__dest,__src,0xff); *(undefined *)(iParm1 + 0x7a93) = 0; FUN_00016e3c(__dest); printf("[%s] url=[%s], args=[%s], method=[%s]\n","process_request",__dest, *(undefined4 *)(iParm1 + 0x7b14),*(undefined4 *)(iParm1 + 0x7b18)); iVar3 = FUN_00018c70(iParm1); if (iVar3 < 0) { return; } if (*pcVar2 == '\0') { *(undefined4 *)(iParm1 + 0x7988) = 1; } else { *(undefined4 *)(iParm1 + 0x7988) = 0; iVar3 = FUN_00018cb8(iParm1); if (iVar3 < 0) { return; } iVar3 = strncasecmp(*(char **)(iParm1 + 0x7620),"multipart/form-data",0x13); if ((((iVar3 != 0) && (*(char **)(iParm1 + 0x7b24) != (char *)0x0)) && (__src = strcasestr(*(char **)(iParm1 + 0x7b24),"FirmwareUpload"), __src == (char *)0x0))&& (0 < (int)(*(int *)(iParm1 + 0x7984) + (uint)(64000 < *(uint *)(iParm1 + 0x7980))))) { FUN_0000bef4(iParm1,*(undefined4 *)(iParm1 + 0xc)); FUN_00014510(iParm1,0x193,"The Content-length is extreme large!"); return; } } uVar1 = FUN_0000deb0(__dest); *(undefined4 *)(iParm1 + 0x76a8) = uVar1; // evaluate_access() if (((*(code **)(PTR_PTR_DAT_00054fac + 0x14) == (code *)0x0) || (iVar3 = (**(code **)(PTR_PTR_DAT_00054fac + 0x14))(iParm1), iVar3 != 2)) && ((*(int *)(iParm1 + 0x76a8) != 0 || (iVar3 = FUN_0001d578(__dest,0,iParm1), iVar3 == 0)))) { *(undefined4 *)(iParm1 + 0x798c) = 0; __src = *(char **)(iParm1 + 0x7b18); iVar3 = strcmp(__src,"HEAD"); if (iVar3 == 0) { *(undefined4 *)(iParm1 + 0x798c) = 1; if (*(int *)(iParm1 + 0x7988) == 0) { FUN_0000eb98(iParm1); } else { *(undefined4 *)(iParm1 + 0x798c) = 0; FUN_00014510(iParm1,400,"Invalid HTTP/0.9 method."); } } else { iVar3 = strcmp(__src,"GET"); if (iVar3 == 0) { FUN_0000eb98(iParm1); } else { iVar3 = strcmp(__src,"POST"); if (iVar3 == 0) { FUN_00014c30(iParm1); } else { FUN_00014510(iParm1,400,"Invalid or unsupported method."); } } } } return;}
复制代码


&& : 逻辑与,前后条件同时满足表达式为真;

|| : 逻辑与,前后条件只要有一个满足表达式为真。


如下面的代码,因为逻辑运算符 && 的优先级大于 || ,因此会先计算 && 的值。所以要先判断 iParm1 + 0x76a8 的值。如果值不为 0 ,则接着执行 逻辑运算符的|| 的表达式。


((((code )(PTR_PTR_DAT_00054fac + 0x14) == (code )0x0) ||(iVar3 = ((code )(PTR_PTR_DAT_00054fac + 0x14))(iParm1), iVar3 != 2)) &&(((int )(iParm1 + 0x76a8) != 0 || (iVar3 = FUN_0001d578(__dest,0,iParm1), iVar3 == 0))))
复制代码


根据 FUN_00015058() 函数的代码,可以看到 iParm1 + 0x76a8 的值是从 FUN_0000deb0(__dest) 获取到的,而 “_dest” 的值在前面可以看出来是用户请求的 URL。


如果 iParm1 + 0x76a8 不为 0 ,那么就能跳过身份验证的函数 evaluate_access(),来直接执行处理 POST 请求的 FUN_00014c30 函数。


接下来进入 FUN_0000deb0() 函数,来查看是怎么处理 URL


undefined4 FUN_0000deb0(char *pcParm1)
{ size_t __n; int iVar1; char *__s; undefined **ppuVar2;
ppuVar2 = &PTR_s_/images/_00054f70; __s = PTR_s_/images/_00054f70; if (PTR_s_/images/_00054f70 == (undefined *)0x0) { return 0; } do { __n = strlen(__s); iVar1 = strncasecmp(pcParm1,__s,__n); if (iVar1 == 0) { return 1; } ppuVar2 = ppuVar2 + 1; __s = *ppuVar2; } while (*ppuVar2 != (char *)0x0); return 0;}
复制代码


函数会将 url 和 &PTRs/images/00054f70 字符串进行比较,直到符合为止,而 &PTR_s/images/_00054f70 的值是 “/images/” ,所以只需要请求的 URL 中带有 “/images/” 字符串,就可以绕过身份认证函数访问其他页面。




前面已经分析出来了身份验证绕过的漏洞点,但是并不能绕过验证访问任意界面,因为在访问的时候,需要正确的 httoken 值。接下来我们来分析设备的 httoken 是怎么获取和生成的,在这个设备里,httoken 是设备的 token 值,并且访问设备的页面需要带有给定的 httoken 值。根据漏洞披露来看,httoken 是在服务端进行生成,然后前端 js 中进行解密,最终向服务器请求的时候,将 httoken 加入到请求数据中,但是漏洞披露并没有说明 httoken 是那一段字符串生成的。。


我在 httpd 的逆向工程中找到了生成 httoken 的函数,


undefined4 FUN_00022520(int iParm1)
{ int iVar1; undefined4 uVar2; undefined *puVar3; size_t sVar4; char cStack120; undefined auStack119 [107];
memset(&cStack120,0,0x65); iVar1 = FUN_00017df0(); if (iVar1 == -1) { uVar2 = 0; } else { puVar3 = (undefined *)(iParm1 + 0x7994); if (puVar3 == (undefined *)0x0) { puVar3 = &DAT_0003d274; } uVar2 = FUN_000393e0(puVar3); sprintf(&cStack120,"%lu",uVar2); sVar4 = strlen(&cStack120); FUN_00017e78(&cStack120,sVar4,auStack119 + sVar4,100 - sVar4); uVar2 = so_printf(iParm1, "<img title=spacersrc=\"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7%s\" border=0>" ,auStack119 + sVar4); } return uVar2;
复制代码


接下来我们来分析 FUN_00022520 函数。在函数中我们可以看到最终生成了一个 img 标签,并且 src 的值是一段“data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 + auStack119 + sVar4 ”字符串,而其中 auStack119 + sVar4 的值是从 FUN_00017e78 函数中进行 base64 以及其他的方式进行处理后的字符串,并且这段字符串就是 httoken 的值。


生成的 img 标签会在设备的 login.html 中 html 代码中出现,如下图所示,



根据生成 httoken 函数拼接这段字符串的方式,使用脚本对把 token 解密出来,可以确定如下图的“372646849” 为设备的 token。


ArcBase.decode(“image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7MTU2OTQzNDE0OA==”)根据解密出来的信息,“;” 后面的字符串 “ 372646849” 就是设备解密后的 httoken 值。


0x04 ping 命令注入+配置选项

这一部分,漏洞披露的相对来说比较详细,很多的网络设备中在 ping 网络诊断 这个功能中,出现过大量的历史漏洞,比如 NetGear,D-Link 等都出现过此类漏洞,因此关于 ping 这一步部分命令拼接就不展开来讲述,但是本漏洞的不同点在于使用设备内部的配置选项 ARC_TELNETD_ENABLE 来开启设备的 telentd,这一点可以在以后的漏洞挖掘中遇到无法执行命令的时候,提供了不同的执行命令的方式。我们来重点的关注一下 ARC_TELNETD_ENABLE 这个配置。在文件 /sbin/arc_telnetd 文件中可以看到文件内容。文件可以获取 ARC_TELNETD_ENABLE 的值,当 ARC_TELNETD_ENABLE 的值为 1 的时候,设备会开启 telnetd。


漏洞复现


0x05 总结

这个身份验证绕过漏洞产生的根本原因在于对请求的 URL 验证不严格,本来”/image”是用来用户请求前端静态资源时,默认不需要通过验证,最终导致通过“/image/” 绕过登录。


另外在前一阵子的披露的 NetGear DGN2200v1 设备中同样存在通过前端静态资源的路径,来绕过身份验证。并且我在其他的一款网络设备的固件中发现类似的问题。


【2021最新整理网络安全\渗透测试/安全学习(全套视频、大厂面经、精品手册。必备工具包)戳我拿】

用户头像

我是一名网络安全渗透师 2021.06.18 加入

关注我,后续将会带来更多精选作品,需要资料+wx:mengmengji08

评论

发布
暂无评论
身份验证绕过漏洞分析