← Back

SQL Injection · 3 Oct 2026

Leaked cookie to a $2,500 SQL injection

A suspicious cookie led to an opportunistic high-impact vulnerability.

This is the story of how I got an SQL injection in a subdomain of the target, starting from a suspicious-looking cookie leaked from the main site.


Finding the program

Until now, I have only hunted on self-hosted programs and not through any of the well-known platforms. I have heard many horror stories of dealing with the triagers there, and I am not a person that likes conflict, so I wanted to avoid that. I was warned by other hunters that this might be a bad idea, as you have no recourse if the company decides to not pay or downgrade your bug, but my experience has been great. I have already submitted several bugs to self-hosted programs and they all replied within 3 days, assigned even higher severities than I expected and paid quickly. So how do I find these programs? Mainly google dorking:

inurl:/bug-bounty "security"

If you put that into Google, you will find websites that host their own bug bounty programs. There is also this great GitHub repo with different variations you can try.

I spent quite some time looking through the different programs before deciding on one. I’m looking, first of all, for a website or product I am actually interested in. When I choose a program, I am planning on spending weeks/months on the same program, so I better find it interesting. Then, I also look for wildcards in scope, such as *.targetcompany.com. That will considerably expand your attack surface, your chaining possibilities and your chance to find bugs. I also look for an application that I am capable of testing fully. An application intended only for large businesses or law firms is something I wouldn’t be able to test much of.

So I chose this marketplace site and started my usual initial testing.


Initial testing and first finding

I always do the same thing when I start looking at a new program. First I run subfinder and waymore (the latter deserves its own post). If you do this, make sure you add your API keys to the config files. There are many free API keys that will considerably increase the number of results you get, such as Chaos (ProjectDiscovery), VirusTotal and AlienVault. All the ones I have in there are free ones, you just need to create accounts on the respective websites. So I leave this running, open Burp Suite, then I start using the app as a regular user would. I create an account, I navigate across the site, I try to explore as many surface-level functionalities as I can. Always check the program rules to see if you need to add a header to your requests, like X-Bug-Bounty: youremail, and do this with BurpSuite Match and Replace.

After using the app for some time, I go to the Proxy History on Burp Suite and start learning how the app works. Do they use REST, gRPC, GraphQL? JWT or another form of authentication? What endpoints are they calling, what cookies are they sending, what headers does the server send back?

Looking at the request cookies, I see among all the usual tracking cookies, a suspicious-looking one: CF_Authorization. What is CF, and what is this cookie doing?

CF_Authorization=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImE0YzEyM2IxNjEyZGQyNzJkMTM3MWMxNzE0OWQ0Mzk1MzZiMzIxNmZkYWVlYjk3NTcyOWZhZTkyM2Q1YTRmZDEifQ.eyJ0eXBlIjoiYXBwIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwODY0MDAsImlzcyI6Imh0dHBzOi8vdGFyZ2V0Y29tcGFueS5jbG91ZGZsYXJlYWNjZXNzLmNvbSIsInN1YiI6IiIsImF1ZCI6IjJhYWJmZTIyOGYyMTllOWNiMGViNTNmMTY5NDdjY2YyNWVjODRkOGRiYzc0MjU0NzcwZjU4OTA0ZGJhNDFlY2MiLCJjb21tb25fbmFtZSI6ImNjM2ZjMTYyNmU1M2ExMzA0M2IwMjZjNDhiYmYzM2ZlLmFjY2VzcyIsImhfSU5URVJOQUxfRE9fTk9UX1VTRSI6ImNvbnRlbnQudGFyZ2V0Y29tcGFueS5jb20ifQ.99NksnRH9ucAUsdMlHUvTCQCyEZDz_TddJ8HyS5SUkCnD8zRA9a9SkpXz9w3QlY7Zkuvqdt7s8Stqcbnr3yBdGBLEPH1qhT61qtc4xatws8phP9nhFyJfm5di4PzJ59FHz5r1pY4OjE2jBMptUsGr7CmY-uCu3ZR1zTOlUcR64cXQLioDnkHIfxIq2HZt_PlJhx2jIclHkCiHp6bR1IqfEouHgxzNNAL5wIScGebcy8F5n3_YNBDRzrZSgqbjG3uhkWKFLf6xuI5aHUQPFeNBTxaQWk8JzFalHlsZfYcMMDktXP_tKsf2rcDkdfrUnW5gcF-Ha6ili8GjHEAD6_Wj9

