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.
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.
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:
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.
The sequence of communciations was:
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.
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('https://mobilebanking.aib.ie/mob/roi/login.htm') # Request 0
r = requests.post(r.url, data=POST_1_data_from_response_0_body(bs4.BeautifulSoup(r.content)), cookies=r.cookies) # Request 1
r = requests.post(r.url, 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()