Akwaaba! abusua. I came upon a case of 'parameter' pollution, but not the usual kind. Stick around and follow along. Also, if you haven't already, check out my previous Android App Content Provider exploit. And yes, you can also land an Easy $150 bounty from this SSO exploit.

🔗READ FOR FREE

Some time ago, I came across a Program on Hackerone which had a wildcard (*) subdomain like *.target.com.

None
Triage

How did I identify a target?

To identify potential targets, I used shodan.io to search for domains and IP addresses that share the same SSL certificate Common Name (CN), specifically those matching a wildcard pattern like *.target.com. This method helps identify services that may belong to the same organization or infrastructure.

For instance, if you're investigating a domain like medium.com, you can inspect its SSL certificate to find the CN value. Once identified, you can use Shodan to enumerate other hosts with certificates containing that same CN, potentially revealing related assets or misconfigured endpoints. Steps to check the CN value for any:

  1. You can do so by clicking on the padlock icon in the address bar.
  2. Click on the Connection is secure which will open another pop-up box.
None

3. On the new pop-up box, click on the certificate looking icon and a final pop-up box will appear. Looking at the Common Name (CN), it should tell you the domain name the certificate is issued for.

Sometimes during a bug hunt, I land on a strange subdomain and I'm not sure if it belongs to the target organization. A quick way to check is by comparing its SSL certificate's CN with that of their main website (or any other known application). If they match, it's likely part of their infrastructure.

None

The Shodan search query used:

Ssl.cert.subject.CN:"*.target.com" http.title:"Login"

From the results, I identified an application that allowed user account signup. Let's call this application insecure.target.com.

Identifying the Vulnerability

During the signup process, there is a final stage where you will have to verify the email. A 6-digit code is sent to your email, and you can verify.

None
Verify email at Sign up

Going back to Burp Suite to see how the API request looks like, I realized something weird:

  1. All the API requests were of the GET method
  2. They had no form data parameters.

I was a bit confused at first, how exactly was the application sending my registration data to the server? So, I took a closer look at each API request. Then, I found it: a GET request to /api/auth/mobileVerifyRequestOTP. Surprisingly, there were no special query parameters. Instead, the user's registration data was being sent entirely through HTTP headers.

The other form data are also sent via different API endpoints via a GET request also.

None
Form data sent through Headers in a GET request

This was unusual but I guess it worked for them.

Before reading how I exploited this, check out how I manipulated an API path to Access ALL user PII.

Exploiting Header Pollution

Using the same idea as parameter pollution, I tried duplicating the Mobileusername header with two different email addresses to see how the server would handle it. Surprisingly, it accepted both and responded with a verificationID. (Note: this isn't the OTP itself, just an ID used to track the registration session.)

None
Duplicate the email header

One of the emails was my wearehackerone.com (which gets redirected to Gmail) and another from Temp-Mail.org. Checking my Gmail first, I saw the OTP code had arrived, and the OTP code was 399336.

None
OTP arrives in first email

I quickly checked the Temp-Mail's inbox, and the same 399336 had arrived there also. That confirmed the vulnerability. The server had accepted both emails and sent the same code to each.

None
Same OTP in second different email

This is because the server was processing the duplicated Mobileusername headers in a weird way. Instead of rejecting or overriding the duplicate, it treated both values as part of an array and went ahead to send the same OTP code to each email address listed. This behavior was confirmed by checking the To header in the email received on my Gmail account, which clearly showed both email addresses separated by a comma (,). Even Gmail showed that another recipient had received the same OTP message.

None
In first email, we see the second email showed in copy

Shamelessly plugging my other article here: Another unique way to Bypass Google CAPTCHA to beat Rate Limiting. Medaase.

What could cause this Vulnerability?

I haven't seen their backend code; this is merely me speculating. But to demonstrate how such a vulnerability could happen, check out the Flask code snippet below.


from flask import Flask, request
import random

@app.route('/api/auth/mobileVerifyRequestOTP?isEmail=true', methods=['GET'])
def send_otp():
    # Get all values of the 'Mobileusername' header from the GET request
    usernames = request.headers.getlist('Mobileusername')

    # Generate a single OTP code
    otp = str(random.randint(100000, 999999))
    verification_id = str(random.randint(100000, 999999))

    '''
     A duplicate header check should be here but none was implemented
    '''

    # Send the same OTP to all the emails
    send_otp_email(usernames, otp)
    return {'verificationID': verification_id}

What is the Impact of such finding?

For starters, it's possible to create and verify an account using someone else's email. I found that the server ties the first Mobileusername header to the account being created. So, a malicious user could set the victim's email as the first header and their own as the second. They'd receive the OTP, complete the verification, and the account would be created, under the victim's email.

None
Register an account with another user's email

Mitigation

This can be properly mitigated by implementing a simple check for duplicate headers. If multiple values are detected for the Mobileusername header, the server should reject the request or only process the first value securely.


from flask import Flask, request
import random

@app.route('/api/auth/mobileVerifyRequestOTP?isEmail=true', methods=['GET'])
def send_otp():
    # Get all values of the 'Mobileusername' header from the GET request
    usernames = request.headers.getlist('Mobileusername')

    # Generate a single OTP code
    otp = str(random.randint(100000, 999999))
    verification_id = str(random.randint(100000, 999999))

    # Validate that exactly one header is present
    if len(usernames) != 1:
        abort(400, description="Invalid request: only one Mobileusername header is required.")

    # Send the same OTP to all the emails
    send_otp_email(usernames, otp)
    return {'verificationID': verification_id}

Hey you, yes you in the back…bye

None
Mr. Marsh

Thanks for reading this, if you have any questions, you can DM me on Twitter @tinopreter. Connect with me on LinkedIn Clement Osei-Somuah.