top of page

The Bank Job

Updated: Nov 13, 2018

TLS is one of the most misunderstood technology. There is a huge disconnect between what TLS guarantees and what developers think TLS guarantees. TLS ensures a client is talking to the actual server it intends to and the communication between a participating client and server are encrypted and tamper proof. Developers tend to think of TLS as a magic blanket which secures their application, thereby making silly architectural mistakes that would have been a sin even during the HTTP era. The following is a deadly story from our consultant during one such encounter during late 2015.


The application in question is a mobile banking application of a public sector (Government run) bank in India. It is no secret that these government run banks are at least a decade old when it comes to implementing cutting edge technology. But the design architecture mistakes we found in this application were nothing short of criminal.

0x0 The Setup

I used my iPhone 6 as a test bed for the App. Although initial thoughts were to go with Android, but then I remembered my frustrations from the last time when I wanted to install a self-signed cert (for Burp Proxy), Android wouldn’t stop complaining. I know it is a good thing, to be constantly reminded that a self-signed third party cert has been added to your device’s trust store, but sometimes you indeed are required to install such self-signed certs (in a corporate environment or test?) because, cost, Duh!, and you don’t want to see that pesky “Your communications can be monitored” message everyday [Note: This was before Let’s Encrypt GA]. Hence the iOS choice. A MacBook Pro running Burp was used as a proxy server.

0x01 SSLv3.0: Are you kidding me

Initial request interception on burp showed the hostname and port, the app was connecting to. It was a middleware application, which made sense because every bank has their own core-banking software from large vendors like TCS (Bancs), Infosys (Finacle), or Oracle Finance (Flexcube). Since this mobile banking application was developed by a different vendor, it makes sense to limit the exposure of their core-banking APIs. Playing a bit with OpenSSL showed that the middleware uses SSL3.0, and can also be forced down to SSL2.0.

I tried to install a self-signed certificate, to capture the plain text request/response on Burp, and it worked like a charm. Which means, no certificate pinning. Considering this is a mobile banking application, lack of certificate pinning is a costly mistake.

0x02 Session IDs: All are welcome Unlimited, and Immortal

This, clearly seems to be a case of an incorrect requirement specification in combination with careless architecture, or sloppy code review. The first request the app makes is to check if there is an update available. This happens even before you login into your account.

This update check request yields a session ID, which can be reused to make real requests (such as Check Account Balance, List Deposits) that must otherwise be possible to do only after an user logs in. This essentially bypasses the login password. Below is an image where I crafted a request to display all my accounts without authenticating. And did I say, the session IDs live forever? Read on.

0x03 Session IDs: Unlimited, and Immortal

While I was playing with the app, The UI threw a pop up (when an idle timer was about to expire?), asking if I wanted to renew the session, or terminate it. Nothing wrong with the solution, but with the countdown timer displayed on the app, it seemed too sketchy, I wanted to know if there was an actual session timer in the backend, which automatically invalidate the session, or is it only possible through the App’s front end interface callback.

My instinct was right. There were no session invalidation controls on the backend. So, unless the App manually invoked the session destroy API or the application server crashes, your session IDs live forever.

0x04 Front end Validation: Phew

I tested the validation of App’s receiver account validation control. The receiver account in a transaction must already exist in the beneficiary accounts list. If it isn’t in the beneficiary list, the “Transfer Funds” screen would throw an error, asking you to add the receiver’s account to your beneficiary list. Adding a new “receiver account” to the beneficiary list would require the Transaction Authorisation PIN (MTPIN — more on this in 0x05 ).

At this point, it wasn’t a surprise to me that this validation was also done on the App’s front end. So invoking the fund transfer API call directly via cURL, bypassed the receiver/beneficiary account validation. I was able to transfer money to accounts that wasn’t on my beneficiary list. There were a bunch of hyper critical controls that I wanted to test (Account Balance validation while transferring funds, Fund Transfer Limitation), but I had to skip it.

(From the response I received from the bank, it seemed that all these hyper critical controls indeed had front end validation).

0x05 All your money are belong to us

