How I won the Hackathon, almost fainting

How I won the Hackathon, almost fainting

A few weeks ago, my friend and I




Hackathon held at Tufts University

Our project

was an app that, Spotify Wrapped-style, generates a report on university dining experiences among students based on data from the Meal Plan section of the service payment portal. Thanks to

competent promotion

project Ben, we were able to attract hundreds of students to use our program in just a couple of days. As a result, we won in

general nomination

and also in the nomination “

the most complete project

” and became

absolute winners


A live demo of our project is here.

Tufts University, like Brown University and many other universities, uses patent-expired Atrium software that is older than both of us. In its marketing statements, the company assures potential users that, unlike legacy systems, this software will only give them “excited” feelings. But as we’ll see later, Atrium’s claim that their software is not “legacy” is overblown.

Our university’s payment portal is called JumboCash and is located at (www mandatory, otherwise the site will not load). However, the final site that is loaded is determined by the parameter cid (Campus/College/Client ID?). Here is the Brown University portal that loads through the Tufts domain: You can use the bare IP: in its entire http:// beauty

Another web development crime: similar to how I attached this photo, the JumboCash portal header is downloaded (hotlinked) from a long-defunct student organization site hosted on

In fact, this portal is a wrapper around ancient software, whose rotting body is visible in stupid decisions and unstable XML responses.

When logging into JumboCash, the first thing Ben and I noticed was that the session key was stored as a URL parameter alongside cid.

As long as the cookie is set, it is ignored. You can copy the URL into a private browser window (or change the domain to another school) and it will continue to work. We delete the parameter


– It stops working. Change the IP address and it crashes? It’s obvious that some kind of security measures have been taken, and it all looks too complicated to be the result of developer ignorance.

▍ Guest access

And since we couldn’t ask students to copy the URL (and with it the session key), we needed another way to access their payment history. Then we turned our attention to the guest access feature of JumboCash.

Apparently, it was intended so that parents could re-enter the children’s accounts and replenish their balance. This feature allows students to give access to their account through their email and set a “presumably” exact set of permissions for guests. I say “as if” because guests can change their permissions and even add other guests. “Disabling” a guest’s access does not actually limit anything, and guests can re-enable it themselves.

When new guests are added, they are sent an email with a password. We bought a great domain and asked students to add an address to their accounts [email protected]. As a result, we intercepted emails using Postmark and extracted passwords, after which we executed a script for authorization and scraping the necessary information.

Letters from JumboCash are almost identical

When I started scraping the payment history, we were lucky in one way: the portal allowed us to export the data in CSV format, which made the task a little easier. However, the HTML table, which actually represents the XML, still caused trouble.

▍ The last test

Our decision to add a guest to the “wonderful” Atrium portal has already created enough tension that we decided not to ask students for their e-mails. Later it became clear that it was worth gathering them from the very beginning and giving students individual addresses.

to make everything go smoothly.

Now, to implement this “magic” trick, we needed to determine the email addresses of the students based on their names, since the portal only allows such. After an unsuccessful attempt to scrape the Outlook directory, we turned our attention to the public directory, which contains the names and emails of all students. It makes a simple request to a JSON API, which is a wrapper around one or more LDAP (Lightweight Directory Access Protocol) directories. Unfortunately, it turned out not to be so simple.

The JumboCash portal displays the full name of the student on the passport (for example, Benjamin, not Ben). The catalog can be searched only on the basis of names specified by students. And although they are not displayed in the interface, the API returns full names in the format Lastname, Firstname M.

Initially, we wanted to implement a directory search by last name and use the levenshtein() function of the PHP standard library to find a match. But this approach proved problematic for common or short surnames – for example “Li” is not only popular, but also found in many Julian or Colin names, resulting in dozens of matches being returned. Because the pagination of the directory was broken (I have an idea “why”, but the problem seems to have been fixed), we were unable to iterate through the pages to find matches. As a result, as a workaround, we just started trying different formats like Lastname, F. Lastname and so on, choosing the closest name. This approach has worked … in a fair number of cases.

