curl · Report #1269242
CVE-2021-22945: UAF and double-free in MQTT sending
How it was found
Vulnerability Description
libcurl version 7.77.0 has a Use-After-Free and a Double-Free in lib/mqtt.c in the function mqtt_doing on lines 556 - 563:
if(mq->nsend) {
/* send the remainder of an outgoing packet */
char *ptr = mq->sendleftovers;
result = mqtt_send(data, mq->sendleftovers, mq->nsend);
free(ptr);
if(result)
return result;
}
As can be seen in the code above mq->sendleftovers gets freed in line 560 but not set to NULL. If mqtt_doing gets called repeatedly and the values of mq->nsend and mq->sendleftovers don't change this can result in
- Sending the metadata of the freed chunk over the network via
mqtt_send - Freeing
mq->sendleftoversmultiple times
mq->nsend and mq->sendleftovers get set in the function mqtt_send if Curl_write cannot send all bytes in the write-buffer at once. This can e.g. happen if write() returns EAGAIN or EWOULDBLOCK. Then Curl_write sets the number of written bytes to 0 and returns CURLE_OK.
This can trigger the vulnerabilities as follows:
- Supply an
mqtt://URL to curl - Have some successfull transmissions with
mqtt_send - At some point have an unsuccessfull transmission such that not all bytes of the write-buffer can be sent.
This causes
mq->sendleftoversandmq->nsendto be set. - Have another invocation of
mqtt_doing. The code mentioned above gets executed.mq->sendleftoversgets freed. Ifmqtt_sendcould send all remaining bytes successfullymq->sendleftoversandmq->nsenddon't get reset. - Have another invocation of
mqtt_doing. Sincemq->nsenddidn't change curl tries to send the leftover bytes again, triggering the vulnerabilities
How to reproduce the bug
- Checkout tag
curl-7_77_0in the curl repository - Apply the following patch that artificially creates a scenario as described above:
diff --git a/lib/sendf.c b/lib/sendf.c
index e41bb805f..773d4b5b6 100644
--- a/lib/sendf.c
+++ b/lib/sendf.c
@@ -294,6 +294,7 @@ void Curl_failf(struct Curl_easy *data, const char *fmt, ...)
* If the write would block (CURLE_AGAIN), we return CURLE_OK and
* (*written == 0). Otherwise we return regular CURLcode value.
*/
+static int CUSTOM_blocked = 0;
CURLcode Curl_write(struct Curl_easy *data,
curl_socket_t sockfd,
const void *mem,
@@ -322,8 +323,13 @@ CURLcode Curl_write(struct Curl_easy *data,
}
#endif
bytes_written = conn->send[num](data, num, mem, len, &result);
+ if(!CUSTOM_blocked) {
+ bytes_written = 0;
+ CUSTOM_blocked = 1;
+ }
*written = bytes_written;
+
if(bytes_written >= 0)
/* we completely ignore the curlcode value when subzero is not returned */
return CURLE_OK;
- Rebuild curl
- Start a simple netcat session with:
nc -lp 5678 - Invoke curl with:
curl mqtt://127.0.0.1:5678/
The output:
free(): double free detected in tcache 2
[1] 199104 abort (core dumped) ./curl mqtt://127.0.0.1:5678
And in the terminal where netcat was launched it can be seen that the content of the freed heap chunk was sent.
Impact
Since double frees of tcache chunks are not detected until glibc version 2.29
this vulnerability is perfectly exploitable for operationg systems using an older
glibc. Causing write() to return EAGAIN is more difficult but not impossible
to manage, e.g. this can always be the case if the peer is not reading as fast as
the curl client is writing (source).
At minimum this can be used to leak heap metadata which can help in exploitation.