Anything that starts with ey is likely a JWT, and I always like to decode those and look at their claims. You can do this with jwt.lannysport.net. I also wrote my own Decoder tool, because I’m always using just those few features and like to have everything in one place (and I find BurpSuite’s Decoder somewhat clunky).

Putting the value of the cookie into the Decoder tool gave me this:

Header
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "a4c123b1612dd272d1371c17149d439536b3216fdaeeb975729fae923d5a4fd1"
}

Payload
{
  "type": "app",
  "iat": 1700000000,
  "exp": 1700086400,
  "iss": "https://targetcompany.cloudflareaccess.com",
  "sub": "",
  "aud": "2aabfe228f219e9cb0eb53f16947ccf25ec84d8dbc74254770f58904dba41ecc",
  "common_name": "cc3fc1626e53a13043b026c48bbf33fe.access",
  "h_INTERNAL_DO_NOT_USE": "content.targetcompany.com"
}

Very alarming-looking claim. h_INTERNAL_DO_NOT_USE and a subdomain almost look like a CTF thing, but it was real. If it said anything else, I would probably not have looked twice at it.


The internal subdomain

The first thing I do is obviously browse to the subdomain in the cookie. I get presented with a Microsoft login which only works with a @targetcompany.com email.

Microsoft sign-in page requiring a @targetcompany.com account

It is a rule of cookie domain-scoping that a cookie set on a parent domain should work on its subdomains too (when its Domain attribute is set, which it was). So I attach the CF_Authorization cookie to my requests through a Match and Replace rule in Burp, and navigate again to the internal subdomain. The Microsoft login has been bypassed.

I would attach a screenshot of what was shown here, but it was the same homepage as the main site.

So what was different about this internal site, if it looked exactly the same as the main one? I proceeded to fuzz directories on it, and found the WordPress endpoints.

WordPress REST API returning the full list of users Wp-users wide open

This site was serving the WordPress content to the main site, which worked through a proxy. I proceeded to enumerate as much as I could, noting users, endpoints, versions. But nothing I found was vulnerable.

So, I could access and enumerate things that were not intended to be public. Should I report this? No! This is like when you find an open redirect. If you report it, and you are lucky, you may get a few bucks and a thank you for it (but most likely, an Informational result), but then you lose this great primitive that you could eventually use in a chain to get a really impactful bug, like an Account Takeover. It is better to keep it in your pocket!

So that’s what I did, and I saved it in my folder named primitives.


The wp2shell news

About two weeks have passed since I found the Microsoft sign-in bypass. Every day I read The Hacker News and (sometimes) X to see what’s going on in the cybersecurity world. It was July 17th, and the world was freaking out about this huge vulnerability in WordPress core that led to SQL injection and RCE. When I found out, the news was a few hours old, so I knew for sure my exposed internal WordPress endpoints had to be vulnerable, especially since they were behind a login, there was less chance that they would have rushed to fix it. It was also Friday night!

There was already an online tool to find out if your site was vulnerable, and obviously my target said Not vulnerable, but I knew that wasn’t true, it was just behind walls. So I started searching for the PoC of wp2shell, and at the time, everywhere it said that the PoC is not available publicly and cannot be found online, but in my experience, this is never true. If you dig hard enough, you can always find it.

So now I had the PoC, and open access to the vulnerable endpoint. I prepare a benign time-based payload, and fire it. That’s when I encounter the second hurdle: CloudFlare blocked me.

Cloudflare "You have been blocked" page