I had enough to write a PoC that would be classified with a Mid to High Severity rating. With 0x02, and 0x03, it was a matter of 5 lines of code to enumerate the bank’s customer records (Current Account Balance, and Deposits).

I dug deeper once again, before starting to write a PoC, which was when I hit a gold mine. Before going any deeper, I will explain the authentication mechanism of this Application. There are two PINs (Authentication PIN [MPIN], Transaction Authorisation PIN [MTPIN]). As the name suggest, you use MPIN for login, and MTPIN for critical controls such as adding a receiver account number to the beneficiary list, or transferring funds, creating a new fixed deposit, closing an existing fixed deposit. The username for this Application is your Customer ID [CID]. Now a valid fund transfer request would look like this.


In the above request, Sender (AC No: 987654321) is trying to send 100,000 to a beneficiary (AC No: 123456789). Once the request is deserialised, it is passed on to a function like this

bool validateAuthenticator(uint64_t decodedMTPIN, uint64_t customerId){
    bool result = false;
    if (getMTPINFromCustomerId(customerId) == decodedMTPIN)
        result = true;
    return result;

Did you spot the bug?

The problem was, the validateAuthenticator function checks if a given MTPIN matches with the MTPIN associated with the supplied customer ID, both of which are obtained from the (user initiated fund transfer) request which the App sends. There were no checks to see if the given customer ID, or the MTPIN actually belong to the sender’s account. So I was able to transfer money from any source account to any destination account, using my own valid CID, and MTPIN. I tested with a bunch of accounts belonging to my family. Few of those accounts don’t even have net banking or mobile backing activated. And it all worked like a charm.

In the image below, the number next to my name, 1303 is the last 4 digits of my customer ID, and list contains all the accounts I have associated with this customer ID. (SB — Savings Account, RIP — Sort of Fixed Deposits).

Below is a successful transaction I made. You can see that I have created a request with my customer ID, and my legitimate MTPIN, but the sender account (6254) does not belong to me.

I quickly wrote a 13 liner to automate this in bash, so that the vendor or the bank can test this out with their dummy accounts?

echo "Enter Victim's CIF"
read vcif
fetchVictimAccInfo=accountInfo=$(curl --data "channel=rc&entityId=XXX&clientAppVer=XXX&appID=XXXXXXXX&customerId=$vcif&serviceID=fetchAllAccounts&mobPlatform=iPhone" -v -k
victimSbAccountNo=$(echo $accountInfo | jq -r '.SB' | jq '.[0].accountno' | tr -d '"')
echo "Obtained Victim's Savings Account Number: $victimSbAccountNo"
echo "Enter Account Number to transfer the funds to: "
read attackerAccountNo
echo "Enter the amount to Siphon"
read amount
dotransaction=$(curl --data $request -v -k
echo $dotransaction

0x06 Notification: Total Pwnage

The Mobile Banking solution provides an instant notification via SMS, to the phone number tied up to the account in which a transaction is performed. The problem with that is they’ve got it horrendously wrong again.

The code that sends the notification message is very similar to the previous one.

bool sendNotification(long unsigned int customerId, string notificationMsg)
    bool res = false;
    long unsigned int mobileNumber = getMobileNumberFromCustomerId(customerId);
        res = sendMsg(mobileNumber);
    return res;

Similar to 0x05, the mobile number to which the notification has to be sent is obtained from the customer id, instead of account number. Thus, when an attacker steals money, from a victim’s account the notification message is delivered to the attacker, instead of the victim.

The image is a screen cap from my phone, which shows the notification of the transaction done 0x05.

0x07 Aftermath

I wrote a PoC as fast as I could, and shot an email to a bunch of General Managers, Deputy General Managers, IT Managers, Branch Managers on Nov 13, 2015. A week passed by, and no response. After 8 days, I had my first unofficial confirmation from one of my contact (Middle Manager at the same bank), that they were investigating this. Around the 9th/10th day, I had a linkedIn visit from a VP of Engineering, at the vendor that developed this App. Finally on the 12 day I got an official reply from the Deputy General Manager of IT confirming the problems and the mitigation steps that they were going to take.




bottom of page