- PROTOCOL.md: base URL, login/session auth, request envelope, push, media - endpoints.md: 100 app endpoints extracted from the Capacitor JS bundle - FINDINGS.md: security findings incl. hardcoded dev account in build - tools: endpoint extractor + credential-safe login script - results: original analysis report (IDs redacted)
4.2 KiB
4.2 KiB
Greenwood School Android app, static RE report
App: com.botieducation.greenwood 2.4.14 (versionCode 24140) XAPK sha256: f46aa8b126f8f8856ea15e10ed12fa259d02e383f8921b0645139521b7820e69 Worktree: ~/android_reverse/greenwood/ (original XAPK untouched, copy analyzed)
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)
Backend
- Base URL constant in 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
Auth protocol (verified live 2026-09-01)
- POST {base}login, multipart/form-data: login=, password=, privacy, remembreMe</li> <li>Response: {status, msg, error, keyToken, userId, parentId, eleves[], _acces, parent{}}</li> <li>Session auth on later calls: key=<keyToken> (GET query param / POST form field)</li> <li>Also supported (code path present, empty at login here): Authorization: Bearer <accessToken>, X-refresh-token header</li> <li>Response with "disconnect":true forces navigateRoot("/") = server-side session kill</li> <li>Login page form controls: login, password, privacy, remembreMe (extracted/assets/public/1028.js)</li> </ul> <h2>API service (main.js, apiservice class)</h2> <ul dir="auto"> <li>get(params, endpoint): URLSearchParams, Content-Type x-www-form-urlencoded</li> <li>post(params, endpoint): FormData; nested objects flattened as key[subkey]; arrays as key[i]; File/Blob appended raw; auto-appends key=<token></li> <li>getBlob(path, params, absolute=false): raw blob download (used for media/PDF)</li> <li>~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</li> </ul> <h2>Endpoints (GET unless noted)</h2> <p dir="auto">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</p> <h2>Findings of interest</h2> <ol dir="auto"> <li>Hardcoded leftover dev/biometric demo account shipped in production build: extracted/assets/public/1028.js: reconnectUser={id:12745, phone:"0649014703", password:"0649014703"}</li> <li>Passwords sent in plaintext form fields over TLS (no hashing client-side); keyToken is a bare 40-hex session key</li> <li>FCM device_token POST {token, user_id, key, uid} for push registration</li> <li>Detailed console.log of params/headers left in production bundle</li> <li>No certificate pinning, no obfuscation of the SPA</li> </ol> <h2>Verification</h2> <p dir="auto">Live POST login with test credentials returned HTTP 200, error:false, keyToken, parent record + _acces=parent. Protocol confirmed end-to-end. Internal IDs redacted.</p> </body></html>