Write-up Santa's Magic Sack
3 décembre 2024, XMAS 🎅 root-me démarre avec une épreuve web "Santa's Magic Sack", on attaque!
Santa has developed a web game where you have to catch presents, and as luck would have it, he's come out on top of the scoreboard. Can you beat him?
Rendez-vous sur l'épreuve
Long story short, la page d'accueil nous demande de renseigner un speudo et nous donne quelques instructions sur le jeu auquel on va jouer. Le jeu lui-même est un classique, on déplace une hotte et on essaie de récupérer des cadeaux qui tombent du ciel. Il faut en récupérer le plus possible pour gagner et cela sur 20secondes. À chaque cadeau récupéré, on gagne des points. Une fois le temps écoulé, on voit notre score, un message qui nous indique notre score puis on voit la scoreboard. Et on recommence...
Le but est de faire le meilleur score et franchement vu le rythme du jeux pour battre le premier à 133337, faut s'accrocher.
On essaie de faire le malin, que se passe-t-il si on réduit la largeur de la fenêtre ? On double à peu près notre score mais ce n'est pas suffisant.
Qu'est-ce qu'il y a sous le capot ? On ouvre les outils de développement et on regarde les requêtes qui sont faites.
On voit une requête POST qui est faite à la fin du jeu vers le endpoint /api/scores
, on regarde les paramètres envoyés et on voit un paramètre data
qui est envoyé.
data
contient une string encodé en base64, on la décode et on voit un objet sérialisé, il doit contenir notre score, c'est sûr!
À ce moment là de mon investigation, j'ai perdu un peu de temps à vouloir désérialiser l'objet, et je me suis rendu compte que c'était inutile.
Quand on fouille un peu les sources du site via la console web, on y trouve un seul fichier javascript index-<HASH>.js
.
On porte nos recherche sur le endpoint /api/scores
et on trouve, localisé proche les unes des autres, des fonctions qui portent sur l'envoie de la requête POST et sur l'encodage notre objet.
On aura même du mal à passer à côté de la constante S4NT4_S3CR3T_K3Y_T0_ENCRYPT_DATA
qui est certainement la clef de chiffrement utilisée.
var Md = hf.exports;
const gf = Rf(Md)
, Ud = "S4NT4_S3CR3T_K3Y_T0_ENCRYPT_DATA";
function Wd(e) {
const t = JSON.stringify(e);
return gf.AES.encrypt(t, Ud).toString()
}
function $d(e, t) {
const r = Math.floor(Math.random() * 9) + 1
, n = `${e}-${t}-${r}`;
return {
checksum: gf.SHA256(n).toString(),
salt: r
}
}
async function Vd(e, t) {
const {checksum: r, salt: n} = $d(e, t)
, l = Wd({
playerName: e,
score: t,
checksum: r,
salt: n
});
try {
return await (await fetch("/api/scores", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
data: l
})
})).json()
} catch (i) {
return console.error("Error submitting score:", i),
{
success: !1
}
}
}
Je prends deux/trois minutes pour souffler dans un sac en papier pour évacuer la frustration de devoir faire du reverse engineering sur un jeu de Noël, et on reprend.
Ou est-ce que je peux intercepter le score avant qu'il soit encodé, envoyé et utilisé par le backend ?
On poursuit l'analyse des sources et on finit par trouver ce bout de code :
const f = async () => {
p.current = !0,
h.current && cancelAnimationFrame(h.current);
const v = s.current;
m(!0);
try {
const C = await Vd(e, v);
C.isNewRecord && C.flag && y(C.flag)
} catch (C) {
console.error("Error submitting score:", C)
}
setTimeout( () => {
m(!1),
t(v)
}
, 5e3)
};
Si on poursuit l'analyse, on voit que la fonction Vd
finit par aller jusque l'encodage du score.
Vd(e, v)
prend deux paramètres et la variable v
est initialisée juste au dessus, qu'y a-t-il dedans ?
🎅Hohoho! On trouve la même valeur que notre score!
On va pouvoir tenter de poser un breakpoint et de change la valeur s.current
.
On relance l'application avec notre score modifié, et c'est flagé!
Le flag est un peu chiant à copier/coller via la modal qui s'ouvre, il est possible de le récupérer via la tabulation "Elements" ou "Réseau" de la console web.
Si comme moi, vous n'avez pas beaucoup expérimenté Burp Suite, recommencez l'épreuve et interceptez la requête POST pour récupérer le flag.
Crédits du challenge root-me.org et son auteur Elweth.
Retrouver les sources du challenge ici.