Document full static RE of Greenwood School (Boti Education) app 2.4.14
- 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)
This commit is contained in:
@@ -1,3 +1,39 @@
|
|||||||
# greenwood-school-re
|
# greenwood-school-re
|
||||||
|
|
||||||
Static reverse-engineering of the Greenwood School (Boti Education) parent app: API protocol, endpoints, findings
|
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).
|
||||||
|
|
||||||
|
Analyzed: 2.4.14 (versionCode 24140), XAPK from APKPure.
|
||||||
|
XAPK sha256: f46aa8b126f8f8856ea15e10ed12fa259d02e383f8921b0645139521b7820e69
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## No credentials here
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Security findings (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.
|
||||||
|
|
||||||
|
## F1. Hardcoded credentials in the shipped production build [HIGH]
|
||||||
|
|
||||||
|
extracted/assets/public/1028.js (login page component) contains:
|
||||||
|
|
||||||
|
this.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.
|
||||||
|
|
||||||
|
## F2. Plaintext credential transmission [MEDIUM]
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## F3. Verbose production logging [LOW]
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
## F4. No certificate pinning, no obfuscation [INFO]
|
||||||
|
|
||||||
|
No pinning config in the bundle, no network_security_config restrictions
|
||||||
|
beyond defaults, JS bundle fully readable. RE cost: about an hour.
|
||||||
|
|
||||||
|
## F5. Broad but plausible permissions [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.
|
||||||
|
|
||||||
|
## Responsible disclosure notes
|
||||||
|
|
||||||
|
- 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.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Boti Education app API protocol (Green Wood School variant)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Login
|
||||||
|
|
||||||
|
POST {base}/login Content-Type: multipart/form-data
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- login phone number, e.g. 06xxxxxxxx
|
||||||
|
- password plaintext
|
||||||
|
- privacy bool ("true"/"false")
|
||||||
|
- remembreMe bool
|
||||||
|
|
||||||
|
Response (HTTP 200, application/json):
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": 200, "msg": "Bienvenue !", "error": false,
|
||||||
|
"keyToken": "<40 hex chars>", // session key
|
||||||
|
"userId": "20887",
|
||||||
|
"parentId": "1376", // when 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
|
||||||
|
"refreshToken": "<jwt>"
|
||||||
|
}
|
||||||
|
|
||||||
|
Errors: {"error": true, "msg": "..."} with HTTP 200.
|
||||||
|
|
||||||
|
### Session use on every subsequent call
|
||||||
|
|
||||||
|
- 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
|
||||||
|
X-refresh-token: Bearer <refreshToken>
|
||||||
|
|
||||||
|
### Server-side session control
|
||||||
|
|
||||||
|
- 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).
|
||||||
|
|
||||||
|
## Request envelope
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## Push notifications
|
||||||
|
|
||||||
|
POST {base}/device_token fields: token=<fcm token>, user_id, key, uid=<device uuid>
|
||||||
|
|
||||||
|
## Endpoint inventory
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Media / files
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## Quirks
|
||||||
|
|
||||||
|
- 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.
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Boti Education app API: endpoints extracted from decompiled bundle
|
||||||
|
|
||||||
|
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>.
|
||||||
|
|
||||||
|
Excluded as pdf.js library false positives (all seen only in chunk 7482.js): BPC, CS, DP, F, FontFile2, H, I, IM, SM, W
|
||||||
|
|
||||||
|
| endpoint | methods | chunk(s) |
|
||||||
|
|---|---|---|
|
||||||
|
| absences | get | 3058.js, 8317.js |
|
||||||
|
| absences-justification | post | 3058.js, 8317.js |
|
||||||
|
| acces_check | get | main.js |
|
||||||
|
| admin_absence_eleve | get | 6763.js |
|
||||||
|
| admin_demande_eleve | get, post | 1514.js, 9714.js, 9964.js |
|
||||||
|
| admin_discipline_eleve | get, post | 1172.js, 9964.js |
|
||||||
|
| admin_fiche_eleve | get | 9964.js |
|
||||||
|
| admin_nouveautes | get | 6154.js |
|
||||||
|
| admin_search_eleve | get | 6996.js |
|
||||||
|
| admin_suivi_eleve | get, post | 3555.js, 9964.js |
|
||||||
|
| album_photo | get | 6124.js, 6925.js |
|
||||||
|
| bibliotheque | get | 2662.js, 6587.js, 7856.js (+1 more) |
|
||||||
|
| cantine | get | 3218.js, 6510.js |
|
||||||
|
| cartable_numeriques | get | 1516.js, 8698.js, 9661.js |
|
||||||
|
| cartable_split | get | 3238.js |
|
||||||
|
| change-password | get, post | 5994.js |
|
||||||
|
| collaborateur_menu | get | 9246.js |
|
||||||
|
| collaborateur_nouveautes | get | 4450.js |
|
||||||
|
| competences | get | 219.js, 5502.js |
|
||||||
|
| compte | get | 2319.js, 7983.js, 8437.js |
|
||||||
|
| compte-eleve | get, post | 4186.js, 6943.js |
|
||||||
|
| compte-parent | get, post | 6941.js |
|
||||||
|
| compte-pick | get, post | 2758.js, 2881.js |
|
||||||
|
| compte-prof | get, post | 7324.js |
|
||||||
|
| contact | get | 112.js, 8171.js |
|
||||||
|
| cours_v2 | get | 6109.js, 6946.js |
|
||||||
|
| demandes | get | 1246.js |
|
||||||
|
| device_token | post | main.js |
|
||||||
|
| devoirs | get, post | 154.js, 3537.js, 4273.js (+1 more) |
|
||||||
|
| devoirs_date_v2 | post | 3537.js, 4273.js |
|
||||||
|
| discipline | get | 5482.js, 8072.js |
|
||||||
|
| encadrant_contact | get | 5962.js |
|
||||||
|
| encadrant_menu | get | 7110.js |
|
||||||
|
| encadrant_notifications | get | 6108.js |
|
||||||
|
| evaluations_releves | get, post | 1213.js, 6153.js |
|
||||||
|
| examen_collectif | get | 1531.js |
|
||||||
|
| examens_v2 | get | 2722.js, 7085.js |
|
||||||
|
| forgot-password | post | 1028.js |
|
||||||
|
| grille_apprentisage | get | 541.js |
|
||||||
|
| icon-top | get | 438.js |
|
||||||
|
| ios | get | 2175.js, 5817.js, 8136.js (+2 more) |
|
||||||
|
| login | post | main.js |
|
||||||
|
| login-page | get | 1028.js, 274.js, 4287.js |
|
||||||
|
| logout | post | main.js |
|
||||||
|
| messages | get | 1201.js, 3943.js, 4785.js (+1 more) |
|
||||||
|
| new_compte | post | 8437.js |
|
||||||
|
| new_compte/child | post | 8437.js |
|
||||||
|
| new_paiements | get | 4110.js |
|
||||||
|
| note_matieres | get | 215.js |
|
||||||
|
| notifications | get | 9963.js |
|
||||||
|
| notifications_new | get | 8052.js |
|
||||||
|
| nouveau-message | post | 1201.js, 2345.js, 9771.js |
|
||||||
|
| nouveautes | get, post | 1815.js, 1878.js, 1901.js (+8 more) |
|
||||||
|
| nouvelle-demande | get, post | 2253.js, 8302.js |
|
||||||
|
| objects | get, post | 7983.js |
|
||||||
|
| online_paiements | get | 4924.js |
|
||||||
|
| overlay | get | 8136.js |
|
||||||
|
| paiements | get, post | 1559.js, 1631.js, 2897.js (+4 more) |
|
||||||
|
| pick_enfants | get, post | 4495.js, 6292.js |
|
||||||
|
| pick_menu | get | 8897.js |
|
||||||
|
| pick_nouveautes | get | 9821.js |
|
||||||
|
| pick_trajets_tst_absent | get, post | 2568.js, 7347.js |
|
||||||
|
| pinned_posts | get | 8853.js, 9010.js |
|
||||||
|
| pointage | get, post | 3394.js, 8656.js |
|
||||||
|
| pointage_borne | get, post | 3151.js, 9073.js |
|
||||||
|
| post_view | get | 1815.js, 1878.js, 6834.js (+2 more) |
|
||||||
|
| prof-competences | get | 5507.js, common.js |
|
||||||
|
| prof-competences-details | get, post | 1272.js |
|
||||||
|
| prof-nouvelle-demande | get, post | 718.js |
|
||||||
|
| prof_cachier_text | get, post | 6919.js |
|
||||||
|
| prof_cartable_numeriques | get | 2891.js, 3358.js |
|
||||||
|
| prof_classes | get, post | 7116.js, 9516.js |
|
||||||
|
| prof_competence_form | get, post | 4225.js |
|
||||||
|
| prof_content | get, post | 4225.js, 6516.js, 6904.js (+2 more) |
|
||||||
|
| prof_contents | get | 7269.js, 9346.js, 9588.js |
|
||||||
|
| prof_cours | get | 5814.js |
|
||||||
|
| prof_cours_details | get, post | 8111.js |
|
||||||
|
| prof_demandes | get, post | 1665.js, 8329.js |
|
||||||
|
| prof_devoir_details | get | 6904.js, 950.js |
|
||||||
|
| prof_examen_form | get, post | 103.js |
|
||||||
|
| prof_examens_details | get, post | 1531.js, 5908.js |
|
||||||
|
| prof_fiche_eleve | get | 6889.js |
|
||||||
|
| prof_generale_appreciation | get, post | 2567.js, 712.js |
|
||||||
|
| prof_media_album | get, post | 322.js, 7859.js, 8667.js |
|
||||||
|
| prof_messages | get, post | 3733.js, 4722.js, 8690.js |
|
||||||
|
| prof_notifications | get | 3602.js |
|
||||||
|
| prof_nouveautes | get | 6657.js |
|
||||||
|
| prof_ressource_partage | get, post | 1267.js, 2984.js, 8638.js |
|
||||||
|
| prof_transmission | get, post | 2629.js |
|
||||||
|
| quiz | get, post | 1140.js, 93.js |
|
||||||
|
| ressource_details | get | 2921.js, 7424.js |
|
||||||
|
| ressources_v2 | get, post | 3641.js, 4655.js |
|
||||||
|
| shop | get, post | 1097.js, 6091.js, 9344.js (+1 more) |
|
||||||
|
| slides | get, post | 1356.js |
|
||||||
|
| slides_content | get | main.js |
|
||||||
|
| student-wallet | get, post | 2699.js, 333.js, 6277.js (+1 more) |
|
||||||
|
| suivi_pedagogique | get | 1414.js, 3965.js |
|
||||||
|
| timeline | get | 274.js, 4287.js |
|
||||||
|
| translations | get | 2757.js, 4674.js, 6113.js (+3 more) |
|
||||||
|
| transmission | get | 2109.js, 63.js |
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# 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/<school-slug>/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=<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)
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## 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.
|
||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# boti.ini format:
|
||||||
|
# [boti]
|
||||||
|
# login = 0600000000
|
||||||
|
# password = whatever
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE="https://boti.education/p/greenwood/botiapi/"
|
||||||
|
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
|
||||||
|
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" \
|
||||||
|
-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
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
key=$(sed -n 's/.*"keyToken":"\([0-9a-f]*\)".*/\1/p' <<<"$resp")
|
||||||
|
if [[ -z "$key" ]]; then
|
||||||
|
echo "no keyToken in response" >&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")\""
|
||||||
Executable
+63
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract the Boti Education app API endpoints from decompiled Capacitor JS chunks.
|
||||||
|
|
||||||
|
Usage: python3 extract_endpoints.py <dir-with-chunks> [--markdown]
|
||||||
|
|
||||||
|
Scans *.js for the Angular ApiService call signature:
|
||||||
|
<http>.post(<payload>, "endpoint") / <http>.get(<payload>, "endpoint")
|
||||||
|
and emits a deduplicated endpoint -> methods table.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
CALL = 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 main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument('directory')
|
||||||
|
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?')
|
||||||
|
|
||||||
|
if args.markdown:
|
||||||
|
print(f'# API endpoints extracted from {os.path.basename(args.directory)}')
|
||||||
|
print()
|
||||||
|
print(f'{len(found)} unique endpoints.')
|
||||||
|
print()
|
||||||
|
print('| endpoint | methods | seen in 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} |')
|
||||||
|
else:
|
||||||
|
for ep in sorted(found):
|
||||||
|
print(ep, sorted(found[ep]))
|
||||||
|
print(f'TOTAL endpoints: {len(found)}', file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user