[0day-rubbish] Parasoft Continuous Testing Platform 10.7.5-SNAPSHOT anonymous deserialization in the database restore path to JSP command execution as uid=1000 (9.8)
Full Disclosuremailing list archivesFrom: disclosure via Fulldisclosure <fulldis 2026-10-20 18:50:3 Author: seclists.org(查看原文) 阅读量:2 收藏

fulldisclosure logo

Full Disclosure mailing list archives


From: disclosure via Fulldisclosure <fulldisclosure () seclists org>
Date: Tue, 20 Oct 2026 02:31:02 +0000

0day Rubbish Research Team is publicly disclosing a vulnerability in Parasoft
Continuous Testing Platform 10.7.5-SNAPSHOT, the closed-source commercial
continuous-testing, service-virtualisation and environment-management product
from Parasoft, a United States software-testing vendor headquartered in
Monrovia, California. The affected component is the em web application inside
the vendor's own container image, running on Tomcat 10.1.57 with Jasper and
Java 17. Three first-party defects compose into an unauthenticated
command-execution chain that needs two HTTP requests and nothing else: an
unfiltered ObjectInputStream over an uploaded backup archive, a licensing
filter that decides whether to run at all by testing the raw un-normalised
request URI, and a remember-me authentication service that hands an anonymous
request an authenticated administrator principal when no external security
provider has been configured.

Type and root cause. Unfiltered deserialization of attacker-controlled data,
CWE-502 primary, composed with an interpretation conflict in the licensing
filter, CWE-436 primary and co-equal with CWE-863 as its authorisation
consequence, and enabled by a fail-open authentication branch, CWE-287. The
honest count is three first-party defects rather than one, and that matters to
whoever fixes it, because the defect owners, the fixes and the independent
value to an attacker all differ.

The licensing defect. EMLicenseFilter is 175 lines and behaves as an
authorisation boundary, being what stops an unlicensed instance from serving
its administrative API. Its decision procedure reads the request URI raw at
line 165 and tests it at line 166 against a chain of exemption clauses
including endsWith("about"), endsWith("welcome"), endsWith("forgot"),
endsWith("notlicensed"), contains("/resources/") and contains("/images/"),
none of which is anchored by a separator or a segment boundary. A branch at
lines 167 to 171 additionally reassigns the whole exemption set rather than
accumulating into it when the request accepts JSON. The test has exactly one
consumer in the class: shouldNotFilter() at line 47 calling isLicenseRequired()
at line 48. So a matching URI does not weaken one decision, it suppresses the
entire doFilterInternal() body, including 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
it performs servlet mapping, the filter and the router disagree about the
identity of one request: a target of /em/database/restore;welcome is exempted
by the filter and still routed to the restore handler by the container. That
disagreement is the textbook shape of CWE-436. Measured read-only on a live
instance, with the control that carries the argument:

    GET /em/database/currentConfig
        -> 302, Location: /em/license, zero-length body
    GET /em/database/currentConfig;welcome
        -> 200, 204 bytes, application/json;charset=utf-8, disclosing the
           schema version, the HyperSQL type, the database user and the
           jdbc:hsqldb:file URL of the platform's own database files
    GET /em/database/currentConfig;about
        -> the same 200/204, so this is the clause list and not one word
    GET /em/database/currentConfigwelcome     (no semicolon)
        -> 404, 792 bytes
    GET /em/tdm/api/v2/servers;welcome
        -> 200, 160 bytes, a well-formed serversResponse XML document with an
           empty servers element, so Jersey routed and ran the resource method
    the same pair over TLS
        -> 200/204 with the bypass and 302/0 without it, so the HTTPS
           listener is an equivalent surface and not a redirect shell

The no-semicolon control is the load-bearing row: the only variable that
changed is the presence of a semicolon path parameter, and the outcome flipped
from a licensing redirect to a 200 carrying live database configuration to a
caller who had never authenticated. The bypass is not a skeleton key, and that
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, while a server-resource
feature gate kept returning 403 with a 94-byte body. What the bypass unlocks is
everything whose enforcement lives only in the filter, which includes the whole
/em/database subtree and the entire /em/tdm/api/v version tree. One related
first-party defect sits in the same filter and matters for detection: line 113
flushes the response output stream and line 115 then calls setStatus(403), so
licence refusals surface as HTTP 200 with a JSON body carrying status 403,
which makes them invisible to any status-based monitoring.

