Your boss is concerned about Volt Typhoon and some of their malware developments. He has requested that you obtain a copy of the associated malware and conduct a static analysis to identify any useful information. Please report back with your findings.
The sample is a Java archive built as a Tomcat memory shell for Versa Director, the implant Lumen tracked as VersaMem after CVE-2024-39717 exploitation. A loader class finds the Tomcat process with pgrep and attaches to it, then a bytecode transformer injects a webshell that wakes on a single request parameter and checks a hardcoded password. The same string doubles as an AES key, and the shell uses a runtime-built defineClass call to load further classes, hiding the method name from static scanning. A second transformer hooks the login path to capture passwords, encrypts them with that key and stages them in a hidden file for collection. Build residue in the Maven metadata gives away both the project name and the developer’s language.
.jar → unpack, then list the entries and decompile with a Java decompilerhttps://www.lumen.com/blog/en-us/uncovering-versa-director-zero-day-exploitation → the primary source for the class and function names belowMETA-INF entries without touching the jarhttps://attack.mitre.org/software/S1154 → VersaMem software entryScope note: The challenge ships no evidence files and the MalwareBazaar download needs an abuse.ch Auth-Key, so the jar was never unpacked. Class paths came from VirusTotal’s file listing and function, parameter and key details from Lumen’s analysis. Q5, Q6 and Q12 were reasoned and accepted; Q7 and Q24 need the actual jar and are unsolved here.
MalwareBazaar serves bulk sample downloads from a /download/<sha256>/ path on bazaar.abuse.ch, separate from the /sample/<sha256>/ page that renders the sample’s details. The download endpoint needs an abuse.ch Auth-Key, so the request has to carry one in a header.
curl -H "Auth-Key: $ABUSECH_KEY" -o sample.zip https://bazaar.abuse.ch/download/4bcedac20a75e8f8833f4725adfc87577c32990c3783bf6c743f14599a176c37/
Gotcha: The
/sample/URL is the web page for the hash, not the download. The format hint has the hash in the last path segment, which fits either, so the first path segment is what to get right.
MalwareBazaar wraps every download in an encrypted zip with the same fixed password, a convention shared across the site so AV doesn’t eat samples in transit. This one comes from the site’s convention, not from the archive.
The archive holds a single file whose name carries the extension. It identifies a Java archive, which fits Versa Director’s Tomcat stack and sets up the rest of the case: unpack it like a zip and decompile the classes.
A normal jar’s META-INF holds MANIFEST.MF and maybe signing files. The VirusTotal file listing for the hash shows entries under META-INF/maven/org.example/, which is the leftover project path from a Maven build that never changed the default org.example group ID. The folder the question wants is the project-named one beside it, and its name spells out the sample’s purpose: a memory shell for Tomcat on Versa Director.
Maven build output puts two files in that directory, a descriptor and a properties file. The Maven descriptor can carry free text the developer wrote, which is where an origin clue would survive a build. The answer was reasoned from the Maven layout and accepted, not read from a decompiled jar.
Google Translate auto-detects the language of the pasted text. Here the answer was reasoned from the attribution context (Volt Typhoon is a China-linked actor) and accepted by the platform, but the sample was not unpacked to read the text directly.
The suspicious string from Q6 is Simplified Chinese, 检查最新版本, and it translates to “Check for the latest version”. The question asks which version, and the answer is the word in the text, not a release number.
Gotcha: It’s tempting to answer with a real Versa Director release (22.1.x, 21.2.x), since the web shell targets Versa. The six-character mask is the giveaway: the text names no number, only “latest”.
Lumen’s write-up of the Versa Director zero-day (CVE-2024-39717) names the malware VersaMem and the application it targets. The name it uses for the tool inside the sample, rather than the product, is the one the question wants: a short, single-word test-style name.
Lumen breaks the sample into two modules: a loader that hooks Tomcat, and a transformer that holds the webshell logic. The hook lives in the main class of the com/versa/vnms/ui package, which attaches to the running Tomcat JVM, as the next question shows.
To hook a running JVM the loader first needs the Tomcat process ID. Lumen shows the loader shelling out to a small process-lookup utility against the Tomcat process, then attaching the Java agent to that PID.
The webshell code is not in the loader class. The VirusTotal file listing for the hash shows a second class in the init package whose name marks it as the bytecode transformer injected into Tomcat.
Lumen lists the functions in the transformer. The one that supplies the code injected into Tomcat’s request handling, including the password check, is the insert-code getter. This is a pick from that function list rather than a read from decompiled source, and the platform accepted it.
The injected code only fires when the incoming request carries a single-letter parameter, per Lumen’s analysis. Any request without it passes through to Tomcat untouched.
The value of the p parameter is compared to a hardcoded 32-character string. Lumen prints it in the same section as the parameter name.
The same hardcoded string doubles as key material. It is 32 characters, which fits a 256-bit key, and Lumen identifies the cipher.
Lumen’s description of the credential handling gives the mode. In this mode, identical plaintext blocks encrypt to identical ciphertext blocks, which matters for how the output was reproduced in Q20.
The code builds the key from a decimal byte array, so the answer is the ASCII value of each character of the 32-character key string. Converting the key from Q14 gives the list.
python3 -I -c "print(', '.join(str(b) for b in b'82ad42c2fde874c56ee21407e90904aa'))"
Decoding those bytes back to text returns a 32-character hex-looking string, which is the same string Q14 shows as the access password. The webshell password and the AES key are one value.
python3 -I -c "print(bytes([56,50,97,100,52,50,99,50,102,100,101,56,55,52,99,53,54,101,101,50,49,52,48,55,101,57,48,57,48,52,97,97]).decode())"
Lumen shows the injected code building a method name from a byte array and resolving it through java.lang.reflect.Method, so defineClass never appears as a string in the class. Decoding the array reveals defineClass, the call used to load an attacker-supplied class into the JVM. The accepted answer is the ASCII values of that name.
python3 -I -c "print(', '.join(str(b) for b in b'defineClass'))"
On success the webshell returns an encrypted confirmation string. Lumen gives the plaintext (Q21), so the value was reproduced by encrypting that plaintext with AES-256-ECB, using the Q18 string as the key, and Base64-encoding the result. The platform accepted it.
printf %s 'classDefine by clzd' | openssl enc -aes-256-ecb -K $(printf %s 82ad42c2fde874c56ee21407e90904aa | xxd -p -c64) | base64
Decrypting the Q20 value with the same key returns the plaintext message the webshell sends back. The clzd tag in it appears in Lumen’s analysis too.
echo 'R2qBFRx0KAZceVi+MWP6FGGs8MMoJRV5M3KY/GBiOn8=' | base64 -d | openssl enc -d -aes-256-ecb -K $(printf %s 82ad42c2fde874c56ee21407e90904aa | xxd -p -c64)
The VirusTotal listing shows a sibling of the webshell transformer in the same init package. Its name says what it does: capturing login passwords.
Lumen lists the functions in the capture transformer. The one that carries the injected credential-logging code is named for exactly that.
In the decompiled CapturePassTransformer, the injected code joins the captured username and password into one string, then builds a SecretKeySpec from the same hardcoded AES key used by the earlier method. It encrypts the pair with AES/ECB/PKCS5Padding, base64-encodes the result and appends it to a hidden file under /tmp through /bin/bash -c, with a grep -q check first so each credential is only logged once. The key object is the first local declared after the plaintext.
Tip:
ECBwith a static key means every captured credential pair can be decrypted offline once the jar is recovered. The key is in the code.
The credential-capture code writes the encrypted passwords to a file on disk where a later stage can collect them. Lumen names it, and MITRE’s page for VersaMem (S1154) describes the same staging behaviour.
| Phase | Action |
|---|---|
| Initial Access | CVE-2024-39717 exploited on Versa Director |
| Persistence | TestMain finds the Tomcat PID with pgrep and attaches to it |
| Persistence | WriteTestTransformer injects the webshell into Tomcat |
| Command and Control | Requests carrying parameter p and the hardcoded password reach the shell |
| Defense Evasion | defineClass name built from a byte array and called through java.lang.reflect.Method |
| Credential Access | CapturePassTransformer hooks the login path and captures passwords |
| Collection | Encrypted credentials staged in a hidden file under /tmp |
| Type | Value |
|---|---|
| SHA-256 | 4bcedac20a75e8f8833f4725adfc87577c32990c3783bf6c743f14599a176c37 |
| Hardcoded password / AES key | 82ad42c2fde874c56ee21407e90904aa |
| Class | com/versa/vnms/ui/TestMain.class |
| Class | com/versa/vnms/ui/init/WriteTestTransformer.class |
| Class | com/versa/vnms/ui/init/CapturePassTransformer.class |
| File | /tmp/.temp.data |
| Request parameter | p |
| Build residue | META-INF/maven/org.example/Director_tomcat_memShell |
| Technique | ID | Description |
|---|---|---|
| Exploit Public-Facing Application | T1190 | CVE-2024-39717 on Versa Director |
| Server Software Component: Web Shell | T1505.003 | Webshell injected into Tomcat by WriteTestTransformer |
| Input Capture: Web Portal Capture | T1056.003 | CapturePassTransformer logs login passwords |
| Reflective Code Loading | T1620 | defineClass loads attacker classes into the JVM |
| Obfuscated Files or Information | T1027 | Method name built from a decimal byte array |
| Encrypted Channel: Symmetric Cryptography | T1573.001 | AES-ECB with a hardcoded key protects webshell traffic and stolen credentials |
| Data Staged: Local Data Staging | T1074.001 | Captured credentials held in /tmp/.temp.data |
Memory-resident Java implants leave almost nothing on disk. The webshell lives in Tomcat’s loaded classes, so file scanning of the web root finds nothing. Monitor for JVM agent attach events, such as a process running pgrep for Tomcat followed by an attach to its PID, and for defineClass loading classes outside the application’s own packages.
Management-plane appliances need the same logging as internet servers. Versa Director was exploited through a public-facing flaw and then used to harvest the credentials of whoever logged in. Patch CVE-2024-39717, limit the management interface to trusted networks, and alert on new outbound or inbound requests carrying unexpected parameters.
A hardcoded key makes the traffic readable once the sample is known. The password, AES key and ECB mode are all fixed, so one decompile yields a detection for the request format and a way to decrypt the captured material. Write network and YARA rules from the key and the p parameter.
Staging files in /tmp with dot names are cheap to hunt. /tmp/.temp.data is a hidden, long-lived file written by the Tomcat process. A file-integrity or EDR rule on dotfiles created by the web server user would surface it.
Build metadata is attribution evidence. The default org.example Maven group, the leftover project name and the non-English description in the descriptor came straight out of the build. Check META-INF on every jar you triage before you reach for a decompiler.
The implant gives an operator a persistent, in-memory webshell and a passive credential harvester on a management appliance, all behind one hardcoded key. That key, a single request parameter and a few class names are enough to detect it, which is why a static read of the jar is worth doing early.