The Sky Has Not Yet Fallen - Curl (CVE-2023-38545)

The Sky Has Not Yet Fallen - Curl (CVE-2023-38545)

There are few packages as ubiquitous as the venerable cURL project. With over 25 years of development, and boasting “over ten billion installations”, it’s easy to see why a major security flaw could bring the Internet to a standstill - it’s on our endpoints, it’s on our routers, it’s on our IoT devices. It’s everywhere. And it’s written in C - what could go wrong?

Well, quite a lot, it seems.

A few months ago, the cURL maintainers warned us of a terrible, world-ending bug in their software, causing administrators and technical end-users alike to panic. Would attackers be able to hijack my enterprise router? My TV? My PDU, even!? The far-reaching effects of a real cURL RCE can not be overstated.

Fortunately, once the bug was released, it turned out to be a bit of (in popular parlance) “a nothingburger”, requiring a very specific environment in order to metastasise into real danger. A close shave, but the world could rest easy again.

Last week, however, it looked liked history was repeating itself, as the cURL project announced a “severity HIGH CVE”. Administrators were urged to be ready to upgrade the second that patches were available (scheduled for today, October 11th), or suffer the dire consequences. Many were skeptical, some were worried.

Theories were abound - could this allow trivial SSRF vulnerabilities to be trivially converted into RCEs across the Internet? Could we see massive libcurl library exploitation in seemingly benign software that just made simple GET requests?

Don’t worry - patches are here, and we’re here to cut through the hype and figure out what’s actually going on.

Fumbling the Embargo

Well, the it seems bug dropped a little bit early, as a publicly-viewable patch was committed to RedHat’s curl repository. The patch looks pretty simple, and fortunately for us, comes with a test case to trigger the bug. Great! Let’s dig in and see what changed.

That looks pretty straightforward. Two things have been removed: a warning (replaced with an error), and an assignment to the socks5_resolve_local variable. The comments above solidify our diagnosis:

If you’ve used a SOCKS5 proxy, you may be aware that DNS resolution usually occurs on the ‘server’ end of the connection - the client requests a hostname, and the server does the neccessary DNS lookup to facillitate connection. However, the SOCKS specification states that hostnames must not exceed 255 characters, and so the developer who wrote this code was careful to avoid sending such long requests. Instead, if a request was submitted with a long hostname, the local system would simply resolve the host the itself, instead of using the SOCKS proxy.

No problem here, but if we take a look at other places where this socks5_resolve_local variable is assigned, we can see it near the top of the main Curl_SOCKS5 function. This function is invoked every time there is activity on the SOCKS connection.

