Back in June of this year (2019), I decided to dig into Kahoot and figure out how so-called “Kahoot bots” work – and how one could be made. Instead, I got a bit side tracked. I looked at their login system, briefly, before looking at the game. What I found was fairly surprising.

First of all, none of the endpoints that I tested had any serious rate limiting or attempt to stop bots. No “I’m not a robot” buttons, no CSRF tokens or any requirement like that. Same goes for the page where you enter a game pin. So instead of making two requests (one to the login page, then another to the page that actually talks with the database), I only had to make the login request. Instagram, for comparison, is significantly more complicated, and has better rate limiting. But that’s another post.

Weak rate limiting and no CSRF protection is a big oversight for a modern company. Not only could someone easily make a fake login page to phish credentials, but anyone with a list of username and password combinations – obtained from some other breach, presumably – can easily test them by the thousands. Kahoot has business clients, and some school districts probably pay for the upgraded plans, so it is pretty irresponsible to leave big clients so vulnerable, not to mention the average user.

The login process

With fewer than 100 lines of Python code, I am able to make about 906 requests every hour, per thread, and I’m running 6 threads right now to make full use of my CPU. It also seems to be the maximum amount that is reasonable, without hitting the limiters. These requests are small, so latency is the main limiting factor as far as multi-threading goes. I don’t think I can push it any faster, since their system starts to block requests. But, if I take a short break every few seconds, I avoid the rate limiting, and from my testing, can go on undetected for a long time. They don’t do IP bans either (Instagram does). This means that any time rate limiting kicks in, it is usually only for a few requests before I am unblocked. So, based on my math, if I can do 906 requests per thread, running 6 threads, every hour, then I can do 5436 requests per device. That’s right, per device: if I run this on multiple small VMs in the cloud, I have little doubt that it would run just fine (I didn’t test that theory; it’s expensive and not nice to Kahoot). Yes, there is a chance they actively monitor the number of bad login attempts somewhere, but considering the lackadaisical attitude towards security I’ve seen so far, I don’t think that’s likely.

So, 5436 requests per hour. Pretty slow, right? Kahoot, allegedly, has 70 million MAU, which may include unregistered people who play games. With this code, it would take me one month to check check a database with an equal number of users, assuming I could get about 18 machines running 24/7, and go undetected the entire time. Obviously 18 machines is much more likely to be detected by them, compared to when I’m running one machine, but let’s just do the math: that’s about $250 worth of g1-small Google Cloud Platform Compute instances. I could run that on a free trial!

Can you see why that would be a problem? If people are paying money for your services, they probably don’t want their account to be hacked. They probably expect some of their money to go back into the development of the platform, so vulnerabilities like these can be patched, and maybe even get noticed before any code ships.

What about the game?

I actually started by looking at the local Javascript code behind the game, but I didn’t go very deep into research there. Basically, when you enter a game pin, the system tries to reserve you a space in that game. If the game doesn’t exist, your browser is told to deny access. Otherwise, you can enter a nickname, and play the game.

As I previously mentioned, there is no ReCAPTCHA on this page. In fact, there is no rate limiting whatsoever on the game side. So, I can make unlimited requests to game pins, easily. Worse: the game pins are numbers, so it’s trivial to increment through 6 digit numbers and find codes. These requests send back a simple response, that can be easily read by the user. A 5 line python script does this perfectly. And once you find a good code to use, adding bots through a variety of “Kahoot Smashers” is quick.

While this part of my research is less important to the concept of “security”, it is much more relevant to the concept of game integrity. In my experience, no teacher with a class of 20 students wants to see a game with 50 players. It makes them angry. And if they’re paying for that account, they don’t want their games to always get “smashed”.

Because of how easy it was for me to just check game pins (and accounts), I’d assume that bots are pretty simple to make as well.

So, what can we learn? First of all, maybe even skimming this article would be beneficial. If you know how to use sessions in [whatever language Kahoot is written in, probably Node], and you know how to create random bytes in that language, then you’re capable of making a CSRF token. That alone would slow down anyone trying to creatively use your website, since they would have request the login page if they want to get a valid CSRF token. Then, adding a second roadblock, like this, would slow things down further, and stop most people.

It’s irresponsible to have a massive service online, and yet protect it like it’s a website that you just made for your friends to use. The fact that purchases can be made on the site makes it even worse, not to mention people have (repeatedly) spammed bots with obscene names, which is not what you want if kids are in the room.

On a related note, I think it’s foolish for large companies to declare all brute-force vulnerabilities as “out of scope” for bug bounties. While Kahoot doesn’t have a bounty, at least they accept feedback about bugs. When I contacted an even bigger company about a brute force vulnerability I found, they said it was out of scope, and refused to make any change to their system.