Traduction française complète + courriel de divulgation responsable

- 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)
This commit is contained in:
2026-09-01 17:29:25 +01:00
parent 358027bf27
commit b6f22ad7c1
8 changed files with 329 additions and 224 deletions
+37 -30
View File
@@ -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=<token> 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=<token> 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.
+79
View File
@@ -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.
+39 -34
View File
@@ -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é.
+61 -54
View File
@@ -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/<school-slug>/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/<slug-école>/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": "<jwt>", // present on some accounts, may be empty
"accessToken": "<jwt>", // présent sur certains comptes, peut être vide
"refreshToken": "<jwt>"
}
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=<keyToken> and user_id=<userId>
- POST: multipart/form-data fields, plus key=<keyToken>
- Optional modern path (supported by the ApiService, may be empty for parent
accounts): headers Authorization: Bearer <accessToken> and
- GET : paramètres d'URL, plus key=<keyToken> et user_id=<userId>
- POST : champs multipart/form-data, plus key=<keyToken>
- Voie moderne optionnelle (prise en charge par l'ApiService, souvent vide
pour les comptes parents) : en-têtes Authorization: Bearer <accessToken> et
X-refresh-token: Bearer <refreshToken>
### 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": "<path>" makes the app navigate there.
- "showSLides": "<key>" 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": "<chemin>" fait naviguer l'app vers ce chemin.
- "showSLides": "<clé>" 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=<keyToken> 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=<keyToken> ajouté automatiquement par l'ApiService
## Push notifications
## Notifications push
POST {base}/device_token fields: token=<fcm token>, user_id, key, uid=<device uuid>
POST {base}/device_token champs : token=<jeton fcm>, user_id, key, uid=<uuid appareil>
## 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.
+8 -6
View File
@@ -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/<endpoint>.
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/<endpoint>.
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 |
+56 -53
View File
@@ -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)
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/<school-slug>/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/<slug-école>/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=<phone>, password=<plaintext>, 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.
+15 -13
View File
@@ -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)"
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")\""
+31 -31
View File
@@ -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()