The deserialization defect. The restore path lands in
com.parasoft.ctp.core.database.backup.AbstractDatabaseBackup, a class of 518
lines and 19,851 bytes:

    :14   import java.io.ObjectInputStream;
    :111  ObjectInputStream ois = new ObjectInputStream(in);
    :112  int numTables = ois.readInt();
    :117  TableMetadata tableMetadata = (TableMetadata) ois.readObject();
    :126  Connection connection = this.provider.getConnection();

where in is the database.ser member of an uploaded zip. Three properties are
decisive. The cast is applied to whatever readObject() returned, so it
constrains nothing: the gadget executes during the call and the
ClassCastException is thrown afterwards. getConnection() sits nine lines
lower, so no database needs to exist, to be reachable or to accept the supplied
parameters for the payload to have already run, which makes that ordering the
cheapest structural fix available and one needing no understanding of gadget
chains. And the int read at line 112 is consumed before the object read. The
caller is the web-layer controller EMDatabaseController, 1359 lines, whose
restore handler at line 707 or 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 any filter is not an inference from one search: in the
whole shipped product 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, in an image unpacked and searched as artefacts against a
positive control of 81 files containing readObject, is a strong result: the
product has no deserialization filtering anywhere in its construction. Two
sibling readObject() calls on the same class of untrusted stream sit in the same
file at lines 323 and 435, in a row-draining routine and a row-binding routine;
they were read from source and not detonated, and they are named so a fix covers
the class rather than the call site.

The fail-open authentication gate. The application has three Spring Security
filter chains and all three register the same remember-me service,
EMRememberMeServices, 483 lines, with the security configuration in
CTPSecurityWebConfig, 318 lines. Neither hop of the chain is an unprotected
route: /database is explicitly matched to authenticated(), and the images
allow-list is /images/welcome/star rather than a doubled-asterisk glob over
/images, so a page written to images/<leaf>.jsp also needs an authenticated
subject at the security layer. Both hops sit behind the authentication
boundary. The finding is therefore described accurately as the product handing
an anonymous request an authenticated administrative principal:

    // EMRememberMeServices.java:140-148, abridged
    if (request.getHeader("Authorization") != null) {
        return null;                    // :140-142 defer to credentials
    }
    pstsecClient = PSTSecServiceClient.getInstance();   // :146
    if (pstsecClient == null) {          // :148 the fail-open branch
        return this.createAuthenticationToken("admin", "admin",
                 EmUserInfo.Role.ADMIN, request);
    }

The token registers new SimpleGrantedAuthority("ROLE_ADMIN") and wraps it in 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 the authenticated() matcher that CTPSecurityWebConfig
line 283 demands for /database is satisfied by it. The condition guarding all of
this is the absence of the external security-provider client, which in the
untouched vendor image, with no setup step performed, does not exist. Measured
with its control in the same run: an anonymous GET of the users/current resource
under the /em/api/v3 context returned 200 with the 39-byte body
{"id":1,"login":"admin","role":"ADMIN"}, and the identical request carrying
Authorization: Bearer garbage returned 401 with an empty body. The control shows
the fail-open branch is specific to requests presenting no credential at all, so
the behaviour is not "authentication is off" but "an absent credential is more
trusted than a wrong one". CSRF is separately not an obstacle, because
CsrfSecurityRequestMatcher is a 28-line class whose anchored exemption alternation
at line 19 includes /database, with GET, HEAD, TRACE and OPTIONS exempt by method
at line 22.

The write primitive and the execution sink. Reconstituting the stream's first
object, a java.util.HashSet, forces a hashCode() on its single element, a
TiedMapEntry wrapping a LazyMap whose transformer factory is a
ConstantTransformer holding attacker bytes and whose decorated map is an
aspectjweaver SimpleCache$StoreableCachingMap whose folder field is an absolute
path:

    readObject() -> HashSet.readObject -> HashMap.putVal -> key.hashCode()
      -> commons-collections4 TiedMapEntry.hashCode() -> getValue()
      -> commons-collections4 LazyMap.get(key)  // transform the key
         -> map.put(key, value)  // invokeinterface, virtual dispatch
      -> aspectjweaver SimpleCache$StoreableCachingMap.put(key, byte[])
      -> writeToPath(folder + File.separator + key, bytes)