▍ Authorization

It took two HTTP requests and four hours of reverse engineering to actually authenticate to JumboCash. I spent so much time on this activity that Ben, who had already finished scraping JumboCash for

previous project

began to work in parallel on the implementation of a backup solution with the help of



But first I will explain how authentication works. on /login.php is leaving POST– request from username and loginphrase, which creates a session key. Then GET-A request sent to the same page “activates” this token. If the second request fails, the UI ends up in a strange “half-authorized” state.

First request instead of header Location returns a JavaScript fragment (<script type="JavaScript">, to be exact) to redirect to another page. The second page (also /login.php with only a few URL parameters) contains a loader and sloppy JS code.

Spinner, unceremoniously copied from some tutorial:

<!-- загрузчик -->
<div class="loader">
    <div class="inner one"></div>
    <div class="inner two"></div>
    <div class="inner three"></div>
<!-- здесь можно указать сообщение -->

For all its abnormality, this dance with a tambourine would not have taken long hours if one of the instruments had not lied to us. When probing a site via HTTPie, the HTML returned is displayed first


-request, and the JS code contained in it is also executed. However, even though the HTTPie web view was also making the request (like a normal browser without our headers), it wasn’t displaying its content.

We were puzzled as to why session keys generated using HTTPie requests worked and those generated even using cURL did not. As it turned out, HTTPie was secretly executing a second “activating” request. Its webview probably shouldn’t be executing JavaScript, especially without displaying the result (it would still be great if the webview stayed disabled instead of being enabled on every request).

JavaScript when intermediately “loading” the screen before polling the XML API for status skey responsibly checks whether the browser supports the newfangled API XMLHttpRequest. And although you can’t do anything with concatenation in this script (shown below), I was lucky to find XSS. It was pretty fun considering the number of domains. .eduon which this software is running.

Roll to the end! There are the lowest!

var isIE = false;
var req;
var messageHash = -1;
var targetId = -1;
var centerCell;
var size=40;
var increment = 100/size;
var attempts=0;

function pollTaskmaster() {
    var url = "login-check.php?cid=248&skey=3620efdb97b88d111e8ec5388244e978"; 

function validate(url) {
    if (window.XMLHttpRequest) {
        req = new XMLHttpRequest();
    } else if (window.ActiveXObject) {
        isIE = true;
        req = new ActiveXObject("Microsoft.XMLHTTP");
    }"GET", url, true);
    req.onreadystatechange = processPollRequest;

function processPollRequest() {
    if (req.readyState == 4) {
        if (req.status == 200) {
            var message = req.responseXML.getElementsByTagName("message")[0];
            if (!message) {
            var remotestatus = message.childNodes[0].nodeValue;
            if (remotestatus == 1 )
            if (remotestatus == -1 )
        } else {
            window.status = "No Update ";
        window.status = "Processing request...";    
        setTimeout("pollTaskmaster()", 5000);

function gotobadpage() {
setTimeout("pollTaskmaster()", 2000);
setTimeout("gotobadpage()", 300000);

▍ Victory in JumboHack

Our site turned out to be responsive, but not particularly so

After dealing with the reverse engineering, we were finally able to relax and have fun creating the product itself. We made the design bright and expressive, similar to Spotify Wrapped. Considering how much time we spent on everything else (including a healthy 8-hour sleep), I think it turned out pretty well.

The program interface works like Instagram stories

We put up flyers around campus that Ben wrote about in his article and made some anonymous posts on Sidechat pretending to be regular users. The reaction to the posts turned out to be quite viral, and the project URL attached to each slide attracted hundreds of students who also want to use our service.

Overall, JumboHack was fun! A subway ride from Providence (only $10!), hanging out with Ben, and coming home with AirPods is a great way to spend President’s Day weekend.

Postcard in a few weeks

Telegram channel with discounts, prize draws and IT news 💻

Related posts