If you are a geography/history nerd, or just a trivia nerd like me, you would have heard about the JetPunk website. It's a really great trivia website with all kinds of quizzes in various categories. I am addicted to the website, so much so that I rank among the top 0.4% users of the website in terms of my rank which is achieved through gaining points from solving quizzes.
Now, this blog is not about JetPunk, I just wanted to introduce it's relevance. Today, using JetPunk, we will learn about the web in a slightly different way. This may be trivial for some but for me this was a new way of implementing everything I have learned through web security. Another footnote I'd like to add is, this is not a security concern or hole on JetPunk's end but merely an attempt on clickbait by me to lure readers in. What I have discovered is that JetPunk's quizzes can be solved instantaneously through console scripts, as all the answers are pre-loaded with the quiz page.
It started one day, when I was using JetPunk with a really slow internet. I noticed all my answers were still instantaneously validated which confused me and made me realize the fact that they are not doing any server communication for this. I used to believe that each answer was validated by a server to prevent users from cheating but this was not the case. I quickly confirmed my hypothesis by checking the Network tab on each attempt:

If we ignore all the other junk, there is no request that looks like an API backend validating the answers. It had mainly loaded some static files till now. I went through each request to confirm this as well. Now my suspicion was confirmed. I just had to look for where the answers were. If the answers were still being validated entirely on the frontend, then that meant the logic had to exist somewhere in JavaScript, since that is what powers dynamic behavior in the browser.
I still had to find some keyword to search for that logic since I was not going through hundreds of lines of obfuscated JavaScript. One unique thing I noticed was that I can use the prompt that the website shows for typing in incorrect answers and search for that in the JavaScript files:

Searching for 'Not a correct answer' in the JS file led me to the logic where it's validating our answers. There were two JS files in the Sources tab in our browser:

I found the search in the text-game.js file, this was the code excerpt:
b(this, "keydownAnswerBox", (t => {
if (9 != t.which) {
if (13 == t.which && "phone" != this.page.device) {
var e = this.getMatchedAnswers(!0);
0 == e.length ? "" != H.normalizeUnicode($("#txt-answer-box").val()) && $("#already-guessed").html(x.translate("Not a correct answer")).show() : $("#already-guessed").html(x.translate("Already guessed!") + this.getAlreadyGuessedText(e)).show()
}
} else
t.preventDefault()
}This pointed me towards a function called getMatchedAnswers() which appeared to be supplying answers to the e variable.
getMatchedAnswers() {
let t = arguments.length > 0 && void 0 !== arguments[0] && arguments[0];
var e = []
, s = H.normalizeUnicode($("#txt-answer-box").val())
, i = this.quiz.yellowBox ? [this.yellowBox.getIdxAnswer()] : this.state.answers;
for (var a of i)
t === a.guessed && this.typeinsMap[a.id].test(s) && e.push(a);
return e
}I found another interesting variable here which was this.state.answers. It was either storing the answers provided by the user or the global array of correct answers. If you've done object-oriented programming, you would know that this refers to a variable defined within the class. I scrolled up to the class definition to see any variables being initialized in the constructor but it was not present.
const {BaseGame: N, Language: x, Overlay: A, PopAnimation: S, Request: O, TypeinsUtil: H, Util: Y} = JetPunk.shared;
class R extends N {
constructor(t) {
var e;
super(t),I have cutoff the rest of the constructor function to focus on the main code here. The R class extends the N class and in its constructor definition, takes in an argument t, and then calls it's parent class' constructor. Since this class did not define this.state.answers, I looked for the N class. The search for the term 'class N' did not yield any results, but I found a definition in the code above which explains the class BaseGame is class N. The next step was then to look for this class in the other file, 'shared.js'. I first searched for 'BaseGame' to find this excerpt:
s.d(a, {
BaseGame: () => L,
DateUtil: () => j,
Language: () => n,
LocalStorage: () => Y,
NavDropdown: () => o,
Overlay: () => b,
Page: () => ae,
PopAnimation: () => q,
Request: () => f,
Scroller: () => M,
SuperTable: () => k,
Timer: () => O,
TypeinsUtil: () => ne,
Util: () => g,
confetti: () => r
});Here, BaseGame is essentially referenced as L. Searching for class L gave us the following class definition:
class L {
constructor(e) {
var t;
R(this, "refreshSuggested", ( () => {
$("#btn-refresh-suggested").prop("disabled", !0),
f.go({
url: "/api/get-suggested.php",
data: {
qid: this.quiz.id,
language: this.page.language,
ignore: JSON.stringify(this.suggestedIds)
},
loading: "DONOTLOAD",
hideErrors: !0,
callback: e => {
$("#btn-refresh-suggested").prop("disabled", !1),
$("#suggested-quizzes").html(e.html),
this.suggestedIds.push(...e.ids)
}
})
}This was truncated, but the constructor variable t can be found further inside. At this point, I was still searching for our variable this.state.answers, when I came across::
this.quiz = e.data.quiz,
this.state = this.createGameState(),
this.page.rateQuiz = new A(e),The function I now had to focus on was createGameState(). Let's examine it:
createGameState() {
var e = this.quiz
, t = {
paused: !1,
pausesLeft: 3,
untimed: !1,
timeUsed: 0,
numGuessed: 0,
active: !1,
finished: !1,
answers: [],
supplemental: null
};
for (var s of e.answers)
s.guessed = !1,
"v" == e.whatkind && (s.display = s.cols[e.answerColIdx]),
["m", "mc", "c"].indexOf(e.whatkind) >= 0 && (s.whichChosen = null),
"ts" == e.whatkind && (s.selected = !1),
t.answers.push(s);
for (var s of (t.answersById = {},
t.answers))
t.answersById[s.id] = s;
return t
}We then see a loop over e.answers:
t.answers.push(s);
this.quiz = e.data.quiz,At this point, s represents each element inside e.answers, where e is this.quiz. This makes it clear that this.quiz.answers is being copied into this.state.answers, with some additional initialization done on each element. Now, by looking at the trail of constructors and their parameters, it can be understood that this.quiz is ultimately passed through class initialization. To better visualize this, this is the flow of variables:
- Class
L/BaseGameassignse.data.quiztothis.quiz, whereeis the constructor argument. - Class
RextendsL/BaseGameand calls its parent constructor usingsuper(t), wheretis its constructor argument. - Another class
UextendsRand also callssuper(t)in its constructor.
Since U did not appear to have any child classes, the next step was to look for where it was initialized.
Searching for new U gives us:
return $(document).ready(( () => {
_page.controller = new U(new z(_page))
}Here is our answer. _page is the main data object containing all the information we had been tracing through the constructors. Since the resulting object is assigned to _page.controller, we can directly access the answers using:
_page.controller.state.answers
OR
_page.controller.quiz.answers
Even though we have the answer, what's this _page? It's a global JavaScript object embedded directly in the HTML served by the server. If it's JavaScript embedded in HTML then it must be in the <script> tags. Let's look at the HTML and there it is:

You could go further and write a script to automatically solve every quiz, but I personally find that unethical and it takes away the fun of solving them legitimately. In hindsight, this entire process could probably have been shortened by simply searching for the answers directly in the HTML from the start, but the reverse engineering process itself was fun and taught me a lot.