That class was read at the bytecode level to establish four facts, all of which
are what make the write arbitrary. The folder field 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,
which assigns whatever the stream carries, and both writeToPath and the store
routine read it with an explicit getfield so the compiler has not inlined it.
writeToPath performs getfield folder, builds a StringBuilder, concatenates a
separator and the key, and stores the result, which is the entire function: there
is no forced extension appended to the key, 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 work, and
because concatenation with an empty folder yields a leading double separator,
which POSIX treats as a valid absolute path, an absolute destination is reachable
too. And it does not fire on deserialization alone, which is the subtle part:
HashMap.readObject reconstitutes entries through putVal, package-private, final
and not overridden, so it bypasses the overridden put that contains the write,
which is why the TiedMapEntry to LazyMap to Map.put prefix is needed to force a
virtual dispatch. The only content constraint found anywhere is that the bytes
must not be exactly the four-byte sentinel IDEM. In the published carrier one
field value is set deliberately, storingTimer to Integer.MAX_VALUE, which
suppresses the index file the caching map would otherwise write alongside the
payload.

A write is only a write until something executes what was written, and here that
something 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. The chosen
landing directory, webapps/em/images/, satisfies three conditions at once: it is
inside the deployed web application root the JSP mapping covers, it is writable by
uid 1000 as drwxrwxr-x parasoft root, and it is exempt from the licensing filter
by the contains("/images/") clause at line 166, so fetching the written file needs
no bypass of its own. A second and independent execution sink was also detonated
and is recorded here so that 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 coming from an init-param in
webapps/em/WEB-INF/web.xml at lines 206-207, with FreeMarker 2.3.34 reached
through jersey-mvc-freemarker 3.1.12. Because freemarker.template.utility.Execute
is on no restriction list, the new() built-in is usable and a template write
becomes command execution without JSP compilation. Measured: a template performing
7*119 returned 200 with a 4-byte body containing 833, and one invoking the Execute
built-in with id returned 200 with a 61-byte body containing the full identity
string verbatim.

