Introduction

The other day, I was invited to a private program for a subscription-based adult content platform. The in-scope application’s core user types are fans and creators, where fans can subscribe to creators, send messages, tips, etc. In this article, I’ll share the reported vulnerabilities I discovered on this platform. Since it’s a private program, I can’t share the company name, so I’ll remove all the company-related info.

Bug #1: Stored DOM XSS at the subscription page allows to steal user’s credit card details

The subscription page generated on https://example.com/<NICKNAME> can be reached at https://secure.example.com/signup/?signup_key=<HASH> in a separate window. During the generation of the page, I noticed that successRedirectUrl and declinedRedirectUrl parameters can accept javascript scheme:

POST /graphql HTTP/1.1
Host: ***.appsync-api.us-east-1.amazonaws.com
Authorization: ***
Content-Type: application/json

{"operationName":"getPaymentSignUpUrl","variables":{"paymentId":"1de202c3-d0d8-4a3e-b57a-1b2c301ba4cc","successRedirectUrl":"javascript:alert();//","declinedRedirectUrl":"javascript:alert();//","paymentMethod":"creditcard","beneficiary":"011fe32e-c3b3-4a27-a849-b65855da7b15"},"query":"query getPaymentSignUpUrl($paymentId: String, $successRedirectUrl: String, $declinedRedirectUrl: String, $paymentMethod: String, $beneficiary: String!) {\n getPaymentSignUpUrl(\n paymentId: $paymentId\n successRedirectUrl: $successRedirectUrl\n declinedRedirectUrl: $declinedRedirectUrl\n paymentMethod: $paymentMethod\n beneficiary: $beneficiary\n )\n}\n"}

After navigating to the URL mentioned in the request’s response, inputting credit card details, or clicking the “Cancel” button, javascript will be executed.

The attack scenario looks like the following:

  1. An attacker generates a malicious payment link.

  2. The attacker’s social media or adult application page mentions the payment https://secure.example.com/signup/?signup_key= page.

  3. Users input their CC or click “Cancel,” which triggers the execution of the attacker’s malicious javascript, causing credit cards (in DOM document.body.innerHTML) to be sent to the attacker-controlled server via fetch request. Suppose the user clicks a “Cancel” button without inputting payment data. In that case, an attacker can take over the victim’s account by changing their email using a same-origin POST request or exhaust money on the victim’s CC by subscribing to the attacker’s creator accounts. secure.example.com (adult app’s payment gateway) has a high authenticity level due to its affiliation with the example.com website, so there’s a fairly high chance users would actually trust it and apply their CC details.

  4. In the first scenario (CC input&javascript executution), an attacker gathers all the CCs to sell them for a bulk price to organized crime rings or to launder the money by buying high-value items online and reselling them. In the second scenario (just JS execution via “Cancel” button), an attacker receives the money from a victim by subscribing them to the attacker’s creator accounts and withdrawing the money.

After shipping the report to the program, I was awarded $1k:

At this point, I tested an app only on fan accounts while speculating about the scenarios involving creator ones since I couldn’t pass verification due to the strictness of the creator verification process. Considering I obtained a direct source of communication with the program and reported a valid vulnerability, I politely asked about giving the green light to my creator account submission, and the team kindly approved it. Now I have an additional hard-to-get user type, greatly expanding the attack surface!

As for the fix, the team wasn’t tackling black/white lists but instead removed the vulnerable parameters from the requests and added /payment-result?result=approved and /payment-result?result=declined as default immutable redirect_uri and error_uri values. It looks like a solid fix to me, which I informed a team after a $50 retest.

Bug #2: Race condition at withdrawal requests and absence of manual payout approval lead to payment duplication and financial losses for the company

