Hacking 17000 people with an IDOR Vulnerability

TL;DR

I discovered a critical IDOR vulnerability in a redacted platform by manipulating URL parameters, which exposed personal data of 17,000 users. The severity of the issue was significant, but it was swiftly patched after I reported it.

Introduction

How did we get here?

First some background on the affected company, due to the nature of the company and the affected personal, much of the actual information must be redacted but I've tried to include as much relevant information as I could!

Also

What is IDOR??

Insecure Direct Object Reference (IDOR) is a type of security flaw where an application improperly controls access to its data. This vulnerability occurs when an application uses user-supplied input to directly access objects, such as database records or files, without sufficient authorization checks. IDOR can allow attackers to bypass intended access controls and retrieve or manipulate resources they should not have access to. This vulnerability is common in web applications that handle data objects using accessible URLs or API endpoints.

Example Scenario

Imagine a web application for an e-learning platform where users can view their profile information by navigating to a URL like:

https://securelearn.example.com/profile?userID=4567

Here, 4567 is a direct reference to the user’s unique profile ID. In a securely designed application, the server would verify whether the authenticated user has the rights to access the profile associated with ID 4567. However, if the application does not properly enforce these checks, it could be exposed to an IDOR attack.

Exploiting IDOR

An attacker could exploit this by manipulating the userID parameter in the URL to access information belonging to other users. For example, changing the URL to:

https://securelearn.example.com/profile?userID=4568

Curiosity Triggers Inquiry

So back to how we got here. I was just looking around the portal of the company logged into my account. The UI seemed a little different and the way the navigation felt was not typical. As I've mostly just made my websites both personal and professional in React or NextJS it felt definitely off. Every time I visit any website I love to use this chrome extension called Wappelyzer. It shows the type of technologies used in the website visited. So on this website I saw that it was using Ruby on Rails.

My curiosity peaked as most of the websites I visit are not made with Ruby on Rails. I opened the Network tab in the Chrome Developer tools and started looking around the website more to see how each of the interactions query the server. I saw that it returns html code for the relevant components that I interact with and the data pre-rendered. Some of the other information I was looking at was like the type of request it was making like GET or POST and the type of authentication that was used to verify the requests which was just Cookies with the user token and CSRF tokens. Now I felt the rush of adrenaline realizing I should request or data such as personnel information from buttons that query for it. To my surprise when one of the buttons that I clicked to add personnel to a group triggered a GET request that fetched an HTML snippet however, the response was not what one would expect. Instead of data scoped to the user's privileges, it included comprehensive details for all personnel registered on the platform with around 17000 records of users with their name, email addresses and most importantly, their value code which will come back later. Here is the HTML code that was returned where all the users are HTML options and the identification of the user as value in the option.

<!-- Simplified version of the initial server response -->
$('.modal-backdrop').remove();
$('#modal-container').html(`
<div class="container">
  <div class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" style='z-index: 1000000'>
    <div class="modal-dialog vertical-align-center" role="document">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h3>Add REDACTED</h3>
          </div>
          <div class="modal-body">
            <form data-turbo="false" action="/REDACTED/BspjzNF0/add_account" accept-charset="UTF-8" method="post">
              <input name="utf8" type="hidden" value="" autocomplete="off"/>
              <input type="hidden" name="authenticity_token" value="REDACTED" autocomplete="off"/>
              <label for="account_Account">Account</label>
              <select class="form-control" name="account[user]" id="account_user">
                <option value="IFmssplz">- ([email protected])</option>
                <!-- Additional options -->
              </select>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
`);

since there was so many options I needed to find a way to parse this HTML to find out actually how many records there were. I then wrote a script using ol’ reliable Golang to parse the HTML.

package main

import (
	"encoding/json"
	"io/ioutil"
	"log"
	"regexp"
	"strings"

	"golang.org/x/net/html"
)

func validateEmails(entries []map[string]string) {
	// Regex pattern for validating an email
	emailRegex := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
	for _, entry := range entries {
		if emailRegex.MatchString(entry["email"]) {
			// fmt.Printf("Valid email: %s\n", entry["email"])
		} else {
			// fmt.Printf("Invalid email: %s\n", entry["email"])
		}
	}
}

