Security theatre at Allied Irish Banks

See also Act 2.

In summary

Allied Irish Banks's web and mobile banking portals are ludicrously insecure. Vast numbers of accounts have easily-guessable registration numbers and are thus 'protected' by a level of security that is twice as easy to crack as would be provided by a single password containing only two lowercase letters.

A person of malicious intent could easily gain access to hundreds, possibly thousands, of accounts as well as completely overwhelm the branch network by locking an estimated several 100,000s of people out of their online banking.

Both AIB and the Irish Financial Services Ombudsman have refused to respond meaningfully to multiple communications each in which these concerns were raised privately.

AIB's online login protocol

An AIB account holder's online credentials consist of two secrets: Each login requires the full 8-digit registration number and 3 digits from the 5-digit personal access code.

The web (though never the mobile) portal used to the have a third step in which a third semi-secret 4-decimal-digit code was also requested but this step was removed several months ago.

The dangers of AIB's online banking portal

The principal danger with AIB's online banking portal stems from the fact that there exist a great many accounts for which the first 6 digits of the 'secret' 8-digit registration number are simply the account holder's birthday in DDMMYY format. Seriously!

Given the large number (estimated at several 100,000s) of extant accounts with such registration numbers, the following procedure can be expected to gain access to many hundreds of accounts (possibly thousands) at a rate of perhaps one account per hour without parallelism:

  1. Pick a random birthdate DDMMYY for somebody aged 18 – 60 (say).
  2. Pick a random number NN between 0 and 99.
  3. Attempt to login using registration number DDMMYYNN.
  4. Pick three random digits between 0 and 9 and submit these for the requested digits of the personal access code.
  5. Repeat step 4 until either the account is locked or access is gained.
  6. Go to step 1.
A brief experiment and back-of-the-envelope estimate reveals that steps 1 – 5 take just under 2 seconds to perform on average and succeed in gaining access to an account with probability approximately 1 in 2,000, based on a guess of 250,000 accounts with the birthday-style registration numbers.

As mentioned, in the event that somebody carried out the procedure above, a side-effect would be that within a few days an estimated hundreds of thousands of account holders would have to visit their branches to have their account unlocked. Such an event would completely overwhelm the branch network and even after the accounts were unlocked, the event could be repeated.

It is possible that the bank might start to block connections from IP addresses that appear to be performing an attack such as that described above but such a pseudo-defense could be easily circumvented either by leveraging legal cloud-computing facilities or renting an illegal botnet.

Apparently it is possible to rent 1,000 hosts for about $25 per hour. As well as completely circumventing potential IP blocking, this would allow parallelism that would speed up the attack by a factor of 1,000, meaning that potentially:

accounts could be compromised at a rate of about one every two seconds.

Unless the bank changes their login protocol to something genuinely secure, the best they could hope would be to slow down such an attack by making their site less performant. Even so, by then presumably many accounts would have been breached and many thousands would have been locked, and the attack would continue, and continue to succeed just a bit more slowly. Other partial defenses exist but nothing that is likely to stop a competent attacker.

Correspondence with AIB and Irish Financial Services Ombudsman

Many people would be unhappy if their grocery store used this sort security theatre to 'protect' their personal information but for a bank this sort of behaviour is seriously dangerous. For this reason, a good citizen diligently raised these concerns both with AIB and with the Irish Financial Services Ombudsman. Unfortunately, as might be expected, the reports were dismissed with essentially off-the-shelf replies.

The sequence of communciations was:

Perhaps follow-up with the Data Protection Commissioner's office in Portarlington might result in progress:
but I am not optimistic.

It is interesting to bear in mind Ross Anderson's quote (from this paper):

One of the observations that sparked interest in information security economics came from banking. In the USA, banks are generally liable for the costs of card fraud; when a customer disputes a transaction, the bank must either show she is trying to cheat it, or refund her money. In the UK, the banks had a much easier ride: they generally got away with claiming that their systems were ‘secure’, and telling customers who complained that they must be mistaken or lying. “Lucky bankers,” one might think; yet UK banks spent more on security and suffered more fraud. This may have been what economists call a moral-hazard effect: UK bank staff knew that customer complaints would not be taken seriously, so they became lazy and careless, leading to an epidemic of fraud.

Just how easy is it to execute such an attack?


The following repository on GitHub, quoted below, demonstrates that the basic mechanism can be implemented in about 20 lines of python:

import re, requests, bs4

regNumber, pacDigits = raw_input('Please enter your 8-digit registration number and 5-digit PAC number separated by space: ').split()

def transactionToken_from_response_body(soup):
    return soup.find_all(id='transactionToken')[0]['value'] # An epoch timestamp in milliseconds

def POST_1_data_from_response_0_body(soup):
    return [('transactionToken', transactionToken_from_response_body(soup)),
            ('jsEnabled', 'TRUE'),
            ('regNumber', regNumber),
            ('_target1', 'true')]

def POST_2_data_from_response_1_body(soup):
    f = lambda css_class : int(re.match(r'Digit ([0-9]):', soup.find_all('div', {'class' : css_class})[0].get_text().strip()).group(1)) - 1
    return [('transactionToken', transactionToken_from_response_body(soup)),
            ('pacDetails.pacDigit1', pacDigits[f('ui-block-a')]),
            ('pacDetails.pacDigit2', pacDigits[f('ui-block-b')]),
            ('pacDetails.pacDigit3', pacDigits[f('ui-block-c')]),
            ('_finish', 'true')]

r = requests.get('') # Request 0
r =, data=POST_1_data_from_response_0_body(bs4.BeautifulSoup(r.content)), cookies=r.cookies) # Request 1
r =, data=POST_2_data_from_response_1_body(bs4.BeautifulSoup(r.content)), cookies=r.cookies) # Request 2

print 'Your balance is a whopping:', bs4.BeautifulSoup(r.content).find_all('p', {'class' : 'hide-funds' })[0].get_text().strip()