Reverse Engineering to Hack a Quiz Website's Highscores

2020-03-27

Backstory

Today my friend sent out a link:

Text invitation my friend sent me

Unfortunately after my first attempt I only got one question right... putting me at the bottom of the highscores 😢.

I got a bad highscore

This couldn't stand so I got to work.

Getting Started!

Instead of just going back through the quiz and inputting the right answers I decided to look at how the quiz was setup.

I immediately hopped to the chrome inspector tab and checked out the sources in the page.

Sources of webpage

The first thing to stand out to me was that under > www.gstatic.com a link to firebasejs/4.10.1 was present.

This indicated to me that all these results were likely written to a firebase database somewhere.

Investigating Their Firebase Setup

When used properly firebase is super secure.

But... this website also had a ton of onclick attributes set in the raw html - so it seemed likely that the creator may have made a mistake.

First I decided to investigate which firebase modules were loaded into the page.

Firebase was loaded into the page

Cool, so it looks like they just use the auth module and the Realtime Database.

I did some more poking around but didn't find any other useful information.

The page source

The page source consisted mainly of a ton of html elements used to construct the quiz, code for google analytics, facebook analytics, bootstrap, jquery, and a few other third party libraries.

I pretty much instantly stumbled upon what I assumed to be the source code: Obfuscated javascript

This looks like obfuscated source code if I've ever seen it.

Reverse Engineering

I dumped it into a file on my desktop and formatted it with js-beautify for further inspection.

The first few lines were like this:

