Description
On November 1, 2022, the OpenSSL project released version 3.0.7 to address a pair of vulnerabilities - CVE-2022-3602 and CVE-2022-3786 - that they labeled as “critical” in their announcements, then downgraded to “high” when they were released. In their blog post, the OpenSSL developers explain that they downgraded the risk because exploitation seems unlikely:
During the week of prenotification, several organisations performed testing and gave us feedback on the issue, looking at the technical details of the overflow and stack layout on common architectures and platforms. […] the stack layout was such that the 4 bytes overwrote an adjacent buffer that was yet to be used and therefore there was no crash or ability to cause remote code execution.
Even though the OpenSSL team knew that the vulnerability was going to be downgraded before they released the patch, they did nothing in the hours leading up to the release to defuse the collective panic. We’d like to see future projects be more open with impact changes, particularly when upcoming releases are going to be less impactful than originally announced.
CVE-2022-3786 refers to an arbitrary-length overflow when parsing Punycode domain names utilizing OpenSSL’s Punycode library, which is part of libcrypto.so and only accessible through certificate-validation functions after certificate validation. In a trusted certificate, this can potentially affect any client application running a vulnerable version of OpenSSL, or any server application that is configured to validate client certificates. Even on affected hosts, the likelihood of meaningful exploitation is low for reasons detailed below.
Affected products are:
- OpenSSL 3.0.0 - 3.0.6 (fixed in OpenSSL 3.0.7; other OpenSSL versions are not affected)
Technical analysis
CVE-2022-3786 is a parsing error in the ossl_a2ulabel function in punycode.c, where period characters (. or 0x2e) can be infinitely appended to a buffer, which typically leads to denial of service conditions (i.e., crashing the process by flooding it with periods). In rare cases, if a useful memory address is stored right after the buffer, and changing the last byte two of the address to 0x2e is useful, and nothing important gets overwritten in the process, a partial address overwrite (like this one) might lead to a exploitable memory corruption. That would have to be very carefully tailored to the target, and the memory layout of the target would need to be just right, making it unlikely to be widely exploited.
The diff for this issue is rather long, but let’s take a look at the vulnerable code to see what’s going on! A brief proof of concept that demonstrates the issue by directly calling the vulnerable function can be found in our Github repo, which overwrites the return address with 0x2e bytes. We’ll use that in our examples below, but note that it’s a very contrived example. The only known path to real-world exploitation is through the certificate-validation function, which requires a signed and trusted certificate.
The function signature for ossl_a2label is:
int ossl_a2ulabel(const char *in, char *out, size_t *outlen);It’s designed to decode possibly-Punycode-encoded DNS names; for example, www.xn--n28haaaaaaaaaaaaa.com would become www.😉😉😉😉😉😉😉😉😉😉😉😉😉😉.com. The code is mostly contained in a while (1) loop, which processes each label (ie, the portions of the name between periods) until it runs out:
while (1) {
char *tmpptr = strchr(inptr, '.');
// [...]
if (tmpptr == NULL)
break;
}
return result;If the label does not start with xn-- (ie, it’s a standard label), it’s basically copied directly into the output buffer. A length check sets the result variable to 0 and stops copying memory when we run out of output space, but importantly this does not exit the parsing loop:
// [...]
if (strncmp(inptr, "xn--", 4) != 0) {
size += delta + 1;
if (size >= *outlen - 1)
result = 0;
if (result > 0) {
memcpy(outptr, inptr, delta + 1);
outptr += delta + 1;
}
} else {
// [...]
}If the label does start with xn--, the else executes, and it decodes using the ossl_punycode_decode function (note that that function is vulnerable to CVE-2022-3602, which means we can write 4 bytes past the end of the stack-based buffer buf, but the next element on the stack does not appear to be a useful target):
} else {
unsigned int bufsize = LABEL_BUF_SIZE;
unsigned int i;
if (ossl_punycode_decode(inptr + 4, delta - 4, buf, &bufsize) <= 0)
return -1;
for (i = 0; i < bufsize; i++) {
unsigned char seed[6];
size_t utfsize = codepoint2utf8(seed, buf[i]);
if (utfsize == 0)
return -1;
size += utfsize;
if (size >= *outlen - 1)
result = 0;
if (result > 0) {
memcpy(outptr, seed, utfsize);
outptr += utfsize;
}
}
// [...]
}Once again, when it reaches the end of the output buffer, it sets result to 0, then stops writing to the output buffer (but, again, doesn’t stop processing). The following code, however, does write to the buffer:
if (tmpptr != NULL) {
*outptr = '.';
outptr++;
size++;
if (size >= *outlen - 1)
result = 0;
}
}
// [...]As long as tmpptr isn’t NULL (meaning, we haven’t reached the final label in the original string), it appends a period, even if result has been set to 0! That means that as long as there are Punycode-encoded labels to decode, we can keep adding periods. Note that the resulting string will look like it’s the correct length. In our contrived PoC, we use the string "a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.xn--.xn--.xn--.xn--.xn--.xn--.xn--[...]", which appears to truncate when viewed as a string:
(gdb) x/s $rbp-0x20
0x7fffffffdea0: "a.b.c.d.e.f.g......."But that’s because a 4-byte NULL value is appended to the buffer at the end, before more period characters (0x2e) are written as far past the buffer as the exploit desires:
(gdb) x/64xb $rbp-0x20
0x7fffffffdea0: 0x61 0x2e 0x62 0x2e 0x63 0x2e 0x64 0x2e
0x7fffffffdea8: 0x65 0x2e 0x66 0x2e 0x67 0x2e 0x2e 0x2e
0x7fffffffdeb0: 0x2e 0x2e 0x2e 0x2e 0x00 0x00 0x00 0x00
0x7fffffffdeb8: 0x2e 0x2e 0x2e 0x2e 0x2e 0x2e 0x2e 0x2e
0x7fffffffdec0: 0x2e 0x2e 0x2e 0x2e 0x2e 0x2e 0x2e 0x2e
0x7fffffffdec8: 0x2e 0x2e 0x2e 0x2e 0x2e 0x2e 0x2e 0x2e
[...]Including overwriting the return address (again, this is contrived, not a real-world attack):
(gdb) cont
[...]
Program received signal SIGSEGV, Segmentation fault.
0x000000000040127e in main (argc=1, argv=0x7fffffffdfd8) at cve-2022-3786-periods.c:33
33 }
(gdb) x/i $rip
=> 0x40127e <main+200>: ret
(gdb) x/xg $rsp
0x7fffffffdec8: 0x2e2e2e2e2e2e2e2eExploiting this issue in real-world applications will be very dependent on how the vulnerable functions are called. At this point, nobody in the community (to our knowledge) has found an application where this overwrite leads to security implications. That doesn’t mean it’s impossible, however; this vulnerability permits us to perform a partial pointer overwrite, where we can change any number of bytes at the end of a memory address to 0x2e, but only if the memory address is after the Punycode-output buffer and nothing else important is overwritten in between.
In the worst case scenario, a buffer overflow such as this could potentially lead to reading, writing, or executing code from the wrong place in memory. In practice, however, it’s unlikely that any targets are vulnerable to this.
Guidance
Organizations that are running an affected version of OpenSSL should update to 3.0.7 when practical, prioritizing operating system-level updates and public-facing shared services with direct dependencies on OpenSSL. Emergency patching is not indicated.



