999,999,999 EPS - The Licence IBM Never Sold

999,999,999 EPS - The Licence IBM Never Sold

in

Summary

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.

Dark server room with rows of glowing racks

QRadar is IBM's Security Information and Event Management product. It collects and correlates logs across an enterprise network, and capacity is metered by licence: events per second (EPS), flows per second (FPS), and separately priced add-ons for vulnerability management, risk management and forensics. The licence file is the enforcement mechanism, and IBM publishes nothing about the machinery underneath.

Reverse a vendor's licensing to land a CVE, or to turn an unlicensed install into a licensed one, and you are in undefined behaviour: the same act counts as research, piracy, or both, depending on who is asked and who is answering email. The behaviour is defined only on the machine. Over one weekend in March 2026, working from two QRadar 7.5.0 ISO images against consoles I own, I executed it end to end. The result on my test console:

QRadar console licensed for 999,999,999 EPS and FPS, permanent, expiring never, every paid add-on included 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.

Three layers, three failures

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.

Layer 1: a 96-bit RSA key

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:

Python output showing modulus 41906015560064597191950651353, bit length 96, exponent 65537 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

How the rho thinks

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 output: factored in 35.0635 seconds, p = 209421416832289, q = 200103772545977 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.

Layer 2: the AES key in plain text

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();

Decompiled Q1LegacyLicense.java with the hardcoded AES key string visible 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.

What a licence file contains

Decrypted, a .key file is plain Java properties:

Decrypted QRadar license file showing identity, type PERMANENT, event limits, expiration 2099 and feature codes 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.

The activation key encoder

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.

Figure lit only by a laptop screen in a dark room

The encoder has a quirk worth documenting. The decompiled method is named toBase36, but it encodes each byte with Long.toString(b, 33), radix 33, then substitutes .replace("O", "X").replace("I", "Y") to avoid lookalike characters. The validator confirms the intent by rejecting any key containing O or I outright.

An implementation using standard base32 or base36 therefore produces keys that fail validation even when the cryptography is correct. The output for my test serial:

Serial:         ec21b6df-b57d-753f-7925-1fe901b2ff61
Activation key: BHUSS-N96DY-DETU6-85HWA

Layer 3: the HMAC over an unsorted HashMap

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");
}

Decompiled CommonUserInfo.java showing keys sorted into a list while the unsorted HashMap keySet is iterated 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:

  1. EndUser.SubjectName
  2. EndUser.Email
  3. EndUser.department
  4. EndUser.contactName
  5. EndUser.serverName
  6. EndUser.CompanyName
  7. EndUser.ITContact

With a valid HMAC, every layer is open.

The full chain, demonstrated

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

QRadar admin page before the exploit, licensed for 5000 EPS 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]

QRadar log viewer showing Appliance Threshold 1003999998.00 EPS and Total EC Throttles 0 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.

Key reuse: the finding that matters

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 config.ini showing the credential encryption AES key identical to the licence key, with the comment that it must never be changed 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.

Disclosure timeline

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.

Recommendations for operators

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.

Scope and limitations

  • Testing covered the two 7.5.0 builds listed above. No claim is made about other versions beyond them.
  • The cross-instance verification used two consoles I own in AWS, installed from different media.
  • Pollard’s rho is probabilistic; the 35-second timing varies between runs and machines, from under a second to about a minute at this modulus size.
  • Feature codes beyond those listed were not fully mapped. The mapping method (diffing editions, reading LicenseKeyManager, UI testing) would extend it.

Assessment

Glowing padlock over a stream of binary

I wrote in the ELK post that an enterprise licence is largely support, SLAs and partnership, and that the licence itself is the least valuable part. The same holds here: nobody serious runs a production SIEM on a forged licence, and the EPS ceiling was never the protection that mattered.

The hardcoded key is different in kind. No support contract or SLA addresses it, and no customer action fixes it: the key is a shipped constant the customer cannot rotate, as the config comment itself acknowledges. Every QRadar deployment has run the same credential vault behind the same lock since at least 2022. That is the finding.

The licence screen at the top of this post is how I got in the door to find it.

Sources and Further Reading

  1. IBM. “QRadar Suite documentation.” Accessed Sep 2026.
  2. NIST. “SP 800-57 Part 1 Rev. 5: Recommendation for Key Management.” May 2020.
  3. Wikipedia. “Pollard’s rho algorithm.” Accessed Sep 2026.
  4. Leibnitz27. “CFR, another Java decompiler.” GitHub.
  5. Oracle. “HashMap (Java Platform SE 8 API Specification).” Order-of-iteration disclaimer quoted verbatim.
  6. MITRE. “CWE-321: Use of Hard-coded Cryptographic Key.” Accessed Sep 2026.
  7. Denise Dubie, Network World. “AlterPoint spins out free network management software.” 14 Nov 2006. The original SourceForge project page is gone.
  8. IBM. “Report a vulnerability (IBM PSIRT).” Accessed Sep 2026.
  9. NIST. “National Vulnerability Database.” No CVE matching this research as of 24 Sep 2026.
  10. This blog. “zip -j trust_me_bro.jar - How One Command Cracks Elasticsearch Enterprise.” 3 Apr 2026.