var _0xb463 = ["\x41\x49\x7A\x61\x53\x79\x42\x74\x56\x41\x5A\x78\x67\x36\x4C\x67\x43\x31\x4B\x6E\x35\x6B\x30\x66\x6A\x31\x78\x46\x70\x64\x30\x75\x4B\x53\x36\x46\x51\x4E\x6F", "\x6E\x65\x77\x71\x7A\x69\x6E\x67\x6F\x2E\x66\x69\x72\x65\x62\x61\x73\x65\x61\x70\x70\x2E\x63\x6F\x6D", "\x68\x74\x74\x70\x73\x3A\x2F\x2F\x6E\x65\x77\x71\x7A\x69\x6E\x67\x6F\x2E\x66\x69\x72\x65\x62\x61\x73\x65\x69\x6F\x2E\x63\x6F\x6D", "\x6E\x65\x77\x71\x7A\x69\x6E\x67\x6F", "\x6E\x65\x77\x71\x7A\x69\x6E\x67\x6F\x2E\x61\x70\x70\x73\x70\x6F\x74\x2E\x63\x6F\x6D", "\x32\x35\x35\x33\x39\x32\x37\x33\x38\x31\x39\x32", "\x69\x6E\x69\x74\x69\x61\x6C\x69\x7A\x65\x41\x70\x70", "\x68\x72\x65\x66", "\x6C\x6F\x63\x61\x74\x69\x6F\x6E", "\x3F\x71\x3D", "\x69\x6E\x64\x65\x78\x4F\x66", "\x73\x75\x62\x73\x74\x72", "\x68\x69\x64\x65", "\x23\x6C\x6F\x61\x64\x69\x6E\x67\x5F\x70\x61\x67\x65", "\x23\x6E\x61\x6D\x65\x5F\x70\x61\x67\x65", "\x23\x70\x61\x67\x65\x5F\x67\x69\x76\x65\x5F\x70\x6F\x6C\x6C", "\x23\x70\x61\x67\x65\x5F\x71\x75\x65\x73\x74\x69\x6F\x6E\x65\x72", "\x73\x68\x6F\x77", "\x23\x65\x72\x72\x6F\x72\x5F\x70\x61\x67\x65", "\x70\x75\x73\x68", "\x68\x74\x74\x70\x73\x3A\x2F\x2F\x62\x75\x64\x64\x79\x6D\x65\x74\x65\x72\x2E\x63\x6F\x6D\x2F\x63\x72\x65\x61\x74\x65\x2E\x68\x74\x6D\x6C", "\x6C\x65\x6E\x67\x74\x68", "\x63\x6F\x64\x65", "\x6D\x65\x73\x73\x61\x67\x65", "\x63\x61\x74\x63\x68", "\x73\x69\x67\x6E\x49\x6E\x41\x6E\x6F\x6E\x79\x6D\x6F\x75\x73\x6C\x79", "\x61\x75\x74\x68", "\x75\x69\x64", "\x76\x61\x6C", "\x74\x68\x65\x6E", "\x76\x61\x6C\x75\x65", "\x6F\x6E\x63\x65", "\x2F\x75\x73\x65\x72\x73\x2F", "\x2F\x71\x75\x69\x7A\x65\x73\x5F\x67\x69\x76\x65\x6E\x2F", "\x2F\x73\x63\x6F\x72\x65", "\x72\x65\x66", "\x64\x61\x74\x61\x62\x61\x73\x65", "\x2F\x73\x74\x61\x74\x75\x73", "\x2F\x71\x75\x69\x7A\x5F\x63\x72\x65\x61\x74\x65\x64\x5F\x69\x64", "\x6F\x6E\x41\x75\x74\x68\x53\x74\x61\x74\x65\x43\x68\x61\x6E\x67\x65\x64", "\x2F\x71\x75\x69\x7A\x65\x73\x2F", "\x63\x68\x69\x6C\x64", "\x71\x75\x65\x73\x74\x69\x6F\x6E\x73", "\x63\x6F\x75\x6E\x74\x72\x79", "\x55\x4B", "\x6C\x61\x6E\x67\x75\x61\x67\x65", "\x45\x4E", "\x61\x6E\x73\x77\x65\x72\x65\x72\x73", "\x4E\x6F\x20\x6F\x6E\x65\x20\x68\x61\x73\x20\x67\x69\x76\x65\x6E\x20\x74\x68\x69\x73\x20\x71\x75\x69\x7A\x20\x79\x65\x74\x2E", "\x74\x65\x78\x74", "\x2E\x6E\x6F\x74\x65\x5F\x66\x6F\x72\x5F\x73\x63\x6F\x72\x65\x62\x6F\x61\x72\x64", "\x46\x52"
...
// and on, and on, and on

Then it opened up into some firebase initialization calls and function calls.

There was a ton of initialization logic and a ton of random other UI related logic.

I went back into the web UI to figure out what function was called on button click:

luckily... they left it in plain sight in the onclick attribute.

<span class="option_text" id="option_1" onclick="option_clicked(1)">Beer</span>

So I went back to the source and looked for option_clicked(1).

Options clicked function

Hoorah!! Looks like the button here sends some sort of information to firebase - I went through and started tracing the logic in the function send_vote_to_firebase.

function send_vote_to_firebase(_0x932cx39, _0x932cx30) {
    var _0x932cx3a = {};
    _0x932cx3a[_0xb463[156] + userID + _0xb463[33] + pollID + _0xb463[157] + _0x932cx39] = _0x932cx30;
    var _0x932cx3b = firebase[_0xb463[36]]()[_0xb463[35]]();
    _0x932cx3b[_0xb463[127]](_0x932cx3a, function(_0x932cx14) {
      if(_0x932cx14){}
    })
}

This was actually a red herring that I won't discuss anymore.

Luckily, there was another function right below it - send_score_to_firebase.

It looked as though I had struck gold!

function send_score_to_firebase(score) {
    var _0x932cx3a = {};
    _0x932cx3a[_0xb463[158] + pollID + _0xb463[159] + userID + _0xb463[160]] = answerer_name;
    _0x932cx3a[_0xb463[158] + pollID + _0xb463[159] + userID + _0xb463[34]] = score;
    var _0x932cx3b = firebase[_0xb463[36]]()[_0xb463[35]]();
    _0x932cx3b[_0xb463[127]](_0x932cx3a, function(_0x932cx14) {
        if (_0x932cx14) {} else {}
    })
}

I quickly figured out what all of this code evaluated to by extracting the indexed chunks from the mega array at the beginning - _0xb463 - and started popping my own variable names in.

I was left with:

function send_score_to_firebase(score) {
  nameKey = "quizes/" + pollID + "/answerers/"+ userID + "/name"
  scoreKey = "quizes/" + pollID+ "/answerers/ " + userID + "/score"
  result = {}
  result[nameKey] = "luke";
  result[scoreKey] = score;
  var database = firebase.database().ref();
  database.update(result, function(e) {
    console.log(e);
  })
}

I tried running this in the console and it actually didn't work due to some scoping issues - but I reverted to this:

function send_score_to_firebase(force_name, force_score) {
    var _0x932cx3a = {};
    _0x932cx3a[_0xb463[158] + pollID + _0xb463[159] + userID + _0xb463[160]] = force_name;
    _0x932cx3a[_0xb463[158] + pollID + _0xb463[159] + userID + _0xb463[34]] = force_score;
    var _0x932cx3b = firebase[_0xb463[36]]()[_0xb463[35]]();
    _0x932cx3b[_0xb463[127]](_0x932cx3a, function(_0x932cx14) {
        if (_0x932cx14) {} else {}
    })
}

and it worked like a charm.

Results

First I tried giving it numbers above the maximum possible score but I got things like:

⚠️ FIREBASE WARNING: update at / failed: permission_denied

Then I thought to myself:

Their security rules probably look something like this:

allow write if score < 10

By this logic decimal numbers in javascript would pass.

so... I tried the number 9.6969696969 and it worked! Hackerman gif

This was really exciting! Here are the highscores now.

My illigetamete highscore