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:
@@ -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
@@ -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
@@ -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
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user