My Profile Photo


Security, IoT, 3GPP, Programming, Finance, Economics, and Rants

The Bank Job

I've held an account in one of the largest public sector banks in India since early 2007. Public Sector Banks (Govt. run Banks) in India are at least a decade old when it comes to implementing cutting edge technology. In late 2015, my bank in collaboration with a vendor, released a Mobile Banking application for Android, and iOS platforms. It was a typical Swedish Winter weekend with no signs of sunshine. So I decided to stay indoors, and poke around this App.

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. My 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 an epic failure.

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.

Session IDs

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 be otherwise 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.

Auth Bypass

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 a actual session timer in the backend, which automatically invalidate the session, or is it possible only 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, 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 that would have been outright illegal. So 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 deserialized, 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).

My Accounts

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.

Success 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 above 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. He (The Vendor?) basically quoted my recommended mitigation with what they were planning to do. It was a truly ecstatic, and scary moment when I realized that I hit bull's eye. It took them 12 days to respond to an e-mail saying "Hey, your several billion worth deposits are at risk", which was stunning.

I replied back to them to ask about when a fix would be released, and if they had any bug bounty program. I knew it was far fetched, but this bank has about 25 Billion USD in Deposits (2015 Data). So why not. And as expected that was the last time I heard back from them. No reply yet.

So there you go Bug Bounty = 0$. Welcome to India!

Response 1

Response 2