From b6f22ad7c1fffd87b6ff124d829b02186c917853 Mon Sep 17 00:00:00 2001 From: omachy form the big pc republic Date: Tue, 1 Sep 2026 17:29:25 +0100 Subject: [PATCH] =?UTF-8?q?Traduction=20fran=C3=A7aise=20compl=C3=A8te=20+?= =?UTF-8?q?=20courriel=20de=20divulgation=20responsable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README, PROTOCOL, FINDINGS, endpoints, rapport et outils traduits en français - docs/DISCLOSURE.md : courriel prêt à envoyer à hello@boti.education (compte démo codé en dur, transmission en clair, logging de production) - extract_endpoints.py vérifié après traduction (100 points d'accès toujours extraits) --- README.md | 67 +++++++++++---------- docs/DISCLOSURE.md | 79 +++++++++++++++++++++++++ docs/FINDINGS.md | 73 ++++++++++++----------- docs/PROTOCOL.md | 115 ++++++++++++++++++++----------------- docs/endpoints.md | 14 +++-- results/report.md | 113 ++++++++++++++++++------------------ tools/boti_login.sh | 30 +++++----- tools/extract_endpoints.py | 62 ++++++++++---------- 8 files changed, 329 insertions(+), 224 deletions(-) create mode 100644 docs/DISCLOSURE.md diff --git a/README.md b/README.md index 77db2a1..7c744ac 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,46 @@ # greenwood-school-re -Static reverse engineering of the Greenwood School parent app -(com.botieducation.greenwood), the "horrible mobile app" that is actually a -Capacitor web wrapper by vendor Boti Education (boti.education). +Ingénierie inverse statique de l'application parents Greenwood School +(com.botieducation.greenwood), la « horrible application mobile » qui est en +réalité un wrapper web Capacitor de l'éditeur Boti Education (boti.education). -Analyzed: 2.4.14 (versionCode 24140), XAPK from APKPure. -XAPK sha256: f46aa8b126f8f8856ea15e10ed12fa259d02e383f8921b0645139521b7820e69 +Version analysée : 2.4.14 (versionCode 24140), XAPK via APKPure. +Empreinte XAPK sha256 : f46aa8b126f8f8856ea15e10ed12fa259d02e383f8921b0645139521b7820e69 -## Contents +## Contenu -- docs/PROTOCOL.md, full API protocol: base URL, auth, request envelope, - push registration, media URLs, quirks -- docs/endpoints.md, all 100 app endpoints with get/post and source chunk -- docs/FINDINGS.md, security findings (yes, including the hardcoded dev account) -- tools/extract_endpoints.py, regenerate the endpoint table from decompiled chunks -- tools/boti_login.sh, login + session key caching (credentials from - ~/.config/boti.ini, never in the repo) -- results/report.md, original analysis notes +- docs/PROTOCOL.md, le protocole API complet : URL de base, authentification, + enveloppe des requêtes, notifications push, URLs médias, particularités +- docs/endpoints.md, les 100 points d'accès de l'app avec méthodes get/post + et chunk source +- docs/FINDINGS.md, constats de sécurité (oui, y compris le compte de démo + codé en dur) +- docs/DISCLOSURE.md, courriel de divulgation responsable prêt à envoyer +- tools/extract_endpoints.py, régénère la table des points d'accès depuis les + chunks décompilés +- tools/boti_login.sh, connexion + mise en cache de la clé de session + (identifiants dans ~/.config/boti.ini, jamais dans le dépôt) +- results/report.md, notes d'analyse originales -## TL;DR +## En résumé -- The app is an Ionic/Angular SPA in a Capacitor 5 shell. All logic in JS. -- Backend: https://boti.education/p/greenwood/botiapi/ (per-school path under - one shared vendor backend) -- Auth: POST login (phone + plaintext password) -> keyToken (40 hex) used as - key= on every call. JWT access/refresh headers supported but usually - empty for parents. -- The build ships a hardcoded dev account in the login page component. -- Verified live with a single login call; no other requests were made against - the school server. +- L'application est une SPA Ionic/Angular dans une coquille Capacitor 5. + Toute la logique est en JavaScript. +- Backend : https://boti.education/p/greenwood/botiapi/ (chemin par école sur + un backend unique partagé entre tous les établissements Boti) +- Auth : POST login (téléphone + mot de passe en clair) -> keyToken (40 hex) + utilisé comme key= sur chaque appel. Jetons JWT access/refresh + supportés mais généralement vides pour les comptes parents. +- La version livrée contient un compte de démonstration codé en dur dans le + composant de la page de connexion. +- Protocole vérifié en direct avec un seul appel de connexion ; aucune autre + requête n'a été faite au serveur de l'école. -## No credentials here +## Pas d'identifiants ici -The repository contains no credentials or personal data of the account holder. -The one phone/password pair quoted in docs/FINDINGS.md is vendor-shipped demo -content quoted as vulnerability evidence; it is present in every public copy of -the APK. Live credentials belong in ~/.config/boti.ini (chmod 600) on the -machine that runs the tools, never in this repo. +Le dépôt ne contient aucune donnée personnelle du titulaire du compte. La +seule paire téléphone/mot de passe citée dans docs/FINDINGS.md est le contenu +de démo livré par l'éditeur, cité comme preuve de vulnérabilité ; il figure +dans chaque copie publique de l'APK. Les identifiants réels vont dans +~/.config/boti.ini (chmod 600) sur la machine qui exécute les outils, jamais +dans ce dépôt. diff --git a/docs/DISCLOSURE.md b/docs/DISCLOSURE.md new file mode 100644 index 0000000..476ca1a --- /dev/null +++ b/docs/DISCLOSURE.md @@ -0,0 +1,79 @@ +Destinataires (contacts publics du développeur sur le Google Play Store) : +- hello@boti.education (contact général) +- cs@boti.education (support) + +--- + +## Objet + +Divulgation responsable : identifiants intégrés dans l'application Greenwood +School (com.botieducation.greenwood 2.4.14) + +## Corps du message + +Bonjour, + +Je suis parent d'élève à la Greenwood School (Casablanca) et j'utilise +l'application mobile Greenwood School, éditée par Boti Education. + +En examinant la version 2.4.14 de l'application Android +(com.botieducation.greenwood), distribuée publiquement, j'ai identifié +plusieurs problèmes de sécurité. Tous proviennent d'une analyse statique de +l'APK. Je n'ai accédé qu'à mon propre compte parent, conformément à une +démarche de divulgation responsable. + +Constat principal (le plus grave) : + +L'application livrée au public contient un compte de démonstration avec +identifiant et mot de passe en clair, codé en dur dans le code de la page de +connexion (fichier 1028.js du paquet JavaScript) : + + reconnectUser = {id: 12745, phone: "0649014703", password: "0649014703"} + +Ce compte de test biométrique est présent dans chaque copie de l'APK +distribuée sur le Play Store. Toute personne disposant de l'application +connaît donc un numéro de téléphone et un mot de passe valides pour votre +serveur boti.education. Si ce compte existe encore côté serveur, il permet +une prise de contrôle potentielle. Un correctif côté client est insuffisant : +le compte devrait être désactivé ou supprimé côté serveur. + +Constats secondaires : + +1. Le mot de passe est transmis en texte clair dans les champs du formulaire + de connexion (protégé par TLS, mais visible dans tout journal intermédiaire + qui enregistre les données de formulaire). +2. La version de production contient des appels console.log détaillés qui + affichent les paramètres de requête et les jetons d'accès. +3. Aucun épinglage de certificat ni obfuscation du code : l'ensemble du + protocole API (environ 100 points d'accès sur + https://boti.education/p/greenwood/botiapi/) se laisse documenter en + quelques heures. + +Documentation technique complète (protocole API, liste des points d'accès, +détail des constats) : dépôt public +https://gitea.oimcloud.myaddr.tools/Omarchy-Big-PC/greenwood-school-re + +Je n'ai publié ce dépôt qu'après avoir retiré toute donnée personnelle ; seul +le compte de démonstration livré par vos soins y est cité, comme preuve. + +Je propose une divulgation coordonnée : merci de m'indiquer un délai de +correction raisonnable (90 jours est l'usage) et une personne de contact. +Je mettrai à jour le dépôt public avec le statut du correctif et la réponse +de votre équipe. + +Cordialement, + +[Votre nom] +Parent d'élève, Greenwood School +[Votre courriel] +[Votre téléphone] + +--- + +## Suivi recommandé + +1. Envoyer à hello@boti.education, copie cs@boti.education. +2. Noter la date d'envoi ici : ____________ +3. Relancer après 14 jours sans réponse. +4. Après correction : mettre à jour docs/FINDINGS.md avec le statut de chaque + constat (corrigé / non corrigé) et le numéro de version du correctif. diff --git a/docs/FINDINGS.md b/docs/FINDINGS.md index 5e444b5..ab6db90 100644 --- a/docs/FINDINGS.md +++ b/docs/FINDINGS.md @@ -1,49 +1,54 @@ -# Security findings (com.botieducation.greenwood 2.4.14) +# Constats de sécurité (com.botieducation.greenwood 2.4.14) -Vendor: Boti Education (boti.education). All findings from static analysis of -the publicly distributed APK; one live login call used to verify the protocol. +Éditeur : Boti Education (boti.education). Tous les constats proviennent de +l'analyse statique de l'APK distribué publiquement ; un seul appel de +connexion réel a servi à vérifier le protocole. -## F1. Hardcoded credentials in the shipped production build [HIGH] +## F1. Identifiants codés en dur dans la build de production [GRAVE] -extracted/assets/public/1028.js (login page component) contains: +extracted/assets/public/1028.js (composant de la page de connexion) contient : - this.reconnectUser = {id:12745, phone:"0649014703", password:"0649014703"} + reconnectUser={id:12745, phone:"0649014703", password:"0649014703"} -This is a leftover dev/demo biometric-reconnect account with phone number as -password, compiled into the production bundle. Anyone with the APK has a -username and password. Status of this account on the server unknown; report to -vendor recommended. +Il s'agit d'un compte de démonstration oublié (reconnexion biométrique) avec +le numéro de téléphone comme mot de passe, compilé dans le bundle de +production. Quiconque possède l'APK connaît donc un identifiant et un mot de +passe. Statut réel de ce compte côté serveur : inconnu ; signalement à +l'éditeur recommandé. -## F2. Plaintext credential transmission [MEDIUM] +## F2. Transmission des identifiants en clair [MOYEN] -POST /login sends the password as a plaintext form field (password=...). -TLS protects the transport, but the credential sits in plaintext in any -intermediary logging form data, and the same pattern is reused by -forgot-password / change-password. No client-side hashing. Common for this -class of app, still worth flagging. +POST /login envoie le mot de passe comme champ de formulaire en clair +(password=...). TLS protège le transport, mais l'identifiant reste en texte +clair dans tout journal intermédiaire qui enregistre les données de +formulaire ; même schéma dans forgot-password et change-password. Pas de +hachage côté client. Courant pour cette classe d'application, à signaler +quand même. -## F3. Verbose production logging [LOW] +## F3. Journalisation verbeuse en production [FAIBLE] -The ApiService logs request payloads, headers and the access token to -console.log in the release build (visible via adb logcat / remote WebView -debugging if enabled). +L'ApiService journalise (console.log) les paramètres, les en-têtes et le +jeton d'accès dans la build release (visibles via adb logcat / débogage +WebView distant s'il est activé). -## F4. No certificate pinning, no obfuscation [INFO] +## F4. Pas d'épinglage de certificat, pas d'obfuscation [INFO] -No pinning config in the bundle, no network_security_config restrictions -beyond defaults, JS bundle fully readable. RE cost: about an hour. +Aucune configuration de pinning dans le bundle, aucune restriction +network_security_config au-delà des valeurs par défaut, bundle JS entièrement +lisible. Coût de la rétro-ingénierie : environ une heure. -## F5. Broad but plausible permissions [INFO] +## F5. Permissions larges mais plausibles [INFO] -CAMERA, fine+coarse LOCATION, microphone (voice recorder plugin), external -storage, FCM. Location use case is bus tracking (pick endpoints); camera for -photo upload. No SMS, contacts, or accessibility permission abuse observed. +CAMERA, localisation fine+grossière, micro (plugin enregistreur vocal), +stockage externe, FCM. Cas d'usage de la localisation : suivi des trajets +(points d'accès pick*) ; caméra pour l'envoi de photos. Aucun abus observé +(SMS, contacts, accessibilité). -## Responsible disclosure notes +## Notes de divulgation responsable -- Vendor contact is via boti.education; the school itself (Green Wood School, - tel 0520744100 per the official welcome message) can escalate. -- F1 is the only finding with direct account-takeover potential and is - trivially verifiable by the vendor. -- Do not test F1 against the live server beyond confirming it exists in the - bundle; that crosses into unauthorized access. +- Contact éditeur via boti.education ; l'école (Green Wood School, + tél 0520744100 d'après le message officiel d'accueil) peut relayer. +- F1 est le seul constat avec un potentiel de prise de contrôle de compte et + il est trivialement vérifiable par l'éditeur. +- Ne pas tester F1 contre le serveur réel au-delà de confirmer sa présence + dans le bundle : au-delà, cela devient un accès non autorisé. diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 47a6610..7cded81 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -1,94 +1,101 @@ -# Boti Education app API protocol (Green Wood School variant) +# Boti Education app API : protocole (variante Green Wood School) -Reverse engineered statically from com.botieducation.greenwood 2.4.14 -(Capacitor/Ionic/Angular bundle) and verified with one live login call on 2026-09-01. +Rétro-ingénierie statique à partir de com.botieducation.greenwood 2.4.14 +(bundle Capacitor/Ionic/Angular), vérifiée avec un seul appel de connexion +réel le 2026-09-01. -## Base URL +## URL de base https://boti.education/p/greenwood/botiapi/ -Pattern per school: https://boti.education/p//botiapi/ -All Boti Education schools share one backend. Media host: media.boti.education -(signed expiring URLs, token appended to path). +Motif par école : https://boti.education/p//botiapi/ +Tous les établissements Boti Education partagent un même backend. Hôte +média : media.boti.education (URLs signées à durée limitée, token dans le +chemin). -## Authentication +## Authentification -### Login +### Connexion POST {base}/login Content-Type: multipart/form-data -Fields: -- login phone number, e.g. 06xxxxxxxx -- password plaintext -- privacy bool ("true"/"false") +Champs : +- login numéro de téléphone, ex. 06xxxxxxxx +- password en clair +- privacy booléen ("true"/"false") - remembreMe bool -Response (HTTP 200, application/json): +Réponse (HTTP 200, application/json) : { "status": 200, "msg": "Bienvenue !", "error": false, - "keyToken": "<40 hex chars>", // session key + "keyToken": "<40 caractères hex>", // clé de session "userId": "20887", - "parentId": "1376", // when role=parent + "parentId": "1376", // quand role=parent "eleveId": "1739", "_acces": "parent", "role": "parent", "eleves": [ { id, nomcomplet, prenom, nom, datenaissance, sexe, niveau, img, parent_nom, parent_image } ], "parent": { id, nomcomplet, tel, email, sexe, image }, - "accessToken": "", // present on some accounts, may be empty + "accessToken": "", // présent sur certains comptes, peut être vide "refreshToken": "" } -Errors: {"error": true, "msg": "..."} with HTTP 200. +Erreurs : {"error": true, "msg": "..."} avec HTTP 200. -### Session use on every subsequent call +### Utilisation de la session sur chaque appel suivant -- GET: query params, plus key= and user_id= -- POST: multipart/form-data fields, plus key= -- Optional modern path (supported by the ApiService, may be empty for parent - accounts): headers Authorization: Bearer and +- GET : paramètres d'URL, plus key= et user_id= +- POST : champs multipart/form-data, plus key= +- Voie moderne optionnelle (prise en charge par l'ApiService, souvent vide + pour les comptes parents) : en-têtes Authorization: Bearer et X-refresh-token: Bearer -### Server-side session control +### Contrôle de session côté serveur -- Any response containing "disconnect": true means the session was killed - client-side (app navigates to login). -- "redirect": "" makes the app navigate there. -- "showSLides": "" pops a slider modal (onboarding/news screens). +- Toute réponse contenant "disconnect": true signifie que la session a été + tuée (l'app navigue vers la page de connexion). +- "redirect": "" fait naviguer l'app vers ce chemin. +- "showSLides": "" ouvre une modale de diapositives (écrans d'accueil ou + d'actualités). -## Request envelope +## Enveloppe des requêtes -GET params observed in the bundle (per feature, subset applies): -user_id, key, acces, versionCode, versionNumber, paltform (sic, typo is in the -app), eleveId, lang, date, startDate, endDate, classe, id, search, page +Paramètres GET observés dans le bundle (par fonctionnalité, sous-ensemble) : +user_id, key, acces, versionCode, versionNumber, paltform (sic, la faute de +frappe est dans l'app), eleveId, lang, date, startDate, endDate, classe, id, +search, page -POST FormData conventions: -- flat fields appended as key=value -- nested object -> key[subkey]=value -- array -> key[0]=..., key[1]=... -- File/Blob -> appended raw under the field name -- key= auto-appended by the ApiService +Conventions POST (FormData) : +- champs plats ajoutés comme clé=valeur +- objet imbriqué -> clé[sous-clé]=valeur +- tableau -> clé[0]=..., clé[1]=... +- File/Blob -> ajouté brut sous le nom du champ +- key= ajouté automatiquement par l'ApiService -## Push notifications +## Notifications push - POST {base}/device_token fields: token=, user_id, key, uid= + POST {base}/device_token champs : token=, user_id, key, uid= -## Endpoint inventory +## Inventaire des points d'accès -See endpoints.md (100 app endpoints, get/post flags, source chunk per endpoint). -Endpoint names are stable Pascal/snake identifiers appended to the base URL. +Voir endpoints.md (100 points d'accès de l'app, méthodes get/post et chunk +source par point d'accès). Les noms sont stables et ajoutés tels quels à +l'URL de base. -## Media / files +## Médias / fichiers -- Files and images are fetched from media.boti.education with a signed, - expiring path (base64url token + timestamp appended). -- PDFs opened in-app via pdf.js (chunk 7482.js). -- getBlob(path, params, absolute) downloads raw blobs for attachments. +- Fichiers et images servis par media.boti.education avec un chemin signé à + durée limitée (token base64url + horodatage ajoutés). +- Les PDF sont ouverts en interne via pdf.js (chunk 7482.js). +- getBlob(path, params, absolute) télécharge les blobs bruts pour les pièces + jointes. -## Quirks +## Particularités -- The ApiService appends the endpoint as a raw string, so endpoint names in the - table are the complete server route (no /api prefix, no .php). -- Param typo "paltform" is sent by the app; server accepts it, do not "fix" it - blindly. -- Detailed console.log of params and headers ships in production. +- L'ApiService ajoute le nom du point d'accès comme chaîne brute : les noms + du tableau sont la route serveur complète (pas de préfixe /api, pas de .php). +- La coquille de paramètre « paltform » est envoyée par l'app ; le serveur + l'accepte, ne pas la « corriger » aveuglément. +- Des console.log détaillés (paramètres, en-têtes) subsistent dans la version + de production. diff --git a/docs/endpoints.md b/docs/endpoints.md index 51478ff..e4fa2bf 100644 --- a/docs/endpoints.md +++ b/docs/endpoints.md @@ -1,12 +1,14 @@ -# Boti Education app API: endpoints extracted from decompiled bundle +# API de l'app Boti Education : points d'accès extraits du bundle décompilé -Source: assets/public/*.js of com.botieducation.greenwood 2.4.14. -100 app endpoints. The endpoint name is the 2nd argument of the ApiService -get()/post() calls; full URL = https://boti.education/p/greenwood/botiapi/. +Source : assets/public/*.js de com.botieducation.greenwood 2.4.14. +100 points d'accès de l'app. Le nom du point d'accès est le 2e argument des +appels get()/post() de l'ApiService ; URL complète = +https://boti.education/p/greenwood/botiapi/. -Excluded as pdf.js library false positives (all seen only in chunk 7482.js): BPC, CS, DP, F, FontFile2, H, I, IM, SM, W +Écartés comme faux positifs pdf.js (tous vus uniquement dans le chunk +7482.js) : BPC, CS, DP, F, FontFile2, H, I, IM, SM, W -| endpoint | methods | chunk(s) | +| endpoint | méthodes | chunk(s) | |---|---|---| | absences | get | 3058.js, 8317.js | | absences-justification | post | 3058.js, 8317.js | diff --git a/results/report.md b/results/report.md index 9eb596b..d915a06 100644 --- a/results/report.md +++ b/results/report.md @@ -1,65 +1,68 @@ -# Greenwood School Android app, static RE report +# Rapport d'analyse statique : app Android Greenwood School -App: com.botieducation.greenwood 2.4.14 (versionCode 24140) -XAPK sha256: f46aa8b126f8f8856ea15e10ed12fa259d02e383f8921b0645139521b7820e69 -Worktree: ~/android_reverse/greenwood/ (original XAPK untouched, copy analyzed) +App : com.botieducation.greenwood 2.4.14 (versionCode 24140) +XAPK sha256 : f46aa8b126f8f8856ea15e10ed12fa259d02e383f8921b0645139521b7820e69 +Arborescence d'analyse : ~/android_reverse/greenwood/ (XAPK original intact, +copie analysée) ## Architecture -- Capacitor 5 shell (assets/capacitor.config.json), Ionic + Angular SPA in assets/public/ -- Native layer is only capacitor plugins: camera, filesystem, geolocation, push (FCM), preferences, file-opener/picker, voice recorder -- All business logic and API access lives in the JS bundle: assets/public/main.js + ~200 lazy chunks (nNNN.js) -- ngsw.json: Angular service worker present (PWA origin) +- Coquille Capacitor 5 (assets/capacitor.config.json), SPA Ionic + Angular + dans assets/public/ +- La couche native n'est que des plugins capacitor : camera, filesystem, + geolocation, push (FCM), preferences, file-opener/picker, voice recorder +- Toute la logique métier et l'accès API vivent dans le bundle JS : + assets/public/main.js + ~200 chunks paresseux (nNNN.js) +- ngsw.json : service worker Angular présent (origine PWA) ## Backend -- Base URL constant in main.js (module 35548 export JW): +- Constante d'URL de base dans main.js (module 35548, export JW) : https://boti.education/p/greenwood/botiapi/ -- Per-school path pattern: https://boti.education/p//botiapi/ -- Static/media host: media.boti.education (signed, expiring URLs) -- Vendor: Boti Education (boti.education), logo boti_robot.png, "boti" prefixes everywhere +- Motif par école : https://boti.education/p//botiapi/ +- Hôte média/statique : media.boti.education (URLs signées expirantes) +- Éditeur : Boti Education (boti.education), logo boti_robot.png, préfixes + « boti » partout -## Auth protocol (verified live 2026-09-01) -- POST {base}login, multipart/form-data: login=, password=, privacy, remembreMe -- Response: {status, msg, error, keyToken, userId, parentId, eleves[], _acces, parent{}} -- Session auth on later calls: key=<keyToken> (GET query param / POST form field) -- Also supported (code path present, empty at login here): Authorization: Bearer <accessToken>, X-refresh-token header -- Response with "disconnect":true forces navigateRoot("/") = server-side session kill -- Login page form controls: login, password, privacy, remembreMe (extracted/assets/public/1028.js) +## Protocole d'authentification (vérifié en direct le 2026-09-01) +- POST {base}login, multipart/form-data : login=<téléphone>, + password=<en clair>, privacy, remembreMe +- Réponse : {status, msg, error, keyToken, userId, parentId, eleves[], + _acces, parent{}} +- Auth de session sur les appels suivants : key=<keyToken> (paramètre GET / + champ POST) +- Voie moderne également prise en charge (présente dans le code, vide à la + connexion ici) : Authorization: Bearer <accessToken>, en-tête + X-refresh-token +- Réponse avec "disconnect":true force navigateRoot("/") = kill de session + côté serveur +- Champs du formulaire de connexion : login, password, privacy, remembreMe + (extracted/assets/public/1028.js) -## API service (main.js, apiservice class) -- get(params, endpoint): URLSearchParams, Content-Type x-www-form-urlencoded -- post(params, endpoint): FormData; nested objects flattened as key[subkey]; arrays as key[i]; File/Blob appended raw; auto-appends key=<token> -- getBlob(path, params, absolute=false): raw blob download (used for media/PDF) -- ~110 endpoints total (endpoint name is 2nd arg of get/post in chunks). Full list via: - grep pattern '\.(post|get|getBlob)\(...,"endpoint"' over assets/public/*.js +## Service API (main.js, classe apiservice) +- get(params, endpoint) : URLSearchParams, Content-Type + x-www-form-urlencoded +- post(params, endpoint) : FormData ; objets imbriqués aplatis en + key[subkey] ; tableaux en key[i] ; File/Blob ajoutés bruts ; ajoute + automatiquement key=<token> +- getBlob(path, params, absolute=false) : téléchargement de blob brut + (médias/PDF) +- ~110 points d'accès au total (le nom du point d'accès est le 2e argument + de get/post dans les chunks). Liste complète via l'expression + '\.(post|get|getBlob)\(...,"endpoint"' sur assets/public/*.js -## Endpoints (GET unless noted) -acces_check, login(POST), logout(POST), device_token(POST), forgot-password(POST), -change-password(GET/POST), translations, slides(GET/POST), slides_content, icon-top, overlay, -nouveautes(GET/POST), pinned_posts, post_view, timeline, notifications, notifications_new, -messages, nouveau-message(POST), pick_enfants(GET/POST), pick_menu, pick_nouveautes, -pick_trajets_tst_absent(GET/POST), pointage(GET/POST), pointage_borne(GET/POST), -devoirs(GET/POST), devoirs_date_v2(POST), cours_v2, examens_v2, examen_collectif, -evaluations_releves(GET/POST), note_matieres, competences, suivi_pedagogique, -absences, absences-justification(POST), discipline, cantine, cartable_numeriques, cartable_split, -ressources_v2(GET/POST), ressource_details, bibliotheque, grille_apprentisage, quiz(GET/POST), -demandes, nouvelle-demande(GET/POST), paiements(GET/POST), new_paiements, online_paiements, -shop(GET/POST), student-wallet(GET/POST), album_photo, objects(GET/POST), -compte(GET/POST), compte-eleve(GET/POST), compte-parent(GET/POST), compte-prof(GET/POST), -compte-pick(GET/POST), new_compte(POST), new_compte/child(POST), -prof_* (teacher side: prof_classes, prof_cours, prof_devoir_details, prof_examens_details, -prof_competence_form, prof_messages, prof_nouveautes, prof_fiche_eleve, ...), -admin_* (admin side: admin_search_eleve, admin_fiche_eleve, admin_suivi_eleve, ...), -encadrant_* (bus supervisor side), collaborateur_menu, ios +## Constats notables +1. Compte de démo/développement oublié, codé en dur dans la build de + production : extracted/assets/public/1028.js : + reconnectUser={id:12745, phone:"0649014703", password:"0649014703"} +2. Mots de passe envoyés en champs de formulaire en clair sur TLS (pas de + hachage côté client) ; keyToken est une clé de session hex nue de 40 + caractères +3. POST device_token {token, user_id, key, uid} pour l'enregistrement push + FCM +4. console.log détaillés des paramètres/en-têtes laissés dans le bundle de + production +5. Pas d'épinglage de certificat, pas d'obfuscation de la SPA -## Findings of interest -1. Hardcoded leftover dev/biometric demo account shipped in production build: - extracted/assets/public/1028.js: reconnectUser={id:12745, phone:"0649014703", password:"0649014703"} -2. Passwords sent in plaintext form fields over TLS (no hashing client-side); keyToken is a bare 40-hex session key -3. FCM device_token POST {token, user_id, key, uid} for push registration -4. Detailed console.log of params/headers left in production bundle -5. No certificate pinning, no obfuscation of the SPA - -## Verification -Live POST login with test credentials returned HTTP 200, error:false, keyToken, -parent record + _acces=parent. Protocol confirmed end-to-end. Internal IDs -redacted. +## Vérification +Un POST login réel avec des identifiants de test a renvoyé HTTP 200, +error:false, keyToken, fiche parent + _acces=parent. Protocole confirmé de +bout en bout. Identifiants internes censurés. diff --git a/tools/boti_login.sh b/tools/boti_login.sh index c3181c5..bc1c575 100755 --- a/tools/boti_login.sh +++ b/tools/boti_login.sh @@ -1,11 +1,13 @@ #!/usr/bin/env bash -# Log in to a Boti Education school API (Green Wood School variant) and cache the session token. -# Credentials are read from boti.ini in $XDG_CONFIG_HOME (default ~/.config), never hard-coded. +# Connexion à l'API Boti Education d'une école (variante Green Wood School) +# et mise en cache de la clé de session. +# Les identifiants sont lus dans boti.ini de $XDG_CONFIG_HOME (~/.config par +# défaut), jamais codés en dur. # -# boti.ini format: +# Format de boti.ini : # [boti] # login = 0600000000 -# password = whatever +# password = motdepasse # set -euo pipefail @@ -14,31 +16,31 @@ CONF="${XDG_CONFIG_HOME:-$HOME/.config}/boti.ini" CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/boti-key" if [[ ! -f "$CONF" ]]; then - echo "missing $CONF (see header of this script for format)" >&2 + echo "fichier $CONF manquant (format : voir l'en-tête de ce script)" >&2 exit 1 fi login=$(awk -F= '/^login/{gsub(/[[:space:]]/,"",$2); print $2}' "$CONF") password=$(awk -F= '/^password/{gsub(/[[:space:]]/,"",$2); print $2}' "$CONF") -resp=$(curl -sS -m 20 "$BASE/login" \ +reponse=$(curl -sS -m 20 "$BASE/login" \ -F "login=$login" \ -F "password=$password" \ -F "privacy=true" \ -F "remembreMe=false") -if ! grep -q '"error":false' <<<"$resp"; then - echo "login failed: $(head -c 200 <<<"$resp")" >&2 +if ! grep -q '"error":false' <<<"$reponse"; then + echo "échec de connexion : $(head -c 200 <<<"$reponse")" >&2 exit 1 fi -key=$(sed -n 's/.*"keyToken":"\([0-9a-f]*\)".*/\1/p' <<<"$resp") -if [[ -z "$key" ]]; then - echo "no keyToken in response" >&2 +cle=$(sed -n 's/.*"keyToken":"\([0-9a-f]*\)".*/\1/p' <<<"$reponse") +if [[ -z "$cle" ]]; then + echo "keyToken absent de la réponse" >&2 exit 1 fi umask 077 -printf '%s' "$key" > "$CACHE" -echo "session key cached at $CACHE (40 hex chars)" -echo "usage: curl \"$BASE/<endpoint>?user_id=<userId>&key=$(cat "$CACHE")\"" +printf '%s' "$cle" > "$CACHE" +echo "clé de session enregistrée dans $CACHE (40 caractères hex)" +echo "usage : curl \"$BASE/<endpoint>?user_id=<userId>&key=$(cat "$CACHE")\"" diff --git a/tools/extract_endpoints.py b/tools/extract_endpoints.py index 4611dbf..2dafe3f 100755 --- a/tools/extract_endpoints.py +++ b/tools/extract_endpoints.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 -"""Extract the Boti Education app API endpoints from decompiled Capacitor JS chunks. +"""Extrait les points d'accès de l'API des apps Boti Education depuis les chunks JS décompilés. -Usage: python3 extract_endpoints.py <dir-with-chunks> [--markdown] +Usage : python3 extract_endpoints.py <dossier-avec-chunks> [--markdown] -Scans *.js for the Angular ApiService call signature: +Cherche dans les *.js la signature d'appel de l'ApiService Angular : <http>.post(<payload>, "endpoint") / <http>.get(<payload>, "endpoint") -and emits a deduplicated endpoint -> methods table. +et produit une table endpoint -> méthodes dédupliquée. """ import re import sys @@ -13,51 +13,51 @@ import glob import os import argparse -CALL = re.compile( +APPEL = re.compile( r'\.(post|get|getBlob)\(\s*' r'(?:"[^"]*"|`[^`]*`|[A-Za-z_$][\w$]*(?:\s*\.\s*[\w$]+)*(?:\([^()]*\))?|\{[^{}]*\})' r'\s*,\s*"([^"]*)"' ) -def scan(directory): - found = {} # endpoint -> {method: [chunks]} - for path in sorted(glob.glob(os.path.join(directory, '*.js'))): - chunk = os.path.basename(path) - src = open(path, encoding='utf8', errors='replace').read() - for m in CALL.finditer(src): - method, ep = m.group(1), m.group(2) - d = found.setdefault(ep, {}) - d.setdefault(method, []).append(chunk) - return found +def scanner(dossier): + trouves = {} # endpoint -> {méthode: [chunks]} + for chemin in sorted(glob.glob(os.path.join(dossier, '*.js'))): + chunk = os.path.basename(chemin) + src = open(chemin, encoding='utf8', errors='replace').read() + for m in APPEL.finditer(src): + methode, ep = m.group(1), m.group(2) + d = trouves.setdefault(ep, {}) + d.setdefault(methode, []).append(chunk) + return trouves -def main(): +def principal(): ap = argparse.ArgumentParser() - ap.add_argument('directory') + ap.add_argument('dossier') ap.add_argument('--markdown', action='store_true') args = ap.parse_args() - found = scan(args.directory) - if not found: - sys.exit('no endpoints found, wrong directory?') + trouves = scanner(args.dossier) + if not trouves: + sys.exit('aucun point d\'accès trouvé, mauvais dossier ?') if args.markdown: - print(f'# API endpoints extracted from {os.path.basename(args.directory)}') + print(f'# Points d\'accès extraits de {os.path.basename(args.dossier)}') print() - print(f'{len(found)} unique endpoints.') + print(f'{len(trouves)} points d\'accès uniques (non filtrés, voir docs/endpoints.md pour la liste nettoyée).') print() - print('| endpoint | methods | seen in chunk(s) |') + print('| endpoint | méthodes | chunk(s) |') print('|---|---|---|') - for ep in sorted(found): - methods = ', '.join(sorted(found[ep])) - chunks = ', '.join(sorted({c for ms in found[ep].values() for c in ms})[:3]) - print(f'| {ep} | {methods} | {chunks} |') + for ep in sorted(trouves): + methodes = ', '.join(sorted(trouves[ep])) + chunks = ', '.join(sorted({c for ms in trouves[ep].values() for c in ms})[:3]) + print(f'| {ep} | {methodes} | {chunks} |') else: - for ep in sorted(found): - print(ep, sorted(found[ep])) - print(f'TOTAL endpoints: {len(found)}', file=sys.stderr) + for ep in sorted(trouves): + print(ep, sorted(trouves[ep])) + print(f'TOTAL points d\'accès : {len(trouves)}', file=sys.stderr) if __name__ == '__main__': - main() + principal()