
Technical deep-dive and anti-POC for CVE-2022-3602, a punycode buffer overflow in OpenSSL 3.0.x, with reproduction scripts, stack analysis, and compiler mitigation evaluation.
This document and repository is a write-up of CVE−2022-3602, a punycode buffer overflow issue in OpenSSL. It's an "anti-POC" (the issue does not appear to exploitable) intended for folks who maintain their own OpenSSL builds and for compiler maintainers.
There is a seperate CVE in the same release, CVE-2022-3786, which also leads to buffer overflows but an attacker can't control the content in that case. There is no reproduction for that issue here, but that issue can lead to a Denial of Service due to crash.
Crashes and Buffer overfllows are never good and if you are using OpenSSL 3.0.x, it is prudent to update as soon as possible.
Feel free to report any errors or omissions via GitHub issues or pull-requests.
There is an off-by-one issue in how ossl_punycode_decode handles punycode
decoding that results in a 4-byte overflow. This issue is reasonable only when
OpenSSL processes a certificate chain and requires two conditions. Firstly, a
CA or Intermediary certificates in a chain must contain a name-constraint field
that uses punycode.
nameConstraints = permitted;email:xn-maccrthaigh-n7a.com
Secondly, the leaf certificate must contain a SubjectAlternateName (SAN) otherName field that specifies a SmtpUTF8Mailbox string.
otherName = 1.3.6.1.5.5.7.8.9;UTF8:[email protected]
when triggered, the punycode in the nameConstraints field, but not the punycode in the otherName field, will be handled by the vulnerable OpenSSL punycode parsing.
David Benjamin and Matt Caswell determined that nameConstraint checking occurs after ordinary certificate chain validation and signature verification. For most applications this means that the issue can not be triggered with a self-signed certificate or invalid chain.
Note that openssl's s_client and s_server applications are intended
for debugging and do not stop processing when a chain is invalid.
A trusted CA or Intermediate will have to contain the malicious payload, and will also have to have signed the leaf certificate that triggers the issue.
There may be some environments where untrusted parties are the CAs or Intermediaries, for example a hosting service that supports customer-provided Private CAs, but this is not common.
The answer for many applications will be "no" because of how the compiler has laid out the stack and because of the presence of other protections such as stack canaries / stack cookies, padding, PIE, FORTIFY_SOURCE.
The the issue does lead to an overflow of 32-bits on the stack. This is not enough to directly execute shell code, but it may enough to alter the control flow of an application. For example jumping to shell-code that has been embedded in an X509 certificate chain may be possible if this data is also copied onto the stack in an executable location.
On every Linux platform I've tested, the overflow occurs into padding and is
harmless. In theory, a compiler may lay out variables such that the overflow
occurs into one of the other variables in the ossl_a2ulabel function.
Depending on inlining the full-list of variables present is:
outptr, inptr, size, result, tmpptr, delta, seed, utfsize
and none appear to me to provide an obvious path to privilege escalation or interesting control.
I've attached a tarball with tools that can be used to create reproductions and
overflows with as much control over all four bytes as is possible. The
reference reproduction string ( xn--ww90271...aaaa) overflows the four bytes
with the values 0xFF 0x0F 0x0F 0x0F. If that does not crash an application,
it is possible (likely?) that that application is not vulnerable.
The shell script run-poc can be used to generate a malicious certificate chain.
A malicous CA certificate is generated from ca.cnf and a triggering leaf
certificate is generated from leaf.cnf.
The CA certificate uses the following reference payload:
xn--ww902716aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
A python script can be used to generate other punycode strings for different payloads.
When executed run-poc will run an openssl client and server and attempt to
exploit the issue ten times.
A vulnerable OpenSSL will likely crash. That doesn't mean that the version of OpenSSL is vulnerable to an RCE, as stack canaries and stack cookie protections also typically cause a (safer) application crash. Also note that this in no changes the severity of the other CVE in the same release.
It is surprisingly nuanced to gain near-full control of all four overflow bytes and requires exploiting OpenSSL's punycode decoder with non-standard / invalid punycode. The tarball attached contains a script which can construct a string that handles the nuance. What follows is an explanation of how it works.
The security issue is in ossl_punycode_decode()
int ossl_punycode_decode(const char *pEncoded, const size_t enc_len,
unsigned int *pDecoded, unsigned int *pout_length)
ossl_punycode_decode is invoked from ossl_a2ulabel. The pEncoded buffer
is a more or less arbitrarily-sized buffer that comes from an X509 certificate
chain. It's the part that comes after any "xn--" in a nameConstraint field. See
the [reproduction] for how to reproduce such a certificate chain.
pDecoded is a LABEL_BUF_SIZE sized array of unsigned ints.
LABEL_BUF_SIZE is 512, and on most platforms an unsigned int is going to be
4 bytes wide. So on most platforms pDecoded is 2048 bytes in length.
Inside ossl_punycode_decode() the crux of the issue is this incorrect length check:
if (written_out > max_out)
max_out corresponds to *pout_length which is always 512. And written_out
keeps track of how many unsigned ints have been written to pDecoded.
Because written_out is incremented later only after writing, this faulty
check allows 513 unsigned ints to be written to pDecoded. The end result
looks something like this ...
pDecoded = [ ... , 'X , 'Y' , 'Z' ] 'P'
// Indices 509 510 511
Here, per C's convention, indices are zero-indexed, so slot number 511 is the
512th element in the array. 'P' is a four-byte payload that has been put out
of bounds, beyond the space allocated on the stack for the buf buffer in
ossl_a2ulabel() which is what pDecoded points to.
Four bytes is a small overflow, and is not enough to carry a nop-sled or directly execute shell code but is enough to alter the control flow of an application. For example jumping to shell-code that has been embedded in an x509 certificate chain may be possible, depending on how this data (or copied fragments of this data) is stored and whether that memory is executable. However there is still more difficulty for a would-be attacker.
Firstly, the compiler's padding and alignment of the stack, or defenses such as stack canaries, may make any exploitation completely impossible.
Secondly, there is only one path to ossl_punycode_decode() and this path uses
a buffer on the stack. This makes it unlikely that the issue can be used for
concurrent 4 byte overflows in different memory locations.