The chain, measured. Hop 1 is one anonymous multipart POST with no cookie, no
Authorization header and no CSRF token:

    POST /em/database/restore;welcome HTTP/1.1
    Content-Type: multipart/form-data; boundary=<generated>
      part backup = zip { version.txt: "93", database.ser: <carrier> }
      part json   = {"typeName":"HyperSQL","mode":"HOST_PORT",
                     "host":"127.0.0.1","port":1,"db":"poc"}
    <- HTTP/1.1 500, size=119
       {"message":"An error occurred while applying changes to the database
        configuration. Please check the log for details."}

The recorded artefacts of that run were a 2,076-byte database.ser with md5
ff9e842a127e9b54b97b6b0ad0a05371 and a 1,378-byte upload archive with md5
00fa45ebd53d30774de3c52bbe3b6f41 carrying a 1,372-byte page; a fresh run of the
published generator with its own nonce and command produces different sizes and
digests, which is expected and is why they are quoted per-run rather than as
constants. The json part is not part of the chain: nothing about the
vulnerability depends on the database being unreachable, only on deserialization
happening first, so the loopback address on port 1 was a convenience making the
failure immediate and predictable. The 500 carries no information about success,
since it is what a fired gadget produces; the success signature came from the
product's own log, 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, which proves readObject() returned an object
rather than throwing during deserialization, and which is distinguishable from a
malformed or unloaded payload that instead produces a ClassNotFoundException
naming whatever class the stream referenced. Counting that signature across runs
gave 2, then 4, then 6 occurrences in the container logs, exactly one increment
per successful hop 1.

Hop 2 is one anonymous GET for the page hop 1 wrote:

    GET /em/images/<leaf>.jsp        (zero credentials)
    <- HTTP/1.1 200, size=538
       RCE_OK <12-hex nonce>
       JSP_EVAL=42
       FILE=/usr/local/parasoft/ctp/webapps/em/images/<leaf>.jsp
       SIZE_SELF=1372
       uid=1000(parasoft) gid=1000(parasoft) groups=1000(parasoft)
       CMD_OUT_BEGIN
       uid=1000(parasoft) gid=1000(parasoft) groups=1000(parasoft)
       -rw-r----- 1 parasoft parasoft 1372 <timestamp elided> ...
       Uid:    1000    1000    1000    1000
       root:x:0:0:root:/root:/bin/bash
       bin:x:1:1:bin:/bin:/sbin/nologin
       CMD_OUT_END

Four independent properties of that body make it attributable to code execution
rather than to anything else. JSP_EVAL=42 is the product's own evaluation of the
expression 6*7 at request time, so this is evaluation and not a source echo,
where the literal expression text would have appeared instead of the value.
SIZE_SELF=1372 is the page reading its own length from disk and agreeing with the
length of the payload the client built, establishing byte-exact content control.
The ls line shows the file owned by the product's own service account, and the
/proc/1/status line shows the product process's uid quadruple read from inside the
container rather than through a container runtime, which would have reported root
regardless of what the process actually runs as. The two passwd lines were read
by the product process, establishing arbitrary read at that identity.

Six negative controls were recorded, each removing a specific alternative
explanation: hop 1 without the semicolon suffix returned 302 with Location:
/em/license, excluding reachability of the restore endpoint on an unlicensed
instance without the bypass; hop 1 with the backup part omitted returned 400 with
59 bytes, excluding the possibility that any POST to the mapping produces the same
result; 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, with the run marker appearing once in the executed body and
zero times in this one; 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;
and the /em/tdm/api/v2/servers resource without the bypass returned 200 with the
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 own 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, and a filesystem search for the run's marker leaf
name under the product tree and under /tmp returned zero results. The container was
destroyed afterwards and its published ports were confirmed free, leaving only a
read-only reference instance running.

A third hop was recorded as a finding in its own right because it has a defensive
consequence that is easy to get wrong. After the page deleted itself and both
Jasper compilation artefacts, the same URL was fetched again on purpose and
returned 200 with a 536-byte 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 was still loaded in the JVM and still
serving requests. An operator who finds a dropped page, deletes it, deletes the
compiled class underneath work/, and sees the file gone has not removed the web
shell; only a context reload or a JVM and container restart does. The published
proof of concept performs that refetch and reports what it finds rather than
claiming to have cleaned the target.

Counting honestly: two end-to-end detonations of the two-hop chain are recorded as
successful. The first proved the chain but left its Jasper compilation artefacts on
disk; the second, with completed cleanup logic, produced the artefact set quoted
above plus the hop-3 forensic finding. Two intermediate iterations did not fire at
all and both failed inside the client script rather than against the product, one
on a malformed URL that raised before a packet was sent and one on a doubled slash
that the container answered with 404; those are recorded as client bugs, not as
evidence about the product, and they are worth recording because they are exactly
the kind of failure that produces false negatives against a real target. The
licensing-bypass leg was measured many times over HTTP and HTTPS on read-only
endpoints and the FreeMarker landing was detonated separately; neither is counted
in the figure of two, which covers only full deserialization-to-execution runs.

Impact. Arbitrary operating-system command execution inside the product process,
from two ordinary HTTP requests against a platform whose documented entry point
for the same function is an administrator menu behind authentication and a valid
licence. The landing identity is uid=1000(parasoft) gid=1000(parasoft)
groups=1000(parasoft), the product's own unprivileged service account, confirmed
three ways: by id executed inside the dropped page, by the file ownership and mode
of the written page, and by the product process's own /proc/1/status reporting
Uid: 1000 1000 1000 1000. It is not root, no claim of root is made, and no
privilege escalation was attempted, enumerated, tested or asserted: no
kernel-exploitation, container-escape or setuid path was touched. What the achieved
identity controls is substantial without being privileged: the product tree under
/usr/local/parasoft/ctp including the deployed em application, 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 the bypassed configuration
endpoint disclosed on its own, and through them every environment definition,
system definition, virtual-service definition and test-data record the platform
manages, plus whatever credentials or connection strings it stores on behalf of the
systems it virtualises; anything else readable by uid 1000 on the host filesystem,
measured concretely by reading /etc/passwd; and the product's own licence and
security state, since arbitrary in-process code can rewrite configuration files,
which is why the unlicensed and unconfigured conditions an operator might rely on
as compensating controls are not durable ones. The arbitrary-path arbitrary-content
write is a primitive in its own right before any command is run, with the folder
field, the map key and the content array all inside the serialized stream and the
only content restriction being the four-byte IDEM sentinel. Persistence that
survives naive cleanup was measured in hop 3. 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; that was not performed and is flagged as an unexercised consequence of a
primitive that was proven, not as a measurement. Weak detection is a property of
the design: 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.

Scoring. One score is published, 9.8 Critical,
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. It was recomputed from the vector
with the CVSS 3.1 base formula rather than inherited, so any reader can reproduce
it: 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 is the honest
classification of the component, which serves the chain over ordinary TCP HTTP and
HTTPS from listeners 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 was the testbed's containment rather than a
product restriction. AC:L because there is no timing window to win, this being the
steady factory state rather than a first-run race, no host-specific information to
gather, no memory-layout condition, no database reachability requirement, no
outbound callback and no cryptographic work. PR:N because no credential belonging
to anybody is supplied or consumed; the authenticated subject is manufactured by
the product itself. UI:N because no administrator action, browser session, induced
click or human upload step exists anywhere in the chain. The impact metrics do not
depend on the identity being privileged and do not rise if it is.

Scope Changed was considered and rejected, and the rejection lowers the published
number, which is worth stating so the call cannot be read as a scoring maneuver.
The argument for Changed looks available: the defect lives in a licensing filter
and a backup-routine class while the impact lands on a different subsystem, the
web-application directory Jasper compiles, and then on the operating system. It is
not two authorities. 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; executing a page that sits inside its own web
application root is the documented intended behaviour of the container
configuration the product ships, so the transition from write to execution crosses
no boundary the product defines; and 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. The Scope-Changed 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. It is rejected and is not published as this finding's score,
so the reading rejected here is the higher one.

Three labelled conditional readings accompany the published score and none replaces
it. Reading A, where the external security provider is configured and the attacker
holds 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, from Exploitability 8.22 x 0.85 x 0.77 x 0.62 x 0.85 = 2.835255 and Impact
5.873119, base 8.708373 rounded up; in that state the fail-open branch does not
apply and both hops need a genuine subject, while the security matcher for the
restore path is authenticated() rather than hasRole("ADMIN"), so low privileges
suffice, and the licensing gate is still bypassed and the sink still unfiltered. It
is a deployment state that was not detonated, and it is the reading an operator of
a configured deployment should work from because it depends on no unverified
capability. Reading B, where the provider is configured and the subject comes from
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. That reading is a static and structural inference
and 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 valid cookie offline, and while forgery was
exercised in the unconfigured state, where it buys nothing, 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 in order to test it was deliberately
abandoned, because installing one would have manufactured the condition being
measured. It 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; the published PR:N rests
on the measured fail-open behaviour of the untouched image and not on any forgery.
Reading C, where the listeners are 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, from Exploitability 8.22
x 0.62 x 0.77 x 0.85 x 0.85 = 2.835255 and Impact 5.873119, base 8.708373; it is
conditional on a deployment change that was not tested and it is downward rather
than upward, existing so that an operator who hardens the network position can see
that the hardening is worth one point of severity and does not fix everything
else.

Verification boundary, stated plainly. 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:
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 since the chain needs 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 accepting connections on HTTP 8080 and
HTTPS 8443 from outside itself and the product shipping no reverse proxy, WAF or
separate management listener, rather than demonstrated between two hosts. Only the
factory configuration state was detonated; a deployment with the external provider
configured was not tested end to end. Only one build was executed, so no
cross-release and no cross-platform claim is published, and no older release, newer
release, Windows variant or differently configured instance was fired at. Two legs
are labelled inference and are 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 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, the absence of JDBC drivers among 238 jars, and the
bytecode-level facts about the caching map's write path.

The affected-version claim is deliberately build-scoped. The artefact self-identifies
as 10.7.5-SNAPSHOT and was obtained through the vendor's mutable
parasoft/ctp:latest container tag, image id prefix f99f7210fc67, from a saved copy
of 997,805,056 bytes whose md5 is 47946c7d1e7d99a00d6c90f4ae675f90. In the Maven
convention the product's own build system uses, -SNAPSHOT marks a build taken from
an in-progress development line rather than an immutable released artefact, and a
mutable tag cannot be mapped onto a released version range with the confidence a
public affected-version list requires. The claim is therefore that this build is
affected and that any other build shipping the same AbstractDatabaseBackup and
EMLicenseFilter source carries the same construction, not that a range of releases
is affected. The vendor's public release naming for this train is CTP 2026.1.x, and
that vendor's own published release notes for the train enumerate security fixes
exclusively as resolving a third-party library update, with no first-party security
entry, which is documentation-level evidence that neither root cause is addressed
there and not a measured claim. The build's image metadata also carries a
Build-Timestamp value and a buildId whose stamp is derived from that timestamp;
both embed a date, and since this advisory series carries no dates they are
described rather than printed. The sibling dtp distribution, whose first-party jar
set does not include com.parasoft.ctp.core, is not claimed.

What was exhausted or ruled out before the post-authentication result was accepted,
so that readers can see what does not need looking for again. Two classical gadget
families are dead on this target, established from the product's own process rather
than from a guess: its JVM's --add-opens list, read from its own command line,
covers 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, and 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,
before being stopped by the module system because java.xml does not export
com.sun.org.apache.xalan.internal.xsltc.trax; 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, and that was 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 talks to
HSQLDB in-process, which is not the same surface. The two bundled libraries that do
supply the working carrier are aspectjweaver 1.9.25.1 and commons-collections4
4.5.0, and neither is blamed: all four classes used live in the unnamed module with
java.base/java.util already open, and the functor chains usually cited against
commons-collections4 are not constructible from a stream at all in 4.5.0 because
InvokerTransformer no longer declares Serializable, an early hypothesis that it was
guarded by an unsafe-serialization check having been disproved by reading the
functor utility class, which contains no such check. Recording those dead ends is
not padding; it is why the published chain has the shape it does, since on a modern
JDK with default module boundaries the usable gadget set is whatever lives entirely
in the unnamed module. 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. 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
and the manager underneath using File.createTempFile, so the JDK's own
temporary-file machinery rejects a suffix containing a separator; the product even
echoed 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.

Several additional first-party defects were measured on the same product and are
reported to the vendor separately, none folded into the published vector, and they
are listed so the scope of this advisory is unambiguous. The generic licence-gate
bypass on its own, exposing the whole /em/database subtree and the /em/tdm/api/v
tree anonymously, is an authorisation bypass and not 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, are privilege
granting and not execution. The offline-forgeable session cookie with its literal
signing key and keyless signature scheme is CWE-321 and CWE-347, demonstrated only
where it is redundant and not verified in the configuration where it would matter.
Anonymous whole-database download and anonymous backup creation are available
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/, and the v2 variant is an undocumented twin of v3, the shipped OpenAPI
description covering only v3, which is why it was found by reading the routing
table rather than the documentation. Anonymous arbitrary file read and anonymous
arbitrary file delete are available 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 an /etc/passwd read
returning the first line verbatim in a 47-byte body and with a DELETE that really
removed a prepared file, which is CWE-22 from the same unvalidated-concatenation
family and not an execution. Anonymous creation of database files in an arbitrary
directory follows 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 were inventoried in the shipped artefacts, seventeen participating in
security decisions: a shipped TLS keystore whose store password is the industry
default changeit and appears in plaintext in server.xml at lines 110-112, meaning
every installation that did not replace the keystore shares one TLS key pair and a
passive observer holding the image can decrypt captured sessions, 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. Also recorded are 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: 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 upon as a path guard. The structural fact
behind all of it is that the em application has four independent routing stacks
behind one set of hand-written gates, / to the Spring DispatcherServlet, /api/star
to a Jersey RestServletContainer, /tdm/api/star to TDMApiServletContainer and
/services/star to CXF, with ten star-path servlet filters in conf/web.xml, so
enforcement is not centralised; 97 occurrences of checkLicensing() appear across 17
files against 31 routed Jersey resource classes, meaning 54.8 percent of routed
resource classes perform any licensing check at all.

Detection notes, including the one that otherwise produces false negatives by
design. Product log lines pairing the restore frame with a cast exception naming a
JDK collection class where backup metadata was expected are the direct signature of
a fired carrier and increment once per attempt. Any request target reaching this
application that contains a semicolon is anomalous on its own, since the product's
own UI does not need path parameters on the administrative endpoints observed, and
its appearance on /em/database paths or the /em/tdm/api/v tree deserves attention. A
non-image file under the deployed web application's images/ directory, and any
compiled jsp source or class under the Jasper work directory whose stem does not
correspond to a page the product ships, are both compromise indicators rather than
noise. Anonymous POST activity against the restore or backup endpoints from a client
that never authenticated is detectable, as are anonymous 200 responses from the
currentConfig endpoint on an instance that should be reporting unlicensed. The
caution is this: HTTP 200 responses whose JSON body carries a status of 403
alongside a licence message are refusals that the product's own dead status write
makes invisible to status-based monitoring, so a rule keyed on HTTP status for
licence failures will not fire at all, and detecting hop 1 cannot come from its
status code either, because 500 is what a successful payload produces. The usable
signals are a file appearing where none belongs and the cast-exception log line.

Mitigation summary, ordered by root cause. Install an ObjectInputFilter allow-list
before the first read in AbstractDatabaseBackup, permitting only the backup metadata
classes the format needs, and apply it at the two sibling readObject() sites too,
with a process-wide jdk.serialFilter as a backstop rather than the only layer.
Reorder the restore routine so no untrusted byte is deserialised before validation,
which alone would have stopped this chain with no filter at all since the supplied
configuration is designed to fail. Retire native serialization for backup files in
favour of a schema-bearing format so no field in the file can name a class, and
bind backups to the server that produced them by signing the archive or recording a
server-side digest and refusing unknown ones. Decide licence and authorisation on
the mapped path rather than the raw URI, stripping path parameters, replacing every
unanchored suffix exemption with a separator-anchored or exact match, and making
the JSON-Accept branch accumulate exemptions instead of reassigning the set. Fix the
dead status write so licence refusals carry the status they describe. Fail closed in
the remember-me service, so an anonymous request never receives an authenticated
admin token because a configuration lookup returned null, and emit a startup warning
when administrative endpoints are served without an identity source; independently,
make the authentication provider verify the password it is handed. Replace the
session signing scheme with an HMAC over a per-deployment random secret held outside
the image. Stop compiling pages at runtime in production, or serve static image
content from outside any JSP-mapped web application root. Confine the temp-file
handler by canonicalising and base-name-extracting its query parameter and rejecting
separators. Centralise licence enforcement in one interceptor with an explicit
reviewed opt-out list. Remove shipped secrets, generating the TLS keystore per
instance and deriving encryption keys from a deployment secret rather than compiling
them in. Document that deleting a dropped page and its compiled artefacts does not
unload a servlet, so a context reload or restart is required. And add a regression
test asserting that a bare path and the same path carrying a semicolon suffix
produce the same licensing decision, that deserialising a HashSet where backup
metadata is expected fails with a filtered-class rejection before any database
connection attempt, and that a session cookie signed with a key read out of the
shipped class file does not authenticate. Until a patch exists, operators should
reject any request target containing a semicolon at whatever proxy or gateway fronts
the platform, which closes the semicolon-welcome, semicolon-about and
semicolon-notlicensed exemptions at once and needs regression testing for legitimate
matrix-parameter use; restrict the administrative paths to named management
addresses behind an authenticating reverse proxy; configure the external security
provider, with the caveat that this deployment state was not detonated end to end so
it should be treated as reducing exposure rather than as a proven fix; monitor the
images directory and the Jasper work directory as described above; replace the
shipped TLS keystore and store password on every installation on the assumption that
the shipped key pair is public to anyone holding the image; and when remediating a
confirmed drop, reload the context or restart the instance before hunting for
lateral movement from the platform's stored credentials and connection strings.

The published proof of concept is Python 3 using only the standard library, with no
third-party dependency and no hardcoded target. 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: the
landing folder, defaulting to the product's own deployed images directory; the leaf
name to write, defaulting to a per-run nonce-based page name; the JSON connection
parameters; the content of the version.txt member, defaulting to 93; the
application context prefix, defaulting to /em; the Catalina base and Jasper work
directory; the restore path, defaulting to the database restore mapping with the
semicolon suffix; the hop-2 images path; a per-request timeout and hop-2 retry count
with delay; a --keep flag so the dropped page does not delete itself; a --no-hop3
flag to skip the post-delete forensic refetch; an -k or --insecure flag for a
self-signed certificate on the https scheme; a --dump-payload option writing the
generated carrier to a path; and a --selftest mode that sends nothing at all,
rebuilding the carrier skeleton from the archived original values and asserting byte
identity with it, then rebuilding with new values and asserting that the new folder,
leaf and content appear verbatim. The carrier itself is a 1,178-byte stream
containing 19 handles, all consumed, whose md5 in its archived form is
6099d3f5fd36e28d8934c2e4e6c95da6; the generator treats it as an immutable skeleton and
rewrites only three marked regions, the leaf name, the content byte array and the
folder path, leaving every class descriptor, serialVersionUID, field order and handle
back-reference byte-for-byte untouched. That self-test is what makes the claim that
the generator produces the stream proven against the product checkable by a reader
with no target to shoot at. The script also 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, a
pre-flight validator that exists because two of the four iterations of the client
failed for reasons unrelated to the target: one produced a URL with no slash between
context and path and raised a nonnumeric-port error before sending, and one produced
a doubled slash that the container answered with 404, its unit test asserting only
that the URL was parseable, which it was, and therefore guarding the wrong property.

Parasoft was notified unconditionally through the vendor-wide technical-support
mailbox that the vendor's own support page publishes for all product support, which
is the channel judged appropriate for a security report and the only one used. The
vendor publishes no RFC 9116 security.txt: the request returns HTTP 404 over https
for the well-known path and for the bare file on the vendor's global www host, and
the apex host answers every path with a 301 to that www home page, so it serves no
such file either and a request for its well-known path resolves to the marketing home
page rather than to a statement. There is no security page, no
vulnerability-disclosure policy and no responsible-disclosure statement on the
vendor's global site: the security, security.txt, vulnerability-reporting,
responsible-disclosure, report-a-vulnerability, trust-center and legal paths all
return HTTP 404 on the www host, and a sitemap of 751 URLs contains no
security, disclosure or vulnerability-reporting page, only product and marketing
pages about security testing. The vendor's terms link resolves to a licence PDF with
no reporting statement. From the researchers' network position the vendor's apex and
www hosts issue a geo-redirect to a regional partner domain before serving content,
so the global www host was reached through an egress route that does not carry that
redirect in order to test those paths, and every 404 reported is for the vendor's
global site rather than the partner site. A separate
corporate mailbox published in the site-wide footer for general inquiries, and a
third mailbox the support page scopes specifically to the Community Edition of the
vendor's virtualisation product, were both available and neither was used. Nothing
was submitted to any pre-publication intake form, because none was found, and no
individual was addressed by name. This disclosure is unconditional: the research is
already public, no permission was asked for, no embargo was requested, and nothing is
withheld pending a reply.

Full technical analysis and the reproducible proof of concept, published
unconditionally:
  https://0day-rubbish.com/blog/parasoft-ctp-unfiltered-deserialization-license-gate-fail-open-jsp-rce

Project archive, ongoing disclosure series:
  https://github.com/Exploit-Garbage/0day-Rubbish

--
0day Rubbish Research Team
disclosure () 0day-rubbish com
https://0day-rubbish.com
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/


Current thread:

  • [0day-rubbish] Parasoft Continuous Testing Platform 10.7.5-SNAPSHOT anonymous deserialization in the database restore path to JSP command execution as uid=1000 (9.8) disclosure via Fulldisclosure (Oct 20)

文章来源: https://seclists.org/fulldisclosure/2026/Oct/31
如有侵权请联系:admin#unsafe.sh