func parseHTMLFile(fileName string) ([]map[string]string, error) {
	// Read the HTML file content
	data, err := ioutil.ReadFile(fileName)
	if err != nil {
		return nil, err
	}

	// Parse the HTML
	document, err := html.Parse(strings.NewReader(string(data)))
	if err != nil {
		return nil, err
	}

	// Traverse the HTML to find the <option> tags
	var result []map[string]string
	var parseFunc func(*html.Node)
	parseFunc = func(n *html.Node) {
		if n.Type == html.ElementNode && n.Data == "option" {
			entry := make(map[string]string)
			for _, a := range n.Attr {
				if a.Key == "value" {
					entry["value"] = a.Val
				}
			}
			if n.FirstChild != nil {
				rawText := html.UnescapeString(n.FirstChild.Data)
				// Splitting the rawText to separate name and email
				parts := strings.SplitN(rawText, " (", 2)
				if len(parts) == 2 {
					entry["name"] = strings.TrimSpace(parts[0])
					email := strings.TrimRight(parts[1], ")")
					// Additional clean-up to ensure no trailing characters
					email = strings.Split(email, "\\")[0]
					email = strings.TrimSpace(strings.TrimSuffix(email, ")"))
					email = strings.TrimSuffix(email, "\u003c")
					entry["email"] = strings.TrimSuffix(email, ")") // Ensure no closing parenthesis remains
				} else {
					// Just in case only email is provided without a name
					entry["email"] = strings.TrimSpace(parts[0])
				}
			}
			result = append(result, entry)
		}
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			parseFunc(c)
		}
	}
	parseFunc(document)

	return result, nil
}

func main() {
	// Parse the HTML file to extract data
	entries, err := parseHTMLFile("test.html") 
	if err != nil {
		log.Fatal(err)
	}

	// Convert the extracted data to JSON and write to a file
	jsonData, err := json.MarshalIndent(entries, "", "  ")
	if err != nil {
		log.Fatal(err)
	}
	err = ioutil.WriteFile("output.json", jsonData, 0644)
	if err != nil {
		log.Fatal(err)
	}
	// fmt.Println("Data extracted and written to output.json")
	validateEmails(entries)
}

After executing this script, I fully grasped the extent of access I had to the personal information of numerous individuals.

But this wasn't enough, I was curious how I was even able to see all of these users in the first place. Then I realized that there was another URI that was used to visit the personnel’s information such as Full name, Full government issued identity number, address, email address, phone number which company they worked at outside of this portal, date of birth. I went back to that URI and tried to query a user that I was supposed to have access to and analysed the query. This is the query that was made

curl 'https://REDACTED.REDACTED.com/settings/user/DSHNJYUw' \
  -H 'accept: text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01' \
  -H 'accept-language: en-US,en;q=0.9' \
  -H 'cookie: ahoy_visitor=; remember_user_token=; ahoy_visit=; _lms_app_session=' \
  -H 'priority: u=1, i' \
  -H 'referer: https://REDACTED.REDACTED.com/REFACTED/cLtLllKP/REDACTED' \
  -H 'sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Brave";v="126"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Linux"' \
  -H 'sec-fetch-dest: empty' \
  -H 'sec-fetch-mode: cors' \
  -H 'sec-fetch-site: same-origin' \
  -H 'sec-gpc: 1' \
  -H 'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' \
  -H 'x-csrf-token: REDACTED' \
  -H 'x-requested-with: XMLHttpRequest'

I realized that in the query parameters there was a weird value that looked similar to the value that was parsed from the html code with all the users as options. Then the adrenaline started pumping again after realizing what if I tried one of the values from that file and re-query that URI with it in the query parameters. and to my absolute shock it returned the data of the personnel.

It felt like I struck gold cause most of the ethical hacking labs such as TryHackMe are just simulated environments which don't feel as real as it should and it feels totally different when you find one in the wild. But before anything I needed to confirm it further. I used the Golang script to enumerate through the value in the file and saw that all of it returned the same data.

What is the Attack Vector?

Upon confirming the vulnerability's presence, it became clear that this was not just a minor issue—it was a critical security flaw with far-reaching implications. The vulnerability allowed any authenticated user to access sensitive personal information of over 17,000 users by simply manipulating a URL parameter. This data included names, email addresses, government-issued identification numbers, and other highly sensitive information.

The simplicity of the attack mechanism significantly amplified the severity of this vulnerability. All that was required to exploit it was the manipulation of a single URL parameter, with authentication relying solely on Cookies and a CSRF token. This low barrier to exploitation meant that even a moderately skilled attacker could compromise the system with minimal effort.

