diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index 89338cd..000b49b 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -72,6 +72,16 @@ Body: POST /portroyalmanager/secure/stationsobservations/ Base: https://estaciones.api.adif.es Headers: User-key para estaciones + +Body: +{ + "stationCodes": ["string"] // Array de códigos de estación (requerido) +} + +Ejemplo: +{ + "stationCodes": ["60000", "71801"] +} ``` ### Circulaciones (Trenes) @@ -84,16 +94,28 @@ Headers: User-key para circulaciones Body: { - "commercialService": "YES|NO|ALL", - "commercialStopType": "YES|NO|ALL", - "destinationStationCode": "string|null", - "originStationCode": "string|null", + "commercialService": "YES|NOT|BOTH", // Estado del servicio comercial + "commercialStopType": "YES|NOT|BOTH", // Tipo de parada comercial + "destinationStationCode": "string|null", // Código estación destino (opcional) + "originStationCode": "string|null", // Código estación origen (opcional) "page": { - "page": number, - "size": number + "pageNumber": number // Número de página }, - "stationCode": "string|null", - "trafficType": "CERCANIAS|MEDIA_DISTANCIA|LARGA_DISTANCIA|ALL" + "stationCode": "string|null", // Código estación (opcional) + "trafficType": "CERCANIAS|AVLDMD|OTHERS|TRAVELERS|GOODS|ALL" // Tipo de tráfico +} + +Ejemplo: +{ + "commercialService": "BOTH", + "commercialStopType": "BOTH", + "destinationStationCode": null, + "originStationCode": null, + "page": { + "pageNumber": 0 + }, + "stationCode": "60000", + "trafficType": "ALL" } ``` @@ -102,7 +124,8 @@ Body: POST /portroyalmanager/secure/circulationpaths/arrivals/traffictype/ Base: https://circulacion.api.adif.es Headers: User-key para circulaciones -Body: Same as departures + +Body: Mismo formato que departures (TrafficCirculationPathRequest) ``` #### Entre estaciones @@ -110,7 +133,8 @@ Body: Same as departures POST /portroyalmanager/secure/circulationpaths/betweenstations/traffictype/ Base: https://circulacion.api.adif.es Headers: User-key para circulaciones -Body: Same as departures + +Body: Mismo formato que departures (TrafficCirculationPathRequest) ``` #### Una ruta específica @@ -121,11 +145,20 @@ Headers: User-key para circulaciones Body: { - "allControlPoints": boolean|null, - "commercialNumber": "string|null", - "destinationStationCode": "string|null", - "launchingDate": timestamp|null, - "originStationCode": "string|null" + "allControlPoints": boolean|null, // Todos los puntos de control (opcional) + "commercialNumber": "string|null", // Número comercial del tren (opcional) + "destinationStationCode": "string|null", // Código estación destino (opcional) + "launchingDate": number|null, // Fecha de lanzamiento en timestamp (Long) (opcional) + "originStationCode": "string|null" // Código estación origen (opcional) +} + +Ejemplo: +{ + "allControlPoints": true, + "commercialNumber": "04138", + "destinationStationCode": "60000", + "launchingDate": 1733356800000, + "originStationCode": "71801" } ``` @@ -134,7 +167,8 @@ Body: POST /portroyalmanager/secure/circulationpathdetails/severalpaths/ Base: https://circulacion.api.adif.es Headers: User-key para circulaciones -Body: Same as onepaths + +Body: Mismo formato que onepaths (OneOrSeveralPathsRequest) ``` ### Composiciones @@ -215,20 +249,21 @@ Headers: Basic auth + X-CanalMovil headers ### TrafficType (Tipos de tráfico) - `CERCANIAS` - Trenes de cercanías -- `MEDIA_DISTANCIA` - Media distancia -- `LARGA_DISTANCIA` - Larga distancia +- `AVLDMD` - Alta Velocidad, Larga y Media Distancia +- `OTHERS` - Otros tipos de tráfico +- `TRAVELERS` - Viajeros +- `GOODS` - Mercancías - `ALL` - Todos los tipos -### State (Estados) +### State (Estados para comercialService y comercialStopType) - `YES` - Sí -- `NO` - No -- `ALL` - Todos +- `NOT` - No +- `BOTH` - Ambos ### PageInfoDTO ```json { - "page": 0, - "size": 20 + "pageNumber": 0 } ``` @@ -239,9 +274,13 @@ Headers: Basic auth + X-CanalMovil headers - Las User-keys son diferentes para cada servicio (estaciones vs circulaciones) - El token de registro `b9034774-c6e4-4663-a1a8-74bf7102651b` está en el código -[CODE] 200 -[METHOD] POST -[URL] https://circulacion.api.adif.es/portroyalmanager/secure/circulationpathdetails/onepaths/ -[URL] https://circulacion.api.adif.es/portroyalmanager/secure/circulationpaths/betweenstations/traffictype/ -[URL] https://circulacion.api.adif.es/portroyalmanager/secure/circulationpaths/departures/traffictype/ -[URL] https://estaciones.api.adif.es/portroyalmanager/secure/stationsobservations/ +## Notas de Implementación + +Esta documentación se ha obtenido mediante ingeniería reversa del código decompilado de la aplicación Android de ADIF Elcano. + +Clases principales analizadas: +- `com.adif.elcanomovil.serviceNetworking.circulations.model.request.TrafficCirculationPathRequest` +- `com.adif.elcanomovil.serviceNetworking.circulations.model.request.OneOrSeveralPathsRequest` +- `com.adif.elcanomovil.serviceNetworking.stationObservations.model.StationObservationsRequest` +- `com.adif.elcanomovil.serviceNetworking.circulations.model.request.CirculationPathRequest` (interface) +- `com.adif.elcanomovil.serviceNetworking.circulations.model.request.TrafficType` (enum) diff --git a/apk_decompiled/sources/com/adif/elcanomovil/domain/entities/station/RequestedStationInfo.java b/apk_decompiled/sources/com/adif/elcanomovil/domain/entities/station/RequestedStationInfo.java index d8c5a65..3268602 100644 --- a/apk_decompiled/sources/com/adif/elcanomovil/domain/entities/station/RequestedStationInfo.java +++ b/apk_decompiled/sources/com/adif/elcanomovil/domain/entities/station/RequestedStationInfo.java @@ -4,10 +4,11 @@ import java.util.List; import kotlin.Metadata; import kotlin.jvm.internal.Intrinsics; +// TODO @Metadata(d1 = {"\u0000L\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010 \n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0018\n\u0002\u0010\u000b\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\u0002\b\u0086\b\u0018\u00002\u00020\u0001Bk\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u0012\b\u0010\u0004\u001a\u0004\u0018\u00010\u0005\u0012\b\u0010\u0006\u001a\u0004\u0018\u00010\u0007\u0012\u000e\u0010\b\u001a\n\u0012\u0004\u0012\u00020\n\u0018\u00010\t\u0012\u000e\u0010\u000b\u001a\n\u0012\u0004\u0012\u00020\f\u0018\u00010\t\u0012\u000e\u0010\r\u001a\n\u0012\u0004\u0012\u00020\u000e\u0018\u00010\t\u0012\u000e\u0010\u000f\u001a\n\u0012\u0004\u0012\u00020\u000e\u0018\u00010\t\u0012\b\u0010\u0010\u001a\u0004\u0018\u00010\u0011¢\u0006\u0002\u0010\u0012J\t\u0010 \u001a\u00020\u0003HÆ\u0003J\u000b\u0010!\u001a\u0004\u0018\u00010\u0005HÆ\u0003J\u000b\u0010\"\u001a\u0004\u0018\u00010\u0007HÆ\u0003J\u0011\u0010#\u001a\n\u0012\u0004\u0012\u00020\n\u0018\u00010\tHÆ\u0003J\u0011\u0010$\u001a\n\u0012\u0004\u0012\u00020\f\u0018\u00010\tHÆ\u0003J\u0011\u0010%\u001a\n\u0012\u0004\u0012\u00020\u000e\u0018\u00010\tHÆ\u0003J\u0011\u0010&\u001a\n\u0012\u0004\u0012\u00020\u000e\u0018\u00010\tHÆ\u0003J\u000b\u0010'\u001a\u0004\u0018\u00010\u0011HÆ\u0003J\u007f\u0010(\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u00032\n\b\u0002\u0010\u0004\u001a\u0004\u0018\u00010\u00052\n\b\u0002\u0010\u0006\u001a\u0004\u0018\u00010\u00072\u0010\b\u0002\u0010\b\u001a\n\u0012\u0004\u0012\u00020\n\u0018\u00010\t2\u0010\b\u0002\u0010\u000b\u001a\n\u0012\u0004\u0012\u00020\f\u0018\u00010\t2\u0010\b\u0002\u0010\r\u001a\n\u0012\u0004\u0012\u00020\u000e\u0018\u00010\t2\u0010\b\u0002\u0010\u000f\u001a\n\u0012\u0004\u0012\u00020\u000e\u0018\u00010\t2\n\b\u0002\u0010\u0010\u001a\u0004\u0018\u00010\u0011HÆ\u0001J\u0013\u0010)\u001a\u00020*2\b\u0010+\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010,\u001a\u00020-HÖ\u0001J\t\u0010.\u001a\u00020\u0003HÖ\u0001R\u0013\u0010\u0010\u001a\u0004\u0018\u00010\u0011¢\u0006\b\n\u0000\u001a\u0004\b\u0013\u0010\u0014R\u0013\u0010\u0006\u001a\u0004\u0018\u00010\u0007¢\u0006\b\n\u0000\u001a\u0004\b\u0015\u0010\u0016R\u0019\u0010\u000f\u001a\n\u0012\u0004\u0012\u00020\u000e\u0018\u00010\t¢\u0006\b\n\u0000\u001a\u0004\b\u0017\u0010\u0018R\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u0019\u0010\u001aR\u0019\u0010\r\u001a\n\u0012\u0004\u0012\u00020\u000e\u0018\u00010\t¢\u0006\b\n\u0000\u001a\u0004\b\u001b\u0010\u0018R\u0013\u0010\u0004\u001a\u0004\u0018\u00010\u0005¢\u0006\b\n\u0000\u001a\u0004\b\u001c\u0010\u001dR\u0019\u0010\b\u001a\n\u0012\u0004\u0012\u00020\n\u0018\u00010\t¢\u0006\b\n\u0000\u001a\u0004\b\u001e\u0010\u0018R\u0019\u0010\u000b\u001a\n\u0012\u0004\u0012\u00020\f\u0018\u00010\t¢\u0006\b\n\u0000\u001a\u0004\b\u001f\u0010\u0018¨\u0006/"}, d2 = {"Lcom/adif/elcanomovil/domain/entities/station/RequestedStationInfo;", "", "stationCode", "", "stationInfo", "Lcom/adif/elcanomovil/domain/entities/station/StationInfo;", "extendedStationInfo", "Lcom/adif/elcanomovil/domain/entities/station/ExtendedStationInfo;", "stationServices", "", "Lcom/adif/elcanomovil/domain/entities/station/StationServices;", "stationTransportServices", "Lcom/adif/elcanomovil/domain/entities/station/StationTransportServices;", "stationCommercialServices", "Lcom/adif/elcanomovil/domain/entities/station/StationCommercialServices;", "stationActivities", "banner", "Lcom/adif/elcanomovil/domain/entities/station/Banner;", "(Ljava/lang/String;Lcom/adif/elcanomovil/domain/entities/station/StationInfo;Lcom/adif/elcanomovil/domain/entities/station/ExtendedStationInfo;Ljava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lcom/adif/elcanomovil/domain/entities/station/Banner;)V", "getBanner", "()Lcom/adif/elcanomovil/domain/entities/station/Banner;", "getExtendedStationInfo", "()Lcom/adif/elcanomovil/domain/entities/station/ExtendedStationInfo;", "getStationActivities", "()Ljava/util/List;", "getStationCode", "()Ljava/lang/String;", "getStationCommercialServices", "getStationInfo", "()Lcom/adif/elcanomovil/domain/entities/station/StationInfo;", "getStationServices", "getStationTransportServices", "component1", "component2", "component3", "component4", "component5", "component6", "component7", "component8", "copy", "equals", "", "other", "hashCode", "", "toString", "domain_proNon_corporateRelease"}, k = 1, mv = {1, 9, 0}, xi = 48) /* loaded from: classes.dex */ public final /* data */ class RequestedStationInfo { - private final Banner banner; + private final Banner banner;StationService private final ExtendedStationInfo extendedStationInfo; private final List stationActivities; private final String stationCode; diff --git a/apk_decompiled/sources/com/adif/elcanomovil/serviceNetworking/ServicePaths.java b/apk_decompiled/sources/com/adif/elcanomovil/serviceNetworking/ServicePaths.java index 4c75a3c..2401474 100644 --- a/apk_decompiled/sources/com/adif/elcanomovil/serviceNetworking/ServicePaths.java +++ b/apk_decompiled/sources/com/adif/elcanomovil/serviceNetworking/ServicePaths.java @@ -3,7 +3,7 @@ package com.adif.elcanomovil.serviceNetworking; import com.adif.elcanomovil.commonNavGraph.arguments.NavArguments; import com.google.firebase.analytics.FirebaseAnalytics; import kotlin.Metadata; - +//TODO @Metadata(d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u000b\bÆ\u0002\u0018\u00002\u00020\u0001:\t\u0003\u0004\u0005\u0006\u0007\b\t\n\u000bB\u0007\b\u0002¢\u0006\u0002\u0010\u0002¨\u0006\f"}, d2 = {"Lcom/adif/elcanomovil/serviceNetworking/ServicePaths;", "", "()V", "AvisaLoginService", "AvisaStationService", "CirculationService", "CompositionService", "Headers", "IncidenceService", "StationObservationsService", "StationService", "SubscriptionsService", "service-networking_proNon_corporateRelease"}, k = 1, mv = {1, 9, 0}, xi = 48) /* loaded from: classes.dex */ public final class ServicePaths { diff --git a/frida_scripts/frida_capture_request_body.js b/frida_scripts/frida_capture_request_body.js index dd49ce9..1fea191 100644 --- a/frida_scripts/frida_capture_request_body.js +++ b/frida_scripts/frida_capture_request_body.js @@ -1,36 +1,77 @@ /** - * Capture REQUEST BODY using writeTo() method + * Capture REQUEST BODY by hooking MoshiRequestBodyConverter */ -console.log("\n[*] Capturing REQUEST Bodies\n"); +console.log("\n[*] Capturing REQUEST Bodies via MoshiRequestBodyConverter\n"); Java.perform(function() { + // Hook MoshiRequestBodyConverter.convert() directly + try { + var MoshiRequestBodyConverter = Java.use("retrofit2.converter.moshi.MoshiRequestBodyConverter"); + console.log("[+] Found MoshiRequestBodyConverter"); + + var convertOriginal = MoshiRequestBodyConverter.convert.overload('java.lang.Object'); + + convertOriginal.implementation = function(obj) { + // BEFORE calling original, serialize the object ourselves to capture it + try { + // Get the adapter field to serialize the object + var adapterField = this.getClass().getDeclaredField("adapter"); + adapterField.setAccessible(true); + var adapter = adapterField.get(this); + + // Create our own buffer and writer to capture the JSON + var Buffer = Java.use("r3.f"); + var tempBuffer = Buffer.$new(); + + // Create JsonWriter with buffer + var JsonWriter = Java.use("Z2.t"); + var JsonWriterConstructor = JsonWriter.class.getDeclaredConstructor([Java.use("r3.i").class]); + JsonWriterConstructor.setAccessible(true); + var tempWriter = JsonWriterConstructor.newInstance([tempBuffer]); + + // Serialize to our buffer + adapter.toJson(tempWriter, obj); + tempWriter.close(); + + // Read the JSON + var jsonContent = tempBuffer.B0(); // readUtf8() + + console.log("\n" + "=".repeat(80)); + console.log("[CAPTURED REQUEST BODY]"); + if (jsonContent && jsonContent.length > 0) { + if (jsonContent.length > 3000) { + console.log(jsonContent.substring(0, 3000)); + console.log("\n... (truncated, total: " + jsonContent.length + " chars)"); + } else { + console.log(jsonContent); + } + } else { + console.log("(empty)"); + } + console.log("=".repeat(80) + "\n"); + + } catch (e) { + console.log("[CAPTURE ERROR] " + e); + } + + // Call original to return the actual RequestBody + return convertOriginal.call(this, obj); + }; + + console.log("[*] MoshiRequestBodyConverter hook installed!\n"); + + } catch (e) { + console.log("[-] Failed to hook MoshiRequestBodyConverter: " + e); + } + + // Also hook the Auth interceptor to show URLs try { var AuthHeaderInterceptor = Java.use("com.adif.elcanomovil.serviceNetworking.interceptors.AuthHeaderInterceptor"); console.log("[+] Found AuthHeaderInterceptor"); - // Try to find Buffer class - var Buffer = null; - var bufferNames = ["r.f", "r3.f", "okio.Buffer", "r3.Buffer"]; - for (var i = 0; i < bufferNames.length; i++) { - try { - Buffer = Java.use(bufferNames[i]); - console.log("[+] Found Buffer class: " + bufferNames[i]); - break; - } catch (e) { - // Try next - } - } - - if (!Buffer) { - console.log("[-] Could not find Buffer class, trying without pre-loading"); - } - AuthHeaderInterceptor.intercept.implementation = function(chain) { - console.log("\n" + "=".repeat(80)); - console.log("[HTTP REQUEST]"); - try { // Cast chain var ChainClass = Java.use("j3.g"); @@ -46,87 +87,26 @@ Java.perform(function() { var urlField = request.getClass().getDeclaredField("a"); urlField.setAccessible(true); var urlObj = urlField.get(request); - console.log("[URL] " + urlObj.toString()); // Get method var methodField = request.getClass().getDeclaredField("b"); methodField.setAccessible(true); var method = methodField.get(request); - console.log("[METHOD] " + method); - // Get request body - var bodyField = request.getClass().getDeclaredField("d"); - bodyField.setAccessible(true); - var reqBody = bodyField.get(request); - - if (reqBody) { - try { - // If Buffer wasn't found, try to load it now - if (!Buffer) { - var bufferNames = ["r.f", "r3.f", "okio.Buffer", "r3.Buffer"]; - for (var i = 0; i < bufferNames.length; i++) { - try { - Buffer = Java.use(bufferNames[i]); - break; - } catch (e) {} - } - } - - if (Buffer) { - // Create a temporary buffer - var buffer = Buffer.$new(); - - // Try to cast buffer to BufferedSink if needed - try { - var BufferedSink = Java.use("r3.i"); - var sink = Java.cast(buffer, BufferedSink); - - // Call writeTo passing the sink - reqBody.writeTo(sink); - } catch (e) { - // If cast fails, try direct call - reqBody.writeTo(buffer); - } - - // Read the content as UTF-8 string - var bodyContent = buffer.B0(); // readUtf8() - - console.log("\n[REQUEST BODY]"); - if (bodyContent && bodyContent.length > 0) { - if (bodyContent.length > 2000) { - console.log(bodyContent.substring(0, 2000)); - console.log("\n... (truncated, total: " + bodyContent.length + " chars)"); - } else { - console.log(bodyContent); - } - } else { - console.log("(empty)"); - } - } else { - console.log("\n[REQUEST BODY] Could not load Buffer class"); - } - - } catch (e) { - console.log("[REQUEST BODY ERROR] " + e); - } - } else { - console.log("[REQUEST BODY] null"); - } + console.log("\n[REQUEST] " + method + " " + urlObj.toString()); } } catch (e) { - console.log("[ERROR] " + e); + console.log("[URL CAPTURE ERROR] " + e); } - console.log("=".repeat(80) + "\n"); - // Call original return this.intercept(chain); }; - console.log("[*] Hook installed!\n"); + console.log("[*] Interceptor hook installed!\n"); } catch (e) { - console.log("[-] Failed: " + e); + console.log("[-] Failed to hook AuthHeaderInterceptor: " + e); } }); diff --git a/frida_scripts/frida_improved_capture.js b/frida_scripts/frida_improved_capture.js new file mode 100644 index 0000000..a909d39 --- /dev/null +++ b/frida_scripts/frida_improved_capture.js @@ -0,0 +1,130 @@ +/** + * Improved REQUEST BODY Capture + * Using correct method names discovered through inspection + */ + +console.log("\n[*] Improved Request Body Capture\n"); + +Java.perform(function() { + + try { + var AuthHeaderInterceptor = Java.use("com.adif.elcanomovil.serviceNetworking.interceptors.AuthHeaderInterceptor"); + console.log("[+] Found AuthHeaderInterceptor"); + + AuthHeaderInterceptor.intercept.implementation = function(chain) { + console.log("\n" + "=".repeat(80)); + console.log("[HTTP REQUEST]"); + + try { + // Cast chain + var ChainClass = Java.use("j3.g"); + var chainObj = Java.cast(chain, ChainClass); + + // Get request + var requestField = chainObj.getClass().getDeclaredField("e"); + requestField.setAccessible(true); + var request = requestField.get(chainObj); + + if (request) { + // Get URL + var urlField = request.getClass().getDeclaredField("a"); + urlField.setAccessible(true); + var urlObj = urlField.get(request); + console.log("[URL] " + urlObj.toString()); + + // Get method + var methodField = request.getClass().getDeclaredField("b"); + methodField.setAccessible(true); + var method = methodField.get(request); + console.log("[METHOD] " + method); + + // Get request headers + try { + var headersField = request.getClass().getDeclaredField("c"); + headersField.setAccessible(true); + var headers = headersField.get(request); + + if (headers) { + console.log("\n[REQUEST HEADERS]"); + var size = headers.size(); + for (var i = 0; i < size; i++) { + var name = headers.c(i); + var value = headers.f(i); + console.log(" " + name + ": " + value); + } + } + } catch (e) { + console.log("[HEADERS ERROR] " + e); + } + + // Get request body + var bodyField = request.getClass().getDeclaredField("d"); + bodyField.setAccessible(true); + var reqBody = bodyField.get(request); + + if (reqBody) { + try { + // Load Buffer class - we know it's r3.f from inspection + var Buffer = Java.use("r3.f"); + var buffer = Buffer.$new(); + + // Call writeTo with the buffer (buffer implements BufferedSink) + reqBody.writeTo(buffer); + + // Try to read using readUtf8 + try { + var bodyContent = buffer.B0(); // readUtf8() + + console.log("\n[REQUEST BODY]"); + if (bodyContent && bodyContent.length > 0) { + if (bodyContent.length > 3000) { + console.log(bodyContent.substring(0, 3000)); + console.log("\n... (truncated, total: " + bodyContent.length + " chars)"); + } else { + console.log(bodyContent); + } + } else { + console.log("(empty)"); + } + } catch (e) { + // If B0() doesn't work, try other common method names + console.log("[READ ERROR] " + e); + console.log("[DEBUG] Trying alternative methods..."); + + try { + // Try snapshot().utf8() + var snapshot = buffer.t0(); // snapshot() + if (snapshot) { + var bodyContent = snapshot.Y(); // utf8() + console.log("\n[REQUEST BODY]"); + console.log(bodyContent); + } + } catch (e2) { + console.log("[ALT METHOD ERROR] " + e2); + } + } + + } catch (e) { + console.log("[REQUEST BODY ERROR] " + e); + } + } else { + console.log("[REQUEST BODY] null"); + } + } + + } catch (e) { + console.log("[ERROR] " + e); + } + + console.log("=".repeat(80) + "\n"); + + // Call original + return this.intercept(chain); + }; + + console.log("[*] Hook installed!\n"); + + } catch (e) { + console.log("[-] Failed: " + e); + } +}); diff --git a/frida_scripts/frida_okhttp_intercept.js b/frida_scripts/frida_okhttp_intercept.js new file mode 100644 index 0000000..5ac71b6 --- /dev/null +++ b/frida_scripts/frida_okhttp_intercept.js @@ -0,0 +1,68 @@ +/** + * Intercept at OkHttp level to capture request bodies + */ + +console.log("\n[*] OkHttp Request Interceptor\n"); + +Java.perform(function() { + + // Hook the RealCall.execute method which actually sends the request + try { + var RealCall = Java.use("i3.j"); // OkHttp's RealCall + console.log("[+] Found RealCall"); + + RealCall.g.implementation = function(chain) { + console.log("\n" + "=".repeat(80)); + console.log("[HTTP REQUEST INTERCEPTED]"); + + try { + // Get the request from chain + var request = chain.b(); + + if (request) { + console.log("[URL] " + request.g().toString()); + console.log("[METHOD] " + request.f()); + + // Get the body + var body = request.d(); + + if (body) { + try { + var Buffer = Java.use("r3.f"); + var buffer = Buffer.$new(); + + // Write body to buffer + body.writeTo(buffer); + + // Read as string + var bodyStr = buffer.B0(); + + console.log("\n[REQUEST BODY]"); + if (bodyStr && bodyStr.length > 0) { + console.log(bodyStr); + } else { + console.log("(empty)"); + } + } catch (e) { + console.log("[BODY ERROR] " + e); + } + } else { + console.log("[BODY] null"); + } + } + } catch (e) { + console.log("[ERROR] " + e); + } + + console.log("=".repeat(80) + "\n"); + + // Call original + return this.g(chain); + }; + + console.log("[*] Hook installed!\n"); + + } catch (e) { + console.log("[-] Failed to hook RealCall: " + e); + } +}); diff --git a/frida_scripts/frida_reflection_capture.js b/frida_scripts/frida_reflection_capture.js new file mode 100644 index 0000000..8a311e5 --- /dev/null +++ b/frida_scripts/frida_reflection_capture.js @@ -0,0 +1,118 @@ +/** + * Request Body Capture using Reflection + * Automatically finds the correct method names + */ + +console.log("\n[*] Request Body Capture (Reflection-based)\n"); + +Java.perform(function() { + + try { + var AuthHeaderInterceptor = Java.use("com.adif.elcanomovil.serviceNetworking.interceptors.AuthHeaderInterceptor"); + console.log("[+] Found AuthHeaderInterceptor"); + + AuthHeaderInterceptor.intercept.implementation = function(chain) { + console.log("\n" + "=".repeat(80)); + console.log("[HTTP REQUEST]"); + + try { + // Cast chain + var ChainClass = Java.use("j3.g"); + var chainObj = Java.cast(chain, ChainClass); + + // Get request + var requestField = chainObj.getClass().getDeclaredField("e"); + requestField.setAccessible(true); + var request = requestField.get(chainObj); + + if (request) { + // Get URL + var urlField = request.getClass().getDeclaredField("a"); + urlField.setAccessible(true); + var urlObj = urlField.get(request); + console.log("[URL] " + urlObj.toString()); + + // Get method + var methodField = request.getClass().getDeclaredField("b"); + methodField.setAccessible(true); + var method = methodField.get(request); + console.log("[METHOD] " + method); + + // Get request body + var bodyField = request.getClass().getDeclaredField("d"); + bodyField.setAccessible(true); + var reqBody = bodyField.get(request); + + if (reqBody) { + try { + // Load Buffer class + var Buffer = Java.use("r3.f"); + var buffer = Buffer.$new(); + + // Call writeTo with the buffer + reqBody.writeTo(buffer); + + // Use reflection to find readUtf8() method + var methods = buffer.getClass().getMethods(); + var readUtf8Method = null; + + for (var i = 0; i < methods.length; i++) { + var method = methods[i]; + var methodName = method.getName(); + var returnType = method.getReturnType().getName(); + var paramCount = method.getParameterTypes().length; + + // Look for a method that returns String and has no parameters + if (returnType === "java.lang.String" && paramCount === 0) { + // This is likely readUtf8() + readUtf8Method = method; + console.log("[DEBUG] Found string method: " + methodName + "()"); + break; + } + } + + if (readUtf8Method) { + readUtf8Method.setAccessible(true); + var bodyContent = readUtf8Method.invoke(buffer); + + console.log("\n[REQUEST BODY]"); + if (bodyContent && bodyContent.length > 0) { + if (bodyContent.length > 3000) { + console.log(bodyContent.substring(0, 3000)); + console.log("\n... (truncated, total: " + bodyContent.length + " chars)"); + } else { + console.log(bodyContent); + } + } else { + console.log("(empty)"); + } + } else { + console.log("[REQUEST BODY] Could not find readUtf8() method"); + } + + } catch (e) { + console.log("[REQUEST BODY ERROR] " + e); + console.log("[STACK] " + e.stack); + } + } else { + console.log("[REQUEST BODY] null"); + } + } + + } catch (e) { + console.log("[ERROR] " + e); + console.log("[STACK] " + e.stack); + } + + console.log("=".repeat(80) + "\n"); + + // Call original + return this.intercept(chain); + }; + + console.log("[*] Hook installed!\n"); + + } catch (e) { + console.log("[-] Failed: " + e); + } +}); diff --git a/test_corrected_api.py b/test_corrected_api.py new file mode 100644 index 0000000..74f39ae --- /dev/null +++ b/test_corrected_api.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +""" +Script para probar los endpoints con los valores correctos +obtenidos del código decompilado +""" + +import requests +import json +from datetime import datetime + +# Headers correctos +HEADERS_CIRCULATION = { + "Content-Type": "application/json;charset=utf-8", + "User-key": "f4ce9fbfa9d721e39b8984805901b5df" +} + +HEADERS_STATIONS = { + "Content-Type": "application/json;charset=utf-8", + "User-key": "0d021447a2fd2ac64553674d5a0c1a6f" +} + +# URLs base +BASE_CIRCULATION = "https://circulacion.api.adif.es" +BASE_STATIONS = "https://estaciones.api.adif.es" + + +def test_endpoint(name, method, url, headers, data=None): + """Probar un endpoint y mostrar resultado""" + print(f"\n{'='*70}") + print(f"TEST: {name}") + print(f"{'='*70}") + print(f"URL: {url}") + + if data: + print(f"Body:\n{json.dumps(data, indent=2)}") + + try: + if method == "GET": + response = requests.get(url, headers=headers, timeout=10) + elif method == "POST": + response = requests.post(url, headers=headers, json=data, timeout=10) + else: + print(f"❌ Método {method} no soportado") + return False + + print(f"\nStatus: {response.status_code}") + + if response.status_code == 200: + print("✅ SUCCESS") + result = response.json() + print(f"\nResponse Preview (primeros 500 chars):") + print(json.dumps(result, indent=2, ensure_ascii=False)[:500]) + if len(json.dumps(result)) > 500: + print("...") + return True + else: + print(f"❌ FAILED") + print(f"Response: {response.text[:300]}") + return False + + except Exception as e: + print(f"❌ EXCEPTION: {str(e)}") + return False + + +def main(): + print("=" * 70) + print("PRUEBAS CON VALORES CORRECTOS DEL CÓDIGO DECOMPILADO") + print("=" * 70) + + results = {} + + # Test 1: Salidas con State correcto (BOTH en lugar de ALL) + print("\n\n### TEST 1: Departures con State=BOTH ###") + results['departures_both'] = test_endpoint( + "Salidas - Madrid Atocha (State=BOTH, TrafficType=ALL)", + "POST", + f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpaths/departures/traffictype/", + HEADERS_CIRCULATION, + { + "commercialService": "BOTH", # Correcto: BOTH (no ALL) + "commercialStopType": "BOTH", # Correcto: BOTH (no ALL) + "destinationStationCode": None, + "originStationCode": None, + "page": { + "pageNumber": 0 # Correcto: pageNumber (no page+size) + }, + "stationCode": "10200", # Madrid Atocha + "trafficType": "ALL" # Correcto: ALL existe en TrafficType + } + ) + + # Test 2: Salidas con State YES y NOT + print("\n\n### TEST 2: Departures con State=YES ###") + results['departures_yes'] = test_endpoint( + "Salidas - Madrid Atocha (State=YES)", + "POST", + f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpaths/departures/traffictype/", + HEADERS_CIRCULATION, + { + "commercialService": "YES", # Correcto: YES + "commercialStopType": "NOT", # Correcto: NOT (no NO) + "destinationStationCode": None, + "originStationCode": None, + "page": { + "pageNumber": 0 + }, + "stationCode": "10200", + "trafficType": "CERCANIAS" + } + ) + + # Test 3: Prueba con TrafficType AVLDMD (correcto) + print("\n\n### TEST 3: Departures con TrafficType=AVLDMD ###") + results['departures_avldmd'] = test_endpoint( + "Salidas - Madrid Atocha (TrafficType=AVLDMD)", + "POST", + f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpaths/departures/traffictype/", + HEADERS_CIRCULATION, + { + "commercialService": "BOTH", + "commercialStopType": "BOTH", + "destinationStationCode": None, + "originStationCode": None, + "page": { + "pageNumber": 0 + }, + "stationCode": "10200", + "trafficType": "AVLDMD" # Correcto: AVLDMD (no LARGA_DISTANCIA) + } + ) + + # Test 4: Station Observations con stationCodes (array) + print("\n\n### TEST 4: Station Observations (stationCodes array) ###") + results['station_observations'] = test_endpoint( + "Observaciones de Estación (array)", + "POST", + f"{BASE_STATIONS}/portroyalmanager/secure/stationsobservations/", + HEADERS_STATIONS, + { + "stationCodes": ["10200", "10302"] # Correcto: stationCodes (array, no stationCode) + } + ) + + # Test 5: OneOrSeveralPaths + print("\n\n### TEST 5: OneOrSeveralPaths ###") + results['onepaths'] = test_endpoint( + "Detalles de Ruta Específica", + "POST", + f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpathdetails/onepaths/", + HEADERS_CIRCULATION, + { + "allControlPoints": True, + "commercialNumber": None, + "destinationStationCode": "71801", # Barcelona Sants + "launchingDate": None, + "originStationCode": "10200" # Madrid Atocha + } + ) + + # Test 6: Between Stations + print("\n\n### TEST 6: Between Stations ###") + results['between_stations'] = test_endpoint( + "Entre Estaciones (Madrid - Barcelona)", + "POST", + f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpaths/betweenstations/traffictype/", + HEADERS_CIRCULATION, + { + "commercialService": "BOTH", + "commercialStopType": "BOTH", + "destinationStationCode": "71801", # Barcelona Sants + "originStationCode": "10200", # Madrid Atocha + "page": { + "pageNumber": 0 + }, + "stationCode": None, + "trafficType": "ALL" + } + ) + + # Resumen + print("\n\n" + "="*70) + print("RESUMEN DE PRUEBAS") + print("="*70) + + total = len(results) + passed = sum(1 for v in results.values() if v) + failed = total - passed + + for test_name, result in results.items(): + status = "✅ PASS" if result else "❌ FAIL" + print(f"{status} - {test_name}") + + print(f"\nTotal: {total} | Pasadas: {passed} | Fallidas: {failed}") + + if passed == total: + print("\n🎉 ¡Todas las pruebas pasaron! La documentación es correcta.") + else: + print(f"\n⚠️ {failed} prueba(s) fallaron. Revisar los errores arriba.") + + +if __name__ == "__main__": + main() diff --git a/test_corrected_api_v2.py b/test_corrected_api_v2.py new file mode 100644 index 0000000..2020f51 --- /dev/null +++ b/test_corrected_api_v2.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +""" +Script para probar los endpoints OMITIENDO campos null +(en lugar de enviarlos explícitamente como null) +""" + +import requests +import json + +# Headers correctos +HEADERS_CIRCULATION = { + "Content-Type": "application/json;charset=utf-8", + "User-key": "f4ce9fbfa9d721e39b8984805901b5df" +} + +HEADERS_STATIONS = { + "Content-Type": "application/json;charset=utf-8", + "User-key": "0d021447a2fd2ac64553674d5a0c1a6f" +} + +# URLs base +BASE_CIRCULATION = "https://circulacion.api.adif.es" +BASE_STATIONS = "https://estaciones.api.adif.es" + + +def test_endpoint(name, method, url, headers, data=None): + """Probar un endpoint y mostrar resultado""" + print(f"\n{'='*70}") + print(f"TEST: {name}") + print(f"{'='*70}") + print(f"URL: {url}") + + if data: + print(f"Body:\n{json.dumps(data, indent=2)}") + + try: + if method == "GET": + response = requests.get(url, headers=headers, timeout=10) + elif method == "POST": + response = requests.post(url, headers=headers, json=data, timeout=10) + else: + print(f"❌ Método {method} no soportado") + return False + + print(f"\nStatus: {response.status_code}") + + if response.status_code == 200: + print("✅ SUCCESS") + result = response.json() + print(f"\nResponse Preview (primeros 1000 chars):") + resp_str = json.dumps(result, indent=2, ensure_ascii=False) + print(resp_str[:1000]) + if len(resp_str) > 1000: + print("...") + return True + else: + print(f"❌ FAILED") + print(f"Response: {response.text[:300]}") + return False + + except Exception as e: + print(f"❌ EXCEPTION: {str(e)}") + return False + + +def main(): + print("=" * 70) + print("PRUEBAS OMITIENDO CAMPOS NULL") + print("=" * 70) + + results = {} + + # Test 1: Salidas - SOLO campos requeridos + print("\n\n### TEST 1: Departures - SOLO campos necesarios ###") + results['departures_minimal'] = test_endpoint( + "Salidas - Madrid Atocha (campos mínimos)", + "POST", + f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpaths/departures/traffictype/", + HEADERS_CIRCULATION, + { + "commercialService": "BOTH", + "commercialStopType": "BOTH", + "page": { + "pageNumber": 0 + }, + "stationCode": "10200", + "trafficType": "ALL" + # Omitiendo destinationStationCode, originStationCode que son null + } + ) + + # Test 2: Station Observations + print("\n\n### TEST 2: Station Observations ###") + results['station_observations'] = test_endpoint( + "Observaciones de Estación", + "POST", + f"{BASE_STATIONS}/portroyalmanager/secure/stationsobservations/", + HEADERS_STATIONS, + { + "stationCodes": ["10200"] + } + ) + + # Test 3: OneOrSeveralPaths - solo campos necesarios + print("\n\n### TEST 3: OneOrSeveralPaths (campos mínimos) ###") + results['onepaths_minimal'] = test_endpoint( + "Detalles de Ruta - solo estaciones", + "POST", + f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpathdetails/onepaths/", + HEADERS_CIRCULATION, + { + "destinationStationCode": "71801", + "originStationCode": "10200" + # Omitiendo allControlPoints, commercialNumber, launchingDate + } + ) + + # Test 4: Between Stations + print("\n\n### TEST 4: Between Stations (campos mínimos) ###") + results['between_stations'] = test_endpoint( + "Entre Estaciones (Madrid - Barcelona)", + "POST", + f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpaths/betweenstations/traffictype/", + HEADERS_CIRCULATION, + { + "commercialService": "BOTH", + "commercialStopType": "BOTH", + "destinationStationCode": "71801", + "originStationCode": "10200", + "page": { + "pageNumber": 0 + }, + "trafficType": "ALL" + # Omitiendo stationCode que es null + } + ) + + # Test 5: Arrivals + print("\n\n### TEST 5: Arrivals ###") + results['arrivals'] = test_endpoint( + "Llegadas - Madrid Atocha", + "POST", + f"{BASE_CIRCULATION}/portroyalmanager/secure/circulationpaths/arrivals/traffictype/", + HEADERS_CIRCULATION, + { + "commercialService": "BOTH", + "commercialStopType": "BOTH", + "page": { + "pageNumber": 0 + }, + "stationCode": "10200", + "trafficType": "ALL" + } + ) + + # Resumen + print("\n\n" + "="*70) + print("RESUMEN DE PRUEBAS") + print("="*70) + + total = len(results) + passed = sum(1 for v in results.values() if v) + failed = total - passed + + for test_name, result in results.items(): + status = "✅ PASS" if result else "❌ FAIL" + print(f"{status} - {test_name}") + + print(f"\nTotal: {total} | Pasadas: {passed} | Fallidas: {failed}") + + if passed == total: + print("\n🎉 ¡Todas las pruebas pasaron!") + elif passed > 0: + print(f"\n✅ {passed} prueba(s) funcionaron correctamente") + else: + print(f"\n⚠️ Todas las pruebas fallaron") + + +if __name__ == "__main__": + main()