The Setup
Okay so this is going to be a weird blog to write. Because I am going to tell you about a vulnerability I found in curl. A real one. With a working proof-of-concept. With Kerberos authentication. With everything.
And then I am going to tell you why I did not get a CVE for it.
But here is the thing - even though I “failed” - I learned more from this one attempt than from reading 100 blog posts about security research. And that is what this blog is about.
If you have been thinking about CVE hunting but do not know where to start, this is for you. If you have tried and “failed” before, this is also for you.
Let’s go.
What I Found Confusing (And Now Don’t)
When I first started looking at curl, I had no idea what I was looking for. Everyone says “read the code and find bugs” but nobody tells you where to look or what patterns to recognize.
I kept thinking - do I just randomly read files until I see something wrong? That felt impossible.
The answer is - you start with patterns from past vulnerabilities. You look at what broke before. Then you check if the same pattern exists elsewhere.
That is exactly what I did.
The Pattern That Caught My Attention
I was analyzing curl’s CVE history. 188 vulnerabilities over 25 years. And I noticed something interesting.
CONNECTION POOL LOGIC BUGS kept showing up. Again. And again. And again.
Here are just a few examples:
- CVE-2014-0015 - NTLM connection reused without credential check
- CVE-2016-0755 - Proxy NTLM auth reuse bug
- CVE-2022-22576 - OAuth bearer token not compared on reuse
The pattern was clear. curl has a connection pool. Connections get reused to save time. But sometimes the code forgets to check if the authentication state from the old connection should actually be shared with the new request.
That is when I thought - if NTLM had this bug multiple times, what about Kerberos?
The Hunt Begins
I downloaded curl’s source code (version 8.5.0 at the time) and started reading.
The connection pool logic lives in lib/conncache.c and lib/url.c. Specifically, there is a function called ConnectionExists() that decides whether an existing connection can be reused or if a new one needs to be created.
Here is what I found.
Finding 1: The Hash Key Excludes Credentials
File: lib/conncache.c, lines 126-150
static void hashkey(struct connectdata *conn, char *buf, size_t len) {
msnprintf(buf, len, "%ld/%s", port, hostname);
}
The connection pool uses a hash table. The key for that hash table is just port + hostname. No username. No password. Nothing about credentials.
That means all connections to example.com:443 go into the same bucket regardless of who is making the request.
Okay so far this makes sense. The hash key is just for grouping. The real check should happen inside ConnectionExists().
Finding 2: HTTP Skips Credential Checks
File: lib/url.c, lines 1133-1143
if(!(needle->handler->flags & PROTOPT_CREDSPERREQUEST)) {
// Check if user and password match
if(Curl_timestrcmp(needle->user, check->user) ||
Curl_timestrcmp(needle->passwd, check->passwd)) {
continue; // Skip this connection
}
}
So there IS a credential check. But it only runs if the protocol does NOT have the PROTOPT_CREDSPERREQUEST flag set.
I checked lib/http.c. All HTTP handlers set that flag. Which means for HTTP/HTTPS, the credential check gets skipped entirely.
Now you might be thinking - that sounds like a bug right there. But actually it is not. Here is why.
HTTP sends credentials in headers on every request. Basic auth, Digest auth, Bearer tokens - they all go in the Authorization: header per-request. So connection-level credential isolation is not needed.
But Kerberos is different.
Finding 3: Kerberos Authenticates The Connection
With Kerberos (also called Negotiate or SPNEGO), the authentication happens on the TCP connection itself. You do a handshake. The handshake establishes a GSS security context. That context stays alive on the connection. All future requests on that connection are implicitly authenticated.
So if Alice authenticates with Kerberos on Connection #0, and then Bob reuses Connection #0, Bob’s request arrives on a connection that is already authenticated as Alice.
The server has no way to tell the difference.
Finding 4: NTLM Has Protection, Kerberos Does Not
This is where it got really interesting.
File: lib/url.c, lines 1214-1273
#if defined(USE_NTLM)
if(wantNTLMhttp) {
if(Curl_timestrcmp(needle->user, check->user) ||
Curl_timestrcmp(needle->passwd, check->passwd)) {
continue; // Skip this connection
}
}
else if(check->http_ntlm_state != NTLMSTATE_NONE) {
continue; // Connection has NTLM state but we don't want NTLM
}
#endif
NTLM gets its own protection block. It checks credentials. It checks the NTLM state. If anything mismatches, the connection is NOT reused.
But when I searched for the equivalent Kerberos check:
grep "USE_SPNEGO" lib/url.c
grep "http_negotiate_state" lib/url.c
Nothing. Zero matches. No protection block. No state check.
The Kerberos state fields exist in the code:
#ifdef USE_SPNEGO
curlnegotiate http_negotiate_state; // line 1024
curlnegotiate proxy_negotiate_state; // line 1025
struct negotiatedata negotiate; // line 1027
#endif
But ConnectionExists() never checks them.
That is the bug.
Building The Proof of Concept
Finding the bug in the code is one thing. Proving it is exploitable is another.
I needed to build a working PoC. That meant setting up a Kerberos environment.
Step 1: Setting Up Kerberos
I installed a Kerberos KDC (Key Distribution Center) on my Linux machine.
sudo apt install krb5-kdc krb5-admin-server krb5-user python3-gssapi
sudo krb5_newrealm # Created realm: TESTLAB.LOCAL
Then I created two test users:
sudo kadmin.local
addprinc -pw testpass123 alice
addprinc -pw bobpass bob
addprinc -randkey HTTP/localhost
ktadd -k /tmp/http.keytab HTTP/localhost
quit
Alice and Bob are two different users. If curl is working correctly, their requests should use separate connections. If the bug exists, Bob will reuse Alice’s connection.
Step 2: Writing The Test Server
I needed a server that requires Kerberos authentication and tracks which user authenticated on which TCP connection.
I wrote a Python test server using the gssapi library:
#!/usr/bin/env python3
import gssapi
import socketserver
class ConnState:
def __init__(self):
self.ctx = None
self.principal = None
self.complete = False
class Handler(socketserver.BaseRequestHandler):
def handle(self):
state = ConnState()
# ... HTTP request parsing ...
if state.complete and state.principal:
# Connection already has a GSS context
log.info("*** CONNECTION REUSED - GSS context persists for %s ***",
state.principal)
When a connection gets reused, the server logs which user’s context is still active.
Step 3: Running The Exploit
Here is the test:
# Alice authenticates with Kerberos
kinit [email protected]
curl --negotiate -u : http://localhost:8080/
# Bob switches user
kdestroy
kinit [email protected]
curl --negotiate -u : http://localhost:8080/ \
--next --negotiate -u : http://localhost:8080/
And here is what I saw:
* Connection #0 to host localhost left intact
* Re-using existing connection with host localhost
* Curl_output_negotiate, no persistent authentication: cleanup existing context
*** CONNECTION REUSED - GSS context persists for [email protected] ***
The bug is real.
Bob reused Alice’s connection. curl’s verbose output even shows Re-using existing connection. The server logged that the GSS context persisted across the reuse.
What I Found Confusing (And Now Don’t) - Part 2
At this point I was excited. I had found a real bug. I had a working PoC. I wrote up the full technical analysis - root cause, exploitation flow, suggested fix, everything.
But then I noticed something in the curl output:
* Curl_output_negotiate, no persistent authentication: cleanup existing context
Wait. Cleanup? What cleanup?
I went back to the code.
The Partial Mitigation I Missed
File: lib/http_negotiate.c, lines 140-152
if(*state == GSS_AUTHSUCC) {
neg_ctx->noauthpersist = !neg_ctx->havemultiplerequests;
}
if(neg_ctx->noauthpersist && *state == GSS_AUTHSUCC) {
Curl_http_auth_cleanup_negotiate(conn);
}
There IS a mitigation. When curl reuses a connection with Kerberos state, it checks whether the original handshake required multiple roundtrips.
- Single-roundtrip (MIT Kerberos default):
noauthpersist = TRUE→ cleanup happens - Multi-roundtrip (Windows AD, IIS):
noauthpersist = FALSE→ no cleanup
So the vulnerability is partially mitigated for MIT Kerberos but fully exposed for Windows AD environments.
Also, even when the mitigation is active, it happens in the wrong layer. The cleanup is in Curl_output_negotiate() (the output layer), not in ConnectionExists() (the connection matching layer). The connection is already matched and reused by the time cleanup happens.
NTLM has protection in both layers. Kerberos has neither.
Still a bug. Just more nuanced than I thought.
Submitting To HackerOne
curl has a HackerOne program. I submitted the full report:
- Technical analysis with line numbers
- Working proof-of-concept
- Test server code
- Root cause explanation
- Suggested fix
I hit submit.
Two minutes later, Daniel Stenberg (the creator of curl) responded.
“Thank you for your report! We will investigate and get back to you within 24 hours.”
I was hyped.
Then he asked:
“Please confirm that this is still true on curl 8.20.0.”
Wait. 8.20.0? I tested on 8.5.0. Let me check.
I downloaded curl 8.20.0. I searched for the Kerberos protection block.
And there it was.
static bool url_match_auth_nego(struct Curl_easy *data,
struct connectdata *needle,
struct connectdata *check) {
if(wantNegotiatehttp) {
if(Curl_timestrcmp(needle->user, check->user) ||
Curl_timestrcmp(needle->passwd, check->passwd)) {
return FALSE; // Credentials don't match
}
}
else if(check->http_negotiate_state != GSS_AUTHNONE) {
return FALSE; // Has Negotiate state but we don't want it
}
// ... proxy variant ...
}
The fix was already there.
Commit 34fa034d9a from February 2026. Reported by Zhicheng Chen. Fixed exactly the way I suggested.
Someone else found it first.
The Reality Check
I replied to Daniel:
“You’re right. The bug existed in 8.5.0 but was fixed in 8.20.0 by commit 34fa034d9a. I didn’t realize the fix had already landed. Sorry for the duplicate report.”
He closed the report as a duplicate.
No CVE. No credit. No Hall of Fame entry.
Just a learning experience.
What I Actually Learned
Here is the thing. I could look at this as a failure. I spent weeks on this. I built a Kerberos lab. I wrote a full PoC. I did deep code analysis. And in the end, someone else got there first.
But that is not how I see it anymore.
What I Proved To Myself
-
I can find real vulnerabilities. This was not a false positive. Not a misunderstanding. An actual CVE-worthy bug that curl developers took seriously enough to fix.
-
My analysis was correct. The fix they implemented matches what I suggested. The attack I demonstrated was real. My understanding of the code was accurate.
-
I know the full process now. Code analysis. PoC development. Report writing. Communication with maintainers. I have done it once. I can do it again.
What I Would Do Differently
-
Check the latest version first. I tested on 8.5.0 because that is what my system had. I should have verified the bug still existed in the current release before investing weeks into the PoC.
-
Monitor the commit history. If I had been watching curl’s git commits, I would have seen the fix land in February and saved myself the effort.
-
Move faster. I spent too long perfecting the PoC and the writeup. In security research, speed matters. Someone else was hunting the same bug.
What I Do NOT Regret
Building the full Kerberos lab. That knowledge is mine now. I understand GSSAPI. I understand SPNEGO. I understand connection pooling internals. I understand curl’s codebase.
That is worth more than one CVE credit.
The Bigger Picture
Security research is not about getting credit. It is about finding problems before attackers do.
Zhicheng Chen found this bug and reported it. Curl fixed it. Everyone who uses curl with Kerberos authentication is now safer because of that work.
I found the same bug independently. That means the bug was findable. The pattern was recognizable. The methodology worked.
Next time, I will be faster. Next time, I will check the latest version first. Next time, I might be the one who gets there first.
But even if I am not - I still learned the process. And that is the real win.
For Anyone Starting Out
If you are thinking about CVE hunting, here is what I would tell you:
Start with patterns, not randomness. Do not just randomly read code hoping to find bugs. Study past vulnerabilities. Look for patterns. Then check if the same pattern exists elsewhere in the code.
Build the full PoC. Finding the bug in the code is step one. Proving it is exploitable is step two. Exploits force you to actually understand the bug, not just theorize about it.
You will “fail” a lot. Duplicates. False positives. Bugs that are too hard to exploit. Bugs that were already fixed. That is normal. Every security researcher goes through this.
The learning is the reward. Even if you do not get a CVE, you learned. You understand the codebase better. You know the process. You can do it again.
And one day, you will be first.
What Is Next For Me
I am not done with CVE hunting. curl has 188 historical vulnerabilities. That means there are patterns to learn. And if connection pool bugs keep coming back, there are probably more lurking.
Next time I will check the latest version first. Next time I will move faster. Next time I might actually get the CVE.
But even if I do not - I will still have learned something new.
And that is the point.
Glossary
CVE - Common Vulnerabilities and Exposures. A unique ID assigned to security bugs. Getting one with your name on it is a big deal in security research.
Connection Pool - curl reuses TCP connections to save time. Instead of opening a new connection for every request, it keeps old connections alive and reuses them when possible.
Kerberos / SPNEGO / Negotiate - An authentication protocol where the authentication happens on the TCP connection itself, not in HTTP headers. Used heavily in corporate environments.
GSS Context - The security state established during Kerberos authentication. It lives on the connection. If a connection gets reused, the context persists.
NTLM - Another connection-based auth protocol. curl already had protection for NTLM. The same protection was missing for Kerberos.
HackerOne - A bug bounty platform where security researchers can report vulnerabilities to companies. curl has a program there.
PoC - Proof of Concept. Code that demonstrates the vulnerability is real and exploitable.
If you are also learning security research - share your “failures” too. We all have them. That is how we get better.
And if you have questions about this process, find me on.I am still learning too.
Note: All the code, PoC scripts, and technical analysis from this hunt are available in my GitHub if you want to learn from them. The bug was real. The methodology worked. Just someone else got there first. That is okay.