Right after the account verification, I sent $1 from the fan to the creator account using a monthly subscription. When navigating to /my-payouts as a creator, I discovered I’m eligible for the withdrawal, which isn’t automatic(!) All I should do to withdraw my funds is fill out the payment information and click the “PAYOUT NOW” button. I filled in my third-party wallet info, intercepted the final payment request, sent it to the Burp turbo intruder, and… you know the drill. I sent 5 simultaneous requests using the following Turbo Intruder script:

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=2,
                           requestsPerConnection=2,
                           pipeline=False
                           )
    a = open("/Users/max/intruder1.txt", "r")
    b = open("/Users/max/intruder2.txt", "r")
    for i in range(1):
        engine.queue(target.req, a.read(), gate='race1')
    for i in range(1):
        engine.queue(target.req, b.read(), gate='race1')
    engine.openGate('race1')
    engine.complete(timeout=60)
def handleResponse(req, interesting):
    table.add(req)

To my surprise, all 5 requests were considered unique payout requests, and I multiplied my payout by 5 times in the third-party wallet!

The impact here would be to drain all the company’s money from third-party wallets by multiplying the creator balance, which would create a substantial money loss for the company. After dropping the report in a program, it was rewarded max bounty and some bonus for exceptional impact:

The bug was promptly addressed by adding a synchronization mechanism lock: only the earliest withdrawal request would be processed while the newer ones return “Balance must be greater than 0.”

Bug #3: Unverified fans can subscribe to arbitrary creators, leading to mass subscriber farming

It is a fairly simple flaw that can be described in a single sentence. When registering a fan account, an app doesn’t impose a verification check, which, in the case of registration automation (no captcha or other speedbumps), allows any creator to add an unlimited number of subscribers.

The primary metric for determining a creator’s attractiveness is the number of subscribers and this bug allows to easily forge these stats. After reporting the issue, a bug was fixed, but no bounty was awarded.

Bug #4: An ability to charge users for a subscription by blocking them allows to continue their subscription despite its termination

While playing with how blocklists work, I found that if the creator blocks a fan when they are subscribed to the creator’s page, a fan still will be charged for the subscription indefinitely despite zero subs being displayed at “Manage Subscriptions.” It’s worth mentioning that even if a fan removes a credit card from their account, the subscription would still be ongoing. The only thing fans can do without contacting support (which doesn’t guarantee subscription removal) to prevent more sub charging is invalidate their credit card on the bank’s side.

The impact here would be charging users indefinitely without them being able to cancel a subscription. Another sneaky attack scenario would be to subscribe fans to our subscription services for a low price (say, $1), then increase the subscription price to $200 while keeping fans blocked. The fans would be charged the new price on the new billing cycle.

The bug was fixed by invalidating existing fan subscriptions after the creator added a fan to the block list.

Bug #5: A creator can message fans without being linked by follow action

The application forbids the creator from messaging users who are not creators and don’t subscribe to or follow the creator. If the fan isn’t subscribed to the creator, the creator is greeted with a UI error when sending a message. However, the check exists only on UI but not on the backend for CREATE_CONVERSATION and sendMessage graphql operations. Restricting creators from messaging fans is typical for similar applications to avoid mass spam and unwanted mailing. When an issue with a similar impact is found, it’s usually treated as a valid vulnerability due to apparent reasons. After sending, the report was accepted and rewarded:

Bug #6: Fix bypass

The team added a fix for the previous report that basically checks whether fans and creators share a follow. If true, the creator is allowed to message that particular fan. However, no restriction is implemented on the backend for the FOLLOW_USER graphql operation. Thus, it’s possible to satisfy the messaging eligibility application logic by following a fan as a creator, allowing the use of CREATE_CONVERSATION and sendMessage graphql operations thereafter. The vulnerability was assessed with the same severity as the previous one.

Final thoughts

  • Identify and reach as many attack surfaces as possible. In my case, creator user type approval was heavily guarded by strict verification, but I found a way to obtain it, which granted me additional bugs and fresh insight into the application.

  • If there’s a potential for duplication of behavior, object, or item through creating repetitive requests, always check for race conditions! All it takes is to quickly throw your request into Turbo Intruder and launch the script. If it works, it might be a win!

  • This research in an adult content platform granted me $7750 with retesting fees.