Simon's Blog

That time I accidentally did a pentest

August 26, 2022

Important note

This story happened over two years ago and the details have been fudged. I offered the company involved an opportunity to review this blog post before publishing and they approved it without requesting any changes.


My wife benefits from a partial refund on some services as a perk provided by her workplace. At the time we were renovating a house so every Euro we could claw back and put into the renovation meant getting one step closer to being able to move in.

To benefit from this refund, we had to provide the receipts of the year before the previous. Meaning that even though we would apply for the refund in (for example) 2016, I would need the receipts of 2014.

The Bills

I logged in to a specific company’s website since the account was in my name and I clicked through to the billing page… only to find the last few bills only available. 🤔. I had opened and been paying my account for longer than that for sure and this would have left me with a few months of bills missing.

I hopped onto their support chat asking for a way to access all of my bills. To authenticate I had to provide my full name, my address and my account number (this is relevant later), only to be told that the system is limited to keeping the last few months of bills only and no more.

This instantly smelled funny to me. As someone who writes software I couldn’t fathom any system let alone a billing system retaining only a few months of data. This data definitely would have been retained, if not for internal purposes then most likely for auditing, reporting and legal reasons.

Frustrated at the prospect of missing out on the full refund, I did what any self respecting programmer does: I pressed F12.

The Network Tab

I started clicking through the billing section of the website again. It was an SPA that was using REST calls to contact the company’s backend, with the responses being encoded in JSON. Simple, solid stuff.

When loading the list of bills the response looked something like this:

    "result": [
        {"id": 1000, "month": "Feb", "year": 2022},
        {"id": 5000, "month": "Mar", "year": 2022},
        {"id": 9500, "month": "Apr", "year": 2022},
        ... etc

Very typical response for a list. The most curious part of it however was the id. My bill’s ids were very different from each other, in irregular intervals. This immediately indicated to me that each bill has a globally unique identifier. This is expected… and the curious part is how the id is:

  1. A monotonic increasing integer (meaning 1, 2, 3, etc)
  2. Globally unique across all users (meaning no two users will ever have the same bill id)
  3. Exposed to the Frontend

This meant that I could, in theory, guess all the bill IDs, including the ones that were older than a few months old.

The Bill

To further reinforce my theory that the bill ID was globally unique, I observed that downloading the bill made an API call to an endpoint similar to this: /api/bill/12345/download. The value 12345 always correlated with the id values returned in the list API call. Calling this API endpoint would return data that the browser was turning into a PDF file of the bill.

The Search

Armed with this knowledge, I realised that the best course of action would be:

  1. Determine the average difference between each bill ID
  2. Use that to generate the potential bill IDs that were mine that are older than a few months old
  3. To narrow down the search, attempt to download each bill ID +- 100. It is expected that each billing cycle there will be less bills or more bills.
  4. Use the HTTP Status Code to know whether it is my bill or not
  5. Download all the bills that return an OK status code
  6. Success!

Penny Drop

To find out what status codes would be returned, I attempted to download a bill with a single digit difference (if my bill ID was 123 I attempted to download 124).

It downloaded just fine.

Wait what?

I opened the PDF file quickly and there it was: someone else’s bill.

I had to confirm if it was a fluke.

I changed the ID by one digit again and tried again.

It downloaded just fine.

Awww shit.

The Vulnerability

So it turns out that the API to download a bill did not check whether I owned the account being billed.

You would not think that this is a big deal (especially if you ignore GDPR)… other than the fact that each bill contained the person’s:

  1. Full Name
  2. Address
  3. Account Number

These are the same three data points I had used to authenticate with their support staff. Retrieving a bill could, in theory, escalate to a total account takeover.

The Fix

I remember feeling scared at the time. I had had a negative impression about the attitude of Maltese companies to Information Security for a while, especially after a huge data leak of voter records where the IT professional in charge it was reported to “thought the report was an April Fool’s Joke”.

I needed to somehow contact someone at the company who would trust I was not malicious and action it as soon as possible. After reaching out to a trusted acquaintance of mine, they linked me up with a Senior Manager at the company. They (with my permission) called me on my phone soon after and told me that both an executive and their security team had been informed and asked me for a high level overview before the security team called me for more details.

I was kind of embarrassed to reveal that the entire thing boiled down to, in my opinion, missing validation, rather than some fancy zero day that gave me root access to all their traffic.

After hanging up, the security team called me and we went over reproduction steps together. They thanked me and told me they would be in touch.

The Aftermath

An hour or so later I got an email from them stating that, due to it being a weekend, they went ahead and disabled the download bill feature and would follow up on Monday. Monday evening they communicated again that they had successfully patched the vulnerability. I tried one more time to download a bill that wasn’t mine and got an HTTP Forbidden status code in return.

I eventually spoke again to the senior manager a few days later. I suggested how they could display an email address or some other method for security related escalation more prominently on their website for these kind of issues

I ended up being invited to apply for one of their open positions. The interview went well (and they waived their technical test for me, which was great) however I ultimately did not join the company due to other factors in my life at the time.

In Conclusion

Sometimes even the simplest of issues could escalate into a potentially disastrous data breach. This kind of issue is trivial to track with an automated test.

Also, never get between a motivated software engineer and what is rightfully their data, unless you want them to poke holes in your software 😉

Written by Simon who lives in Malta. You can find out more about me on the about page, or get in contact.