CloudFlare is a web application firewall that blocks requests that carry certain signatures, such as SQL queries or XSS payloads. However, there are techniques to bypass it. There is this technique discovered by Shubham Shah (of whom I am a fan) in this GitHub repo. It consists of inserting junk data before the malicious payload, and because WAFs have limitations for how much data they can process from a request, it is that simple to bypass them.

So all I had to do was create a new ‘padding’ key in the JSON request with about ~130kb of filler, change Content-Type to application/json, and the request sailed through.

{
  "padding": "<~130KB OF FILLER>",
  "requests": [
    { "method": "POST", "path": "///" },
    {
      "method": "POST",
      "path": "/wp/v2/posts",
      "body": {
        "requests": [
          { "method": "POST", "path": "///" },
          { "method": "GET", "path": "/wp/v2/users?author_exclude=<SQL INJECTION HERE>" },
          { "method": "GET", "path": "/wp/v2/posts" }
        ]
      }
    },
    { "method": "POST", "path": "/batch/v1", "body": { "requests": [] } }
  ]
}

The batch request, with the padding key.


Confirmed injection

The SQL injection was confirmed working with a time-based payload. Usually, this can be enough for a report, but I like to provide tangible proof, such as database name, version and a few table names, to make my report stronger.

So I changed my payload to a boolean-based one (it’s faster) and proceeded to carefully extract exactly that.

In this type of SQL injection, the server can’t just respond with the actual answers in full, but it can respond to yes/no questions (via responses that read either true or false).

To pull the name of the database, I first work out how long it is. I ask it the first yes/no question: is the length greater than some number? The server comes back true or false, and I halve the range with every answer until I have the exact length. Then I go one character at a time. For each position I ask whether its ASCII value is above a certain number, and again I halve the range with each answer until only one value is left, revealing the character. (ASCII is just a table that gives every character a number. Lowercase a is 97, for example.)

For example, this means ‘Is the first character of the database name w?’:

0) AND (ASCII(SUBSTRING((DATABASE()),1,1))=119)-- 

It had to be URL-encoded before sending, so what I actually sent as a payload was:

0%29%20AND%20%28ASCII%28SUBSTRING%28%28DATABASE%28%29%29%2C1%2C1%29%29%3D119%29--%20

The server will tell me true or false.

Of course, once the exact mechanism is determined, I create a script to do this, I don’t do it manually, or it would take ages.

Once I had enough, I closed everything and proceeded to write the report that same night.


Writing the report and result

I like to start my reports by immediately presenting the proof (after a quick hello). I imagine they get a lot of reports screaming CRITICAL VULNERABILITY! that turn out to be nothing, so leading with the proof shows them right away I’m not wasting their time.

The report I sent, leading with the proof section (redacted)

The one thing I regret and I am a bit embarrassed about, is including that description about what an SQL injection is and how serious it is. Obviously their engineering team already know that. I don’t know why I wrote that, but I was rushing with all the adrenaline of having found an SQL injection (my favorite bug type) and it was also 12 am.

I actually almost didn’t send the report. Many programs have policies where they do not consider reports about 0-day vulnerabilities until after a reasonable amount of time has passed, usually 15 days or more. I checked the bug bounty policy of this program again, and it didn’t mention anything about 0-days. I decided to send it, but had doubts whether they would reward it because of this. In any case, I was happy about my finding.

They replied the next Tuesday afternoon, with the reward amount: $2500.

The program&#x27;s reply awarding the $2500 reward (redacted)

Doesn’t seem like a lot compared to what other hunters are getting out there, but that’s a P1 on their program, and for the amount of time of actual work it took me (about 6-8 hours?), I was really pleased. They made the deposit two days later.


Closing thoughts

In bug bounty, I’ve learned that it is important to be patient and really think about the impact of a bug. It is not a numbers game, but an impact game. If there is no tangible impact, and it’s just a primitive that “someday, somebody could maybe exploit, if the conditions are right”, I’d rather not report it and keep the primitive for myself. I save mine in a primitives folder with all the others, and new ideas sometimes come to me as I look through them and find ways to chain them, which gets you the highest bounty instead of a P4.


Comments

← Back