Parasoft Continuous Testing Platform 10.7.5 - JSP command execution

EDB-ID:

52694

CVE:

N/A




Platform:

Linux

Date:

2026-10-21


Title: Parasoft Continuous Testing Platform 10.7.5- JSP command execution
Author: 0day Rubbish Research Team
Contact: disclosure@0day-rubbish.com
Type: remote
Platform: Linux
CVE: pending

Description:
Parasoft Continuous Testing Platform (CTP) from Parasoft (United States), a closed-source commercial continuous-testing product bundling environment management, service virtualisation and test-data management, distributes an em web application running on Tomcat 10.1.57 with the Jasper JSP engine and Java 17 inside the vendor's own container image, and contains three composing first-party defects that yield unauthenticated remote code execution in two HTTP requests: an unfiltered ObjectInputStream over an uploaded backup archive (CWE-502, primary), a licensing filter that decides whether to run at all by testing the raw un-normalised request URI (CWE-436, primary and co-equal, with CWE-863 as its authorisation consequence), and a remember-me authentication service that hands an anonymous request an authenticated administrator principal when no external security provider has been configured (CWE-287, enabling gate). Defect one, the licensing filter: EMLicenseFilter is 175 lines and reads request.getRequestURI() raw at line 165, testing it at line 166 against exemption clauses including endsWith("about"), endsWith("welcome"), endsWith("forgot"), endsWith("notlicensed"), contains("/resources/") and contains("/images/"), none anchored by a separator or segment boundary, with a further branch at lines 167 to 171 that reassigns the entire exemption set rather than accumulating into it when the request accepts JSON. That test has exactly one consumer in the class, shouldNotFilter() at line 47 calling isLicenseRequired() at line 48, so a matching URI suppresses the whole doFilterInternal() body rather than weakening one decision, taking with it the /tdm/api/v guard at line 99 and the unlicensed-instance redirect to /em/license at line 118. Because a servlet container strips semicolon-delimited path parameters from each segment before servlet mapping, filter and router disagree about the identity of one request, and a target of /em/database/restore;welcome is exempted by the filter while still being routed to the restore handler. Defect two, the sink: com.parasoft.ctp.core.database.backup.AbstractDatabaseBackup, a class of 518 lines and 19,851 bytes, wraps the uploaded database.ser zip member at line 111 with new ObjectInputStream(in), reads an int at line 112 and calls readObject() at line 117 casting the result to TableMetadata, nine lines above the getConnection() call at line 126, so the cast constrains nothing (the gadget executes during the call and the ClassCastException is thrown afterwards) and no database needs to exist, be reachable or accept the supplied parameters for the payload already to have run. The caller is the web-layer controller EMDatabaseController, 1359 lines, whose restore handler at lines 707 and 708 takes a StandardMultipartHttpServletRequest and hard-compares zip member names against the literals "version.txt" at line 725 and "database.ser" at line 733, a shape check that says nothing about content. The absence of a filter is a corpus-wide artefact measurement rather than an inference: ObjectInputStream appears in 68 files with 149 occurrences in the core tree and in 3 files in the web layer, readObject appears in 81 files, and ObjectInputFilter appears in 0 files, jdk.serialFilter in 0 files and serialFilter in 0 files, three zeros across a class name, a system property and its short alias against a positive control of 81 files containing readObject. Two sibling readObject() calls on the same class of untrusted stream sit at lines 323 and 435 of the same class, in a row-draining and a row-binding routine, read from source and not detonated, named so a fix covers the class rather than the call site. Defect three, the fail-open gate: EMRememberMeServices is 483 lines and, at line 140 to 142, returns null when an Authorization header is present, then at line 146 fetches PSTSecServiceClient.getInstance() and at line 148, when that client is null, returns createAuthenticationToken("admin", "admin", EmUserInfo.Role.ADMIN, request), which registers new SimpleGrantedAuthority("ROLE_ADMIN") inside a Spring Security User whose authenticated flag is true, so the resulting principal is a genuinely authenticated administrator rather than the container's anonymous principal and satisfies the authenticated() matcher that CTPSecurityWebConfig line 283 demands for /database. Neither hop is an unprotected route, /database being explicitly matched to authenticated() and the images allow-list being /images/welcome/* rather than a widened glob, so the accurate description is not that the endpoints are unauthenticated but that the product mints an administrative principal for an anonymous request. CsrfSecurityRequestMatcher is a 28-line class whose anchored exemption alternation at line 19 includes /database, so the state-changing POST needs no CSRF token. The write primitive is a HashSet forcing hashCode() on a TiedMapEntry wrapping a LazyMap whose factory is a ConstantTransformer holding attacker bytes and whose decorated map is an aspectjweaver SimpleCache$StoreableCachingMap with an absolute folder field, reaching writeToPath(folder + File.separator + key, bytes). That class was read at the bytecode level for four facts: folder is declared private final String, is not transient, and the class declares no readObject of its own, so it is populated by the inherited HashMap.readObject path through defaultReadObject and both writer routines read it with an explicit getfield that the compiler has not inlined; writeToPath is getfield folder, StringBuilder, separator, key, store, with no forced extension appended to the key, an assumption that had to be disproved at the bytecode level because it would have removed any execution surface, and the value passes a checkcast to a byte array so the content is any bytes at all; validation is absent in every form, with zero occurrences of File.getName(), Path.normalize(), getCanonicalPath(), startsWith(), a regular expression or a length truncation in that write path, keys carrying slashes or relative traversals both working, and an absolute destination reachable because concatenation with an empty folder yields a leading double separator that POSIX treats as a valid absolute path; and the gadget does not fire on deserialization alone, because HashMap.readObject reconstitutes entries through putVal, which is package-private, final and not overridden, so it bypasses the overridden put containing the write, which is why the TiedMapEntry to LazyMap to Map.put prefix is needed to force a virtual dispatch. The only content constraint anywhere is that the bytes must not equal the four-byte sentinel IDEM, and the published carrier sets storingTimer to Integer.MAX_VALUE to suppress the index file the caching map would otherwise write alongside the payload. The execution sink is the product's own container configuration: conf/web.xml declares the Jasper jsp servlet and its mapping at lines 274-275 and 448-450, and webapps/em/images/ is inside the deployed web application root the mapping covers, writable by uid 1000 as drwxrwxr-x parasoft root, and exempt from the licensing filter by the contains("/images/") clause at line 166, so fetching the written page needs no bypass of its own and the product compiles it into work/Catalina/localhost/em/org/apache/jsp/images/<stem>_jsp.java and .class. A second independent execution sink was also detonated and is recorded so a fix removing JSP compilation does not close half the problem: WEB-INF/classes/com/parasoft/rest/api/core/docs/templates/ is the FreeMarker template root, its prefix from an init-param in webapps/em/WEB-INF/web.xml at lines 206-207 with FreeMarker 2.3.34 through jersey-mvc-freemarker 3.1.12, and because freemarker.template.utility.Execute is on no restriction list the new() built-in is usable, a template performing 7*119 returning 200 with a 4-byte body containing 833 and a template invoking Execute with id returning 200 with a 61-byte body containing the full identity string. Measured chain: one anonymous multipart POST to the restore path carrying a semicolon-welcome suffix, with no cookie, no Authorization header and no CSRF token, returning HTTP 500 with the product's own 119-byte "An error occurred while applying changes to the database configuration" message, which is the success signature rather than a failure because readObject() precedes getConnection(); the product log then carried one occurrence per run of a stack trace naming AbstractDatabaseBackup.restore(AbstractDatabaseBackup.java:114) with "class java.util.HashSet cannot be cast to class ...TableMetadata" and a java.lang.ClassCastException, counted at 2, then 4, then 6 across runs, and distinguishable from a malformed payload which produces a ClassNotFoundException instead. One anonymous GET of the written page then returned 200 with a 538-byte body containing a per-run marker with a 12-hex nonce, JSP_EVAL=42 from server-side evaluation of the expression 6*7, proving evaluation rather than source echo, SIZE_SELF=1372 agreeing with the length of the payload the client built, which proves byte-exact content control, the identity string uid=1000(parasoft) gid=1000(parasoft) groups=1000(parasoft), an ls line showing the page owned by the product's own service account, a /proc/1/status line reporting Uid: 1000 1000 1000 1000, and two /etc/passwd lines read by the product process. Two end-to-end detonations of the two-hop chain are recorded as successful, and two intermediate iterations failed inside the client script rather than against the product. No vendor-supplied credential, no licence installation or forgery, no outbound connection, no DNS or RMI callback, no cross-version payload adaptation, no memory corruption and no privilege escalation is involved: the upstream libraries carrying the gadget are carriers and not the root cause, executing a jsp page is the product's own configuration, and the landing identity is the product's unprivileged service account, uid=1000(parasoft), confirmed three ways (id inside the executed page, the written file's ownership and mode, and the product process's own /proc/1/status read from inside the container rather than through a container runtime which would report root regardless), with root not reached and not claimed.

CVSS: 9.8 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) is the single score published for this finding, recomputed from the vector with the CVSS 3.1 base formula rather than inherited: ISS = 1 - ((1 - 0.56) x (1 - 0.56) x (1 - 0.56)) = 0.914816; Impact for Scope Unchanged = 6.42 x 0.914816 = 5.873119; Exploitability = 8.22 x 0.85 (AV:N) x 0.77 (AC:L) x 0.85 (PR:N) x 0.85 (UI:N) = 3.887043; base = min(5.873119 + 3.887043, 10) = 9.760161; rounded up to the nearest tenth = 9.8. AV:N because the chain is two ordinary HTTP requests to TCP listeners the product serves itself over HTTP 8080 and HTTPS 8443, both accepting connections from outside the container, with no shipped reverse proxy, WAF or management-plane separation in front of em, and the loopback publication of the test port being the research testbed's containment rather than a product restriction. AC:L because there is no timing window to win (this is the steady factory state, not a first-run race), no host-specific reconnaissance, no memory-layout condition, no database reachability requirement (the readObject at line 117 runs before getConnection at line 126), no outbound callback and no cryptographic work; the only condition is a configuration state the vendor's own image is in when no external security provider has been configured. PR:N because no credential belonging to anybody is supplied or consumed, the authenticated subject being manufactured by the product itself for an anonymous request. UI:N because no administrator action, browser session, induced click or human upload step exists anywhere in the chain. C:H, I:H and A:H describe compromise of the product, its managed data and its availability, and do not depend on the landing identity being privileged, nor rise if a deployment runs the platform as root, which was not measured. Scope Unchanged, with Scope Changed considered and rejected on an argument stated here so the call cannot be read as a scoring maneuver: the rejected S:C variant of the same metrics, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H, computes to 10.0 from ISS 0.914816, Impact for Scope Changed 6.128026, Exploitability 3.887043 and 1.08 times their sum capped at the maximum, so the reading rejected here is the higher one and rejecting it lowers the published number from 10.0 to 9.8. It is not two authorities because the filter, the deserialising controller, the security service, the web application root and the JSP mapping all belong to one deployed application in one JVM, the platform being a single product with several routers rather than several products; because executing a page inside its own web application root is the documented intended behaviour of the shipped container configuration, so the transition from write to execution crosses no boundary the product defines; and because the executed code carries the product process's own uid in that process's own filesystem view, entering no other account, machine or security domain. Three labelled conditional readings accompany the published score and none replaces it. Conditional A, external security provider configured and the attacker holding any valid account, is CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H = 8.8 High (Exploitability 8.22 x 0.85 x 0.77 x 0.62 x 0.85 = 2.835255, Impact 5.873119, base 8.708373 rounded up), since the security matcher for the restore path is authenticated() rather than hasRole("ADMIN") so low privileges suffice while the licensing gate is still bypassed and the sink still unfiltered; this deployment state was not detonated, and it is the reading an operator of a configured deployment should work from because it depends on no unverified capability. Conditional B, external security provider configured with the subject supplied by an offline-forged session cookie, is CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8 Critical, arithmetically identical to the published score, and is a static and structural inference that was never verified: the platform signs its remember-me cookie PST_LT_FEATURES_INFORMATION, whose signed plaintext takes the form user:expiry::localAddr:"rememberMeAuthenticated", with a key that is a compile-time string literal and a signature scheme, bcrypt, that carries no secret, so anyone holding the shipped class can compute a cookie offline, and forgery was exercised only in the unconfigured state where it buys nothing, while the combination of a configured deployment driven end to end with a forged subject has no run behind it at all; building a fake security provider to test it was deliberately abandoned because installing one would have manufactured the condition being measured. Conditional B is published so an operator of a configured deployment knows which exposure to check and so the vendor knows which mechanism to replace, never as a demonstrated capability and never as the headline score, and the published PR:N rests on the measured fail-open behaviour of the untouched image rather than on any forgery. Conditional C, listeners reachable only from a management network, is CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 8.8 High (Exploitability 8.22 x 0.62 x 0.77 x 0.85 x 0.85 = 2.835255, Impact 5.873119, base 8.708373), conditional on a deployment change that was not tested and downward rather than upward, so an operator who hardens the network position can see that it is worth one point of severity and fixes nothing else. No version-range claim is made: only CTP 10.7.5-SNAPSHOT was executed, obtained through the vendor's mutable parasoft/ctp:latest tag (image id prefix f99f7210fc67, from a saved copy of 997,805,056 bytes with md5 47946c7d1e7d99a00d6c90f4ae675f90), and in the Maven convention the product's own build system uses a -SNAPSHOT version marks an in-progress development line rather than an immutable released artefact, so a mutable tag cannot be mapped onto a released version range with the confidence a public affected-version list requires; the claim is that this build is affected and that any other build shipping the same AbstractDatabaseBackup and EMLicenseFilter source carries the same construction. CWE classification: CWE-502 primary and CWE-436 co-equal for the two root causes, CWE-863 and CWE-287 contributing for the authorisation and authentication gates, with CWE-321 and CWE-347 reported separately for the literal session-cookie signing key and its keyless signature scheme and CWE-22 reported separately for a sibling file-path primitive that is not part of this chain, none of the contributing items raising any metric of the published vector, and no upstream component blamed since the gadget libraries are carriers and the jsp execution is the product's own container configuration.

Impact: unauthenticated remote code execution inside the product process from two ordinary HTTP requests, on a platform whose documented entry point for the same function is an administrator menu behind authentication and a valid licence. Before any command runs, the chain carries an arbitrary-path arbitrary-content file write as a primitive in its own right, the folder field, the map key (observed to accept both nested and relative-traversal forms) and the content byte array all living inside the serialized stream, with the only content restriction being that the bytes must not equal the four-byte IDEM sentinel. Execution as uid 1000 inside the product's JVM puts in scope the product tree under /usr/local/parasoft/ctp including the deployed em application (writable, which is how the page was created), the container's own conf/ directory holding server.xml and web.xml, and the Jasper work directory; the platform's HSQLDB database files, whose location and database user the bypassed configuration endpoint disclosed to an anonymous caller on its own, and through them every environment definition, system definition, virtual-service definition and test-data-management record the platform manages plus whatever credentials or connection strings it stores on behalf of the systems it virtualises; anything else on the host filesystem readable by uid 1000, measured concretely by reading /etc/passwd; and the product's own licence and security state, since arbitrary in-process code can rewrite configuration files and restart the application's view of them, which is why the unlicensed and unconfigured conditions an operator might rely on as compensating controls are not durable ones. There is no window: this is not a first-run race, a setup-mode condition or a state that closes when an administrator first logs in, the fail-open branch applying for as long as the external provider is unconfigured, the licensing bypass for as long as the filter reads raw URIs, and the sink unconditionally, and the chain was fired repeatedly against one instance with no state consumed in between. The cost to an attacker is one crafted multipart POST and one GET with no reconnaissance, no credential acquisition, no timing precision, no outbound infrastructure and no target-specific adaptation. Persistence that survives naive cleanup was measured rather than argued: after the dropped page and both of its Jasper compilation artefacts were deleted, the same URL still returned 200 with a body two bytes shorter than the first response, precisely because the ls command inside it could no longer find the file it was listing, the compiled servlet instance still being loaded in the JVM. An operator who deletes the file and the compiled class has therefore not removed the web shell; only a context reload or a JVM and container restart does. Also within reach structurally rather than by demonstration is the ability to overwrite the deployed application's own pages or class files, converting a single write into persistence surviving a restart, which was not performed and is flagged as an unexercised consequence of a proven primitive rather than as a measurement. Detection is weak by construction: hop 1 is a documented administrative function returning an ordinary 500 with the product's own configuration-error message, and hop 2 is an anonymous GET for a file under a static-assets path, so the strongest indicators are a request target containing a semicolon, a non-image file appearing under the deployed images directory, a compiled jsp stem under the Jasper work directory that the product does not ship, and the product log line pairing the restore frame with a cast exception naming a JDK collection class where backup metadata was expected. One caution for status-based monitoring: the licensing filter flushes its response output stream at line 113 before calling setStatus(403) at line 115, so licence refusals surface as HTTP 200 with a JSON body carrying status 403, and any rule keyed on HTTP status for licence failures will not fire at all.

Verification boundary: live black-box HTTP verification against a running instance started from the unmodified vendor image in an isolated container sandbox with no setup step performed, plus source-level reading of the decompiled first-party classes and of the shipped conf/web.xml and webapps/em/WEB-INF/web.xml; it is not an emulation and not a protocol replica. The instance state was factory, unlicensed and with no external security provider configured, and that state was confirmed rather than assumed from a shipped-code fact about the pristine image and not a property of anything the researchers configured: requesting the provider-configuration endpoint at /em/pstsecconfig with an Accept header naming application/json returned 404 with a zero-length body, matching the controller's own getActiveConfig()==null branch, and it reproduced on two instances so it is not an artefact of one container. The client was Python 3.11.6 using only the standard library, run by an ordinary unprivileged user, and no packet left the sandbox at any point because the chain requires no outbound connectivity and none was available to it. Nothing was driven from a separate network host: the client ran on the same sandbox host as the container, reaching it through a published loopback port, so reachability from a second machine over a routed network is argued from the shipped listener topology (the container accepts connections on its HTTP and HTTPS ports from outside itself and the product ships no front-end proxy or filter of its own) and not demonstrated between two hosts, while every other claim in the chain, the bypass, the principal, the write, the execution, the identity and the persistence of the compiled servlet, was measured over real HTTP against a real product process. Only the factory configuration state was detonated; a deployment with the external security provider configured was not tested end to end and is scored as a conditional reading. Only one build was executed, with no older release, no newer release, no Windows variant and no differently configured instance fired at, so no cross-release and no cross-platform claim is published. Two legs are labelled inference and never presented as measurement: the forged session cookie in a configured deployment, and the JavaScript-engine evaluation path, whose capability layer was measured in isolation while its two reachability preconditions were never satisfied, so no claim about it appears in the rated chain or the impact section. Read from source but not separately detonated were the two sibling readObject() sites at lines 323 and 435 and the hardening of the other upload and zip-handling paths examined and ruled out; measured on shipped artefacts rather than over the network were the corpus-wide absence of any deserialization filter, the JVM's own --add-opens list read from its process command line, the absence of JDBC drivers among 238 jars, and the bytecode-level facts about the caching map's write path from single-class disassembly rather than a whole-library dump. Six negative controls were exercised, each removing a specific alternative explanation: hop 1 without the semicolon suffix returned 302 with Location /em/license; hop 1 with the backup part omitted returned 400 with 59 bytes; hop 2 against a never-written file in the same directory returned 404 with 762 bytes and the container's own "JSP file not found" page carrying an Apache Tomcat/10.1.57 footer, the run marker appearing once in the executed body and zero times here; the licensing-bypass read with the suffix but no semicolon returned 404 with 792 bytes, excluding a literal-suffix match; the identity endpoint with Authorization: Bearer garbage returned 401 with a zero-length body, showing the fail-open branch is specific to requests presenting no credential at all so the behaviour is not that authentication is off but that an absent credential is more trusted than a wrong one; and the /em/tdm/api/v2/servers resource without the bypass returned 200 with a 90-byte unlicensed body, excluding the possibility that the bypass variant's well-formed XML came from the fixed unlicensed path. Everything the client reported was re-verified from the target side rather than trusted from the script's output: the images directory's file count went 39 before the run, 40 immediately after and back to 39 after self-deletion, a single-file difference matching a single payload; the Jasper residue under work/ after cleanup was zero entries; a filesystem search for the run's marker leaf name under the product tree and under /tmp returned zero results; and the container was destroyed afterwards with its published ports confirmed free, leaving only a read-only reference instance running. Two classical gadget families were established dead on this target from the product's own process rather than from a guess, its JVM's --add-opens list covering java.base/java.lang, java.lang.reflect, java.io, java.util and java.util.concurrent plus java.rmi/sun.rmi.transport with no SecurityManager installed: a comparator-driven PriorityQueue chain with a bean comparator targeting TemplatesImpl was deserialized and did fire, the stack showing PriorityQueue.readObject:801 into heapify:734 into BeanComparator.compare:142, and was then stopped by the module system because java.xml does not export com.sun.org.apache.xalan.internal.xsltc.trax; and a JdbcRowSetImpl chain using autoCommit as the trigger with an rmi URL was stopped the same way, java.sql.rowset not exporting com.sun.rowset, confirmed independently by a native listener on the sandbox host receiving no inbound connection during a 25-second window. No JDBC driver of the commonly abused families is present either, a survey of all 238 jars finding no PostgreSQL, MySQL, Oracle, Derby or H2 driver, so the JDBC-connection style of gadget has nothing to connect with, the platform talking to HSQLDB in-process, which is not the same surface; on a modern JDK with default module boundaries the usable gadget set is whatever lives entirely in the unnamed module, which is why the published chain has the shape it does. Classic zip-slip is closed in the live extraction helper whose entry-name checks are hardened, a second older zip utility in the tree has zero callers and is dead code, and the import path used by restore reads members into an in-memory map rather than extracting them to disk; the restore handler's own member selection is a hard comparison against two literals so no alternative member name can be smuggled in to change which bytes reach the stream. A file-API variant taking an extension parameter appeared to allow a traversal into a jsp suffix and is closed by two independent walls, the extension helper taking everything after the last dot so traversal sequences are discarded and the manager underneath using File.createTempFile so the JDK's own temporary-file machinery rejects a suffix containing a separator, the product even echoing back its own failure message containing the attempted suffix. The upload, temp and task endpoints under /em/tdm/api/ have no mounted handler at all although the generated WADL advertises paths that do not exist, which is a documentation defect and a trap for anyone mapping this surface from the WADL alone. The licensing bypass is not a skeleton key, which was measured rather than assumed: handlers performing their own licensing check are unaffected, the coverage-agent endpoints reading the license manager directly and continuing to return 403 with a 90-byte unlicensed body across all seventeen of their mappings even with the bypass applied, and a server-resource feature gate continuing to return 403 with a 94-byte body, so what the bypass unlocks is only what whose enforcement lives in the filter, being the whole /em/database subtree and the entire /em/tdm/api/v tree. Several additional first-party defects were measured on the same product and reported to the vendor separately with none folded into the published vector: the generic licence-gate bypass on its own, an authorisation bypass rather than an execution; the fail-open remember-me branch and an authentication provider that does not verify the submitted password at all, at PSTAuthenticationProvider.java:144, privilege granting rather than execution; the offline-forgeable session cookie with its literal signing key and keyless signature scheme, demonstrated only where redundant and not verified where it would matter; anonymous whole-database download and anonymous backup creation through both /em/api/v2/database/backups and /em/api/v3/database/backups, because the 213-line resource class backing them contains zero occurrences of checkLicensing() while the filter's exemption clause covers everything under /em/api/, the v2 variant being an undocumented twin of v3 that the shipped OpenAPI description does not cover and that was found by reading the routing table; anonymous arbitrary file read and anonymous arbitrary file delete through a temp-file handler building new File(tempDir, fileID) by pure concatenation from a query parameter with no base-name extraction or canonicalisation, measured with a passwd read returning the first line verbatim in a 47-byte body and with a DELETE that really removed a prepared file, being CWE-22 from the same unvalidated-concatenation family and not an execution; anonymous creation of database files in an arbitrary directory from a JSON connection URL passed verbatim into a JDBC file URL, measured as six new files appearing in a chosen directory, with the directory and base name attacker-controlled while the extension is fixed by the engine and the content is not, so it reaches no execution surface; twenty-eight hardcoded secret values of which seventeen participate in security decisions, being a shipped TLS keystore whose store password is the industry default changeit and appears in plaintext in server.xml at lines 110-112 (so every installation that did not replace the keystore shares one TLS key pair, checked once by listing the keystore with the documented password and succeeding on the first attempt with no brute-forcing), three hardcoded AES-128 keys in AES128Cipher.java at lines 41 to 43 that use the key as the IV removing the semantic-security benefit of an IV entirely, a literal Blowfish argument with iteration count 5 in NativeBlowfish.cc at line 36 used by the licensing subsystem to decode licence feature data, and a salt present as a string literal rather than generated material; a transactional-commit defect in which a malformed JSON body can leave orphaned rows because the checked exception thrown by the JSON library does not mark the surrounding transaction for rollback; and an inconsistent upload guard family, being header validation applied to only two of the seventeen files that set a content-disposition header, a zip validator testing only the extension, and a routine named canonicalize that performs a UTF-8 round trip rather than any path canonicalisation while being relied on as a path guard. The structural fact behind the unevenness is that the em application has four independent routing stacks behind one set of hand-written gates, / to the Spring DispatcherServlet, /api/* to a Jersey RestServletContainer, /tdm/api/* to TDMApiServletContainer and /services/* to CXF, with ten star-path servlet filters in conf/web.xml, so enforcement is not centralised and licence enforcement lives in two places at once, the filter and per-controller calls to checkLicensing(), of which 97 occurrences appear across 17 files against 31 routed Jersey resource classes, meaning 54.8 percent of routed resource classes perform any licensing check at all. Exactly one payload shape was executed for the published chain, a command capture plus a self-listing and a passwd read, and the dropped page deleted itself and its two Jasper compilation artefacts before the forensic refetch; nothing destructive was performed beyond the single written page, no product file other than that page was modified or deleted, and the container was destroyed afterwards.

Proof-of-concept and full analysis, published unconditionally:
https://0day-rubbish.com/blog/parasoft-ctp-unfiltered-deserialization-license-gate-fail-open-jsp-rce

PoC location in the public repository: product/parasoft/ctp-10.7.5/unfiltered-deserialization-license-gate-fail-open-jsp-rce/exploit/parasoft_ctp_unfiltered_deserialization_license_gate_fail_open_jsp_rce.py
Repository: https://github.com/Exploit-Garbage/0day-Rubbish

The shipped proof of concept is Python 3 using only the standard library, with no third-party dependency and no hardcoded target address. It takes the target URL and the command either as positionals or as -t and -c, defaulting the command to id, and exposes every coordinate of the chain as an argument rather than a constant: --folder for the landing directory, defaulting to the product's own deployed images directory; --leaf for the page name, defaulting to a per-run nonce-based name; --json for the connection parameters, whose default names a loopback address on port 1 so the post-deserialization database attempt fails immediately and predictably; --version for the content of the version.txt zip member, defaulting to 93; --web-prefix for the application context path, defaulting to /em; --catalina-base and --work-dir for the container layout; --restore-path for the hop-1 target, defaulting to the database restore mapping carrying the semicolon suffix; --images-path for the hop-2 directory; --timeout, --retries and --retry-delay for request behaviour; --keep so the dropped page does not delete itself; --no-hop3 to skip the post-delete forensic refetch that establishes the servlet is still loaded; -k or --insecure for a self-signed certificate on the https scheme; --dump-payload to write the generated carrier to a path; and --selftest, which sends nothing at all. The self-test encodes the offline discipline: it rebuilds the carrier skeleton using the original values of its three patch regions and asserts byte identity with the archived 1,178-byte carrier whose md5 is 6099d3f5fd36e28d8934c2e4e6c95da6, then rebuilds with new values and asserts that the new folder, leaf and content appear verbatim, which is what makes the claim that this generator produces the stream proven against the product checkable by a reader with no target to shoot at. The carrier is a 1,178-byte stream containing 19 handles, all consumed, treated as an immutable skeleton with three marked patch regions, the leaf name, the content byte array length-prefixed and raw, and the folder path, leaving every class descriptor, serialVersionUID, field order and handle back-reference byte-for-byte untouched. The written page carries the operator's command base64-encoded and decoded with java.util.Base64 at request time so no quoting conflict can arise between the page source, the Java string literal and the shell, invokes it through ProcessBuilder with /bin/sh -c and the error stream redirected into the output stream with LC_ALL and LANG pinned to C so id and ls output is machine-readable, and prints a per-run marker, a server-evaluated arithmetic expression distinguishing evaluation from source echo, its own absolute path and size, the identity output, the delimited command output and, unless asked not to, the result of deleting itself and both Jasper compilation artefacts before the forensic refetch. The script refuses to fire at all if the target URL has no scheme, no host, a non-rooted path or a doubled slash in the path; that pre-flight validator exists because two of the four iterations of the client failed for reasons unrelated to the target, one producing a URL with no slash between context and path and raising a nonnumeric-port error before a packet was sent, and one producing a doubled slash that the container answered with 404 while its unit test asserted only that the URL was parseable, which it was, and so guarded the wrong property. The exploit installs, forges, modifies or clears no licence, uses no MITM, DNS hijacking, TLS interception, outbound connection or callback, attempts no privilege escalation and claims no root, does not persist through any startup mechanism or second page or account creation, and its cleanup report is a forensic finding rather than a guarantee, since deleting the dropped page and its compiled artefacts does not unload the servlet from a running JVM.

--
0day Rubbish Research Team
disclosure@0day-rubbish.com
https://0day-rubbish.com