zip -j trust_me_bro.jar - How One Command Cracks Elasticsearch Enterprise
I got bored one day and ended up bypassing Elasticsearch's entire Enterprise licensing with a single file swap.
In April, in a post about Elasticsearch licensing, I described ELK as a great SIEM, “unlike QRadar and shit cough cough”. The cough was this research.
The undefined behaviour, defined: licensed for anything, permanent, expiring never.
999,999,999 EPS. 999,999,999 FPS. Every paid add-on. No expiry before December 2099. Three days of work, and the hardest cryptography took 35 seconds.
Disclosure: I reported this research to IBM in March 2026. Six months later there has been no update, no CVE and no advisory. The keys documented below ship inside every QRadar 7.5 installation, so withholding them protects no one who runs the product. This post is the full disclosure.
Two builds were tested, and both carried byte-identical keys:
| Primary ISO | Secondary ISO | |
|---|---|---|
| File | IBM_QRADAR_CORE_SOFTWARE_V7.5.0_R.iso |
qradar.iso |
| Version | 7.5.0 (2021.6.4) | 7.5.0 (2021.6.14) |
| Built | 29 Nov 2022 | 17 Oct 2025 |
| MD5 | 0dfaa7ccb4c597ddbae19da9cb56c8f1 |
6bad644994d11efcdcf1e9bb7a219acc |
Three years between builds. Same RSA modulus. Same AES key. Same broken HMAC.
| Layer | Purpose | What shipped |
|---|---|---|
| RSA signature | Prove IBM issued the activation key | A 96-bit modulus, factorable on a laptop in 35 seconds |
| AES-128 encryption | Keep licence files unreadable | The key hardcoded in bytecode, identical on every install |
| HMAC-SHA256 | Detect tampering after deployment | A serialisation bug, plus the AES key reused as the HMAC key |
Everything below was done against consoles I own, on serials I own, in my own AWS lab, using CFR 0.152 for decompilation and Python 3 for the cryptographic work.
Activation keys are the strings typed into the UI when a licence is uploaded, formatted XXXXX-XXXXX-XXXXX-XXXXX. QRadar verifies them with an RSA public key embedded in ActivationKey.class inside q1labs_core.jar.
Extraction is two commands and a decompiler: unpack the JAR from the ISO, run the class through CFR. The public key appears as a Base64 DER blob. Decoding it gives the parameters:
The public key extracted from ActivationKey.class: a 96-bit modulus with the standard exponent.
The modulus is 41906015560064597191950651353: 96 bits, with the standard exponent e = 65537. NIST SP 800-57 sets the minimum for RSA at 2048 bits, which was settled practice a decade before the older of these builds shipped in 2022.
| Key size | Status | Factoring effort |
|---|---|---|
| 2048-bit | Current standard | Not feasible |
| 1024-bit | Deprecated since 2010 | Nation-state budget |
| 512-bit | Broken in 1999 | Hours |
| 96-bit | QRadar’s choice | 35 seconds, one laptop |
For scale: RSA-512 was considered broken in 1999, and this modulus is smaller. RSA-768, whose factorisation made international news in 2009, took a research team hundreds of machines and more than two years. This key took one laptop 35 seconds. That size shipped in a security product through at least 2025.
Pollard’s rho does it, the same classroom algorithm used to factor toy numbers:
n = 41906015560064597191950651353
def pollard_rho(n):
x, y, d = 2, 2, 1
f = lambda x: (x * x + 1) % n
while d == 1:
x = f(x)
y = f(f(y))
d = gcd(abs(x - y), n)
return d
The idea is a birthday party. Keep applying f(x) = x² + 1 mod n, and the values bounce around apparently at random. But reduce the same sequence modulo one of n’s unknown prime factors p, and the values now live in a much smaller room. Small rooms run out of space fast: after about sqrt(p) steps, two values share a birthday, the same mathematics that lets 23 people in a room make a shared birthday more likely than not, even though guaranteeing one takes 366. Those two values are congruent modulo p and almost certainly not modulo n, so their difference is a multiple of p, and the gcd in the loop hands over the factor.
The x = f(x), y = f(f(y)) pair is Floyd’s cycle detection. Remembering every value visited would need unbounded memory; two runners need none. The tortoise steps once, the hare steps twice, and once both are inside the loop the hare is guaranteed to lap the tortoise and land on the same spot. Their meeting leaks p through the gcd. Draw the path the sequence takes and you get the Greek letter rho, a tail into a loop. The algorithm is named after its own picture.
Why this hammer for this nail: both prime factors sit near 2 x 10^14, so the small room has side roughly sqrt(2 x 10^14) ≈ 1.5 x 10^7. That is the entire cost, a few tens of millions of multiplications, seconds in Python. Trial division would need around 2 x 10^14 divisions, which is years, and the general number field sieve that factors real RSA keys is wasted machinery on a 96-bit number.
The walkers, step by step: press Next until the hare laps the tortoise inside the loop.
Pollard’s rho on a laptop: the modulus splits in 35.06 seconds.
The script returned p = 209421416832289 and q = 200103772545977; their product is n exactly. The private exponent follows by modular inverse: d = 5292523166923284271751576321, verified by checking that e times d leaves remainder 1 modulo phi(n). The signature layer is now defeated. I can produce activation keys that QRadar’s own verification code accepts as IBM’s.
An activation key only validates a licence file, and licence files are encrypted. .key files are Base64 blobs between BEGIN/END REG KEY markers, AES-128-CBC underneath. To read one, I decompiled the class that decrypts them, Q1LegacyLicense.class:
// Q1LegacyLicense.java, decompiled
private static final String AESKey = "K3Y8hlLWpgn0OAv+Sk2sxQ==";
private static final byte[] iv = "a*x1z4pk41xc*j7t".getBytes();
The licence encryption key as a string constant in Q1LegacyLicense.java.
The licence encryption key is a string constant in shipped bytecode, with a static IV beside it. It is not derived per installation, not obfuscated, and not read from any configuration the operator could rotate. Both ISO builds in the table above, compiled three years apart, contain this exact key. Every QRadar that loads this JAR decrypts its licences with a key every customer already holds.
Decrypted, a .key file is plain Java properties:
A decrypted licence: plain Java properties, blunt field names.
The capacity and entitlement fields are direct:
| Field | What it controls | Legitimate | Forged |
|---|---|---|---|
consoleEventLimit |
Max EPS | 5,000 | 999,999,999 |
nonConsoleEventLimit |
Max EPS on managed hosts | 10,000 | 999,999,999 |
flowLimit |
Max flows per second | 200,000 | 999,999,999 |
licenseExpiration |
End date, yyyyMMddHH:mm:ss |
Time-limited | 2099123123:59:59 |
type |
Licence class | TEMPORARY | PERMANENT |
70 |
QVM + QRM + Forensics | Paid add-on | true |
I mapped the numeric feature flags three ways: decrypting licences from different editions and diffing them, reading how LicenseKeyManager consumes each code, and uploading test licences to observe which UI elements appeared. Codes 20 through 50 are baseline SIEM features present everywhere, 90 is advanced analytics, and 70 enables all three separately priced add-ons, QVM, QRM and Forensics. The entire upsell rests on one integer field.
Java’s String.hashCode() of the appliance serial, truncated to 16 bits, plus version and flag bytes, forms an 11-byte payload. The payload is RSA-signed with the recovered private exponent.
Serial: ec21b6df-b57d-753f-7925-1fe901b2ff61
Activation key: BHUSS-N96DY-DETU6-85HWA
The forged licence uploaded but failed deployment. Deployed licences carry a code= field: an HMAC-SHA256 over the licence data, verified afterwards. The design intent is sound. Possession of the AES key alone should not be enough to mint a valid HMAC without knowing the exact serialisation it covers.
Two defects undermine it. First, the HMAC key is the same hardcoded AES key, full key reuse across encryption and integrity. Second, the serialisation is built by CommonUserInfo.serialize():
// CommonUserInfo.java, decompiled
ArrayList<String> keys = new ArrayList<String>(this.data.keySet());
Collections.sort(keys, new Comparator<String>() {
public int compare(String o1, String o2) {
return o1.compareToIgnoreCase(o2);
}
});
// ...and then:
for (String key : this.data.keySet()) { // the sorted list is never used
string.append(key).append("=")
.append(this.data.get(key)).append("\n");
}
CommonUserInfo.serialize(): the sorted list is built, then the unsorted HashMap is iterated.
The method builds a sorted list of the EndUser fields, then iterates the unsorted HashMap instead; the sorted variable is never used. The HashMap documentation states the class “makes no guarantees as to the order of the map”, so the HMAC covers fields in whatever bucket order the JVM produces.
I reached the bug by elimination. Alphabetical order: no match. Case-insensitive alphabetical: no match. CRLF line endings, no trailing newline, every sensible serialisation I could construct: no match. But HashMap iteration order for a fixed set of keys is deterministic, which makes the search space finite. Seven EndUser fields give 7! = 5,040 possible orders:
for i, perm in enumerate(permutations(ENDUSER_FIELDS)):
if generate_hmac(sample_license, list(perm)) == expected_code:
print(f"MATCH at permutation #{i+1}: {perm}")
break
The match was permutation #9, the order QRadar’s HashMap actually produces:
EndUser.SubjectNameEndUser.EmailEndUser.departmentEndUser.contactNameEndUser.serverNameEndUser.CompanyNameEndUser.ITContactWith a valid HMAC, every layer is open.
Environment: my own lab console in AWS, fresh QRadar 7.5.0, temporary licence capped at 5,000 EPS.
# 1. serial number
sudo /opt/qradar/bin/getserial
# ec21b6df-b57d-753f-7925-1fe901b2ff61
# 2. forge licence + activation key
python3 qradar_full_exploit.py "ec21b6df-b57d-753f-7925-1fe901b2ff61"
# 3. upload .key, enter activation key, deploy
# Admin > System and License Management > Upload License
Before: a temporary licence capped at 5,000 EPS.
After deployment, the same page reads “Licensed For Anything (PERMANENT)”. The UI raised no warning, and neither did the deployment process. The after-state is the licence screen at the top of this post.
The throttling engine’s own log records the new ceiling:
Mar 8 16:54:35 [ecs-ec-ingress] Event Rate Threshold Set [1.003999998E9]
Mar 8 16:54:35 [ecs-ec-ingress] EC_Ingress Throttle Initialized with [Threshold:1.003999998E9 eps]
The throttling engine’s own log: a 1,003,999,998 EPS ceiling and zero throttles.
1.003999998E9 is 1,003,999,998 events per second, 200,800 times the original 5,000 EPS cap. The enforcement database confirms the change: license_pool_allocation went from allocated_eps_rate = 5000 to allocated_eps_rate = 999999999 in a single deploy. Licence file and enforcement layer, both updated, neither objecting.
The licence bypass affects IBM’s revenue. The next finding affects IBM’s customers.
Testing where else the AES key appears:
grep -r 'K3Y8hlLWpgn0OAv+Sk2sxQ==' /opt/qradar/
Two hits. One is licence code. The other is config.ini inside the bundled ZipTie server. ZipTie is a network configuration management framework AlterPoint released as open source in 2006; inside QRadar it backs Risk Manager (QRM), the add-on that stores credentials for routers, switches and firewalls in order to collect and push their configurations:
# AES Key used for encrypting credentials in the database.
# This value must not be changed after first start.
org.ziptie.provider.credentials.aes.key=K3Y8hlLWpgn0OAv+Sk2sxQ==
ZipTie’s config.ini: the credential encryption key is the licence key, and it must never be changed.
The same string that encrypts licence files encrypts the stored QRM credentials: SSH passwords, SNMP community strings, enable secrets.
Decompiling the ZipTie bundles confirms the path. The OSGi activator reads org.ziptie.provider.credentials.aes.key via System.getProperty(), passes it to new CredentialEncoder(keyString), and CredentialValueType uses that encoder for every credential stored to and read from the database. There is no per-installation derivation anywhere in the chain.
I verified the key’s universality on two independent consoles in my own AWS lab:
| Instance 1 | Instance 2 | |
|---|---|---|
Credential key in config.ini |
K3Y8hlLWpgn0OAv+Sk2sxQ== |
K3Y8hlLWpgn0OAv+Sk2sxQ== |
Encrypt TestPassword123! |
KYFildQ5PPt2meTZrDRWnw== |
KYFildQ5PPt2meTZrDRWnw== |
| Decrypt blob from the other instance | works |
Identical ciphertext from independent installs confirms identical keys, and decrypting one instance’s blob on the other is the formality that proves it. The consequence: anyone who obtains a QRadar database, a backup file, or a disk image can decrypt every stored QRM credential offline, on hardware they control, with a key IBM ships to every customer. No further access to the QRadar itself is needed. This is CWE-321, use of a hard-coded cryptographic key, and it is the central finding of this research.
| Date | Event |
|---|---|
| 6 Mar 2026 | Reconnaissance begins, ISO mounted |
| 6 Mar 2026 | RSA key factored, AES key found in bytecode |
| 7 Mar 2026 | HMAC serialisation bug identified |
| 8 Mar 2026 | Full chain demonstrated on lab console |
| Mar 2026 | Reported to IBM |
| Mar to Sep 2026 | No updates, no CVE, no advisory |
| 24 Sep 2026 | This post |
For comparison, the Elastic security team answered and coordinated the Elasticsearch licensing research before publication. IBM has not replied in six months to a report about its customers’ device credentials.
No patch existed when I tested, and none has been announced since. My testing ended in March 2026, and a vendor that leaves a security report unanswered for six months may just as quietly have shipped a patch without an advisory. Run this command to check if your SIEM is affected:
sudo grep 'org.ziptie.provider.credentials.aes.key' \
/opt/qradar/bin/ca_jail/usr/share/ziptie-server/osgi-config/config.ini
If that still returns the key above, the finding applies to your build. If your deployment uses QRM or any ZipTie device integration, treat the credential database and every backup of it as plaintext, because that is its effective protection level. Rotate the device credentials themselves; rotating keys inside QRadar cannot help while the encryption key is a shipped constant. Audit license_pool_allocation and the licence list in the admin UI against what you actually purchased, since a forged licence is indistinguishable in the interface. Watch IBM’s bulletins; when a fix lands, the question that matters is whether it rotates the credential key, and what happens to credentials already stored under the old one.
LicenseKeyManager, UI testing) would extend it.The licence screen at the top of this post is how I got in the door to find it.