For threat actors, especially those already engaged in cookie-stealing malware campaigns, this vulnerability would have been a goldmine. The potential for widespread identity theft, targeted phishing attacks, and other malicious activities was enormous, given the volume and sensitivity of the exposed data.

The efficiency of exploiting this vulnerability was particularly alarming. My tests, using a straightforward Golang script, demonstrated that an attacker could harvest comprehensive details on all 17,000 users within seconds. This rapid data extraction could lead to the entire user database being compromised almost instantaneously, turning a potential risk into an immediate and severe threat to data integrity and privacy.

The broad scope of this vulnerability, combined with the ease of exploitation, underscores the critical importance of robust security measures. Without proper authorization checks, even a small oversight can lead to catastrophic consequences, as was nearly the case here.

Reporting and Mitigation

Upon confirming the vulnerability, I promptly reported it to In charge at the company and emphasized the severity of the issue which then culminated in a swift patch to the system after my report.

Now, when I attempt the same query, I receive an HTTP 500 error code, but I can still access the data of the personnel I am authorized to view.

How It might have looked on Ruby on Rails

Below is a simplified example of vulnerable server-side code in Ruby on Rails, which fails to implement necessary authorization checks:

# GET /profile
def show
  @user = User.find(params[:userID])
end

In this Ruby on Rails controller, the show method retrieves a user profile based on the userID provided in the URL. The method assumes the ID is valid and does not verify if the logged-in user is authorized to access the specified user profile.

To mitigate this vulnerability, the application should implement robust authorization checks to ensure that the logged-in user has the right to access the requested resource. Here’s a revised version of the Ruby on Rails code to include an authorization check:

# GET /profile
def show
  @user = User.find(params[:userID])

  # Check if the current_user is authorized to view @user
  unless current_user.can_view?(@user)
    redirect_to root_url, alert: "You are not authorized to view this profile."
  end
end

In the updated code, there is an additional check to determine if the current_user (the user currently logged in) is authorized to view the profile. If not, the application redirects the user to the homepage with an alert message, thereby preventing unauthorized access.

If the application lacks proper authorization mechanisms, this straightforward modification could allow the attacker to view or modify another user’s profile information. Moreover, if an attacker gains access to credentials that are not even supposed to have access to certain data, they could potentially access that data by exploiting IDOR vulnerabilities. This makes the issue especially critical as it could lead to unauthorized access even beyond the stolen user's intended permissions.

Conclusion

As we wrap up this exploration into the IDOR vulnerability discovered within the REDACTED platform, it’s essential to reflect on the broader implications of what was uncovered. This vulnerability was not merely a technical oversight; it was a glaring breach waiting to be exploited, exposing the personal data of thousands of users to potential misuse.

The discovery process, fuelled by curiosity and enhanced by a background in cybersecurity, underscores the critical importance of vigilance in the digital age. What started as an innocent exploration of the platform’s infrastructure using tools like Wappalyzer and Chrome Developer Tools, quickly escalated into uncovering a severe security flaw that could have had devastating consequences if left unchecked.

Lessons Learned and Moving Forward

This experience serves as a potent reminder of the ongoing arms race in cybersecurity. It exemplifies the need for continuous education, rigorous security practices, and proactive vulnerability assessments to stay ahead of threats. For developers and security professionals, it reinforces the critical importance of implementing robust access controls and authorization checks in every layer of an application.

The Role of Ethical Hacking

Moreover, this incident highlights the invaluable role of ethical hacking and responsible disclosure in strengthening cyber defenses. By responsibly reporting the vulnerability and collaborating with the platform’s security team, we not only mitigated a critical risk but also contributed to the broader knowledge base of security best practices.

A Call to Action

For those involved in building and maintaining web applications, let this serve as a call to action to prioritize security, not as an afterthought, but as a cornerstone of development. Implement comprehensive security reviews and adopt a security-first mindset to protect users from potential threats.

As we continue to navigate the complex landscape of cybersecurity, let us take these lessons to heart, ensuring a safer digital environment for all. Your thoughts and experiences with similar security challenges are invaluable—feel free to share them, as each story adds a layer of defense against future vulnerabilities.

Acknowledgements

A special thank you to George Chan and Anthony Gui for their continuous support and guidance in the industry and uncovering this vulnerability.


Click here to share this article with your friends on X if you liked it.