CURLcode Curl_SOCKS5( .. )
{
bool socks5_resolve_local =
    (conn->socks_proxy.proxytype == CURLPROXY_SOCKS5) ? TRUE : FALSE;

This line simply enables the default behavior of resolving remotely, which has the unfortunate effect of undoing all the careful hostname-length checking that has been done in the previous invokation, leading to an attempt to remotely resolve a hostname which has failed the length check. The remote-resolution logic assumes that the hostname will never exceed 255 bytes, understandably, leading to a heap overflow.

The overflow actually occurs in the same function, in the following code snippet:

unsigned char *socksreq = &conn->cnnct.socksreq[0];

len = 0;
socksreq[len++] = 5; /* version (SOCKS5) */
socksreq[len++] = 1; /* connect */
socksreq[len++] = 0; /* must be zero */

if(!socks5_resolve_local) {
  socksreq[len++] = 3; /* ATYP: domain name = 3 */
  socksreq[len++] = (char) hostname_len; /* one byte address length */
  memcpy(&socksreq[len], hostname, hostname_len); /* address w/o NULL */
...

As you can see, we’re copying the hostname into this socksreq variable, which is itself assigned from the conn->cnnct.socksreq field. Since the limit of 255 bytes has been bypassed, the hostname may be anywhere up to approximately 64KB.

What does the bug affect?

Of course, vulnerability (and thus exploitation) hinges on this destination buffer cnnct.socksreq being smaller than the size of a potential hostname. At first glance, the official documentation appears to state that, in the default configuration, this is not the case:

An overflow is only possible in applications that do not set CURLOPT_BUFFERSIZE or set 
it smaller than 65541. Since the curl tool sets CURLOPT_BUFFERSIZE to 100kB by default 
it is not vulnerable unless rate limiting was set by the user to a rate smaller than 
65541 bytes/second.

But we mustn’t jump to conclusions without reading the full document. There’s a (bolded!) caveat at the bottom of the page.

**The analysis in this section is specific to curl version 8.** Some older versions of curl 
version 7 have less restriction on hostname length and/or a smaller SOCKS negotiation 
buffer size that cannot be overridden by [CURLOPT_BUFFERSIZE](<https://curl.se/libcurl/c/CURLOPT_BUFFERSIZE.html>).

It is, indeed, easy to cause version 7.74.0 of the cURL runtime to segfault with a public PoC:

# gdb --args curl  -vvv -x socks5h://172.17.0.1:9050 $(python3 -c "print(('A'*10000), end='')")
[truncated]
(gdb) run
Starting program: /usr/local/bin/curl -vvv -x socks5h://172.17.0.1:9050 AAAAAAAAA[truncated]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
*   Trying 172.17.0.1:9050...
* SOCKS5: server resolving disabled for hostnames of length > 255 [actual len=10000]
* SOCKS5 connect to AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[truncated]
Program received signal SIGSEGV, Segmentation fault.
0x00007f529d9f0aec in Curl_resolver_kill () from /usr/local/lib/libcurl.so.4
(gdb) x/1i $pc
=> 0x7f529d9f0aec <Curl_resolver_kill+28>:      cmp    QWORD PTR [rdi],0x0
(gdb) info registers rdi
rdi            0x4141414141414141  4702111234474983745
(gdb)

Yikes! Clearly the out-of-the box cURL is vulnerable, to something at least! Although this simple PoC isn’t actually overflowing the SOCKS context buffer itself, we’ve broken the error handling and caused enough corruption to read an arbitrary memory location - clearly something’s not so right. Given this instability, combined with the lack of public analysis on version 7, we’d advise anyone still running version 7 to upgrade to version 8 at their earliest convenience..

Summary

So here we have it - a heap overflow in cURL, and in libcURL, the embedded version. Fortunately (for defenders) it requires use of a SOCKS proxy, and it only affects a well-defined range of cURL versions.

So it’s not world-ending, but it’s fairly bad, particularly for users of a 7.x version of cURL. The real problem with bugs such as these is the amount of disparate codebases and embedded devices that contain libcURL, and will now need patching.

A frank and informative analysis by the author of the vulnerable code notes that the bug has been in the cURL codebase for 1315 days - enough to filter through into appliances and other “oops I forgot about that one” devices. I suspect we’ll be patching this one for quite some time.

FAQ

In what is becoming a regular feature of watchTowr blog posts, here’s a quick question-and-answer section to quell any hopefully misplaced panic:

Q) Which versions are affected?

A) Affected versions are libcURL above (and including) 7.69.0 and up to (and including) 8.3.0. Versions below 7.69.0 are not affected.

Q) Is watchTowr aware of any trivially-exploitable enterprise-grade software where this vulnerability could be abused?

A) At this point in time, no.

Q) Are the conditions necessary for an external-party to exploit this vulnerability common?

A) No.

Q) Does this bug require a malicious SOCKS proxy?

A) No, it does not, it just needs a connection to a malicious host over a SOCKS proxy.

Q) I use the command-line version of cURL all the time! It’s version 8, but below the fixed 8.4.0. Do I need to upgrade?

A) Well, you should always upgrade, because it’s a Best Practice (TM). But the bug likely isn’t exploitable on your system, so rest easy.

Q) I use the command-line version of cURL all the time, and it’s only version 7 - but I never use a SOCKS proxy. Should I be worried?

A) The bug doesn’t affect you, since you don’t use a SOCKS proxy. Having said that, I’d advise caution - it may be difficult to ensure that malicious users can’t force you to use a SOCKS proxy (for example, as an elevation or lateral-movement path). Upgrading, if at all possible, is always the safest option.

Q) I use the command-line version of cURL all the time, and it’s only version 7 - and I use a SOCKS proxy. How about me?

A) Ooof, that’s a bad scenario. While the cURL documentation isn’t clear on the exploitability of the v7 commandline tool, we’ve shown that at least some versions will segfault, although it isn’t clear if this is due to a heap overflow condition. In the absence of a clear answer on exploitability, we would advise you to upgrade to version 8 as a matter of urgency.

Q) I can’t upgrade! How can I mitigate this vulnerability?

A) You can mitigate by avoiding the use of a SOCKS proxy, if at all possible. If you run cURL in a privileged context, be aware that various options may enable a SOCKS proxy, both from the command-line, URL, or environment variables, and you may need to sanitize these to prevent a malicious user from connecting to a malicious SOCKS proxy.

Q) Is this vulnerability going to turn every SSRF into RCE?

A) Sadly not.