What happens
If a Scapy TLS server's certificate file also contains its private key — a common way to store the
two together — the server sends the private key to anyone who connects to it.
An ordinary 50-byte hello was enough. The server answered with a 2,146-byte certificate message
that had the complete private key inside it. The same server with the key kept in a separate file
sent 930 bytes and no key. Nothing about the connection has to be unusual, and this happens before
the client has proved anything about itself.
Background — what this code does
Scapy's Cert class loads X.509 certificates from DER or PEM files for TLS and other protocols.
Its
shared PEM loader supports files containing more than one PEM object by concatenating every
decoded
object at
scapy/layers/tls/cert.py:243-280.
TLSServerAutomaton is Scapy's active TLS server. It stores the configured certificate in the TLS
session at
scapy/layers/tls/automaton_srv.py:286-300
and sends that object in the server Certificate message at
scapy/layers/tls/automaton_srv.py:393-398.
How the code is reached
An application starts TLSServerAutomaton with mycert and mykey filenames. Combined PEM files
containing a certificate followed by its private key are a conventional certificate-deployment
format. The attacker only needs to connect and send a TLS 1.2 ClientHello that selects a
certificate-based cipher suite; the Certificate message is sent before client authentication.
The reproduction uses the standard TLS server automaton, its existing test certificate and key,
and default global Scapy configuration. It does not change conf.* or any Scapy cache file.
Why it matters
Every client that connects gets the key, and it is the whole key. Whoever holds it can impersonate
the service anywhere that certificate is trusted, and can decrypt any recorded session that used
RSA key exchange with it. The reproducer shows the key being handed over; it does not go on to do
either of those things.
The demonstrated disclosure is limited to additional valid PEM objects in the configured
certificate file. No claim is made that arbitrary trailing non-PEM bytes are emitted.
Reproduce it
Save the supplied script as reproduce.py in the Scapy checkout and run:
./.venv/bin/python reproduce.py
Expected result on commit 1f870205baae8baf1718bc20700d0d9ffc0e4324:
mixed PEM: boundary_held=False certificate_entries=1 entry_bytes=2146 private_key_der_bytes=1216 private_key_der_occurrences=1
certificate-only PEM: boundary_held=True certificate_entries=1 entry_bytes=930 private_key_der_occurrences=0
The two exchanges send equivalent ClientHello records to servers using the same certificate and
key. Only the mycert file differs: the first appends the valid private-key PEM, while the
control
contains the certificate alone. With the patch applied, both cases produce a 930-byte certificate
entry with zero private-key occurrences.
This was reproduced with Scapy 2.7.1rc1.post100, Python 3.13.14, default global configuration,
and conf.debug_dissector = 0.
Where it goes wrong
_PKIObjMaker.__call__() at
scapy/layers/tls/cert.py:268
decodes and concatenates every PEM object:
if b"-----BEGIN" in _raw:
frmt = "PEM"
pem = _raw
der_list = split_pem(pem)
der = b"".join(map(pem2der, der_list))
That behavior is useful for callers that expect multiple objects, but Cert represents one
certificate. _CertMaker.__call__() at
scapy/layers/tls/cert.py:911
parses the concatenation as one X509_Cert and retains any remaining bytes as that packet's
payload:
obj = _PKIObjMaker.__call__(cls, cert_path, _MAX_CERT_SIZE, "CERTIFICATE")
obj.__class__ = Cert
obj.marker = "CERTIFICATE"
try:
cert = X509_Cert(obj._der)
except Exception:
if conf.debug_dissector:
raise
raise Exception("Unable to import certificate")
obj.import_from_asn1pkt(cert)
The der property serializes the whole parsed packet at
scapy/layers/tls/cert.py:1127-1133.
The retained private-key payload therefore becomes part of the TLS certificate entry.
Suggested fix
try:
cert = X509_Cert(obj._der)
except Exception:
if conf.debug_dissector:
raise
raise Exception("Unable to import certificate")
+ cert.remove_payload()
obj.import_from_asn1pkt(cert)
Cert is a singular certificate wrapper, so it should discard bytes after the first parsed X.509
object before storing it. This preserves certificate-only DER and PEM behavior while ensuring that
additional PEM objects are not returned by Cert.der.
The regression test is in test/scapy/layers/tls/cert.uts. Adding only the test to the affected
source makes it fail; restoring the source fix makes it pass. The complete certificate suite
passes 69 of 69 tests with the patch.
Performance impact of the fix, measured on one computer by running the same test before and after:
discarding the payload took 608.5 ns before and 526 ns after, 13.6% faster — a difference of 82
ns. Repeat runs of that test moved by about 8%, so that difference is larger than the test's own
variation.
Affected
- Package: Scapy · Branch:
master
- Confirmed on: commit
1f870205baae8baf1718bc20700d0d9ffc0e4324,
version 2.7.1rc1.post100
- Severity: High —
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N (7.5). The score reflects
unauthenticated remote disclosure of the server's complete private key, without a separately
demonstrated integrity or availability effect.
- CWE: CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor
Credit
Reported by: Clinton Thomas (@KernelClint) of Trail of Bits, in collaboration with OpenAI.
Found with GPT-5.6-Cyber as part of the Patch the Planet security initiative.
What happens
If a Scapy TLS server's certificate file also contains its private key — a common way to store the
two together — the server sends the private key to anyone who connects to it.
An ordinary 50-byte hello was enough. The server answered with a 2,146-byte certificate message
that had the complete private key inside it. The same server with the key kept in a separate file
sent 930 bytes and no key. Nothing about the connection has to be unusual, and this happens before
the client has proved anything about itself.
Background — what this code does
Scapy's
Certclass loads X.509 certificates from DER or PEM files for TLS and other protocols.Its
shared PEM loader supports files containing more than one PEM object by concatenating every
decoded
object at
scapy/layers/tls/cert.py:243-280.TLSServerAutomatonis Scapy's active TLS server. It stores the configured certificate in the TLSsession at
scapy/layers/tls/automaton_srv.py:286-300and sends that object in the server Certificate message at
scapy/layers/tls/automaton_srv.py:393-398.How the code is reached
An application starts
TLSServerAutomatonwithmycertandmykeyfilenames. Combined PEM filescontaining a certificate followed by its private key are a conventional certificate-deployment
format. The attacker only needs to connect and send a TLS 1.2 ClientHello that selects a
certificate-based cipher suite; the Certificate message is sent before client authentication.
The reproduction uses the standard TLS server automaton, its existing test certificate and key,
and default global Scapy configuration. It does not change
conf.*or any Scapy cache file.Why it matters
Every client that connects gets the key, and it is the whole key. Whoever holds it can impersonate
the service anywhere that certificate is trusted, and can decrypt any recorded session that used
RSA key exchange with it. The reproducer shows the key being handed over; it does not go on to do
either of those things.
The demonstrated disclosure is limited to additional valid PEM objects in the configured
certificate file. No claim is made that arbitrary trailing non-PEM bytes are emitted.
Reproduce it
Save the supplied script as
reproduce.pyin the Scapy checkout and run:Expected result on commit
1f870205baae8baf1718bc20700d0d9ffc0e4324:The two exchanges send equivalent ClientHello records to servers using the same certificate and
key. Only the
mycertfile differs: the first appends the valid private-key PEM, while thecontrol
contains the certificate alone. With the patch applied, both cases produce a 930-byte certificate
entry with zero private-key occurrences.
This was reproduced with Scapy
2.7.1rc1.post100, Python3.13.14, default global configuration,and
conf.debug_dissector = 0.Where it goes wrong
_PKIObjMaker.__call__()atscapy/layers/tls/cert.py:268decodes and concatenates every PEM object:
That behavior is useful for callers that expect multiple objects, but
Certrepresents onecertificate.
_CertMaker.__call__()atscapy/layers/tls/cert.py:911parses the concatenation as one
X509_Certand retains any remaining bytes as that packet'spayload:
The
derproperty serializes the whole parsed packet atscapy/layers/tls/cert.py:1127-1133.The retained private-key payload therefore becomes part of the TLS certificate entry.
Suggested fix
try: cert = X509_Cert(obj._der) except Exception: if conf.debug_dissector: raise raise Exception("Unable to import certificate") + cert.remove_payload() obj.import_from_asn1pkt(cert)Certis a singular certificate wrapper, so it should discard bytes after the first parsed X.509object before storing it. This preserves certificate-only DER and PEM behavior while ensuring that
additional PEM objects are not returned by
Cert.der.The regression test is in
test/scapy/layers/tls/cert.uts. Adding only the test to the affectedsource makes it fail; restoring the source fix makes it pass. The complete certificate suite
passes 69 of 69 tests with the patch.
Performance impact of the fix, measured on one computer by running the same test before and after:
discarding the payload took 608.5 ns before and 526 ns after, 13.6% faster — a difference of 82
ns. Repeat runs of that test moved by about 8%, so that difference is larger than the test's own
variation.
Affected
master1f870205baae8baf1718bc20700d0d9ffc0e4324,version
2.7.1rc1.post100CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N(7.5). The score reflectsunauthenticated remote disclosure of the server's complete private key, without a separately
demonstrated integrity or availability effect.
Credit
Reported by: Clinton Thomas (@KernelClint) of Trail of Bits, in collaboration with OpenAI.
Found with GPT-5.6-Cyber as part of the Patch the Planet security initiative.